refactor(web,server): use feature flags for oauth (#3928)

* refactor: oauth to use feature flags

* chore: open api

* chore: e2e test for authorize endpoint
This commit is contained in:
Jason Rasmussen
2023-09-01 07:08:42 -04:00
committed by GitHub
parent c7d53a5006
commit a26ed3d1a6
26 changed files with 660 additions and 110 deletions

View File

@@ -2477,6 +2477,37 @@
]
}
},
"/oauth/authorize": {
"post": {
"operationId": "authorizeOAuth",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthConfigDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthAuthorizeResponseDto"
}
}
},
"description": ""
}
},
"tags": [
"OAuth"
]
}
},
"/oauth/callback": {
"post": {
"operationId": "callback",
@@ -2510,6 +2541,8 @@
},
"/oauth/config": {
"post": {
"deprecated": true,
"description": "@deprecated use feature flags and /oauth/authorize",
"operationId": "generateConfig",
"parameters": [],
"requestBody": {
@@ -6202,6 +6235,17 @@
],
"type": "string"
},
"OAuthAuthorizeResponseDto": {
"properties": {
"url": {
"type": "string"
}
},
"required": [
"url"
],
"type": "object"
},
"OAuthCallbackDto": {
"properties": {
"url": {

View File

@@ -1,5 +1,12 @@
import { SystemConfig, UserEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import {
BadRequestException,
Inject,
Injectable,
InternalServerErrorException,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import cookieParser from 'cookie';
import { IncomingHttpHeaders } from 'http';
import { DateTime } from 'luxon';
@@ -27,6 +34,7 @@ import {
mapAdminSignupResponse,
mapLoginResponse,
mapUserToken,
OAuthAuthorizeResponseDto,
OAuthConfigResponseDto,
} from './response-dto';
import { IUserTokenRepository } from './user-token.repository';
@@ -201,6 +209,22 @@ export class AuthService {
return { ...response, buttonText, url, autoLaunch };
}
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
const config = await this.configCore.getConfig();
if (!config.oauth.enabled) {
throw new BadRequestException('OAuth is not enabled');
}
const client = await this.getOAuthClient(config);
const url = await client.authorizationUrl({
redirect_uri: this.normalize(config, dto.redirectUri),
scope: config.oauth.scope,
state: generators.state(),
});
return { url };
}
async callback(
dto: OAuthCallbackDto,
loginDetails: LoginDetails,
@@ -280,8 +304,13 @@ export class AuthService {
const redirectUri = this.normalize(config, url.split('?')[0]);
const client = await this.getOAuthClient(config);
const params = client.callbackParams(url);
const tokens = await client.callback(redirectUri, params, { state: params.state });
return client.userinfo<OAuthProfile>(tokens.access_token || '');
try {
const tokens = await client.callback(redirectUri, params, { state: params.state });
return client.userinfo<OAuthProfile>(tokens.access_token || '');
} catch (error: Error | any) {
this.logger.error(`Unable to complete OAuth login: ${error}`, error?.stack);
throw new InternalServerErrorException(`Unable to complete OAuth login: ${error}`, { cause: error });
}
}
private async getOAuthClient(config: SystemConfig) {

View File

@@ -1,9 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class OAuthConfigDto {
@IsNotEmpty()
@IsString()
@ApiProperty()
redirectUri!: string;
}

View File

@@ -5,3 +5,7 @@ export class OAuthConfigResponseDto {
buttonText?: string;
autoLaunch?: boolean;
}
export class OAuthAuthorizeResponseDto {
url!: string;
}

View File

@@ -3,6 +3,7 @@ import {
AuthUserDto,
LoginDetails,
LoginResponseDto,
OAuthAuthorizeResponseDto,
OAuthCallbackDto,
OAuthConfigDto,
OAuthConfigResponseDto,
@@ -31,12 +32,19 @@ export class OAuthController {
};
}
/** @deprecated use feature flags and /oauth/authorize */
@PublicRoute()
@Post('config')
generateConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
return this.service.generateConfig(dto);
}
@PublicRoute()
@Post('authorize')
authorizeOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
return this.service.authorize(dto);
}
@PublicRoute()
@Post('callback')
async callback(

View File

@@ -0,0 +1,42 @@
import { AppModule, OAuthController } from '@app/immich';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { errorStub } from '../fixtures';
import { api, db } from '../test-utils';
describe(`${OAuthController.name} (e2e)`, () => {
let app: INestApplication;
let server: any;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = await moduleFixture.createNestApplication().init();
server = app.getHttpServer();
});
beforeEach(async () => {
await db.reset();
await api.adminSignUp(server);
});
afterAll(async () => {
await db.disconnect();
await app.close();
});
describe('POST /oauth/authorize', () => {
beforeEach(async () => {
await db.reset();
});
it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(server).post('/oauth/authorize').send({});
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest);
});
});
});