-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/auth #3
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/auth #3
Changes from all commits
91fa77f
1d3ec10
a0ee9b3
9d50392
31b5591
5f404f0
a2f1683
b5dfd8e
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,95 @@ | ||
version: 2.1 | ||
defaults: &defaults | ||
docker: | ||
- image: cimg/python:3.13.5-browsers | ||
install_dependency: &install_dependency | ||
name: Installation of build and deployment dependencies. | ||
command: | | ||
pip3 install awscli --upgrade | ||
install_deploysuite: &install_deploysuite | ||
name: Installation of install_deploysuite. | ||
command: | | ||
git clone --branch v1.4.17 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript | ||
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 |
||
cp ./../buildscript/master_deploy.sh . | ||
cp ./../buildscript/buildenv.sh . | ||
cp ./../buildscript/awsconfiguration.sh . | ||
cp ./../buildscript/psvar-processor.sh . | ||
|
||
restore_cache_settings_for_build: &restore_cache_settings_for_build | ||
key: docker-node-modules-{{ checksum "pnpm-lock.yaml" }} | ||
|
||
save_cache_settings: &save_cache_settings | ||
key: docker-node-modules-{{ checksum "pnpm-lock.yaml" }} | ||
paths: | ||
- node_modules | ||
|
||
|
||
builddeploy_steps: &builddeploy_steps | ||
- checkout | ||
- setup_remote_docker | ||
- run: *install_dependency | ||
- run: *install_deploysuite | ||
- restore_cache: *restore_cache_settings_for_build | ||
- run: | ||
name: "Build docker image" | ||
command: | | ||
./build.sh | ||
- save_cache: *save_cache_settings | ||
- deploy: | ||
name: Running MasterScript. | ||
command: | | ||
./awsconfiguration.sh $DEPLOY_ENV | ||
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 |
||
source awsenvconf | ||
./psvar-processor.sh -t appenv -p /config/${APPNAME}/deployvar | ||
source deployvar_env | ||
./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -j /config/${APPNAME}/appvar -i ${APPNAME} -p FARGATE | ||
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 |
||
jobs: | ||
# Build & Deploy against development backend | ||
"build-dev": | ||
!!merge <<: *defaults | ||
environment: | ||
DEPLOY_ENV: "DEV" | ||
LOGICAL_ENV: "dev" | ||
APPNAME: "tc-mcp" | ||
steps: *builddeploy_steps | ||
|
||
"build-qa": | ||
!!merge <<: *defaults | ||
environment: | ||
DEPLOY_ENV: "QA" | ||
LOGICAL_ENV: "qa" | ||
APPNAME: "tc-mcp" | ||
steps: *builddeploy_steps | ||
|
||
"build-prod": | ||
!!merge <<: *defaults | ||
environment: | ||
DEPLOY_ENV: "PROD" | ||
LOGICAL_ENV: "prod" | ||
APPNAME: "tc-mcp" | ||
steps: *builddeploy_steps | ||
|
||
workflows: | ||
version: 2 | ||
build: | ||
jobs: | ||
# Development builds are executed on "develop" branch only. | ||
- "build-dev": | ||
context: org-global | ||
filters: | ||
branches: | ||
only: | ||
- dev | ||
|
||
- "build-qa": | ||
context: org-global | ||
filters: | ||
branches: | ||
only: | ||
- qa | ||
|
||
- "build-prod": | ||
context: org-global | ||
filters: | ||
branches: | ||
only: master |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
#!/bin/bash | ||
set -eo pipefail | ||
docker buildx build --no-cache=true -t ${APPNAME}}:latest . | ||
docker buildx build --no-cache=true -t ${APPNAME}:latest . | ||
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 |
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 |
---|---|---|
|
@@ -19,7 +19,7 @@ export class HealthCheckController { | |
|
||
@Public() | ||
@Version([VERSION_NEUTRAL, '1']) | ||
@Get('/healthcheck') | ||
@Get('/health') | ||
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 |
||
healthCheck(): Promise<GetHealthCheckResponseDto> { | ||
const response = new GetHealthCheckResponseDto(); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,11 @@ | ||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; | ||
import { McpModule } from '@rekog/mcp-nest'; | ||
import { McpModule } from '@tc/mcp-nest'; | ||
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 |
||
import { QueryChallengesTool } from './mcp/tools/challenges/queryChallenges.tool'; | ||
import { randomUUID } from 'crypto'; | ||
import { GlobalProvidersModule } from './shared/global/globalProviders.module'; | ||
import { TopcoderModule } from './shared/topcoder/topcoder.module'; | ||
import { HealthCheckController } from './api/health-check/healthCheck.controller'; | ||
import { TokenValidatorMiddleware } from './core/auth/middleware/tokenValidator.middleware'; | ||
import { CreateRequestStoreMiddleware } from './core/request/createRequestStore.middleware'; | ||
import { AuthGuard, RolesGuard } from './core/auth/guards'; | ||
import { APP_GUARD } from '@nestjs/core'; | ||
import { nanoid } from 'nanoid'; | ||
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 |
||
|
||
@Module({ | ||
imports: [ | ||
|
@@ -17,30 +14,18 @@ import { APP_GUARD } from '@nestjs/core'; | |
version: '1.0.0', | ||
streamableHttp: { | ||
enableJsonResponse: false, | ||
sessionIdGenerator: () => randomUUID(), | ||
sessionIdGenerator: () => nanoid(), | ||
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 |
||
statelessMode: false, | ||
}, | ||
// guards: [AuthGuard, RolesGuard], | ||
}), | ||
GlobalProvidersModule, | ||
TopcoderModule, | ||
], | ||
controllers: [HealthCheckController], | ||
providers: [ | ||
// { | ||
// provide: APP_GUARD, | ||
// useClass: AuthGuard, | ||
// }, | ||
// { | ||
// provide: APP_GUARD, | ||
// useClass: RolesGuard, | ||
// }, | ||
QueryChallengesTool, | ||
], | ||
providers: [QueryChallengesTool], | ||
}) | ||
export class AppModule implements NestModule { | ||
configure(consumer: MiddlewareConsumer) { | ||
// consumer.apply(TokenValidatorMiddleware).forRoutes('*'); | ||
// consumer.apply(CreateRequestStoreMiddleware).forRoutes('*'); | ||
consumer.apply(TokenValidatorMiddleware).forRoutes('*'); | ||
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 |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,4 +16,8 @@ export class ConfigEnv { | |
|
||
@IsString() | ||
AUTH0_CLIENT_ID!: string; | ||
|
||
@IsString() | ||
@IsOptional() | ||
API_BASE = '/v6/mcp'; | ||
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 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
export enum Role { | ||
Admin = 'administrator', | ||
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 |
||
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 ?? ''))) { | ||
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 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; | ||
}; |
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. low |
||
|
||
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); | ||
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'; | ||
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 |
||
export * from './role.guard'; | ||
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 |
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. high |
||
|
||
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; | ||
}; |
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.
high
correctness
The Docker image
cimg/python:3.13.5-browsers
does not exist. Consider using a valid version or checking for typos in the version number.