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