diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index b4bf97063a..c1ac3c5770 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 f3618a08d5..c02dae9b73 100644 --- a/mobile/openapi/doc/SystemConfigOAuthDto.md +++ b/mobile/openapi/doc/SystemConfigOAuthDto.md @@ -18,6 +18,7 @@ Name | Type | Description | Notes **mobileOverrideEnabled** | **bool** | | **mobileRedirectUri** | **String** | | **scope** | **String** | | +**signingAlgorithm** | **String** | | **storageLabelClaim** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 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 1de193c20c..603ff8a952 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -23,6 +23,7 @@ class SystemConfigOAuthDto { required this.mobileOverrideEnabled, required this.mobileRedirectUri, required this.scope, + required this.signingAlgorithm, required this.storageLabelClaim, }); @@ -46,6 +47,8 @@ class SystemConfigOAuthDto { String scope; + String signingAlgorithm; + String storageLabelClaim; @override @@ -60,6 +63,7 @@ class SystemConfigOAuthDto { other.mobileOverrideEnabled == mobileOverrideEnabled && other.mobileRedirectUri == mobileRedirectUri && other.scope == scope && + other.signingAlgorithm == signingAlgorithm && other.storageLabelClaim == storageLabelClaim; @override @@ -75,10 +79,11 @@ class SystemConfigOAuthDto { (mobileOverrideEnabled.hashCode) + (mobileRedirectUri.hashCode) + (scope.hashCode) + + (signingAlgorithm.hashCode) + (storageLabelClaim.hashCode); @override - String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, scope=$scope, storageLabelClaim=$storageLabelClaim]'; + String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim]'; Map toJson() { final json = {}; @@ -92,6 +97,7 @@ class SystemConfigOAuthDto { json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled; json[r'mobileRedirectUri'] = this.mobileRedirectUri; json[r'scope'] = this.scope; + json[r'signingAlgorithm'] = this.signingAlgorithm; json[r'storageLabelClaim'] = this.storageLabelClaim; return json; } @@ -114,6 +120,7 @@ class SystemConfigOAuthDto { mobileOverrideEnabled: mapValueOfType(json, r'mobileOverrideEnabled')!, mobileRedirectUri: mapValueOfType(json, r'mobileRedirectUri')!, scope: mapValueOfType(json, r'scope')!, + signingAlgorithm: mapValueOfType(json, r'signingAlgorithm')!, storageLabelClaim: mapValueOfType(json, r'storageLabelClaim')!, ); } @@ -172,6 +179,7 @@ class SystemConfigOAuthDto { 'mobileOverrideEnabled', 'mobileRedirectUri', 'scope', + 'signingAlgorithm', 'storageLabelClaim', }; } 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 5fde153f1c..2bcbb64efd 100644 --- a/mobile/openapi/test/system_config_o_auth_dto_test.dart +++ b/mobile/openapi/test/system_config_o_auth_dto_test.dart @@ -66,6 +66,11 @@ void main() { // TODO }); + // String signingAlgorithm + test('to test the property `signingAlgorithm`', () async { + // TODO + }); + // String storageLabelClaim test('to test the property `storageLabelClaim`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 835b87ffdd..b031456a2a 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 f914e96263..f8866b7904 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 c04bbc2630..78462cb490 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 ff4ea43032..2acade6365 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 e13048761c..52aa47a7ea 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 0a20e5cc2a..0516e04043 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 191480b2b7..e8fa3f62e8 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 95d25ab26b..b0aef8f2be 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 3b4f4d3ec3..a3fed43d32 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 926449cb65..b0eab903ae 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'; + } };