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:
Tin Pecirep 2025-04-23 16:05:00 +02:00 committed by Zack Pollard
parent 13d6bd67b1
commit b7a0cf2470
18 changed files with 469 additions and 192 deletions

View file

@ -29,17 +29,35 @@ export class OAuthController {
}
@Post('authorize')
startOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
return this.service.authorize(dto);
async startOAuth(
@Body() dto: OAuthConfigDto,
@Res({ passthrough: true }) res: Response,
@GetLoginDetails() loginDetails: LoginDetails,
): Promise<OAuthAuthorizeResponseDto> {
const { url, state, codeVerifier } = await this.service.authorize(dto);
return respondWithCookie(
res,
{ url },
{
isSecure: loginDetails.isSecure,
values: [
{ key: ImmichCookie.OAUTH_STATE, value: state },
{ key: ImmichCookie.OAUTH_CODE_VERIFIER, value: codeVerifier },
],
},
);
}
@Post('callback')
async finishOAuth(
@Req() request: Request,
@Res({ passthrough: true }) res: Response,
@Body() dto: OAuthCallbackDto,
@GetLoginDetails() loginDetails: LoginDetails,
): Promise<LoginResponseDto> {
const body = await this.service.callback(dto, loginDetails);
const body = await this.service.callback(dto, request.headers, loginDetails);
res.clearCookie(ImmichCookie.OAUTH_STATE);
res.clearCookie(ImmichCookie.OAUTH_CODE_VERIFIER);
return respondWithCookie(res, body, {
isSecure: loginDetails.isSecure,
values: [
@ -52,8 +70,12 @@ export class OAuthController {
@Post('link')
@Authenticated()
linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
return this.service.link(auth, dto);
linkOAuthAccount(
@Req() request: Request,
@Auth() auth: AuthDto,
@Body() dto: OAuthCallbackDto,
): Promise<UserAdminResponseDto> {
return this.service.link(auth, dto, request.headers);
}
@Post('unlink')