From 603ad8088c83f7661ff5ea9415cf49915a920af7 Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Fri, 13 Sep 2024 16:41:44 +0200 Subject: [PATCH] enhancement(auth/oidc): allow manual defining end_session_endpoint URL For non-OIDC compliant OAuth2 providers it was only possible to define the authorize, token and userinfo URLs but not the end_session_endpoint. This commit adds that functionality. Signed-off-by: Erik Michelson --- backend/src/config/auth.config.spec.ts | 49 ++++++++++++++++++++- backend/src/config/auth.config.ts | 3 ++ backend/src/config/utils.ts | 1 + backend/src/identity/oidc/oidc.service.ts | 1 + docs/content/references/config/auth/oidc.md | 1 + 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/backend/src/config/auth.config.spec.ts b/backend/src/config/auth.config.spec.ts index 5d9b93ca9..7b9ea9694 100644 --- a/backend/src/config/auth.config.spec.ts +++ b/backend/src/config/auth.config.spec.ts @@ -524,7 +524,7 @@ describe('authConfig', () => { }); }); - describe('odic', () => { + describe('oidc', () => { const oidcNames = ['gitlab']; const providerName = 'Gitlab oAuth2'; const issuer = 'https://gitlab.example.org'; @@ -534,7 +534,8 @@ describe('authConfig', () => { const authorizeUrl = 'https://example.org/auth'; const tokenUrl = 'https://example.org/token'; const userinfoUrl = 'https://example.org/user'; - const scope = 'some scopr'; + const endSessionUrl = 'https://example.org/end'; + const scope = 'some scope'; const defaultScope = 'openid profile email'; const userIdField = 'login'; const defaultUserIdField = 'sub'; @@ -556,6 +557,7 @@ describe('authConfig', () => { HD_AUTH_OIDC_GITLAB_AUTHORIZE_URL: authorizeUrl, HD_AUTH_OIDC_GITLAB_TOKEN_URL: tokenUrl, HD_AUTH_OIDC_GITLAB_USERINFO_URL: userinfoUrl, + HD_AUTH_OIDC_GITLAB_END_SESSION_URL: endSessionUrl, HD_AUTH_OIDC_GITLAB_SCOPE: scope, HD_AUTH_OIDC_GITLAB_USER_ID_FIELD: userIdField, HD_AUTH_OIDC_GITLAB_USER_NAME_FIELD: userNameField, @@ -587,6 +589,7 @@ describe('authConfig', () => { expect(firstOidc.theme).toEqual(theme); expect(firstOidc.authorizeUrl).toEqual(authorizeUrl); expect(firstOidc.tokenUrl).toEqual(tokenUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); expect(firstOidc.userIdField).toEqual(userIdField); @@ -620,6 +623,7 @@ describe('authConfig', () => { expect(firstOidc.authorizeUrl).toEqual(authorizeUrl); expect(firstOidc.tokenUrl).toEqual(tokenUrl); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userIdField).toEqual(userIdField); expect(firstOidc.userNameField).toEqual(userNameField); @@ -652,6 +656,7 @@ describe('authConfig', () => { expect(firstOidc.authorizeUrl).toBeUndefined(); expect(firstOidc.tokenUrl).toEqual(tokenUrl); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userIdField).toEqual(userIdField); expect(firstOidc.userNameField).toEqual(userNameField); @@ -684,6 +689,7 @@ describe('authConfig', () => { expect(firstOidc.authorizeUrl).toEqual(authorizeUrl); expect(firstOidc.tokenUrl).toBeUndefined(); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userIdField).toEqual(userIdField); expect(firstOidc.userNameField).toEqual(userNameField); @@ -716,6 +722,40 @@ describe('authConfig', () => { expect(firstOidc.authorizeUrl).toEqual(authorizeUrl); expect(firstOidc.tokenUrl).toEqual(tokenUrl); expect(firstOidc.userinfoUrl).toBeUndefined(); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); + expect(firstOidc.scope).toEqual(scope); + expect(firstOidc.userIdField).toEqual(userIdField); + expect(firstOidc.userNameField).toEqual(userNameField); + expect(firstOidc.displayNameField).toEqual(displayNameField); + expect(firstOidc.profilePictureField).toEqual(profilePictureField); + expect(firstOidc.emailField).toEqual(emailField); + restore(); + }); + it('when HD_AUTH_OIDC_GITLAB_END_SESSION_URL is not set', () => { + const restore = mockedEnv( + { + /* eslint-disable @typescript-eslint/naming-convention */ + ...neededAuthConfig, + ...completeOidcConfig, + HD_AUTH_OIDC_GITLAB_END_SESSION_URL: undefined, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + { + clear: true, + }, + ); + const config = authConfig(); + expect(config.oidc).toHaveLength(1); + const firstOidc = config.oidc[0]; + expect(firstOidc.identifier).toEqual(oidcNames[0]); + expect(firstOidc.issuer).toEqual(issuer); + expect(firstOidc.clientID).toEqual(clientId); + expect(firstOidc.clientSecret).toEqual(clientSecret); + expect(firstOidc.theme).toEqual(theme); + expect(firstOidc.authorizeUrl).toEqual(authorizeUrl); + expect(firstOidc.tokenUrl).toEqual(tokenUrl); + expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toBeUndefined(); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userIdField).toEqual(userIdField); expect(firstOidc.userNameField).toEqual(userNameField); @@ -748,6 +788,7 @@ describe('authConfig', () => { expect(firstOidc.authorizeUrl).toEqual(authorizeUrl); expect(firstOidc.tokenUrl).toEqual(tokenUrl); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.scope).toEqual(defaultScope); expect(firstOidc.userIdField).toEqual(userIdField); expect(firstOidc.userNameField).toEqual(userNameField); @@ -781,6 +822,7 @@ describe('authConfig', () => { expect(firstOidc.tokenUrl).toEqual(tokenUrl); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.userIdField).toEqual(defaultUserIdField); expect(firstOidc.userNameField).toEqual(userNameField); expect(firstOidc.displayNameField).toEqual(displayNameField); @@ -813,6 +855,7 @@ describe('authConfig', () => { expect(firstOidc.tokenUrl).toEqual(tokenUrl); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.userIdField).toEqual(userIdField); expect(firstOidc.userNameField).toEqual(userNameField); expect(firstOidc.displayNameField).toEqual(defaultDisplayNameField); @@ -845,6 +888,7 @@ describe('authConfig', () => { expect(firstOidc.tokenUrl).toEqual(tokenUrl); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.userIdField).toEqual(userIdField); expect(firstOidc.userNameField).toEqual(userNameField); expect(firstOidc.displayNameField).toEqual(displayNameField); @@ -879,6 +923,7 @@ describe('authConfig', () => { expect(firstOidc.tokenUrl).toEqual(tokenUrl); expect(firstOidc.scope).toEqual(scope); expect(firstOidc.userinfoUrl).toEqual(userinfoUrl); + expect(firstOidc.endSessionUrl).toEqual(endSessionUrl); expect(firstOidc.userIdField).toEqual(userIdField); expect(firstOidc.userNameField).toEqual(userNameField); expect(firstOidc.displayNameField).toEqual(displayNameField); diff --git a/backend/src/config/auth.config.ts b/backend/src/config/auth.config.ts index 68c218306..b6963f647 100644 --- a/backend/src/config/auth.config.ts +++ b/backend/src/config/auth.config.ts @@ -43,6 +43,7 @@ export interface OidcConfig extends InternalIdentifier { authorizeUrl?: string; tokenUrl?: string; userinfoUrl?: string; + endSessionUrl?: string; scope: string; userNameField: string; userIdField: string; @@ -139,6 +140,7 @@ const authSchema = Joi.object({ authorizeUrl: Joi.string().optional(), tokenUrl: Joi.string().optional(), userinfoUrl: Joi.string().optional(), + endSessionUrl: Joi.string().optional(), scope: Joi.string().default('openid profile email').optional(), userIdField: Joi.string().default('sub').optional(), userNameField: Joi.string().default('preferred_username').optional(), @@ -206,6 +208,7 @@ export default registerAs('authConfig', () => { authorizeUrl: process.env[`HD_AUTH_OIDC_${oidcName}_AUTHORIZE_URL`], tokenUrl: process.env[`HD_AUTH_OIDC_${oidcName}_TOKEN_URL`], userinfoUrl: process.env[`HD_AUTH_OIDC_${oidcName}_USERINFO_URL`], + endSessionUrl: process.env[`HD_AUTH_OIDC_${oidcName}_END_SESSION_URL`], scope: process.env[`HD_AUTH_OIDC_${oidcName}_SCOPE`], userIdField: process.env[`HD_AUTH_OIDC_${oidcName}_USER_ID_FIELD`], userNameField: process.env[`HD_AUTH_OIDC_${oidcName}_USER_NAME_FIELD`], diff --git a/backend/src/config/utils.ts b/backend/src/config/utils.ts index bf083ffc4..2241aa4c1 100644 --- a/backend/src/config/utils.ts +++ b/backend/src/config/utils.ts @@ -84,6 +84,7 @@ export function replaceAuthErrorsWithEnvironmentVariables( newMessage = newMessage.replace('.authorizeUrl', '_AUTHORIZE_URL'); newMessage = newMessage.replace('.tokenUrl', '_TOKEN_URL'); newMessage = newMessage.replace('.userinfoUrl', '_USERINFO_URL'); + newMessage = newMessage.replace('.endSessionUrl', '_END_SESSION_URL'); newMessage = newMessage.replace('.scope', '_SCOPE'); newMessage = newMessage.replace('.tlsCaCerts', '_TLS_CERT_PATHS'); newMessage = newMessage.replace('.issuer', '_ISSUER'); diff --git a/backend/src/identity/oidc/oidc.service.ts b/backend/src/identity/oidc/oidc.service.ts index 5fbfc0fbb..39e0377c4 100644 --- a/backend/src/identity/oidc/oidc.service.ts +++ b/backend/src/identity/oidc/oidc.service.ts @@ -89,6 +89,7 @@ export class OidcService { authorization_endpoint: oidcConfig.authorizeUrl, token_endpoint: oidcConfig.tokenUrl, userinfo_endpoint: oidcConfig.userinfoUrl, + end_session_endpoint: oidcConfig.endSessionUrl, /* eslint-enable @typescript-eslint/naming-convention */ }); diff --git a/docs/content/references/config/auth/oidc.md b/docs/content/references/config/auth/oidc.md index 524304c28..abfb0298d 100644 --- a/docs/content/references/config/auth/oidc.md +++ b/docs/content/references/config/auth/oidc.md @@ -31,6 +31,7 @@ no OIDC (e.g., GitHub or Discord). In this case, you need the following addition | `HD_AUTH_OIDC_$NAME_AUTHORIZE_URL` | - | `https://auth.example.com/oauth2/auth` | The URL to which the user should be redirected to start the OAuth2 flow. | | `HD_AUTH_OIDC_$NAME_TOKEN_URL` | - | `https://auth.example.com/oauth2/token` | The URL to which the user should be redirected to exchange the code for an access token. | | `HD_AUTH_OIDC_$NAME_USERINFO_URL` | - | `https://auth.example.com/oauth2/userinfo` | The URL to which the user should be redirected to get the user information. | +| `HD_AUTH_OIDC_$NAME_END_SESSION_URL` | - | `https://auth.example.com/oauth2/logout` | The URL to which the user should be redirected to end the session. | | `HD_AUTH_OIDC_$NAME_SCOPE` | - | `profile` | The scope that should be requested to get the user information. | | `HD_AUTH_OIDC_$NAME_USER_ID_FIELD` | `sub` | `sub`, `id` | The unique identifier that is returned for the user from the OAuth2 provider. | | `HD_AUTH_OIDC_$NAME_USER_ID_FIELD` | `sub` | `sub`, `id` | The unique identifier that is returned for the user from the OAuth2 provider. |