Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/api/winnings/winnings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.',
Expand Down
41 changes: 31 additions & 10 deletions src/core/auth/guards/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class AuthGuard implements CanActivate {
if (isPublic) return true;

const req = context.switchToHttp().getRequest();
const isM2M = this.reflector.getAllAndOverride<boolean>(IS_M2M_KEY, [
const routeM2MOnly = this.reflector.getAllAndOverride<boolean>(IS_M2M_KEY, [
context.getHandler(),
context.getClass(),
]);
Expand All @@ -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<M2mScope[]>(
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<M2mScope[]>(
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));
}
}
7 changes: 6 additions & 1 deletion src/core/auth/guards/roles.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
18 changes: 18 additions & 0 deletions src/shared/topcoder/topcoder-m2m.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down