diff --git a/package-lock.json b/package-lock.json index 3cbc723..1573180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "class-validator": "^0.14.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "passport-openidconnect": "^0.1.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -36,6 +37,7 @@ "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "@types/passport-jwt": "^4.0.1", + "@types/passport-openidconnect": "^0.1.3", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -3354,6 +3356,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/passport": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", @@ -3373,6 +3385,19 @@ "@types/passport-strategy": "*" } }, + "node_modules/@types/passport-openidconnect": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@types/passport-openidconnect/-/passport-openidconnect-0.1.3.tgz", + "integrity": "sha512-k1Ni7bG/9OZNo2Qpjg2W6GajL+pww6ZPaNWMXfpteCX4dXf4QgaZLt2hjR5IiPrqwBT9+W8KjCTJ/uhGIoBx/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, "node_modules/@types/passport-strategy": { "version": "0.2.38", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", @@ -8689,6 +8714,12 @@ "node": ">=8" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8930,6 +8961,23 @@ "passport-strategy": "^1.0.0" } }, + "node_modules/passport-openidconnect": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz", + "integrity": "sha512-JX3rTyW+KFZ/E9OF/IpXJPbyLO9vGzcmXB5FgSP2jfL3LGKJPdV7zUE8rWeKeeI/iueQggOeFa3onrCmhxXZTg==", + "license": "MIT", + "dependencies": { + "oauth": "0.10.x", + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", diff --git a/package.json b/package.json index 2da877b..4cc3728 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "class-validator": "^0.14.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "passport-openidconnect": "^0.1.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -47,6 +48,7 @@ "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "@types/passport-jwt": "^4.0.1", + "@types/passport-openidconnect": "^0.1.3", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts index 082f5e7..7217bf6 100644 --- a/src/common/guards/roles.guard.ts +++ b/src/common/guards/roles.guard.ts @@ -9,6 +9,8 @@ import { ROLES_KEY } from '../decorators/roles.decorators'; import { Roles } from '.prisma/client'; import { JwtPayload } from 'src/modules/authentication/strategies/jwt.strategy'; + + interface RequestWithUser extends Request { user: JwtPayload; } @@ -17,6 +19,23 @@ interface RequestWithUser extends Request { export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} + /** + * @swagger + * @function canActivate + * @description + * Authorization guard that checks whether the current user has one of the required roles + * to access a specific route handler. It uses metadata defined by the `@Roles()` decorator + * and verifies the user's role accordingly. + * + * If no roles are specified for the route, access is granted by default. + * If the user is not authenticated or does not have a required role, access is denied. + * + * @param {ExecutionContext} ctx - The current execution context, which provides access + * to route metadata and the HTTP request. + * + * @returns {boolean} - Returns `true` if access is allowed, otherwise throws a `ForbiddenException` + * or returns `false` if the user is not authenticated. + */ canActivate(ctx: ExecutionContext): boolean { const requiredRoles = this.reflector.get( ROLES_KEY, diff --git a/src/modules/authentication/index.ts b/src/modules/authentication/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/authentication/services/jwt-auth.service.ts b/src/modules/authentication/services/jwt-auth.service.ts new file mode 100644 index 0000000..20bc90f --- /dev/null +++ b/src/modules/authentication/services/jwt-auth.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { UUID } from 'crypto'; +import { UsersService } from 'src/modules/users-management/services/users.service'; +import { JwtService } from '@nestjs/jwt'; + +@Injectable() +export class AuthService { + constructor( + private usersService: UsersService, + private jwtService: JwtService + ) {} + + async validateUser(user_id: UUID): Promise { + const user = await this.usersService.findOne(user_id); + if (user) { + return user; + } + return null; + } +} diff --git a/src/modules/authentication/strategies/authentik.strategy.ts b/src/modules/authentication/strategies/authentik.strategy.ts new file mode 100644 index 0000000..5d31abe --- /dev/null +++ b/src/modules/authentication/strategies/authentik.strategy.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-openidconnect'; + +export interface AuthentikPayload { + //TODO: check Authentik payload contents +} + +@Injectable() +export class AuthentikStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + issuer: process.env.AUTHENTIK_ISSUER || "ISSUER MISSING", + clientID: process.env.AUTHENTIK_CLIENT_ID || "CLIENT ID MISSING", + clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || "CLIENT SECRET MISSING", + callbackURL: process.env.AUTHENTIK_CALLBACK_URL || "CALLBACK URL MISSING", + authorizationURL: `${process.env.AUTHENTIK_ISSUER}/authorize/`, + tokenURL: `${process.env.AUTHENTIK_ISSUER}/token/`, + userInfoURL: `${process.env.AUTHENTIK_ISSUER}/userinfo/`, + scope: [], + }) + } + + async validate(): Promise { + // Check what kind of payload we're actually handling here + return null; + } +} \ No newline at end of file