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';
+ }
};