From d3404f927cb2f20b499ce9ef9c325445f8d0bafe Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 2 Feb 2024 06:27:54 +0100 Subject: [PATCH] feat(server)!: oauth encryption algorithm setting (#6818) * feat: add oauth signing algorithm setting * chore: open api * chore: change default to RS256 * feat: test and clean up --------- Co-authored-by: Jason Rasmussen --- docs/docs/administration/oauth.md | 2 + mobile/openapi/doc/SystemConfigOAuthDto.md | Bin 770 -> 810 bytes .../lib/model/system_config_o_auth_dto.dart | Bin 5819 -> 6162 bytes .../test/system_config_o_auth_dto_test.dart | Bin 1666 -> 1783 bytes open-api/immich-openapi-specs.json | 4 + open-api/typescript-sdk/client/api.ts | 6 + server/src/domain/auth/auth.service.spec.ts | 2 +- server/src/domain/auth/auth.service.ts | 24 ++- .../dto/system-config-oauth.dto.ts | 36 ++-- .../system-config/system-config.core.ts | 11 +- .../system-config.service.spec.ts | 1 + .../infra/entities/system-config.entity.ts | 30 +-- web/src/api/utils.ts | 2 + .../settings/oauth/oauth-settings.svelte | 176 ++++++++++-------- .../lib/components/forms/login-form.svelte | 6 +- 15 files changed, 174 insertions(+), 126 deletions(-) diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index b4bf97063..c1ac3c577 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -66,8 +66,10 @@ Once you have a new OAuth client application configured, Immich can be configure | Client ID | string | (required) | Required. Client ID (from previous step) | | Client Secret | string | (required) | Required. Client Secret (previous step) | | Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) | +| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | | Button Text | string | Login with OAuth | Text for the OAuth button on the web | | Auto Register | boolean | true | When true, will automatically register a user the first time they sign in | +| Storage Claim | string | preferred_username | Claim mapping for the user's storage label | | [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process | | [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI | diff --git a/mobile/openapi/doc/SystemConfigOAuthDto.md b/mobile/openapi/doc/SystemConfigOAuthDto.md index f3618a08d51793c50fd0cfb0996166674e1c2fa3..c02dae9b732ff4a88b398ec40b4782c9960340fc 100644 GIT binary patch delta 31 mcmZo-TgA4)oQXd(Jufpa-7zOUzbLaLBX=?*ljLM`rqckZ(F&RX delta 11 ScmZ3**2K2KoN01A(`f(}vjgD( 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 1de193c20cb903fca796125076aa3eda35f65261..603ff8a952cb2213a49e9bbee759ab9809a33483 100644 GIT binary patch delta 259 zcmdn3JIP?fH75Sd^t{ZxbjO_Z{G!Z~jNHu!nZy|}1Wz+-Fe#&{RIs&GC`J?8{DgHS zrtT{C0!BqNm9{DvdY^MdGHIeIQ;$`!RVc~GEY`zNZonPMg6`1Cg8YgC7%HpzRoO9A IP8U-F039u1dH?_b delta 46 zcmV+}0MY-FFuN_V)&jHo0s;ZE7z7&vvt-PA304BLF$_rrv&aw^2b1>~ E8+YXo8~^|S diff --git a/mobile/openapi/test/system_config_o_auth_dto_test.dart b/mobile/openapi/test/system_config_o_auth_dto_test.dart index 5fde153f1cd46470098d6153d6f69cd1af6a5ef2..2bcbb64efd7a343f8cee4d77f43db14f41782ad3 100644 GIT binary patch delta 57 ycmZqT{m#2VfQ>IRJufpa-7zOUzbLaLW3oJ(GP+P8oBCu2Cick<% { jest.spyOn(generators, 'state').mockReturnValue('state'); jest.spyOn(Issuer, 'discover').mockResolvedValue({ - id_token_signing_alg_values_supported: ['HS256'], + id_token_signing_alg_values_supported: ['RS256'], Client: jest.fn().mockResolvedValue({ issuer: { metadata: { diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index ff4ea4303..2acade636 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -318,12 +318,25 @@ export class AuthService { const redirectUri = this.normalize(config, url.split('?')[0]); const client = await this.getOAuthClient(config); const params = client.callbackParams(url); - const tokens = await client.callback(redirectUri, params, { state: params.state }); - return client.userinfo(tokens.access_token || ''); + try { + const tokens = await client.callback(redirectUri, params, { state: params.state }); + return client.userinfo(tokens.access_token || ''); + } catch (error: Error | any) { + if (error.message.includes('unexpected JWT alg received')) { + this.logger.warn( + [ + 'Algorithm mismatch. Make sure the signing algorithm is set correctly in the OAuth settings.', + 'Or, that you have specified a signing key in your OAuth provider.', + ].join(' '), + ); + } + + throw error; + } } private async getOAuthClient(config: SystemConfig) { - const { enabled, clientId, clientSecret, issuerUrl } = config.oauth; + const { enabled, clientId, clientSecret, issuerUrl, signingAlgorithm } = config.oauth; if (!enabled) { throw new BadRequestException('OAuth2 is not enabled'); @@ -337,10 +350,7 @@ export class AuthService { try { const issuer = await Issuer.discover(issuerUrl); - const algorithms = (issuer.id_token_signing_alg_values_supported || []) as string[]; - if (algorithms[0] === 'HS256') { - metadata.id_token_signed_response_alg = algorithms[0]; - } + metadata.id_token_signed_response_alg = signingAlgorithm; return new issuer.Client(metadata); } catch (error: any | AggregateError) { diff --git a/server/src/domain/system-config/dto/system-config-oauth.dto.ts b/server/src/domain/system-config/dto/system-config-oauth.dto.ts index e13048761..52aa47a7e 100644 --- a/server/src/domain/system-config/dto/system-config-oauth.dto.ts +++ b/server/src/domain/system-config/dto/system-config-oauth.dto.ts @@ -5,12 +5,13 @@ const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrid export class SystemConfigOAuthDto { @IsBoolean() - enabled!: boolean; + autoLaunch!: boolean; + + @IsBoolean() + autoRegister!: boolean; - @ValidateIf(isEnabled) - @IsNotEmpty() @IsString() - issuerUrl!: string; + buttonText!: string; @ValidateIf(isEnabled) @IsNotEmpty() @@ -22,20 +23,13 @@ export class SystemConfigOAuthDto { @IsString() clientSecret!: string; - @IsString() - scope!: string; - - @IsString() - storageLabelClaim!: string; - - @IsString() - buttonText!: string; - @IsBoolean() - autoRegister!: boolean; + enabled!: boolean; - @IsBoolean() - autoLaunch!: boolean; + @ValidateIf(isEnabled) + @IsNotEmpty() + @IsString() + issuerUrl!: string; @IsBoolean() mobileOverrideEnabled!: boolean; @@ -43,4 +37,14 @@ export class SystemConfigOAuthDto { @ValidateIf(isOverrideEnabled) @IsUrl() mobileRedirectUri!: string; + + @IsString() + scope!: string; + + @IsString() + @IsNotEmpty() + signingAlgorithm!: string; + + @IsString() + storageLabelClaim!: string; } diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 0a20e5cc2..0516e0404 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -88,17 +88,18 @@ export const defaults = Object.freeze({ enabled: true, }, oauth: { - enabled: false, - issuerUrl: '', + autoLaunch: false, + autoRegister: true, + buttonText: 'Login with OAuth', clientId: '', clientSecret: '', + enabled: false, + issuerUrl: '', mobileOverrideEnabled: false, mobileRedirectUri: '', scope: 'openid email profile', + signingAlgorithm: 'RS256', storageLabelClaim: 'preferred_username', - buttonText: 'Login with OAuth', - autoRegister: true, - autoLaunch: false, }, passwordLogin: { enabled: true, diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 191480b2b..e8fa3f62e 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -98,6 +98,7 @@ const updatedConfig = Object.freeze({ mobileOverrideEnabled: false, mobileRedirectUri: '', scope: 'openid email profile', + signingAlgorithm: 'RS256', storageLabelClaim: 'preferred_username', }, passwordLogin: { diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index 95d25ab26..b0aef8f2b 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -77,17 +77,18 @@ export enum SystemConfigKey { NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled', - OAUTH_ENABLED = 'oauth.enabled', - OAUTH_ISSUER_URL = 'oauth.issuerUrl', + OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch', + OAUTH_AUTO_REGISTER = 'oauth.autoRegister', + OAUTH_BUTTON_TEXT = 'oauth.buttonText', OAUTH_CLIENT_ID = 'oauth.clientId', OAUTH_CLIENT_SECRET = 'oauth.clientSecret', - OAUTH_SCOPE = 'oauth.scope', - OAUTH_STORAGE_LABEL_CLAIM = 'oauth.storageLabelClaim', - OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch', - OAUTH_BUTTON_TEXT = 'oauth.buttonText', - OAUTH_AUTO_REGISTER = 'oauth.autoRegister', + OAUTH_ENABLED = 'oauth.enabled', + OAUTH_ISSUER_URL = 'oauth.issuerUrl', OAUTH_MOBILE_OVERRIDE_ENABLED = 'oauth.mobileOverrideEnabled', OAUTH_MOBILE_REDIRECT_URI = 'oauth.mobileRedirectUri', + OAUTH_SCOPE = 'oauth.scope', + OAUTH_SIGNING_ALGORITHM = 'oauth.signingAlgorithm', + OAUTH_STORAGE_LABEL_CLAIM = 'oauth.storageLabelClaim', PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled', @@ -216,17 +217,18 @@ export interface SystemConfig { enabled: boolean; }; oauth: { - enabled: boolean; - issuerUrl: string; + autoLaunch: boolean; + autoRegister: boolean; + buttonText: string; clientId: string; clientSecret: string; - scope: string; - storageLabelClaim: string; - buttonText: string; - autoRegister: boolean; - autoLaunch: boolean; + enabled: boolean; + issuerUrl: string; mobileOverrideEnabled: boolean; mobileRedirectUri: string; + scope: string; + signingAlgorithm: string; + storageLabelClaim: string; }; passwordLogin: { enabled: boolean; diff --git a/web/src/api/utils.ts b/web/src/api/utils.ts index 3b4f4d3ec..a3fed43d3 100644 --- a/web/src/api/utils.ts +++ b/web/src/api/utils.ts @@ -45,8 +45,10 @@ export const oauth = { const redirectUri = location.href.split('?')[0]; const { data } = await api.oauthApi.startOAuth({ oAuthConfigDto: { redirectUri } }); window.location.href = data.url; + return true; } catch (error) { handleError(error, 'Unable to login with OAuth'); + return false; } }, login: (location: Location) => { diff --git a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte index 926449cb6..b0eab903a 100644 --- a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte @@ -69,94 +69,106 @@ >.

- -
- + - - - - - - - - - - - - - - - handleToggleOverride()} - bind:checked={config.oauth.mobileOverrideEnabled} - /> - - {#if config.oauth.mobileOverrideEnabled} + {#if config.oauth.enabled} +
+ + + + + + + + + + + + + + + + + + handleToggleOverride()} + bind:checked={config.oauth.mobileOverrideEnabled} + /> + + {#if config.oauth.mobileOverrideEnabled} + + {/if} {/if} { oauthLoading = true; oauthError = ''; - await oauth.authorize(window.location); + const success = await oauth.authorize(window.location); + if (!success) { + oauthLoading = false; + oauthError = 'Unable to login with OAuth'; + } };