From 399312ead3c5433f482b94ddf10daee7ac6bbd16 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 30 Jun 2023 21:49:30 -0400 Subject: [PATCH] refactor(server): api key auth (#3054) --- .../domain/api-key/api-key-response.dto.ts | 22 -------------- server/src/domain/api-key/api-key.core.ts | 28 ----------------- server/src/domain/api-key/api-key.dto.ts | 12 ++++++++ .../domain/api-key/api-key.service.spec.ts | 1 + server/src/domain/api-key/api-key.service.ts | 25 +++++++++++----- server/src/domain/api-key/index.ts | 1 - server/src/domain/auth/auth.service.ts | 30 ++++++++++++++----- 7 files changed, 53 insertions(+), 66 deletions(-) delete mode 100644 server/src/domain/api-key/api-key-response.dto.ts delete mode 100644 server/src/domain/api-key/api-key.core.ts diff --git a/server/src/domain/api-key/api-key-response.dto.ts b/server/src/domain/api-key/api-key-response.dto.ts deleted file mode 100644 index 541984179c..0000000000 --- a/server/src/domain/api-key/api-key-response.dto.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { APIKeyEntity } from '@app/infra/entities'; - -export class APIKeyCreateResponseDto { - secret!: string; - apiKey!: APIKeyResponseDto; -} - -export class APIKeyResponseDto { - id!: string; - name!: string; - createdAt!: Date; - updatedAt!: Date; -} - -export function mapKey(entity: APIKeyEntity): APIKeyResponseDto { - return { - id: entity.id, - name: entity.name, - createdAt: entity.createdAt, - updatedAt: entity.updatedAt, - }; -} diff --git a/server/src/domain/api-key/api-key.core.ts b/server/src/domain/api-key/api-key.core.ts deleted file mode 100644 index 1b075a9c09..0000000000 --- a/server/src/domain/api-key/api-key.core.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { AuthUserDto } from '../auth'; -import { ICryptoRepository } from '../crypto'; -import { IKeyRepository } from './api-key.repository'; - -@Injectable() -export class APIKeyCore { - constructor(private crypto: ICryptoRepository, private repository: IKeyRepository) {} - - async validate(token: string): Promise { - const hashedToken = this.crypto.hashSha256(token); - const keyEntity = await this.repository.getKey(hashedToken); - if (keyEntity?.user) { - const user = keyEntity.user; - - return { - id: user.id, - email: user.email, - isAdmin: user.isAdmin, - isPublicUser: false, - isAllowUpload: true, - externalPath: user.externalPath, - }; - } - - throw new UnauthorizedException('Invalid API key'); - } -} diff --git a/server/src/domain/api-key/api-key.dto.ts b/server/src/domain/api-key/api-key.dto.ts index 5482ba31bb..06309b8dc0 100644 --- a/server/src/domain/api-key/api-key.dto.ts +++ b/server/src/domain/api-key/api-key.dto.ts @@ -12,3 +12,15 @@ export class APIKeyUpdateDto { @IsNotEmpty() name!: string; } + +export class APIKeyCreateResponseDto { + secret!: string; + apiKey!: APIKeyResponseDto; +} + +export class APIKeyResponseDto { + id!: string; + name!: string; + createdAt!: Date; + updatedAt!: Date; +} diff --git a/server/src/domain/api-key/api-key.service.spec.ts b/server/src/domain/api-key/api-key.service.spec.ts index 1e33d9a6b2..eaf2ae38f4 100644 --- a/server/src/domain/api-key/api-key.service.spec.ts +++ b/server/src/domain/api-key/api-key.service.spec.ts @@ -56,6 +56,7 @@ describe(APIKeyService.name, () => { it('should update a key', async () => { keyMock.getById.mockResolvedValue(keyStub.admin); + keyMock.update.mockResolvedValue(keyStub.admin); await sut.update(authStub.admin, 'random-guid', { name: 'New Name' }); diff --git a/server/src/domain/api-key/api-key.service.ts b/server/src/domain/api-key/api-key.service.ts index bba778a78d..34fe86e69f 100644 --- a/server/src/domain/api-key/api-key.service.ts +++ b/server/src/domain/api-key/api-key.service.ts @@ -1,8 +1,8 @@ +import { APIKeyEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AuthUserDto } from '../auth'; import { ICryptoRepository } from '../crypto'; -import { APIKeyCreateResponseDto, APIKeyResponseDto, mapKey } from './api-key-response.dto'; -import { APIKeyCreateDto } from './api-key.dto'; +import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto'; import { IKeyRepository } from './api-key.repository'; @Injectable() @@ -20,7 +20,7 @@ export class APIKeyService { userId: authUser.id, }); - return { secret, apiKey: mapKey(entity) }; + return { secret, apiKey: this.map(entity) }; } async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise { @@ -29,9 +29,9 @@ export class APIKeyService { throw new BadRequestException('API Key not found'); } - return this.repository.update(authUser.id, id, { - name: dto.name, - }); + const key = await this.repository.update(authUser.id, id, { name: dto.name }); + + return this.map(key); } async delete(authUser: AuthUserDto, id: string): Promise { @@ -48,11 +48,20 @@ export class APIKeyService { if (!key) { throw new BadRequestException('API Key not found'); } - return mapKey(key); + return this.map(key); } async getAll(authUser: AuthUserDto): Promise { const keys = await this.repository.getByUserId(authUser.id); - return keys.map(mapKey); + return keys.map((key) => this.map(key)); + } + + private map(entity: APIKeyEntity): APIKeyResponseDto { + return { + id: entity.id, + name: entity.name, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; } } diff --git a/server/src/domain/api-key/index.ts b/server/src/domain/api-key/index.ts index 181110c5d9..5fa9e5723f 100644 --- a/server/src/domain/api-key/index.ts +++ b/server/src/domain/api-key/index.ts @@ -1,4 +1,3 @@ -export * from './api-key-response.dto'; export * from './api-key.dto'; export * from './api-key.repository'; export * from './api-key.service'; diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index 87df987d9e..4f45dc5a22 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -10,7 +10,6 @@ import { import cookieParser from 'cookie'; import { IncomingHttpHeaders } from 'http'; import { IKeyRepository } from '../api-key'; -import { APIKeyCore } from '../api-key/api-key.core'; import { ICryptoRepository } from '../crypto/crypto.repository'; import { OAuthCore } from '../oauth/oauth.core'; import { ISharedLinkRepository } from '../shared-link'; @@ -35,17 +34,16 @@ export class AuthService { private authCore: AuthCore; private oauthCore: OAuthCore; private userCore: UserCore; - private keyCore: APIKeyCore; private logger = new Logger(AuthService.name); constructor( - @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, + @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IUserRepository) userRepository: IUserRepository, @Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository, @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository, - @Inject(IKeyRepository) keyRepository: IKeyRepository, + @Inject(IKeyRepository) private keyRepository: IKeyRepository, @Inject(INITIAL_SYSTEM_CONFIG) initialConfig: SystemConfig, ) { @@ -53,7 +51,6 @@ export class AuthService { this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig); this.oauthCore = new OAuthCore(configRepository, initialConfig); this.userCore = new UserCore(userRepository, cryptoRepository); - this.keyCore = new APIKeyCore(cryptoRepository, keyRepository); } public async login( @@ -153,7 +150,7 @@ export class AuthService { } if (apiKey) { - return this.keyCore.validate(apiKey); + return this.validateApiKey(apiKey); } throw new UnauthorizedException('Authentication required'); @@ -192,7 +189,7 @@ export class AuthService { return cookies[IMMICH_ACCESS_COOKIE] || null; } - async validateSharedLink(key: string | string[]): Promise { + private async validateSharedLink(key: string | string[]): Promise { key = Array.isArray(key) ? key[0] : key; const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url'); @@ -216,4 +213,23 @@ export class AuthService { } throw new UnauthorizedException('Invalid share key'); } + + private async validateApiKey(key: string): Promise { + const hashedKey = this.cryptoRepository.hashSha256(key); + const keyEntity = await this.keyRepository.getKey(hashedKey); + if (keyEntity?.user) { + const user = keyEntity.user; + + return { + id: user.id, + email: user.email, + isAdmin: user.isAdmin, + isPublicUser: false, + isAllowUpload: true, + externalPath: user.externalPath, + }; + } + + throw new UnauthorizedException('Invalid API key'); + } }