mirror of
https://github.com/immich-app/immich.git
synced 2025-03-01 15:11:21 +01:00

* refactor: flatten infra folders * fix: database migrations * fix: test related import * fix: github actions workflow * chore: rename schemas to typesense-schemas
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
import { SystemConfig } from '@app/infra/entities';
|
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
|
import { ClientMetadata, custom, generators, Issuer, UserinfoResponse } from 'openid-client';
|
|
import { ISystemConfigRepository } from '../system-config';
|
|
import { SystemConfigCore } from '../system-config/system-config.core';
|
|
import { OAuthConfigDto } from './dto';
|
|
import { MOBILE_REDIRECT } from './oauth.constants';
|
|
import { OAuthConfigResponseDto } from './response-dto';
|
|
|
|
type OAuthProfile = UserinfoResponse & {
|
|
email: string;
|
|
};
|
|
|
|
@Injectable()
|
|
export class OAuthCore {
|
|
private readonly logger = new Logger(OAuthCore.name);
|
|
private configCore: SystemConfigCore;
|
|
|
|
constructor(configRepository: ISystemConfigRepository, private config: SystemConfig) {
|
|
this.configCore = new SystemConfigCore(configRepository);
|
|
|
|
custom.setHttpOptionsDefaults({
|
|
timeout: 30000,
|
|
});
|
|
|
|
this.configCore.config$.subscribe((config) => (this.config = config));
|
|
}
|
|
|
|
async generateConfig(dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
|
|
const response = {
|
|
enabled: this.config.oauth.enabled,
|
|
passwordLoginEnabled: this.config.passwordLogin.enabled,
|
|
};
|
|
|
|
if (!response.enabled) {
|
|
return response;
|
|
}
|
|
|
|
const { scope, buttonText, autoLaunch } = this.config.oauth;
|
|
const url = (await this.getClient()).authorizationUrl({
|
|
redirect_uri: this.normalize(dto.redirectUri),
|
|
scope,
|
|
state: generators.state(),
|
|
});
|
|
|
|
return { ...response, buttonText, url, autoLaunch };
|
|
}
|
|
|
|
async callback(url: string): Promise<OAuthProfile> {
|
|
const redirectUri = this.normalize(url.split('?')[0]);
|
|
const client = await this.getClient();
|
|
const params = client.callbackParams(url);
|
|
const tokens = await client.callback(redirectUri, params, { state: params.state });
|
|
return await client.userinfo<OAuthProfile>(tokens.access_token || '');
|
|
}
|
|
|
|
isAutoRegisterEnabled() {
|
|
return this.config.oauth.autoRegister;
|
|
}
|
|
|
|
asUser(profile: OAuthProfile) {
|
|
return {
|
|
firstName: profile.given_name || '',
|
|
lastName: profile.family_name || '',
|
|
email: profile.email,
|
|
oauthId: profile.sub,
|
|
};
|
|
}
|
|
|
|
async getLogoutEndpoint(): Promise<string | null> {
|
|
if (!this.config.oauth.enabled) {
|
|
return null;
|
|
}
|
|
return (await this.getClient()).issuer.metadata.end_session_endpoint || null;
|
|
}
|
|
|
|
private async getClient() {
|
|
const { enabled, clientId, clientSecret, issuerUrl } = this.config.oauth;
|
|
|
|
if (!enabled) {
|
|
throw new BadRequestException('OAuth2 is not enabled');
|
|
}
|
|
|
|
const metadata: ClientMetadata = {
|
|
client_id: clientId,
|
|
client_secret: clientSecret,
|
|
response_types: ['code'],
|
|
};
|
|
|
|
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];
|
|
}
|
|
|
|
return new issuer.Client(metadata);
|
|
}
|
|
|
|
private normalize(redirectUri: string) {
|
|
const isMobile = redirectUri === MOBILE_REDIRECT;
|
|
const { mobileRedirectUri, mobileOverrideEnabled } = this.config.oauth;
|
|
if (isMobile && mobileOverrideEnabled && mobileRedirectUri) {
|
|
return mobileRedirectUri;
|
|
}
|
|
return redirectUri;
|
|
}
|
|
}
|