-
Notifications
You must be signed in to change notification settings - Fork 0
105 Add Authentication Check to Projects Routes #140
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| import { CognitoJwtVerifier } from 'aws-jwt-verify'; | ||
| import db from './db'; | ||
|
|
||
| const COGNITO_USER_POOL_ID = process.env.COGNITO_USER_POOL_ID || ''; | ||
| const COGNITO_CLIENT_ID = process.env.COGNITO_CLIENT_ID || ''; | ||
|
|
||
| let verifier: any = null; | ||
| function getVerifier() { | ||
| if (!verifier) { | ||
| if (!COGNITO_USER_POOL_ID) { | ||
| throw new Error('COGNITO_USER_POOL_ID environment variable is not set'); | ||
| } | ||
| verifier = CognitoJwtVerifier.create({ | ||
| userPoolId: COGNITO_USER_POOL_ID, | ||
| tokenUse: 'access', | ||
| clientId: COGNITO_CLIENT_ID, | ||
| }); | ||
| } | ||
| return verifier; | ||
| } | ||
|
|
||
| export interface AuthenticatedUser { | ||
| cognitoSub: string; | ||
| userId?: number; | ||
| email?: string; | ||
| isAdmin: boolean; | ||
| cognitoGroups?: string[]; | ||
| } | ||
|
|
||
| export interface AuthContext { | ||
| user?: AuthenticatedUser; | ||
| isAuthenticated: boolean; | ||
| } | ||
|
|
||
| export interface AuthorizationResult { | ||
| allowed: boolean; | ||
| reason?: string; | ||
| } | ||
|
|
||
| function extractToken(event: any): string | null { | ||
| const authHeader = event.headers?.Authorization || event.headers?.authorization; | ||
| if (!authHeader) return null; | ||
| const parts = authHeader.split(' '); | ||
| if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') { | ||
| return parts[1]; | ||
| } | ||
| return authHeader; | ||
| } | ||
|
|
||
| export async function authenticateRequest(event: any): Promise<AuthContext> { | ||
| const token = extractToken(event); | ||
| if (!token) return { isAuthenticated: false }; | ||
|
|
||
| try { | ||
| const payload = await getVerifier().verify(token); | ||
| const dbUser = await db | ||
| .selectFrom('branch.users') | ||
| .where('cognito_sub', '=', payload.sub) | ||
| .selectAll() | ||
| .executeTakeFirst(); | ||
|
|
||
| if (!dbUser) { | ||
| console.warn('User authenticated with Cognito but not found in database:', payload.sub); | ||
| return { isAuthenticated: false }; | ||
| } | ||
|
|
||
| const user: AuthenticatedUser = { | ||
| cognitoSub: payload.sub, | ||
| userId: dbUser.user_id, | ||
| email: payload.email as string | undefined, | ||
| isAdmin: dbUser.is_admin === true, | ||
| cognitoGroups: payload['cognito:groups'] as string[] | undefined, | ||
| }; | ||
|
|
||
| if (user.cognitoGroups?.includes('Admins')) { | ||
| user.isAdmin = true; | ||
| } | ||
|
|
||
| return { user, isAuthenticated: true }; | ||
| } catch (error) { | ||
| console.error('Token verification failed:', error); | ||
| return { isAuthenticated: false }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Authorization levels: | ||
| * AUTHENTICATED — any logged-in user | ||
| * PROJECT_MEMBER — admin, or any role on the project | ||
| * ADMIN_OR_PI — admin, or PI role on the project | ||
| * ADMIN — global admin only | ||
| */ | ||
| export async function checkAuthorization( | ||
| authContext: AuthContext, | ||
| level: 'AUTHENTICATED' | 'PROJECT_MEMBER' | 'ADMIN_OR_PI' | 'ADMIN', | ||
| projectId?: string | ||
| ): Promise<AuthorizationResult> { | ||
| if (!authContext.isAuthenticated || !authContext.user) { | ||
| return { allowed: false, reason: 'Authentication required' }; | ||
| } | ||
|
|
||
| const { user } = authContext; | ||
|
|
||
| if (level === 'AUTHENTICATED') { | ||
| return { allowed: true }; | ||
| } | ||
|
|
||
| if (level === 'ADMIN') { | ||
| return user.isAdmin | ||
| ? { allowed: true } | ||
| : { allowed: false, reason: 'Admin access required' }; | ||
| } | ||
|
|
||
| // PROJECT_MEMBER and ADMIN_OR_PI both need a projectId | ||
| if (!projectId) { | ||
| return { allowed: false, reason: 'Project ID required for authorization' }; | ||
| } | ||
|
|
||
| if (user.isAdmin) return { allowed: true }; | ||
|
|
||
| const membership = await db | ||
| .selectFrom('branch.project_memberships') | ||
| .where('project_id', '=', Number(projectId)) | ||
| .where('user_id', '=', user.userId!) | ||
| .select('role') | ||
| .executeTakeFirst(); | ||
|
|
||
| if (level === 'PROJECT_MEMBER') { | ||
| return membership | ||
| ? { allowed: true } | ||
| : { allowed: false, reason: 'Project membership required' }; | ||
| } | ||
|
|
||
| // ADMIN_OR_PI | ||
| if (membership && ['PI', 'Admin'].includes(membership.role)) { | ||
| return { allowed: true }; | ||
| } | ||
| return { allowed: false, reason: 'PI or Admin role required' }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; | ||
| import db from './db'; | ||
| import { ProjectValidationUtils } from './validation-utils'; | ||
| import { authenticateRequest } from './auth'; | ||
|
|
||
|
|
||
| export const handler = async (event: any): Promise<APIGatewayProxyResult> => { | ||
|
|
@@ -17,6 +18,11 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => { | |
| return json(200, { ok: true, timestamp: new Date().toISOString() }); | ||
| } | ||
|
|
||
| const authContext = await authenticateRequest(event); | ||
| if (!authContext.isAuthenticated || !authContext.user) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the right idea, but we actually want to do this in a more fine grained way. Also, looks like CI is failing because aws-jwt-verify is not installed, should just be able to npm install this |
||
| return json(401, { message: 'Authentication required' }); | ||
| } | ||
|
|
||
| // >>> ROUTES-START (do not remove this marker) | ||
| // CLI-generated routes will be inserted here | ||
|
|
||
|
|
@@ -110,4 +116,4 @@ function json(statusCode: number, body: unknown): APIGatewayProxyResult { | |
| }, | ||
| body: JSON.stringify(body) | ||
| }; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait omg never mind idk why i thought it was sql. ignore this comment