diff --git a/i18n/en.json b/i18n/en.json
index 239936471..f1ab30a6d 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -192,26 +192,22 @@
"oauth_auto_register": "Auto register",
"oauth_auto_register_description": "Automatically register new users after signing in with OAuth",
"oauth_button_text": "Button text",
- "oauth_client_id": "Client ID",
- "oauth_client_secret": "Client Secret",
+ "oauth_client_secret_description": "Required if PKCE (Proof Key for Code Exchange) is not supported by the OAuth provider",
"oauth_enable_description": "Login with OAuth",
- "oauth_issuer_url": "Issuer URL",
"oauth_mobile_redirect_uri": "Mobile redirect URI",
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like '{callback}'",
- "oauth_profile_signing_algorithm": "Profile signing algorithm",
- "oauth_profile_signing_algorithm_description": "Algorithm used to sign the user profile.",
- "oauth_scope": "Scope",
"oauth_settings": "OAuth",
"oauth_settings_description": "Manage OAuth login settings",
"oauth_settings_more_details": "For more details about this feature, refer to the docs.",
- "oauth_signing_algorithm": "Signing algorithm",
"oauth_storage_label_claim": "Storage label claim",
"oauth_storage_label_claim_description": "Automatically set the user's storage label to the value of this claim.",
"oauth_storage_quota_claim": "Storage quota claim",
"oauth_storage_quota_claim_description": "Automatically set the user's storage quota to the value of this claim.",
"oauth_storage_quota_default": "Default storage quota (GiB)",
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).",
+ "oauth_timeout": "Request Timeout",
+ "oauth_timeout_description": "Timeout for requests in milliseconds",
"offline_paths": "Offline Paths",
"offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.",
"password_enable_description": "Login with email and password",
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index b8ea4b924..d46945f64 100644
Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index e845099bd..ba64363c9 100644
Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 7586cc1ae..6abe576ac 100644
Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index cc517d48a..5f9d15c08 100644
Binary files a/mobile/openapi/lib/api_helper.dart and b/mobile/openapi/lib/api_helper.dart differ
diff --git a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart
new file mode 100644
index 000000000..fc528888b
Binary files /dev/null and b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart 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 9125bb7bb..24384a47b 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/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index f4ec92937..826af5a2e 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -10824,6 +10824,13 @@
],
"type": "object"
},
+ "OAuthTokenEndpointAuthMethod": {
+ "enum": [
+ "client_secret_post",
+ "client_secret_basic"
+ ],
+ "type": "string"
+ },
"OnThisDayDto": {
"properties": {
"year": {
@@ -13404,6 +13411,17 @@
},
"storageQuotaClaim": {
"type": "string"
+ },
+ "timeout": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "tokenEndpointAuthMethod": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod"
+ }
+ ]
}
},
"required": [
@@ -13421,7 +13439,9 @@
"scope",
"signingAlgorithm",
"storageLabelClaim",
- "storageQuotaClaim"
+ "storageQuotaClaim",
+ "timeout",
+ "tokenEndpointAuthMethod"
],
"type": "object"
},
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 647c5c4ad..743eeadf0 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -1315,6 +1315,8 @@ export type SystemConfigOAuthDto = {
signingAlgorithm: string;
storageLabelClaim: string;
storageQuotaClaim: string;
+ timeout: number;
+ tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
};
export type SystemConfigPasswordLoginDto = {
enabled: boolean;
@@ -3859,6 +3861,10 @@ export enum LogLevel {
Error = "error",
Fatal = "fatal"
}
+export enum OAuthTokenEndpointAuthMethod {
+ ClientSecretPost = "client_secret_post",
+ ClientSecretBasic = "client_secret_basic"
+}
export enum TimeBucketSize {
Day = "DAY",
Month = "MONTH"
diff --git a/server/src/config.ts b/server/src/config.ts
index 566adbd69..a9fdffbd6 100644
--- a/server/src/config.ts
+++ b/server/src/config.ts
@@ -5,6 +5,7 @@ import {
CQMode,
ImageFormat,
LogLevel,
+ OAuthTokenEndpointAuthMethod,
QueueName,
ToneMapping,
TranscodeHWAccel,
@@ -96,6 +97,8 @@ export interface SystemConfig {
scope: string;
signingAlgorithm: string;
profileSigningAlgorithm: string;
+ tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
+ timeout: number;
storageLabelClaim: string;
storageQuotaClaim: string;
};
@@ -260,6 +263,8 @@ export const defaults = Object.freeze({
profileSigningAlgorithm: 'none',
storageLabelClaim: 'preferred_username',
storageQuotaClaim: 'immich_quota',
+ tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
+ timeout: 30_000,
},
passwordLogin: {
enabled: true,
diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts
index eaef40a5e..6991baf10 100644
--- a/server/src/dtos/system-config.dto.ts
+++ b/server/src/dtos/system-config.dto.ts
@@ -25,6 +25,7 @@ import {
Colorspace,
ImageFormat,
LogLevel,
+ OAuthTokenEndpointAuthMethod,
QueueName,
ToneMapping,
TranscodeHWAccel,
@@ -33,7 +34,7 @@ import {
VideoContainer,
} from 'src/enum';
import { ConcurrentQueueName } from 'src/types';
-import { IsCronExpression, ValidateBoolean } from 'src/validation';
+import { IsCronExpression, Optional, ValidateBoolean } from 'src/validation';
const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
@@ -344,10 +345,19 @@ class SystemConfigOAuthDto {
clientId!: string;
@ValidateIf(isOAuthEnabled)
- @IsNotEmpty()
@IsString()
clientSecret!: string;
+ @IsEnum(OAuthTokenEndpointAuthMethod)
+ @ApiProperty({ enum: OAuthTokenEndpointAuthMethod, enumName: 'OAuthTokenEndpointAuthMethod' })
+ tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod;
+
+ @IsInt()
+ @IsPositive()
+ @Optional()
+ @ApiProperty({ type: 'integer' })
+ timeout!: number;
+
@IsNumber()
@Min(0)
defaultStorageQuota!: number;
diff --git a/server/src/enum.ts b/server/src/enum.ts
index c88e2e942..4e725e1c1 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -605,3 +605,8 @@ export enum NotificationType {
SystemMessage = 'SystemMessage',
Custom = 'Custom',
}
+
+export enum OAuthTokenEndpointAuthMethod {
+ CLIENT_SECRET_POST = 'client_secret_post',
+ CLIENT_SECRET_BASIC = 'client_secret_basic',
+}
diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts
index 05d2d45f4..2ac3715a5 100644
--- a/server/src/repositories/logging.repository.ts
+++ b/server/src/repositories/logging.repository.ts
@@ -5,7 +5,7 @@ import { Telemetry } from 'src/decorators';
import { LogLevel } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository';
-type LogDetails = any[];
+type LogDetails = any;
type LogFunction = () => string;
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts
index d3e037208..ea9f0b190 100644
--- a/server/src/repositories/oauth.repository.ts
+++ b/server/src/repositories/oauth.repository.ts
@@ -1,16 +1,19 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import type { UserInfoResponse } from 'openid-client' with { 'resolution-mode': 'import' };
+import { OAuthTokenEndpointAuthMethod } from 'src/enum';
import { LoggingRepository } from 'src/repositories/logging.repository';
export type OAuthConfig = {
clientId: string;
- clientSecret: string;
+ clientSecret?: string;
issuerUrl: string;
mobileOverrideEnabled: boolean;
mobileRedirectUri: string;
profileSigningAlgorithm: string;
scope: string;
signingAlgorithm: string;
+ tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
+ timeout: number;
};
export type OAuthProfile = UserInfoResponse;
@@ -76,12 +79,10 @@ export class OAuthRepository {
);
}
- if (error.code === 'OAUTH_INVALID_RESPONSE') {
- this.logger.warn(`Invalid response from authorization server. Cause: ${error.cause?.message}`);
- throw error.cause;
- }
+ this.logger.error(`OAuth login failed: ${error.message}`);
+ this.logger.error(error);
- throw error;
+ throw new Error('OAuth login failed', { cause: error });
}
}
@@ -103,6 +104,8 @@ export class OAuthRepository {
clientSecret,
profileSigningAlgorithm,
signingAlgorithm,
+ tokenEndpointAuthMethod,
+ timeout,
}: OAuthConfig) {
try {
const { allowInsecureRequests, discovery } = await import('openid-client');
@@ -114,14 +117,38 @@ export class OAuthRepository {
response_types: ['code'],
userinfo_signed_response_alg: profileSigningAlgorithm === 'none' ? undefined : profileSigningAlgorithm,
id_token_signed_response_alg: signingAlgorithm,
- timeout: 30_000,
},
- undefined,
- { execute: [allowInsecureRequests] },
+ await this.getTokenAuthMethod(tokenEndpointAuthMethod, clientSecret),
+ {
+ execute: [allowInsecureRequests],
+ timeout,
+ },
);
} catch (error: any | AggregateError) {
this.logger.error(`Error in OAuth discovery: ${error}`, error?.stack, error?.errors);
throw new InternalServerErrorException(`Error in OAuth discovery: ${error}`, { cause: error });
}
}
+
+ private async getTokenAuthMethod(tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod, clientSecret?: string) {
+ const { None, ClientSecretPost, ClientSecretBasic } = await import('openid-client');
+
+ if (!clientSecret) {
+ return None();
+ }
+
+ switch (tokenEndpointAuthMethod) {
+ case OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST: {
+ return ClientSecretPost(clientSecret);
+ }
+
+ case OAuthTokenEndpointAuthMethod.CLIENT_SECRET_BASIC: {
+ return ClientSecretBasic(clientSecret);
+ }
+
+ default: {
+ return None();
+ }
+ }
+ }
}
diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts
index 936acf27a..176e6d6f0 100644
--- a/server/src/services/system-config.service.spec.ts
+++ b/server/src/services/system-config.service.spec.ts
@@ -6,6 +6,7 @@ import {
CQMode,
ImageFormat,
LogLevel,
+ OAuthTokenEndpointAuthMethod,
QueueName,
ToneMapping,
TranscodeHWAccel,
@@ -119,6 +120,8 @@ const updatedConfig = Object.freeze({
scope: 'openid email profile',
signingAlgorithm: 'RS256',
profileSigningAlgorithm: 'none',
+ tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
+ timeout: 30_000,
storageLabelClaim: 'preferred_username',
storageQuotaClaim: 'immich_quota',
},
diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte
index 67da6bb7f..b2454b06c 100644
--- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte
@@ -1,16 +1,17 @@