-
Notifications
You must be signed in to change notification settings - Fork 1
PM-1437 Feat/auth #4
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
export enum Role { | ||
Admin = 'administrator', | ||
kkartunov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
User = 'Topcoder User', | ||
} | ||
|
||
export enum M2mScope {} | ||
export enum M2mScope { | ||
QueryPublicChallenges = 'query:public:challenges', | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
export * from './m2m.decorator'; | ||
export * from './m2mScope.decorator'; | ||
export * from './public.decorator'; | ||
export * from './roles.decorator'; | ||
export * from './user.decorator'; |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,59 +1,16 @@ | ||
import { | ||
CanActivate, | ||
ExecutionContext, | ||
Injectable, | ||
UnauthorizedException, | ||
} from '@nestjs/common'; | ||
import { Reflector } from '@nestjs/core'; | ||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; | ||
import { IS_M2M_KEY } from '../decorators/m2m.decorator'; | ||
import { M2mScope } from '../auth.constants'; | ||
import { SCOPES_KEY } from '../decorators/m2mScope.decorator'; | ||
|
||
@Injectable() | ||
export class AuthGuard implements CanActivate { | ||
constructor(private reflector: Reflector) {} | ||
|
||
canActivate(context: ExecutionContext): boolean { | ||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ | ||
context.getHandler(), | ||
context.getClass(), | ||
]); | ||
|
||
if (isPublic) return true; | ||
|
||
const req = context.switchToHttp().getRequest(); | ||
const isM2M = this.reflector.getAllAndOverride<boolean>(IS_M2M_KEY, [ | ||
context.getHandler(), | ||
context.getClass(), | ||
]); | ||
|
||
const { m2mUserId } = req; | ||
if (m2mUserId) { | ||
req.user = { | ||
id: m2mUserId, | ||
handle: '', | ||
}; | ||
} | ||
|
||
// Regular authentication - check that we have user's email and have verified the id token | ||
if (!isM2M) { | ||
return Boolean(req.email && req.idTokenVerified); | ||
} | ||
|
||
// M2M authentication - check scopes | ||
if (!req.idTokenVerified || !req.m2mTokenScope) | ||
throw new UnauthorizedException(); | ||
|
||
const allowedM2mScopes = this.reflector.getAllAndOverride<M2mScope[]>( | ||
SCOPES_KEY, | ||
[context.getHandler(), context.getClass()], | ||
); | ||
|
||
const reqScopes = req.m2mTokenScope.split(' '); | ||
if (reqScopes.some((reqScope) => allowedM2mScopes.includes(reqScope))) { | ||
return true; | ||
} | ||
import { Request } from 'express'; | ||
import { decodeAuthToken } from './guards.utils'; | ||
|
||
/** | ||
* Auth guard function to validate the authorization token from the request headers. | ||
* | ||
* @param req - The incoming HTTP request object. | ||
* @returns A promise that resolves to `true` if the authorization token is valid, otherwise `false`. | ||
*/ | ||
export const authGuard = async (req: Request) => { | ||
if (!(await decodeAuthToken(req.headers.authorization ?? ''))) { | ||
kkartunov marked this conversation as resolved.
Show resolved
Hide resolved
kkartunov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as jwt from 'jsonwebtoken'; | ||
import { Logger } from 'src/shared/global'; | ||
import { getSigningKey } from '../jwt'; | ||
|
||
const logger = new Logger('guards.utils()'); | ||
|
||
/** | ||
* Decodes and verifies a JWT token from the provided authorization header. | ||
* | ||
* @param authHeader - The authorization header containing the token, expected in the format "Bearer <token>". | ||
* @returns A promise that resolves to the decoded JWT payload if the token is valid, | ||
* a string if the payload is a string, or `false` if the token is invalid or the header is improperly formatted. | ||
* | ||
* @throws This function does not throw directly but will return `false` if an error occurs during verification. | ||
*/ | ||
export const decodeAuthToken = async ( | ||
authHeader: string, | ||
): Promise<boolean | jwt.JwtPayload | string> => { | ||
const [type, idToken] = authHeader?.split(' ') ?? []; | ||
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. high |
||
|
||
if (type !== 'Bearer' || !idToken) { | ||
return false; | ||
} | ||
|
||
let decoded: jwt.JwtPayload | string; | ||
try { | ||
const signingKey = await getSigningKey(idToken); | ||
decoded = jwt.verify(idToken, signingKey); | ||
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. medium |
||
} catch (error) { | ||
logger.error('Error verifying JWT', error); | ||
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. high |
||
return false; | ||
} | ||
|
||
return decoded; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './auth.guard'; | ||
export * from './roles.guard'; | ||
export * from './m2m-scope.guard'; | ||
export * from './role.guard'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { Request } from 'express'; | ||
import { decodeAuthToken } from './guards.utils'; | ||
import { JwtPayload } from 'jsonwebtoken'; | ||
import { M2mScope } from '../auth.constants'; | ||
|
||
/** | ||
* A utility function to check if the required M2M (Machine-to-Machine) scopes are present | ||
* in the authorization token provided in the request headers. | ||
* | ||
* @param {...M2mScope[]} requiredM2mScopes - The list of required M2M scopes to validate against. | ||
* @returns {Promise<(req: Request) => boolean>} A function that takes an Express `Request` object | ||
* and returns a boolean indicating whether the required scopes are present. | ||
* | ||
* The function decodes the authorization token from the request headers and checks if | ||
* the required scopes are included in the token's scope claim. | ||
*/ | ||
export const checkM2MScope = | ||
(...requiredM2mScopes: M2mScope[]) => | ||
async (req: Request) => { | ||
const decodedAuth = await decodeAuthToken(req.headers.authorization ?? ''); | ||
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. medium |
||
|
||
const authorizedScopes = ((decodedAuth as JwtPayload).scope ?? '').split( | ||
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. medium |
||
' ', | ||
); | ||
if (!requiredM2mScopes.some((scope) => authorizedScopes.includes(scope))) { | ||
return false; | ||
} | ||
|
||
return true; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { Request } from 'express'; | ||
import { decodeAuthToken } from './guards.utils'; | ||
import { Role } from '../auth.constants'; | ||
|
||
/** | ||
* A utility function to check if the required user role are present | ||
* in the authorization token provided in the request headers. | ||
* | ||
* @param {...Role[]} requiredUserRoles - The list of required user roles to validate against. | ||
* @returns {Promise<(req: Request) => boolean>} A function that takes an Express `Request` object | ||
* and returns a boolean indicating whether the required scopes are present. | ||
* | ||
* The function decodes the authorization token from the request headers and checks if | ||
* the required user roles are included in the token's scope claim. | ||
*/ | ||
export const checkHasUserRole = | ||
(...requiredUserRoles: Role[]) => | ||
async (req: Request) => { | ||
const decodedAuth = await decodeAuthToken(req.headers.authorization ?? ''); | ||
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. high |
||
|
||
const decodedUserRoles = Object.keys(decodedAuth).reduce((roles, key) => { | ||
if (key.match(/claims\/roles$/gi)) { | ||
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. medium |
||
return decodedAuth[key] as string[]; | ||
} | ||
|
||
return roles; | ||
}, []); | ||
|
||
if (!requiredUserRoles.some((role) => decodedUserRoles.includes(role))) { | ||
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. medium |
||
return false; | ||
} | ||
|
||
return true; | ||
}; |
This file was deleted.
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.