From 5e644b6a6ca6d1a79a8fe027b76eca5857f98884 Mon Sep 17 00:00:00 2001 From: Nicolas Drolet Date: Tue, 22 Jul 2025 16:41:13 -0400 Subject: [PATCH] refactor(auth): add auth controller, but almost certain that implementation is shaky at best. Will rework structure after further learning NestJS architecture --- package-lock.json | 29 ++++++--- src/modules/authentication/auth.module.ts | 4 ++ .../controllers/authentik-controller.ts | 19 ++++++ .../services/authentik-auth.service.ts | 14 +++++ .../services/jwt-auth.service.ts | 2 +- .../strategies/authentik.strategy.ts | 62 ++++++++++++------- .../services/abstract-user.service.ts | 10 ++- 7 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 src/modules/authentication/controllers/authentik-controller.ts create mode 100644 src/modules/authentication/services/authentik-auth.service.ts diff --git a/package-lock.json b/package-lock.json index 1573180..01ac1f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2438,13 +2438,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.3.tgz", - "integrity": "sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz", + "integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==", + "license": "MIT", "dependencies": { "cors": "2.8.5", "express": "5.1.0", - "multer": "2.0.1", + "multer": "2.0.2", "path-to-regexp": "8.2.0", "tslib": "2.8.1" }, @@ -4628,7 +4629,8 @@ "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" }, "node_modules/arch": { "version": "3.0.0", @@ -5428,6 +5430,7 @@ "engines": [ "node >= 6.0" ], + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -8557,6 +8560,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -8570,9 +8574,10 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", - "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", @@ -8590,6 +8595,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8598,6 +8604,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8606,6 +8613,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -8617,6 +8625,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -10817,7 +10826,8 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" }, "node_modules/typescript": { "version": "5.8.3", @@ -11355,6 +11365,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", "engines": { "node": ">=0.4" } diff --git a/src/modules/authentication/auth.module.ts b/src/modules/authentication/auth.module.ts index c48fb8a..bd01723 100644 --- a/src/modules/authentication/auth.module.ts +++ b/src/modules/authentication/auth.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; +import { AuthentikAuthService } from './services/authentik-auth.service'; @Module({ imports: [ @@ -10,5 +11,8 @@ import { PassportModule } from '@nestjs/passport'; signOptions: { expiresIn: '1h' }, }), ], + providers: [ AuthentikAuthService, ], + exports: [ AuthentikAuthService ], + }) export class AuthenticationModule {} diff --git a/src/modules/authentication/controllers/authentik-controller.ts b/src/modules/authentication/controllers/authentik-controller.ts new file mode 100644 index 0000000..13eb537 --- /dev/null +++ b/src/modules/authentication/controllers/authentik-controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get, Post, Req, UseGuards } from '@nestjs/common'; +import { Request } from 'express'; + +@Controller('auth') +export class AuthController { + @Post('/login') + login() { + // Passport handles redirection to Authentik + } + + @Get('callback') + // @UseGuards(AuthGuard('openidconnect')) + callback(@Req() req: Request) { + console.log('✅ Auth complete, here is req.user:'); + console.dir(req.user, { depth: null }); + + return req.user; + } +} diff --git a/src/modules/authentication/services/authentik-auth.service.ts b/src/modules/authentication/services/authentik-auth.service.ts new file mode 100644 index 0000000..9d6514c --- /dev/null +++ b/src/modules/authentication/services/authentik-auth.service.ts @@ -0,0 +1,14 @@ + +import { Injectable } from '@nestjs/common'; +import { UsersService } from 'src/modules/users-management/services/users.service'; + +@Injectable() +export class AuthentikAuthService { + constructor(private usersService: UsersService) {} + + async validateUser(user_email: string): Promise { + const user = await this.usersService.findOneByEmail(user_email); + + return user; + } +} diff --git a/src/modules/authentication/services/jwt-auth.service.ts b/src/modules/authentication/services/jwt-auth.service.ts index 20bc90f..a4c65e2 100644 --- a/src/modules/authentication/services/jwt-auth.service.ts +++ b/src/modules/authentication/services/jwt-auth.service.ts @@ -11,7 +11,7 @@ export class AuthService { ) {} async validateUser(user_id: UUID): Promise { - const user = await this.usersService.findOne(user_id); + const user = await this.usersService.findOne( user_id ); if (user) { return user; } diff --git a/src/modules/authentication/strategies/authentik.strategy.ts b/src/modules/authentication/strategies/authentik.strategy.ts index 5d31abe..a6bb526 100644 --- a/src/modules/authentication/strategies/authentik.strategy.ts +++ b/src/modules/authentication/strategies/authentik.strategy.ts @@ -1,28 +1,48 @@ -import { Injectable } from '@nestjs/common'; + +import { Strategy as OIDCStrategy } from 'passport-openidconnect'; import { PassportStrategy } from '@nestjs/passport'; -import { Strategy } from 'passport-openidconnect'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { AuthentikAuthService } from '../services/authentik-auth.service'; export interface AuthentikPayload { - //TODO: check Authentik payload contents + iss: string; // Issuer + sub: string; // Subject (user ID) + aud: string; // Audience (client ID) + exp: number; // Expiration time (Unix) + iat: number; // Issued at time (Unix) + auth_time: number; // Time of authentication (Unix) + acr?: string; // Auth Context Class Reference + amr?: string[]; // Auth Method References (e.g., ['pwd']) + email: string; + email_verified: boolean; + name?: string; + given_name?: string; + preferred_username?: string; + nickname?: string; + groups?: string[]; } @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: [], - }) - } +export class AuthentikStrategy extends PassportStrategy(OIDCStrategy) { + constructor(private authentikAuthService: AuthentikAuthService) { + 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: ['openid', 'email', 'profile'], + }); + } - async validate(): Promise { - // Check what kind of payload we're actually handling here - return null; - } -} \ No newline at end of file + async validate(payload: AuthentikPayload): Promise { + // if ( !payload.email_verified ) { throw new UnauthorizedException(); } + + const user = await this.authentikAuthService.validateUser(payload.email); + if ( !user ) { throw new UnauthorizedException(); } + + return user; + } +} diff --git a/src/modules/users-management/services/abstract-user.service.ts b/src/modules/users-management/services/abstract-user.service.ts index 896627d..51fd719 100644 --- a/src/modules/users-management/services/abstract-user.service.ts +++ b/src/modules/users-management/services/abstract-user.service.ts @@ -10,7 +10,7 @@ export abstract class AbstractUserService { return this.prisma.users.findMany(); } - async findOne(id: string): Promise { + async findOne( id: string ): Promise { const user = await this.prisma.users.findUnique({ where: { id } }); if (!user) { throw new NotFoundException(`User #${id} not found`); @@ -18,6 +18,14 @@ export abstract class AbstractUserService { return user; } + async findOneByEmail( email: string ): Promise { + const user = await this.prisma.users.findUnique({ where: { email } }); + if (!user) { + throw new NotFoundException(`No user with email #${email} exists`); + } + return user; + } + async remove(id: string): Promise { await this.findOne(id); return this.prisma.users.delete({ where: { id } });