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 f3618a08d..c02dae9b7 100644 Binary files a/mobile/openapi/doc/SystemConfigOAuthDto.md and b/mobile/openapi/doc/SystemConfigOAuthDto.md differ 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 1de193c20..603ff8a95 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/mobile/openapi/test/system_config_o_auth_dto_test.dart b/mobile/openapi/test/system_config_o_auth_dto_test.dart index 5fde153f1..2bcbb64ef 100644 Binary files a/mobile/openapi/test/system_config_o_auth_dto_test.dart and b/mobile/openapi/test/system_config_o_auth_dto_test.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 835b87ffd..b031456a2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9679,6 +9679,9 @@ "scope": { "type": "string" }, + "signingAlgorithm": { + "type": "string" + }, "storageLabelClaim": { "type": "string" } @@ -9694,6 +9697,7 @@ "mobileOverrideEnabled", "mobileRedirectUri", "scope", + "signingAlgorithm", "storageLabelClaim" ], "type": "object" diff --git a/open-api/typescript-sdk/client/api.ts b/open-api/typescript-sdk/client/api.ts index f914e9626..f8866b790 100644 --- a/open-api/typescript-sdk/client/api.ts +++ b/open-api/typescript-sdk/client/api.ts @@ -4073,6 +4073,12 @@ export interface SystemConfigOAuthDto { * @memberof SystemConfigOAuthDto */ 'scope': string; + /** + * + * @type {string} + * @memberof SystemConfigOAuthDto + */ + 'signingAlgorithm': string; /** * * @type {string} diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index c04bbc263..78462cb49 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -73,7 +73,7 @@ describe('AuthService', () => { 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'; + } };