feat(server): add OIDC logout URL override option (#27389)

* feat(server): add OIDC logout URL override option
- Added toggle and field consistent with existing mobile redirect URI override.
- Existing auto-discovery remains default.
- Update tests and docs for new feature.

* fix(server): changes from review for OIDC logout URL override
- Rename 'logoutUri' to 'endSessionEndpoint'
- Remove toggle, just use override if provided
- Moved field in settings UI
This commit is contained in:
LJspice
2026-04-17 21:18:21 -07:00
committed by GitHub
parent 384d3a0984
commit b8591cb591
13 changed files with 72 additions and 2 deletions
+2
View File
@@ -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<SystemConfig>({
defaultStorageQuota: null,
enabled: false,
issuerUrl: '',
endSessionEndpoint: '',
mobileOverrideEnabled: false,
mobileRedirectUri: '',
prompt: '',
+6
View File
@@ -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'),
@@ -22,6 +22,7 @@ export type OAuthConfig = {
clientId: string;
clientSecret?: string;
issuerUrl: string;
endSessionEndpoint: string;
mobileOverrideEnabled: boolean;
mobileRedirectUri: string;
profileSigningAlgorithm: string;
+26
View File
@@ -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();
+4
View File
@@ -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;
}
@@ -138,6 +138,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
defaultStorageQuota: null,
enabled: false,
issuerUrl: '',
endSessionEndpoint: '',
mobileOverrideEnabled: false,
mobileRedirectUri: '',
prompt: '',