mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 18:19:10 +03:00
feat: add oauth2 code verifier
* fix: ensure oauth state param matches before finishing oauth flow Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * chore: upgrade openid-client to v6 Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * feat: use PKCE for oauth2 on supported clients Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * feat: use state and PKCE in mobile app Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * fix: remove obsolete oauth repository init Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * fix: rewrite callback url if mobile redirect url is enabled Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * fix: propagate oidc client error cause when oauth callback fails Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * fix: adapt auth service tests to required state and PKCE params Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * fix: update sdk types Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * fix: adapt oauth e2e test to work with PKCE Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> * fix: allow insecure (http) oauth clients Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> --------- Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com> Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
committed by
Zack Pollard
parent
13d6bd67b1
commit
b7a0cf2470
@@ -1,5 +1,5 @@
|
||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { custom, generators, Issuer, UserinfoResponse } from 'openid-client';
|
||||
import type { UserInfoResponse } from 'openid-client' with { 'resolution-mode': 'import' };
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export type OAuthConfig = {
|
||||
@@ -12,7 +12,7 @@ export type OAuthConfig = {
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
};
|
||||
export type OAuthProfile = UserinfoResponse;
|
||||
export type OAuthProfile = UserInfoResponse;
|
||||
|
||||
@Injectable()
|
||||
export class OAuthRepository {
|
||||
@@ -20,30 +20,47 @@ export class OAuthRepository {
|
||||
this.logger.setContext(OAuthRepository.name);
|
||||
}
|
||||
|
||||
init() {
|
||||
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
async authorize(config: OAuthConfig, redirectUrl: string) {
|
||||
async authorize(config: OAuthConfig, redirectUrl: string, state?: string, codeChallenge?: string) {
|
||||
const { buildAuthorizationUrl, randomState, randomPKCECodeVerifier, calculatePKCECodeChallenge } = await import(
|
||||
'openid-client'
|
||||
);
|
||||
const client = await this.getClient(config);
|
||||
return client.authorizationUrl({
|
||||
state ??= randomState();
|
||||
let codeVerifier: string | null;
|
||||
if (codeChallenge) {
|
||||
codeVerifier = null;
|
||||
} else {
|
||||
codeVerifier = randomPKCECodeVerifier();
|
||||
codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||
}
|
||||
const url = buildAuthorizationUrl(client, {
|
||||
redirect_uri: redirectUrl,
|
||||
scope: config.scope,
|
||||
state: generators.state(),
|
||||
});
|
||||
state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
}).toString();
|
||||
return { url, state, codeVerifier };
|
||||
}
|
||||
|
||||
async getLogoutEndpoint(config: OAuthConfig) {
|
||||
const client = await this.getClient(config);
|
||||
return client.issuer.metadata.end_session_endpoint;
|
||||
return client.serverMetadata().end_session_endpoint;
|
||||
}
|
||||
|
||||
async getProfile(config: OAuthConfig, url: string, redirectUrl: string): Promise<OAuthProfile> {
|
||||
async getProfile(
|
||||
config: OAuthConfig,
|
||||
url: string,
|
||||
expectedState: string,
|
||||
codeVerifier: string,
|
||||
): Promise<OAuthProfile> {
|
||||
const { authorizationCodeGrant, fetchUserInfo, ...oidc } = await import('openid-client');
|
||||
const client = await this.getClient(config);
|
||||
const params = client.callbackParams(url);
|
||||
const pkceCodeVerifier = client.serverMetadata().supportsPKCE() ? codeVerifier : undefined;
|
||||
|
||||
try {
|
||||
const tokens = await client.callback(redirectUrl, params, { state: params.state });
|
||||
const profile = await client.userinfo<OAuthProfile>(tokens.access_token || '');
|
||||
const tokens = await authorizationCodeGrant(client, new URL(url), { expectedState, pkceCodeVerifier });
|
||||
const profile = await fetchUserInfo(client, tokens.access_token, oidc.skipSubjectCheck);
|
||||
if (!profile.sub) {
|
||||
throw new Error('Unexpected profile response, no `sub`');
|
||||
}
|
||||
@@ -59,6 +76,11 @@ export class OAuthRepository {
|
||||
);
|
||||
}
|
||||
|
||||
if (error.code === 'OAUTH_INVALID_RESPONSE') {
|
||||
this.logger.warn(`Invalid response from authorization server. Cause: ${error.cause?.message}`);
|
||||
throw error.cause;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -83,14 +105,20 @@ export class OAuthRepository {
|
||||
signingAlgorithm,
|
||||
}: OAuthConfig) {
|
||||
try {
|
||||
const issuer = await Issuer.discover(issuerUrl);
|
||||
return new issuer.Client({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
response_types: ['code'],
|
||||
userinfo_signed_response_alg: profileSigningAlgorithm === 'none' ? undefined : profileSigningAlgorithm,
|
||||
id_token_signed_response_alg: signingAlgorithm,
|
||||
});
|
||||
const { allowInsecureRequests, discovery } = await import('openid-client');
|
||||
return await discovery(
|
||||
new URL(issuerUrl),
|
||||
clientId,
|
||||
{
|
||||
client_secret: clientSecret,
|
||||
response_types: ['code'],
|
||||
userinfo_signed_response_alg: profileSigningAlgorithm === 'none' ? undefined : profileSigningAlgorithm,
|
||||
id_token_signed_response_alg: signingAlgorithm,
|
||||
timeout: 30_000,
|
||||
},
|
||||
undefined,
|
||||
{ execute: [allowInsecureRequests] },
|
||||
);
|
||||
} 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 });
|
||||
|
||||
Reference in New Issue
Block a user