feat: configure token endpoint auth method (#17968)

This commit is contained in:
Jason Rasmussen 2025-04-29 15:17:48 -04:00 committed by GitHub
parent 3ce353393a
commit d89e88bb3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 249 additions and 44 deletions

View file

@ -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<SystemConfig>({
profileSigningAlgorithm: 'none',
storageLabelClaim: 'preferred_username',
storageQuotaClaim: 'immich_quota',
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
timeout: 30_000,
},
passwordLogin: {
enabled: true,

View file

@ -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;

View file

@ -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',
}

View file

@ -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];

View file

@ -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();
}
}
}
}

View file

@ -6,6 +6,7 @@ import {
CQMode,
ImageFormat,
LogLevel,
OAuthTokenEndpointAuthMethod,
QueueName,
ToneMapping,
TranscodeHWAccel,
@ -119,6 +120,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
scope: 'openid email profile',
signingAlgorithm: 'RS256',
profileSigningAlgorithm: 'none',
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
timeout: 30_000,
storageLabelClaim: 'preferred_username',
storageQuotaClaim: 'immich_quota',
},