From e3948805394bb74d3e02bfb04f5d67420cb06a51 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Fri, 2 Dec 2022 22:33:02 +0200 Subject: [PATCH] openid-configuration --- src/app.controller.ts | 36 +++++++++++++++++++ src/modules/jwt/jwt.providers.ts | 8 +++++ src/modules/jwt/jwt.service.ts | 10 ++++++ src/modules/oauth2/adapter/jwt.adapter.ts | 4 ++- src/modules/oauth2/oauth2.module.ts | 2 +- src/modules/oauth2/oauth2.service.ts | 5 ++- .../oauth2-client/oauth2-client.service.ts | 2 ++ .../oauth2-router/oauth2-router.controller.ts | 13 ++++++- 8 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/app.controller.ts b/src/app.controller.ts index 86b3312..f2899ba 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,10 +1,46 @@ import { Controller, Get, Redirect } from '@nestjs/common'; +import { ConfigurationService } from './modules/config/config.service'; @Controller() export class AppController { + constructor(private config: ConfigurationService) {} + @Get() @Redirect('/account/general') getHello() { return; } + + @Get('/.well-known/openid-configuration') + openidConfiguration() { + const base = this.config.get('app.base_url'); + return { + issuer: this.config.get('jwt.issuer'), + authorization_endpoint: `${base}/oauth2/authorize`, + token_endpoint: `${base}/oauth2/token`, + jwks_uri: `${base}/oauth2/jwks`, + userinfo_endpoint: `${base}/api/user`, + introspection_endpoint: `${base}/oauth2/introspect`, + response_types_supported: ['code', 'id_token'], + id_token_signing_alg_values_supported: [this.config.get('jwt.algorithm')], + subject_types_supported: ['public'], + scopes_supported: ['openid', 'profile', 'email'], + claims_supported: [ + 'aud', + 'exp', + 'iat', + 'iss', + 'sub', + 'name', + 'preferred_username', + 'profile', + 'picture', + 'updated_at', + 'email', + 'email_verified', + ], + code_challenge_methods_supported: ['plain', 'S256'], + grant_types_supported: ['authorization_code', 'refresh_token'], + }; + } } diff --git a/src/modules/jwt/jwt.providers.ts b/src/modules/jwt/jwt.providers.ts index dd7e3a6..e38cd7f 100644 --- a/src/modules/jwt/jwt.providers.ts +++ b/src/modules/jwt/jwt.providers.ts @@ -17,4 +17,12 @@ export const jwtProviders = [ useFactory: async (path: string) => readFile(join(path, 'jwt.public.pem')), inject: ['PRIVATE_PATH'], } as FactoryProvider, + { + provide: 'JWT_JWKS', + useFactory: async (path: string) => + JSON.parse( + await readFile(join(path, 'jwks.json'), { encoding: 'utf-8' }), + ), + inject: ['PRIVATE_PATH'], + } as FactoryProvider>, ]; diff --git a/src/modules/jwt/jwt.service.ts b/src/modules/jwt/jwt.service.ts index 943272d..ff10605 100644 --- a/src/modules/jwt/jwt.service.ts +++ b/src/modules/jwt/jwt.service.ts @@ -12,6 +12,7 @@ export class JWTService { constructor( @Inject('JWT_PRIVATE_KEY') private _privateKey: Buffer, @Inject('JWT_PUBLIC_KEY') private _publicKey: Buffer, + @Inject('JWT_JWKS') private _jwks: Record, private _config: ConfigurationService, ) {} @@ -41,4 +42,13 @@ export class JWTService { audience, }) as jwt.JwtPayload; } + + // TODO: generate on-the-go + public get jwks() { + return { + alg: this._config.get('jwt.algorithm'), + ...this._jwks, + use: 'sig', + }; + } } diff --git a/src/modules/oauth2/adapter/jwt.adapter.ts b/src/modules/oauth2/adapter/jwt.adapter.ts index 8a9346d..f78b563 100644 --- a/src/modules/oauth2/adapter/jwt.adapter.ts +++ b/src/modules/oauth2/adapter/jwt.adapter.ts @@ -26,7 +26,9 @@ export class IcyJWTAdapter implements JWTAdapter { } if ( - (scope.includes('image') || scope.includes('profile')) && + (scope.includes('image') || + scope.includes('picture') || + scope.includes('profile')) && user.picture ) { userData.picture = `${this._client.config.get('app.base_url')}/uploads/${ diff --git a/src/modules/oauth2/oauth2.module.ts b/src/modules/oauth2/oauth2.module.ts index 1e3d59c..1eeef88 100644 --- a/src/modules/oauth2/oauth2.module.ts +++ b/src/modules/oauth2/oauth2.module.ts @@ -15,6 +15,6 @@ import { OAuth2Service } from './oauth2.service'; JWTModule, ], providers: [OAuth2Service], - exports: [OAuth2Service], + exports: [OAuth2Service, JWTModule], }) export class OAuth2Module {} diff --git a/src/modules/oauth2/oauth2.service.ts b/src/modules/oauth2/oauth2.service.ts index 647e97c..274cb15 100644 --- a/src/modules/oauth2/oauth2.service.ts +++ b/src/modules/oauth2/oauth2.service.ts @@ -40,7 +40,10 @@ export class OAuth2Service { let disallowedScopes = [...ALWAYS_UNAVAILABLE]; Object.keys(SCOPE_DESCRIPTION).forEach((item) => { - if (scope.includes(item)) { + if ( + scope.includes(item) || + (item === 'image' && scope.includes('picture')) + ) { allowedScopes.push(SCOPE_DESCRIPTION[item]); } else { disallowedScopes.push(SCOPE_DESCRIPTION[item]); diff --git a/src/modules/objects/oauth2-client/oauth2-client.service.ts b/src/modules/objects/oauth2-client/oauth2-client.service.ts index bd25865..3138480 100644 --- a/src/modules/objects/oauth2-client/oauth2-client.service.ts +++ b/src/modules/objects/oauth2-client/oauth2-client.service.ts @@ -22,6 +22,8 @@ export class OAuth2ClientService { public availableScopes = [ 'image', + 'picture', + 'profile', 'email', 'privileges', 'management', diff --git a/src/modules/ssr-front-end/oauth2-router/oauth2-router.controller.ts b/src/modules/ssr-front-end/oauth2-router/oauth2-router.controller.ts index 034a017..a94cc18 100644 --- a/src/modules/ssr-front-end/oauth2-router/oauth2-router.controller.ts +++ b/src/modules/ssr-front-end/oauth2-router/oauth2-router.controller.ts @@ -14,6 +14,7 @@ import { Scope } from 'src/decorators/scope.decorator'; import { CurrentUser } from 'src/decorators/user.decorator'; import { OAuth2Guard } from 'src/guards/oauth2.guard'; import { ConfigurationService } from 'src/modules/config/config.service'; +import { JWTService } from 'src/modules/jwt/jwt.service'; import { User } from 'src/modules/objects/user/user.entity'; import { OAuth2Service } from '../../oauth2/oauth2.service'; @@ -23,6 +24,7 @@ export class OAuth2Controller { constructor( private _service: OAuth2Service, private _config: ConfigurationService, + private _jwt: JWTService, ) {} // These requests are just passed straight on to the provider controller @@ -96,7 +98,9 @@ export class OAuth2Controller { } if ( - (scope.includes('image') || scope.includes('user:image')) && + (scope.includes('image') || + scope.includes('picture') || + scope.includes('user:image')) && user.picture ) { userData.image = `${this._config.get('app.base_url')}/uploads/${ @@ -114,4 +118,11 @@ export class OAuth2Controller { return userData; } + + @Get('jwks') + getJWKS() { + return { + keys: [this._jwt.jwks], + }; + } }