diff --git a/src/api/winnings/winnings.controller.ts b/src/api/winnings/winnings.controller.ts index 81a64ff..d1fe3ec 100644 --- a/src/api/winnings/winnings.controller.ts +++ b/src/api/winnings/winnings.controller.ts @@ -7,8 +7,8 @@ import { ApiBearerAuth, } from '@nestjs/swagger'; -import { M2mScope } from 'src/core/auth/auth.constants'; -import { M2M, AllowedM2mScope, User } from 'src/core/auth/decorators'; +import { M2mScope, Role } from 'src/core/auth/auth.constants'; +import { AllowedM2mScope, User, Roles, M2M } from 'src/core/auth/decorators'; import { ResponseDto, ResponseStatusType } from 'src/dto/api-response.dto'; import { UserInfo } from 'src/dto/user.type'; import { @@ -29,8 +29,8 @@ export class WinningsController { ) {} @Post() - @M2M() @AllowedM2mScope(M2mScope.CreatePayments) + @Roles(Role.PaymentAdmin, Role.PaymentEditor) @ApiOperation({ summary: 'Create winning with payments.', description: 'User must have "create:payments" scope to access.', diff --git a/src/core/auth/guards/auth.guard.ts b/src/core/auth/guards/auth.guard.ts index 921ae5b..b96245e 100644 --- a/src/core/auth/guards/auth.guard.ts +++ b/src/core/auth/guards/auth.guard.ts @@ -23,7 +23,7 @@ export class AuthGuard implements CanActivate { if (isPublic) return true; const req = context.switchToHttp().getRequest(); - const isM2M = this.reflector.getAllAndOverride(IS_M2M_KEY, [ + const routeM2MOnly = this.reflector.getAllAndOverride(IS_M2M_KEY, [ context.getHandler(), context.getClass(), ]); @@ -36,24 +36,45 @@ export class AuthGuard implements CanActivate { }; } - // Regular authentication - check that we have user's email and have verified the id token - if (!isM2M) { + const tokenIsM2M = Boolean(req.m2mTokenScope); + + // If route explicitly requires M2M, enforce M2M + scope + if (routeM2MOnly) { + if (!req.idTokenVerified || !tokenIsM2M) { + throw new UnauthorizedException(); + } + + const allowedM2mScopes = this.reflector.getAllAndOverride( + SCOPES_KEY, + [context.getHandler(), context.getClass()], + ); + const reqScopes = String(req.m2mTokenScope || '').split(' '); + return reqScopes.some((s) => allowedM2mScopes.includes(s as M2mScope)); + } + + // Hybrid (default) route behavior: allow either + // - Verified user JWT (email present), OR + // - Verified M2M token but only if scope matches when scopes are declared on the route + + // User JWT branch + if (!tokenIsM2M) { return Boolean(req.email && req.idTokenVerified); } - // M2M authentication - check scopes - if (!req.idTokenVerified || !req.m2mTokenScope) + // M2M branch on non-M2M-only route: require declared scopes, otherwise deny + if (!req.idTokenVerified) { throw new UnauthorizedException(); + } const allowedM2mScopes = this.reflector.getAllAndOverride( SCOPES_KEY, [context.getHandler(), context.getClass()], ); - - const reqScopes = req.m2mTokenScope.split(' '); - if (reqScopes.some((reqScope) => allowedM2mScopes.includes(reqScope))) { - return true; + if (!allowedM2mScopes || allowedM2mScopes.length === 0) { + // No scopes declared for this route, do not allow M2M by default + return false; } - return false; + const reqScopes = String(req.m2mTokenScope || '').split(' '); + return reqScopes.some((s) => allowedM2mScopes.includes(s as M2mScope)); } } diff --git a/src/core/auth/guards/roles.guard.ts b/src/core/auth/guards/roles.guard.ts index 8145cce..9f6126e 100644 --- a/src/core/auth/guards/roles.guard.ts +++ b/src/core/auth/guards/roles.guard.ts @@ -17,7 +17,12 @@ export class RolesGuard implements CanActivate { } const request = context.switchToHttp().getRequest(); - const { auth0User } = request; + const tokenIsM2M = Boolean(request.m2mTokenScope); + if (tokenIsM2M) { + return Boolean(request.idTokenVerified); + } + + const { auth0User = {} } = request; const userRoles = Object.keys(auth0User).reduce((roles, key) => { if (key.match(/claims\/roles$/gi)) { return auth0User[key] as string[]; diff --git a/src/shared/topcoder/topcoder-m2m.service.ts b/src/shared/topcoder/topcoder-m2m.service.ts index 06cc33a..cd6fcc6 100644 --- a/src/shared/topcoder/topcoder-m2m.service.ts +++ b/src/shared/topcoder/topcoder-m2m.service.ts @@ -36,6 +36,24 @@ export class TopcoderM2MService { }), }); + if (!response.ok) { + let jsonError: any; + try { + jsonError = await response.json(); + } catch { + jsonError = null; + } + + this.logger.error( + 'Failed to fetch M2M token', + tokenURL, + response.status, + response.statusText, + jsonError, + ); + return undefined; + } + const jsonResponse = await response.json(); const m2mToken = jsonResponse.access_token as string;