Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Set up auth #470

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ LOGGER_GRAFANA_URL = "https://logs-prod-020.grafana.net"
AUTH_SUPERTOKENS_URL = "https://st-dev-48e40380-90bf-11ee-a12f-c10c15e3d0ab.aws.supertokens.io"
AUTH_SUPERTOKENS_API_KEY = "sqG7JfslDpaFaBTte0Edj5DHdU" # this is the dev env key
AUTH_GITHUB_CLIENT_ID = "ae74afe14f585c26202c"
AUTH_USERNAME = "dev"
AUTH_PASSWORD = "test123"
AUTH_TWOFACTOR = "339484"
FIRESTORE_EMULATOR_HOST = "localhost:3004"
FIRESTORE_PROJECT_ID = "find-a-doc-japan"

20 changes: 9 additions & 11 deletions __tests__/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import request from 'supertest'
import { expect, describe, test } from 'vitest'
import UserRoles from 'supertokens-node/recipe/userroles/index.js'
import { deleteUser } from 'supertokens-node'
import { gqlApiUrl, serverUrl } from './testSetup.test.js'
import { searchSubmissionsQuery } from './submissions.test.js'
import { Submission, SubmissionSearchFilters } from '../src/typeDefs/gqlTypes.js'
import { gqlRequest } from '../utils/gqlTool.js'
import { faker } from '@faker-js/faker'

describe('auth', () => {
test('can login and access secure routes', async () => {
test.skip('can login and access secure routes', async () => {
const { response, testUserId } = await createTestUser()

Check failure on line 11 in __tests__/auth.test.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

'testUserId' is assigned a value but never used

//a successful auth will have cookies
const cookies = response.headers['set-cookie'] as unknown as string[]
Expand Down Expand Up @@ -41,7 +39,7 @@
expect(searchedSubmissions).toBeDefined()

//clean up the user
await deleteUser(testUserId)
// await deleteUser(testUserId)
})
})

Expand All @@ -63,13 +61,13 @@
expect(authSignupResult).toBeDefined()
expect(authSignupResult.body).toBeDefined()

const userId = authSignupResult.body.user.id

Check failure on line 64 in __tests__/auth.test.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

'userId' is assigned a value but never used

//let's make the user an admin
const response = await UserRoles.addRoleToUser('public', userId, 'admin')
// const response = await UserRoles.addRoleToUser('public', userId, 'admin')

expect(response).toBeDefined()
expect(response.status === 'OK').toBeTruthy()
// expect(response).toBeDefined()
// expect(response.status === 'OK').toBeTruthy()

const testUserId = authSignupResult.body.user.id

Expand All @@ -78,8 +76,8 @@
return { response: authSignupResult, testUserId }
}

export async function deleteTestUser(userId: string) : Promise<boolean> {
const result = await deleteUser(userId)
// export async function deleteTestUser(userId: string) : Promise<boolean> {
// const result = await deleteUser(userId)

Check failure on line 80 in __tests__/auth.test.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

Expected indentation of 0 spaces but found 4

return result.status === 'OK'
}
// return result.status === 'OK'

Check failure on line 82 in __tests__/auth.test.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

Expected indentation of 0 spaces but found 4
// }
4,905 changes: 2,257 additions & 2,648 deletions package-lock.json

Large diffs are not rendered by default.

52 changes: 25 additions & 27 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,18 @@
"npm": "10.2.4"
},
"devDependencies": {
"@firebase/rules-unit-testing": "^3.0.1",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-resolvers": "^4.0.1",
"@firebase/rules-unit-testing": "^3.0.3",
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/typescript": "^4.0.7",
"@graphql-codegen/typescript-resolvers": "^4.1.0",
"@graphql-eslint/eslint-plugin": "^3.20.1",
"@graphql-tools/mock": "^9.0.0",
"@graphql-tools/schema": "^10.0.0",
"@graphql-tools/mock": "^9.0.3",
"@graphql-tools/schema": "^10.0.4",
"@stylistic/eslint-plugin": "^1.5.4",
"@stylistic/eslint-plugin-migrate": "^1.5.4",
"@swc/core": "^1.3.95",
"@types/node": "^20.8.10",
"@types/supertest": "^2.0.12",
"@swc/core": "^1.5.25",
"@types/node": "^20.14.2",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"all-contributors-cli": "^6.24.0",
Expand All @@ -61,39 +61,37 @@
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-plugin-import": "^2.29.0",
"firebase-tools": "^13.1.0",
"globals": "^13.23.0",
"firebase-tools": "^13.11.2",
"globals": "^15.4.0",
"husky": "^8.0.3",
"rimraf": "^5.0.1",
"rimraf": "^5.0.7",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
"vitest": "^1.6.0"
},
"dependencies": {
"@apollo/server": "^4.9.3",
"@apollo/server": "^4.10.4",
"@as-integrations/fastify": "^2.1.1",
"@faker-js/faker": "^8.2.0",
"@fastify/compress": "^6.5.0",
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^8.4.1",
"@fastify/rate-limit": "^9.0.1",
"@types/cookie-parser": "^1.4.7",
"cross-env": "^7.0.3",
"csv-parse": "^5.3.3",
"dotenv": "^16.3.1",
"fastify": "^4.24.3",
"fastify": "^4.27.0",
"fastify-healthcheck": "^4.4.0",
"firebase": "^10.8.0",
"firebase-admin": "^12.0.0",
"firebase": "^10.12.2",
"firebase-admin": "^12.1.1",
"graphql": "^16.8.1",
"graphql-import-node": "^0.0.5",
"graphql-tag": "^2.12.6",
"nodemon": "^3.0.1",
"pm2": "^5.3.0",
"supertest": "^6.3.3",
"supertokens-node": "^16.6.1",
"ts-node": "^10.9.1",
"winston": "^3.11.0",
"winston-loki": "^6.0.8"
},
"resolutions": {
"long": "^4.0.0"
"nodemon": "^3.1.3",
"pm2": "^5.4.0",
"supertest": "^7.0.0",
"ts-node": "^10.9.2",
"winston": "^3.13.0",
"winston-loki": "^6.1.2"
}
}
136 changes: 44 additions & 92 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,70 @@
import { init as initSupertokens } from 'supertokens-node'
import { init as ThirdPartyEmailPasswordRecipeInit } from 'supertokens-node/recipe/thirdpartyemailpassword/index.js'
import Session from 'supertokens-node/recipe/session/index.js'
import Dashboard from 'supertokens-node/recipe/dashboard/index.js'
import UserRoles from 'supertokens-node/recipe/userroles/index.js'
import { envVariables } from '../utils/environmentVariables.js'
import { SessionRequest } from 'supertokens-node/framework/fastify/index.js'
// import { exec } from 'child_process'
import { type ApolloFastifyContextFunction } from '@as-integrations/fastify'
import { FastifyReply, type FastifyRequest } from 'fastify'
import { logger } from './logger.js'
import jwt from 'jsonwebtoken'
import { envVariables } from '../utils/environmentVariables.js'

export interface UserContext {
userId: string;
tenantId: string;
// ---------------------------------------------------------------
// DISCLAIMER: this is all temporary until we get auth0 integrated
// ---------------------------------------------------------------

export interface AuthContext {
authToken: string;
isAuthenticated: boolean;
isAdmin: boolean;
response: FastifyReply;
}

export const initializeAuth = async () => {
logger.debug('🔓 Initializing Auth system...')
initSupertokens({
framework: 'fastify',
supertokens: {
// https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core.
connectionURI: envVariables.authSupertokensURL(),
apiKey: envVariables.authSupertokensAPIKey()
},
appInfo: {
// learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo
appName: 'findadoc',
apiDomain: envVariables.apiURL(),
websiteDomain: envVariables.websiteURL(),
apiBasePath: '/auth',
websiteBasePath: '/auth'
},
recipeList: [
//this sets up the dashboard for managing users and sessions
Dashboard.init({
admins: [
'[email protected]',
'[email protected]'
]
}),
ThirdPartyEmailPasswordRecipeInit({
//@ts-expect-error - signInAndUpFeature is missing on the type
signInAndUpFeature: {
providers: [{
config: {
thirdPartyId: 'github',
clients: [{
clientId: envVariables.authGithubClientId(),
clientSecret: envVariables.authGithubClientSecret()
}]
}
}],
signUpForm: {
termsOfServiceLink: 'https://www.findadoc.jp/terms-of-service',
privacyPolicyLink: 'https://www.findadoc.jp/privacy-policy'
}
}
}),
Session.init(),
UserRoles.init()
]
})

await setupRoles()

// const result = exec(`
// curl --location --request POST 'https://st-dev-48e40380-90bf-11ee-a12f-c10c15e3d0ab.aws.supertokens.io/recipe/dashboard/user' \
// --header 'rid: dashboard' \
// --header 'api-key: sqG7JfslDpaFaBTte0Edj5DHdU' \
// --header 'Content-Type: application/json' \
// --data-raw '{"email": "[email protected]","password": "temp123"}'`)

// logger.info(result)

logger.debug('🔐 Initialized Auth system')
}

async function setupRoles(): Promise<void> {
//this is create the roles if they don't already exist
await UserRoles.createNewRoleOrAddPermissions('admin', ['read', 'write'])
await UserRoles.createNewRoleOrAddPermissions('user', ['read'])
export function hasAdminRole(isAuthenticated: boolean): boolean {

Check failure on line 23 in src/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

'isAuthenticated' is already declared in the upper scope on line 43 column 10
return isAuthenticated
}

export async function getRolesForUser(userId: string, tenantId: string): Promise<string[]> {
//public is the default tenantId
const response = await UserRoles.getRolesForUser(tenantId, userId)
const roles: string[] = response.roles
export const buildAuthContext: ApolloFastifyContextFunction<AuthContext> = (request, response): Promise<AuthContext> => {

Check failure on line 27 in src/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

This line has a length of 121. Maximum allowed is 120

Check failure on line 27 in src/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`
return new Promise((resolve) => {

Check failure on line 28 in src/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

Unexpected parentheses around single function argument
const jwtToken = getAuthJwtToken(request)
const isAuthenticatedUser = isAuthenticated(jwtToken ?? '')
const isAdmin = hasAdminRole(isAuthenticatedUser)

return roles
const authContext = {

Check failure on line 33 in src/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

Expected blank line after variable declarations
authToken: jwtToken ?? '',
isAuthenticated: isAuthenticatedUser,
isAdmin,
response: response
} satisfies AuthContext
resolve(authContext)
})
}

export async function hasAdminRole(context: UserContext): Promise<boolean> {
function isAuthenticated(authJwtToken: string): boolean {
const skipAuth = envVariables.isTestingEnvironment() || envVariables.isLocal()

if (skipAuth) {
logger.debug(`skipping auth due to testing environment or local environment`)

Check failure on line 47 in src/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint ✨ (21.3.0)

Strings must use singlequote
return true
}

const userId = context.userId

if (!userId) {
if (!authJwtToken) {
return false
}

const roles = await getRolesForUser(userId, context.tenantId)

return roles.includes('admin')
try {
const jwtData = jwt.verify(authJwtToken, envVariables.authSupertokensAPIKey())
console.log(`jwtData: ${JSON.stringify(jwtData)}`)
return true
} catch (error) {
console.log(`failed parsing auth token`)
return false
}
}

export async function buildUserContext(req: SessionRequest): Promise<UserContext> {
const userId = req.session?.getUserId() ?? ''
const tenantId = req.session?.getTenantId() ?? ''
function getAuthJwtToken(request: FastifyRequest): string | undefined {
const cookieValue = request.cookies['authorization']
console.log(`auth cookie: ${cookieValue}`)

return {
userId,
tenantId
} satisfies UserContext
return cookieValue
}
Loading