1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

refactor(server): api key auth (#3054)

This commit is contained in:
Jason Rasmussen 2023-06-30 21:49:30 -04:00 committed by GitHub
parent f9671dfbf7
commit 399312ead3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 53 additions and 66 deletions

View File

@ -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,
};
}

View File

@ -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<AuthUserDto | null> {
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');
}
}

View File

@ -12,3 +12,15 @@ export class APIKeyUpdateDto {
@IsNotEmpty() @IsNotEmpty()
name!: string; name!: string;
} }
export class APIKeyCreateResponseDto {
secret!: string;
apiKey!: APIKeyResponseDto;
}
export class APIKeyResponseDto {
id!: string;
name!: string;
createdAt!: Date;
updatedAt!: Date;
}

View File

@ -56,6 +56,7 @@ describe(APIKeyService.name, () => {
it('should update a key', async () => { it('should update a key', async () => {
keyMock.getById.mockResolvedValue(keyStub.admin); keyMock.getById.mockResolvedValue(keyStub.admin);
keyMock.update.mockResolvedValue(keyStub.admin);
await sut.update(authStub.admin, 'random-guid', { name: 'New Name' }); await sut.update(authStub.admin, 'random-guid', { name: 'New Name' });

View File

@ -1,8 +1,8 @@
import { APIKeyEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AuthUserDto } from '../auth'; import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto'; import { ICryptoRepository } from '../crypto';
import { APIKeyCreateResponseDto, APIKeyResponseDto, mapKey } from './api-key-response.dto'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto';
import { APIKeyCreateDto } from './api-key.dto';
import { IKeyRepository } from './api-key.repository'; import { IKeyRepository } from './api-key.repository';
@Injectable() @Injectable()
@ -20,7 +20,7 @@ export class APIKeyService {
userId: authUser.id, userId: authUser.id,
}); });
return { secret, apiKey: mapKey(entity) }; return { secret, apiKey: this.map(entity) };
} }
async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> { async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> {
@ -29,9 +29,9 @@ export class APIKeyService {
throw new BadRequestException('API Key not found'); throw new BadRequestException('API Key not found');
} }
return this.repository.update(authUser.id, id, { const key = await this.repository.update(authUser.id, id, { name: dto.name });
name: dto.name,
}); return this.map(key);
} }
async delete(authUser: AuthUserDto, id: string): Promise<void> { async delete(authUser: AuthUserDto, id: string): Promise<void> {
@ -48,11 +48,20 @@ export class APIKeyService {
if (!key) { if (!key) {
throw new BadRequestException('API Key not found'); throw new BadRequestException('API Key not found');
} }
return mapKey(key); return this.map(key);
} }
async getAll(authUser: AuthUserDto): Promise<APIKeyResponseDto[]> { async getAll(authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
const keys = await this.repository.getByUserId(authUser.id); 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,
};
} }
} }

View File

@ -1,4 +1,3 @@
export * from './api-key-response.dto';
export * from './api-key.dto'; export * from './api-key.dto';
export * from './api-key.repository'; export * from './api-key.repository';
export * from './api-key.service'; export * from './api-key.service';

View File

@ -10,7 +10,6 @@ import {
import cookieParser from 'cookie'; import cookieParser from 'cookie';
import { IncomingHttpHeaders } from 'http'; import { IncomingHttpHeaders } from 'http';
import { IKeyRepository } from '../api-key'; import { IKeyRepository } from '../api-key';
import { APIKeyCore } from '../api-key/api-key.core';
import { ICryptoRepository } from '../crypto/crypto.repository'; import { ICryptoRepository } from '../crypto/crypto.repository';
import { OAuthCore } from '../oauth/oauth.core'; import { OAuthCore } from '../oauth/oauth.core';
import { ISharedLinkRepository } from '../shared-link'; import { ISharedLinkRepository } from '../shared-link';
@ -35,17 +34,16 @@ export class AuthService {
private authCore: AuthCore; private authCore: AuthCore;
private oauthCore: OAuthCore; private oauthCore: OAuthCore;
private userCore: UserCore; private userCore: UserCore;
private keyCore: APIKeyCore;
private logger = new Logger(AuthService.name); private logger = new Logger(AuthService.name);
constructor( constructor(
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IUserRepository) userRepository: IUserRepository, @Inject(IUserRepository) userRepository: IUserRepository,
@Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository, @Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository, @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
@Inject(IKeyRepository) keyRepository: IKeyRepository, @Inject(IKeyRepository) private keyRepository: IKeyRepository,
@Inject(INITIAL_SYSTEM_CONFIG) @Inject(INITIAL_SYSTEM_CONFIG)
initialConfig: SystemConfig, initialConfig: SystemConfig,
) { ) {
@ -53,7 +51,6 @@ export class AuthService {
this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig); this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
this.oauthCore = new OAuthCore(configRepository, initialConfig); this.oauthCore = new OAuthCore(configRepository, initialConfig);
this.userCore = new UserCore(userRepository, cryptoRepository); this.userCore = new UserCore(userRepository, cryptoRepository);
this.keyCore = new APIKeyCore(cryptoRepository, keyRepository);
} }
public async login( public async login(
@ -153,7 +150,7 @@ export class AuthService {
} }
if (apiKey) { if (apiKey) {
return this.keyCore.validate(apiKey); return this.validateApiKey(apiKey);
} }
throw new UnauthorizedException('Authentication required'); throw new UnauthorizedException('Authentication required');
@ -192,7 +189,7 @@ export class AuthService {
return cookies[IMMICH_ACCESS_COOKIE] || null; return cookies[IMMICH_ACCESS_COOKIE] || null;
} }
async validateSharedLink(key: string | string[]): Promise<AuthUserDto | null> { private async validateSharedLink(key: string | string[]): Promise<AuthUserDto> {
key = Array.isArray(key) ? key[0] : key; key = Array.isArray(key) ? key[0] : key;
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url'); const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
@ -216,4 +213,23 @@ export class AuthService {
} }
throw new UnauthorizedException('Invalid share key'); throw new UnauthorizedException('Invalid share key');
} }
private async validateApiKey(key: string): Promise<AuthUserDto> {
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');
}
} }