Skip to content

Commit cd5f685

Browse files
authored
Merge pull request #4 from topcoder-platform/feat/auth
PM-1437 Feat/auth
2 parents a0ee9b3 + b5dfd8e commit cd5f685

17 files changed

+207
-225
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"@nestjs/common": "^11.0.1",
2525
"@nestjs/core": "^11.0.1",
2626
"@nestjs/platform-express": "^11.0.1",
27-
"@rekog/mcp-nest": "^1.6.2",
2827
"class-transformer": "^0.5.1",
2928
"class-validator": "^0.14.2",
3029
"dotenv": "^16.5.0",
@@ -34,6 +33,7 @@
3433
"nanoid": "^5.1.5",
3534
"reflect-metadata": "^0.2.2",
3635
"rxjs": "^7.8.1",
36+
"@tc/mcp-nest": "topcoder-platform/MCP-Nest.git",
3737
"zod": "^3.25.67"
3838
},
3939
"devDependencies": {

pnpm-lock.yaml

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.module.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
2-
import { McpModule } from '@rekog/mcp-nest';
2+
import { McpModule } from '@tc/mcp-nest';
33
import { QueryChallengesTool } from './mcp/tools/challenges/queryChallenges.tool';
4-
import { randomUUID } from 'crypto';
54
import { GlobalProvidersModule } from './shared/global/globalProviders.module';
65
import { TopcoderModule } from './shared/topcoder/topcoder.module';
76
import { HealthCheckController } from './api/health-check/healthCheck.controller';
87
import { TokenValidatorMiddleware } from './core/auth/middleware/tokenValidator.middleware';
9-
import { CreateRequestStoreMiddleware } from './core/request/createRequestStore.middleware';
10-
import { AuthGuard, RolesGuard } from './core/auth/guards';
11-
import { APP_GUARD } from '@nestjs/core';
8+
import { nanoid } from 'nanoid';
129

1310
@Module({
1411
imports: [
@@ -17,30 +14,18 @@ import { APP_GUARD } from '@nestjs/core';
1714
version: '1.0.0',
1815
streamableHttp: {
1916
enableJsonResponse: false,
20-
sessionIdGenerator: () => randomUUID(),
17+
sessionIdGenerator: () => nanoid(),
2118
statelessMode: false,
2219
},
23-
// guards: [AuthGuard, RolesGuard],
2420
}),
2521
GlobalProvidersModule,
2622
TopcoderModule,
2723
],
2824
controllers: [HealthCheckController],
29-
providers: [
30-
// {
31-
// provide: APP_GUARD,
32-
// useClass: AuthGuard,
33-
// },
34-
// {
35-
// provide: APP_GUARD,
36-
// useClass: RolesGuard,
37-
// },
38-
QueryChallengesTool,
39-
],
25+
providers: [QueryChallengesTool],
4026
})
4127
export class AppModule implements NestModule {
4228
configure(consumer: MiddlewareConsumer) {
43-
// consumer.apply(TokenValidatorMiddleware).forRoutes('*');
44-
// consumer.apply(CreateRequestStoreMiddleware).forRoutes('*');
29+
consumer.apply(TokenValidatorMiddleware).forRoutes('*');
4530
}
4631
}

src/core/auth/auth.constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export enum Role {
2+
Admin = 'administrator',
23
User = 'Topcoder User',
34
}
45

5-
export enum M2mScope {}
6+
export enum M2mScope {
7+
QueryPublicChallenges = 'query:public:challenges',
8+
}

src/core/auth/decorators/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from './m2m.decorator';
22
export * from './m2mScope.decorator';
33
export * from './public.decorator';
4-
export * from './roles.decorator';
54
export * from './user.decorator';

src/core/auth/decorators/roles.decorator.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/core/auth/guards/auth.guard.ts

Lines changed: 14 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,16 @@
1-
import {
2-
CanActivate,
3-
ExecutionContext,
4-
Injectable,
5-
UnauthorizedException,
6-
} from '@nestjs/common';
7-
import { Reflector } from '@nestjs/core';
8-
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
9-
import { IS_M2M_KEY } from '../decorators/m2m.decorator';
10-
import { M2mScope } from '../auth.constants';
11-
import { SCOPES_KEY } from '../decorators/m2mScope.decorator';
12-
13-
@Injectable()
14-
export class AuthGuard implements CanActivate {
15-
constructor(private reflector: Reflector) {}
16-
17-
canActivate(context: ExecutionContext): boolean {
18-
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
19-
context.getHandler(),
20-
context.getClass(),
21-
]);
22-
23-
if (isPublic) return true;
24-
25-
const req = context.switchToHttp().getRequest();
26-
const isM2M = this.reflector.getAllAndOverride<boolean>(IS_M2M_KEY, [
27-
context.getHandler(),
28-
context.getClass(),
29-
]);
30-
31-
const { m2mUserId } = req;
32-
if (m2mUserId) {
33-
req.user = {
34-
id: m2mUserId,
35-
handle: '',
36-
};
37-
}
38-
39-
// Regular authentication - check that we have user's email and have verified the id token
40-
if (!isM2M) {
41-
return Boolean(req.email && req.idTokenVerified);
42-
}
43-
44-
// M2M authentication - check scopes
45-
if (!req.idTokenVerified || !req.m2mTokenScope)
46-
throw new UnauthorizedException();
47-
48-
const allowedM2mScopes = this.reflector.getAllAndOverride<M2mScope[]>(
49-
SCOPES_KEY,
50-
[context.getHandler(), context.getClass()],
51-
);
52-
53-
const reqScopes = req.m2mTokenScope.split(' ');
54-
if (reqScopes.some((reqScope) => allowedM2mScopes.includes(reqScope))) {
55-
return true;
56-
}
1+
import { Request } from 'express';
2+
import { decodeAuthToken } from './guards.utils';
3+
4+
/**
5+
* Auth guard function to validate the authorization token from the request headers.
6+
*
7+
* @param req - The incoming HTTP request object.
8+
* @returns A promise that resolves to `true` if the authorization token is valid, otherwise `false`.
9+
*/
10+
export const authGuard = async (req: Request) => {
11+
if (!(await decodeAuthToken(req.headers.authorization ?? ''))) {
5712
return false;
5813
}
59-
}
14+
15+
return true;
16+
};

src/core/auth/guards/guards.utils.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as jwt from 'jsonwebtoken';
2+
import { Logger } from 'src/shared/global';
3+
import { getSigningKey } from '../jwt';
4+
5+
const logger = new Logger('guards.utils()');
6+
7+
/**
8+
* Decodes and verifies a JWT token from the provided authorization header.
9+
*
10+
* @param authHeader - The authorization header containing the token, expected in the format "Bearer <token>".
11+
* @returns A promise that resolves to the decoded JWT payload if the token is valid,
12+
* a string if the payload is a string, or `false` if the token is invalid or the header is improperly formatted.
13+
*
14+
* @throws This function does not throw directly but will return `false` if an error occurs during verification.
15+
*/
16+
export const decodeAuthToken = async (
17+
authHeader: string,
18+
): Promise<boolean | jwt.JwtPayload | string> => {
19+
const [type, idToken] = authHeader?.split(' ') ?? [];
20+
21+
if (type !== 'Bearer' || !idToken) {
22+
return false;
23+
}
24+
25+
let decoded: jwt.JwtPayload | string;
26+
try {
27+
const signingKey = await getSigningKey(idToken);
28+
decoded = jwt.verify(idToken, signingKey);
29+
} catch (error) {
30+
logger.error('Error verifying JWT', error);
31+
return false;
32+
}
33+
34+
return decoded;
35+
};

src/core/auth/guards/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './auth.guard';
2-
export * from './roles.guard';
2+
export * from './m2m-scope.guard';
3+
export * from './role.guard';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Request } from 'express';
2+
import { decodeAuthToken } from './guards.utils';
3+
import { JwtPayload } from 'jsonwebtoken';
4+
import { M2mScope } from '../auth.constants';
5+
6+
/**
7+
* A utility function to check if the required M2M (Machine-to-Machine) scopes are present
8+
* in the authorization token provided in the request headers.
9+
*
10+
* @param {...M2mScope[]} requiredM2mScopes - The list of required M2M scopes to validate against.
11+
* @returns {Promise<(req: Request) => boolean>} A function that takes an Express `Request` object
12+
* and returns a boolean indicating whether the required scopes are present.
13+
*
14+
* The function decodes the authorization token from the request headers and checks if
15+
* the required scopes are included in the token's scope claim.
16+
*/
17+
export const checkM2MScope =
18+
(...requiredM2mScopes: M2mScope[]) =>
19+
async (req: Request) => {
20+
const decodedAuth = await decodeAuthToken(req.headers.authorization ?? '');
21+
22+
const authorizedScopes = ((decodedAuth as JwtPayload).scope ?? '').split(
23+
' ',
24+
);
25+
if (!requiredM2mScopes.some((scope) => authorizedScopes.includes(scope))) {
26+
return false;
27+
}
28+
29+
return true;
30+
};

0 commit comments

Comments
 (0)