diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 95ad8db72d..fea73684fd 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -68,6 +68,7 @@ Once you have a new OAuth client application configured, Immich can be configure | `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | | `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) | | `prompt` | string | (empty) | Prompt parameter for authorization url (examples: select_account, login, consent) | +| `end_session_endpoint` | URL | (empty) | Http(s) alternative end session endpoint (logout URI) | | Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up | | Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** | | Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** | @@ -186,6 +187,7 @@ Configuration of OAuth in Immich System Settings | Scope | openid email profile immich_scope | | ID Token Signed Response Algorithm | RS256 | | Userinfo Signed Response Algorithm | RS256 | +| End Session Endpoint | https://auth.example.com/logout?rd=https://immich.example.com/ | | Storage Label Claim | uid | | Storage Quota Claim | immich_quota | | Default Storage Quota (GiB) | 0 (empty for unlimited quota) | diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 3355750603..4754497d90 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -193,6 +193,7 @@ The default configuration looks like this: "defaultStorageQuota": null, "enabled": false, "issuerUrl": "", + "endSessionEndpoint": "", "mobileOverrideEnabled": false, "mobileRedirectUri": "", "profileSigningAlgorithm": "none", diff --git a/i18n/en.json b/i18n/en.json index d5a176b117..add755c05d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -276,6 +276,7 @@ "oauth_button_text": "Button text", "oauth_client_secret_description": "Required for confidential client, or if PKCE (Proof Key for Code Exchange) is not supported for public client.", "oauth_enable_description": "Login with OAuth", + "oauth_end_session_url_description": "Redirect the user to this URI when they log out.", "oauth_mobile_redirect_uri": "Mobile redirect URI", "oauth_mobile_redirect_uri_override": "Mobile redirect URI override", "oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''", diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index e481fe2cdf..3fd22978ff 100644 Binary files a/mobile/openapi/lib/model/system_config_o_auth_dto.dart and b/mobile/openapi/lib/model/system_config_o_auth_dto.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index be32f5452a..852fe907e0 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -24331,6 +24331,10 @@ "description": "Enabled", "type": "boolean" }, + "endSessionEndpoint": { + "description": "End session endpoint", + "type": "string" + }, "issuerUrl": { "description": "Issuer URL", "type": "string" @@ -24390,6 +24394,7 @@ "clientSecret", "defaultStorageQuota", "enabled", + "endSessionEndpoint", "issuerUrl", "mobileOverrideEnabled", "mobileRedirectUri", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ae1d2bc271..da24059a2a 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2518,6 +2518,8 @@ export type SystemConfigOAuthDto = { defaultStorageQuota: number | null; /** Enabled */ enabled: boolean; + /** End session endpoint */ + endSessionEndpoint: string; /** Issuer URL */ issuerUrl: string; /** Mobile override enabled */ diff --git a/server/src/config.ts b/server/src/config.ts index ae057e9476..476b0eb160 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -104,6 +104,7 @@ export type SystemConfig = { defaultStorageQuota: number | null; enabled: boolean; issuerUrl: string; + endSessionEndpoint: string; mobileOverrideEnabled: boolean; mobileRedirectUri: string; prompt: string; @@ -297,6 +298,7 @@ export const defaults = Object.freeze({ defaultStorageQuota: null, enabled: false, issuerUrl: '', + endSessionEndpoint: '', mobileOverrideEnabled: false, mobileRedirectUri: '', prompt: '', diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index ebe5d46724..35f61032b0 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -190,6 +190,12 @@ const SystemConfigOAuthSchema = z .describe('Issuer URL'), scope: z.string().describe('Scope'), prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'), + endSessionEndpoint: z + .string() + .refine((url) => url.length === 0 || z.url().safeParse(url).success, { + error: 'endSessionEndpoint must be an empty string or a valid URL', + }) + .describe('End session endpoint'), signingAlgorithm: z.string().describe('Signing algorithm'), profileSigningAlgorithm: z.string().describe('Profile signing algorithm'), storageLabelClaim: z.string().describe('Storage label claim'), diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index ea9e69e146..a22c9d56e2 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -22,6 +22,7 @@ export type OAuthConfig = { clientId: string; clientSecret?: string; issuerUrl: string; + endSessionEndpoint: string; mobileOverrideEnabled: boolean; mobileRedirectUri: string; profileSigningAlgorithm: string; diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 61a5bfbd11..a824b68814 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -164,6 +164,32 @@ describe(AuthService.name, () => { }); }); + it('should return the custom end session endpoint if provided', async () => { + const auth = AuthFactory.create(); + + mocks.systemMetadata.get.mockResolvedValue({ + oauth: { enabled: true, endSessionEndpoint: 'http://custom-logout-url' }, + }); + + await expect(sut.logout(auth, AuthType.OAuth)).resolves.toEqual({ + successful: true, + redirectUri: 'http://custom-logout-url', + }); + }); + + it('should return the auto-discovered end session endpoint if custom endpoint is not provided', async () => { + const auth = AuthFactory.create(); + + mocks.systemMetadata.get.mockResolvedValue({ + oauth: { enabled: true, endSessionEndpoint: '' }, + }); + + await expect(sut.logout(auth, AuthType.OAuth)).resolves.toEqual({ + successful: true, + redirectUri: 'http://end-session-endpoint', + }); + }); + it('should return the default redirect', async () => { const auth = AuthFactory.create(); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index bbc4591477..628e863712 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -455,6 +455,10 @@ export class AuthService extends BaseService { return LOGIN_URL; } + if (config.oauth.endSessionEndpoint) { + return config.oauth.endSessionEndpoint; + } + return (await this.oauthRepository.getLogoutEndpoint(config.oauth)) || LOGIN_URL; } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index b527ba41cd..fb07eb1438 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -138,6 +138,7 @@ const updatedConfig = Object.freeze({ defaultStorageQuota: null, enabled: false, issuerUrl: '', + endSessionEndpoint: '', mobileOverrideEnabled: false, mobileRedirectUri: '', prompt: '', diff --git a/web/src/routes/admin/system-settings/AuthSettings.svelte b/web/src/routes/admin/system-settings/AuthSettings.svelte index 50280f58c8..774fb70aa5 100644 --- a/web/src/routes/admin/system-settings/AuthSettings.svelte +++ b/web/src/routes/admin/system-settings/AuthSettings.svelte @@ -1,7 +1,6 @@