mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: configure token endpoint auth method (#17968)
This commit is contained in:
parent
3ce353393a
commit
d89e88bb3f
18 changed files with 249 additions and 44 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue