1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02:00

refactor(server): auth guard (#1472)

* refactor: auth guard

* chore: move auth guard to middleware

* chore: tests

* chore: remove unused code

* fix: migration to uuid without dataloss

* chore: e2e tests

* chore: removed unused guards
This commit is contained in:
Jason Rasmussen 2023-01-31 13:11:49 -05:00 committed by GitHub
parent 68af4cd5ba
commit d2a9363fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 331 additions and 505 deletions

View File

@ -19,7 +19,7 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
async handleConnection(client: Socket) {
try {
this.logger.log(`New websocket connection: ${client.id}`);
const user = await this.authService.validate(client.request.headers);
const user = await this.authService.validate(client.request.headers, {});
if (user) {
client.join(user.id);
} else {

View File

@ -21,9 +21,8 @@ import {
SystemConfigController,
UserController,
} from './controllers';
import { PublicShareStrategy } from './modules/immich-auth/strategies/public-share.strategy';
import { APIKeyStrategy } from './modules/immich-auth/strategies/api-key.strategy';
import { UserAuthStrategy } from './modules/immich-auth/strategies/user-auth.strategy';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './middlewares/auth.guard';
@Module({
imports: [
@ -61,7 +60,7 @@ import { UserAuthStrategy } from './modules/immich-auth/strategies/user-auth.str
SystemConfigController,
UserController,
],
providers: [UserAuthStrategy, APIKeyStrategy, PublicShareStrategy],
providers: [{ provide: APP_GUARD, useExisting: AuthGuard }, AuthGuard],
})
export class AppModule implements NestModule {
// TODO: check if consumer is needed or remove

View File

@ -1,25 +1,28 @@
import { UseGuards } from '@nestjs/common';
import { AdminRolesGuard } from '../middlewares/admin-role-guard.middleware';
import { RouteNotSharedGuard } from '../middlewares/route-not-shared-guard.middleware';
import { AuthGuard } from '../modules/immich-auth/guards/auth.guard';
import { applyDecorators, SetMetadata } from '@nestjs/common';
interface AuthenticatedOptions {
admin?: boolean;
isShared?: boolean;
}
export enum Metadata {
AUTH_ROUTE = 'auth_route',
ADMIN_ROUTE = 'admin_route',
SHARED_ROUTE = 'shared_route',
}
export const Authenticated = (options?: AuthenticatedOptions) => {
const guards: Parameters<typeof UseGuards> = [AuthGuard];
const decorators = [SetMetadata(Metadata.AUTH_ROUTE, true)];
options = options || {};
if (options.admin) {
guards.push(AdminRolesGuard);
decorators.push(SetMetadata(Metadata.ADMIN_ROUTE, true));
}
if (!options.isShared) {
guards.push(RouteNotSharedGuard);
if (options.isShared) {
decorators.push(SetMetadata(Metadata.SHARED_ROUTE, true));
}
return UseGuards(...guards);
return applyDecorators(...decorators);
};

View File

@ -1,23 +0,0 @@
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { Request } from 'express';
import { UserResponseDto } from '@app/domain';
interface UserRequest extends Request {
user: UserResponseDto;
}
@Injectable()
export class AdminRolesGuard implements CanActivate {
logger = new Logger(AdminRolesGuard.name);
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<UserRequest>();
const isAdmin = request.user?.isAdmin || false;
if (!isAdmin) {
this.logger.log(`Denied access to admin only route: ${request.path}`);
return false;
}
return true;
}
}

View File

@ -0,0 +1,46 @@
import { AuthService } from '@app/domain';
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { Metadata } from '../decorators/authenticated.decorator';
@Injectable()
export class AuthGuard implements CanActivate {
private logger = new Logger(AuthGuard.name);
constructor(private reflector: Reflector, private authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const targets = [context.getHandler(), context.getClass()];
const isAuthRoute = this.reflector.getAllAndOverride(Metadata.AUTH_ROUTE, targets);
const isAdminRoute = this.reflector.getAllAndOverride(Metadata.ADMIN_ROUTE, targets);
const isSharedRoute = this.reflector.getAllAndOverride(Metadata.SHARED_ROUTE, targets);
if (!isAuthRoute) {
return true;
}
const req = context.switchToHttp().getRequest<Request>();
const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>);
if (!authDto) {
this.logger.warn(`Denied access to authenticated route: ${req.path}`);
return false;
}
if (authDto.isPublicUser && !isSharedRoute) {
this.logger.warn(`Denied access to non-shared route: ${req.path}`);
return false;
}
if (isAdminRoute && !authDto.isAdmin) {
this.logger.warn(`Denied access to admin only route: ${req.path}`);
return false;
}
req.user = authDto;
return true;
}
}

View File

@ -1,21 +0,0 @@
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { Request } from 'express';
import { AuthUserDto } from '../decorators/auth-user.decorator';
@Injectable()
export class RouteNotSharedGuard implements CanActivate {
logger = new Logger(RouteNotSharedGuard.name);
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const user = request.user as AuthUserDto;
// Inverse logic - I know it is weird
if (user.isPublicUser) {
this.logger.warn(`Denied attempt to access non-shared route: ${request.path}`);
return false;
}
return true;
}
}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard as PassportAuthGuard } from '@nestjs/passport';
import { API_KEY_STRATEGY } from '../strategies/api-key.strategy';
import { AUTH_COOKIE_STRATEGY } from '../strategies/user-auth.strategy';
import { PUBLIC_SHARE_STRATEGY } from '../strategies/public-share.strategy';
@Injectable()
export class AuthGuard extends PassportAuthGuard([PUBLIC_SHARE_STRATEGY, AUTH_COOKIE_STRATEGY, API_KEY_STRATEGY]) {}

View File

@ -1,21 +0,0 @@
import { APIKeyService, AuthUserDto } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { IStrategyOptions, Strategy } from 'passport-http-header-strategy';
export const API_KEY_STRATEGY = 'api-key';
const options: IStrategyOptions = {
header: 'x-api-key',
};
@Injectable()
export class APIKeyStrategy extends PassportStrategy(Strategy, API_KEY_STRATEGY) {
constructor(private apiKeyService: APIKeyService) {
super(options);
}
validate(token: string): Promise<AuthUserDto | null> {
return this.apiKeyService.validate(token);
}
}

View File

@ -1,22 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { IStrategyOptions, Strategy } from 'passport-http-header-strategy';
import { AuthUserDto, ShareService } from '@app/domain';
export const PUBLIC_SHARE_STRATEGY = 'public-share';
const options: IStrategyOptions = {
header: 'x-immich-share-key',
param: 'key',
};
@Injectable()
export class PublicShareStrategy extends PassportStrategy(Strategy, PUBLIC_SHARE_STRATEGY) {
constructor(private shareService: ShareService) {
super(options);
}
validate(key: string): Promise<AuthUserDto | null> {
return this.shareService.validate(key);
}
}

View File

@ -1,18 +0,0 @@
import { AuthService, AuthUserDto } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { Strategy } from 'passport-custom';
export const AUTH_COOKIE_STRATEGY = 'auth-cookie';
@Injectable()
export class UserAuthStrategy extends PassportStrategy(Strategy, AUTH_COOKIE_STRATEGY) {
constructor(private authService: AuthService) {
super();
}
validate(request: Request): Promise<AuthUserDto | null> {
return this.authService.validate(request.headers);
}
}

View File

@ -2,11 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { clearDb, getAuthUser, authCustom } from './test-utils';
import { InfraModule } from '@app/infra';
import { AlbumModule } from '../src/api-v1/album/album.module';
import { CreateAlbumDto } from '../src/api-v1/album/dto/create-album.dto';
import { AuthUserDto } from '../src/decorators/auth-user.decorator';
import { AuthService, DomainModule, UserService } from '@app/domain';
import { AuthService, UserService } from '@app/domain';
import { DataSource } from 'typeorm';
import { AppModule } from '../src/app.module';
@ -20,9 +18,7 @@ describe('Album', () => {
describe('without auth', () => {
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [DomainModule.register({ imports: [InfraModule] }), AppModule],
}).compile();
const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = moduleFixture.createNestApplication();
database = app.get(DataSource);
@ -46,9 +42,7 @@ describe('Album', () => {
let authService: AuthService;
beforeAll(async () => {
const builder = Test.createTestingModule({
imports: [DomainModule.register({ imports: [InfraModule] }), AlbumModule],
});
const builder = Test.createTestingModule({ imports: [AppModule] });
authUser = getAuthUser(); // set default auth user
const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile();

View File

@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext } from '@nestjs/common';
import { TestingModuleBuilder } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import { AuthUserDto } from '../src/decorators/auth-user.decorator';
import { AuthGuard } from '../src/modules/immich-auth/guards/auth.guard';
import { AuthGuard } from '../src/middlewares/auth.guard';
type CustomAuthCallback = () => AuthUserDto;
@ -34,5 +34,5 @@ export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCa
return true;
},
};
return builder.overrideGuard(AuthGuard).useValue(canActivate);
return builder.overrideProvider(AuthGuard).useValue(canActivate);
}

View File

@ -2,10 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { clearDb, authCustom } from './test-utils';
import { InfraModule } from '@app/infra';
import { DomainModule, CreateUserDto, UserService, AuthUserDto } from '@app/domain';
import { CreateUserDto, UserService, AuthUserDto } from '@app/domain';
import { DataSource } from 'typeorm';
import { UserController } from '../src/controllers';
import { AuthService } from '@app/domain';
import { AppModule } from '../src/app.module';
@ -24,10 +22,7 @@ describe('User', () => {
describe('without auth', () => {
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [DomainModule.register({ imports: [InfraModule] }), AppModule],
controllers: [UserController],
}).compile();
const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = moduleFixture.createNestApplication();
database = app.get(DataSource);
@ -50,10 +45,7 @@ describe('User', () => {
let authUser: AuthUserDto;
beforeAll(async () => {
const builder = Test.createTestingModule({
imports: [DomainModule.register({ imports: [InfraModule] })],
controllers: [UserController],
});
const builder = Test.createTestingModule({ imports: [AppModule] });
const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile();
app = moduleFixture.createNestApplication();

View File

@ -0,0 +1,27 @@
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,
};
}
throw new UnauthorizedException('Invalid API key');
}
}

View File

@ -1,20 +1,9 @@
import { APIKeyEntity } from '@app/infra/db/entities';
import { BadRequestException } from '@nestjs/common';
import { authStub, userEntityStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '../../test';
import { ICryptoRepository } from '../auth';
import { authStub, keyStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '../../test';
import { ICryptoRepository } from '../crypto';
import { IKeyRepository } from './api-key.repository';
import { APIKeyService } from './api-key.service';
const adminKey = Object.freeze({
id: 1,
name: 'My Key',
key: 'my-api-key (hashed)',
userId: authStub.admin.id,
user: userEntityStub.admin,
} as APIKeyEntity);
const token = Buffer.from('my-api-key', 'utf8').toString('base64');
describe(APIKeyService.name, () => {
let sut: APIKeyService;
let keyMock: jest.Mocked<IKeyRepository>;
@ -28,10 +17,8 @@ describe(APIKeyService.name, () => {
describe('create', () => {
it('should create a new key', async () => {
keyMock.create.mockResolvedValue(adminKey);
keyMock.create.mockResolvedValue(keyStub.admin);
await sut.create(authStub.admin, { name: 'Test Key' });
expect(keyMock.create).toHaveBeenCalledWith({
key: 'cmFuZG9tLWJ5dGVz (hashed)',
name: 'Test Key',
@ -42,7 +29,7 @@ describe(APIKeyService.name, () => {
});
it('should not require a name', async () => {
keyMock.create.mockResolvedValue(adminKey);
keyMock.create.mockResolvedValue(keyStub.admin);
await sut.create(authStub.admin, {});
@ -66,7 +53,7 @@ describe(APIKeyService.name, () => {
});
it('should update a key', async () => {
keyMock.getById.mockResolvedValue(adminKey);
keyMock.getById.mockResolvedValue(keyStub.admin);
await sut.update(authStub.admin, 1, { name: 'New Name' });
@ -84,7 +71,7 @@ describe(APIKeyService.name, () => {
});
it('should delete a key', async () => {
keyMock.getById.mockResolvedValue(adminKey);
keyMock.getById.mockResolvedValue(keyStub.admin);
await sut.delete(authStub.admin, 1);
@ -102,7 +89,7 @@ describe(APIKeyService.name, () => {
});
it('should get a key by id', async () => {
keyMock.getById.mockResolvedValue(adminKey);
keyMock.getById.mockResolvedValue(keyStub.admin);
await sut.getById(authStub.admin, 1);
@ -112,29 +99,11 @@ describe(APIKeyService.name, () => {
describe('getAll', () => {
it('should return all the keys for a user', async () => {
keyMock.getByUserId.mockResolvedValue([adminKey]);
keyMock.getByUserId.mockResolvedValue([keyStub.admin]);
await expect(sut.getAll(authStub.admin)).resolves.toHaveLength(1);
expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.id);
});
});
describe('validate', () => {
it('should throw an error for an invalid id', async () => {
keyMock.getKey.mockResolvedValue(null);
await expect(sut.validate(token)).resolves.toBeNull();
expect(keyMock.getKey).toHaveBeenCalledWith('bXktYXBpLWtleQ== (hashed)');
});
it('should validate the token', async () => {
keyMock.getKey.mockResolvedValue(adminKey);
await expect(sut.validate(token)).resolves.toEqual(authStub.admin);
expect(keyMock.getKey).toHaveBeenCalledWith('bXktYXBpLWtleQ== (hashed)');
});
});
});

View File

@ -1,5 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AuthUserDto, ICryptoRepository } from '../auth';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { IKeyRepository } from './api-key.repository';
import { APIKeyCreateDto } from './dto/api-key-create.dto';
import { APIKeyCreateResponseDto } from './response-dto/api-key-create-response.dto';
@ -55,22 +56,4 @@ export class APIKeyService {
const keys = await this.repository.getByUserId(authUser.id);
return keys.map(mapKey);
}
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,
};
}
return null;
}
}

View File

@ -1,12 +1,10 @@
import { SystemConfig, UserEntity } from '@app/infra/db/entities';
import { IncomingHttpHeaders } from 'http';
import { ISystemConfigRepository } from '../system-config';
import { SystemConfigCore } from '../system-config/system-config.core';
import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant';
import { ICryptoRepository } from './crypto.repository';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { LoginResponseDto, mapLoginResponse } from './response-dto';
import { IUserTokenRepository, UserTokenCore } from '@app/domain';
import cookieParser from 'cookie';
import { IUserTokenRepository, UserTokenCore } from '../user-token';
export type JwtValidationResult = {
status: boolean;
@ -59,21 +57,4 @@ export class AuthCore {
}
return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
}
extractTokenFromHeader(headers: IncomingHttpHeaders) {
if (!headers.authorization) {
return this.extractTokenFromCookie(cookieParser.parse(headers.cookie || ''));
}
const [type, accessToken] = headers.authorization.split(' ');
if (type.toLowerCase() !== 'bearer') {
return null;
}
return accessToken;
}
extractTokenFromCookie(cookies: Record<string, string>) {
return cookies?.[IMMICH_ACCESS_COOKIE] || null;
}
}

View File

@ -1,25 +1,34 @@
import { SystemConfig, UserEntity } from '@app/infra/db/entities';
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { IncomingHttpHeaders } from 'http';
import { generators, Issuer } from 'openid-client';
import { Socket } from 'socket.io';
import {
userEntityStub,
authStub,
keyStub,
loginResponseStub,
newCryptoRepositoryMock,
newKeyRepositoryMock,
newSharedLinkRepositoryMock,
newSystemConfigRepositoryMock,
newUserRepositoryMock,
newUserTokenRepositoryMock,
sharedLinkStub,
systemConfigStub,
userEntityStub,
userTokenEntityStub,
} from '../../test';
import { IKeyRepository } from '../api-key';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { ISharedLinkRepository } from '../share';
import { ISystemConfigRepository } from '../system-config';
import { IUserRepository } from '../user';
import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant';
import { IUserTokenRepository } from '../user-token';
import { AuthType } from './auth.constant';
import { AuthService } from './auth.service';
import { ICryptoRepository } from './crypto.repository';
import { SignUpDto } from './dto';
import { IUserTokenRepository } from '@app/domain';
import { newUserTokenRepositoryMock } from '../../test/user-token.repository.mock';
import { IncomingHttpHeaders } from 'http';
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
const email = 'test@immich.com';
const sub = 'my-auth-user-sub';
@ -51,6 +60,8 @@ describe('AuthService', () => {
let userMock: jest.Mocked<IUserRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let userTokenMock: jest.Mocked<IUserTokenRepository>;
let shareMock: jest.Mocked<ISharedLinkRepository>;
let keyMock: jest.Mocked<IKeyRepository>;
let callbackMock: jest.Mock;
let create: (config: SystemConfig) => AuthService;
@ -81,8 +92,10 @@ describe('AuthService', () => {
userMock = newUserRepositoryMock();
configMock = newSystemConfigRepositoryMock();
userTokenMock = newUserTokenRepositoryMock();
shareMock = newSharedLinkRepositoryMock();
keyMock = newKeyRepositoryMock();
create = (config) => new AuthService(cryptoMock, configMock, userMock, userTokenMock, config);
create = (config) => new AuthService(cryptoMock, configMock, userMock, userTokenMock, shareMock, keyMock, config);
sut = create(systemConfigStub.enabled);
});
@ -218,63 +231,73 @@ describe('AuthService', () => {
});
describe('validate - socket connections', () => {
it('should throw token is not provided', async () => {
await expect(sut.validate({}, {})).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should validate using authorization header', async () => {
userMock.get.mockResolvedValue(userEntityStub.user1);
userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken);
const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
await expect(sut.validate((client as Socket).request.headers)).resolves.toEqual(userEntityStub.user1);
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual(userEntityStub.user1);
});
});
describe('validate - api request', () => {
it('should throw if no user is found', async () => {
describe('validate - shared key', () => {
it('should not accept a non-existent key', async () => {
shareMock.getByKey.mockResolvedValue(null);
const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' };
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should not accept an expired key', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' };
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should not accept a key without a user', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
userMock.get.mockResolvedValue(null);
await expect(sut.validate({ email: 'a', userId: 'test' })).resolves.toBeNull();
const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' };
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should accept a valid key', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
userMock.get.mockResolvedValue(userEntityStub.admin);
const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' };
await expect(sut.validate(headers, {})).resolves.toEqual(authStub.adminSharedLink);
});
});
describe('validate - user token', () => {
it('should throw if no token is found', async () => {
userTokenMock.get.mockResolvedValue(null);
const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' };
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should return an auth dto', async () => {
userMock.get.mockResolvedValue(userEntityStub.user1);
userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken);
await expect(
sut.validate({ cookie: 'immich_access_token=auth_token', email: 'a', userId: 'test' }),
).resolves.toEqual(userEntityStub.user1);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual(userEntityStub.user1);
});
});
describe('extractTokenFromHeader - Cookie', () => {
it('should extract the access token', () => {
const cookie: IncomingHttpHeaders = {
cookie: `${IMMICH_ACCESS_COOKIE}=signed-jwt;${IMMICH_AUTH_TYPE_COOKIE}=password`,
};
expect(sut.extractTokenFromHeader(cookie)).toEqual('signed-jwt');
describe('validate - api key', () => {
it('should throw an error if no api key is found', async () => {
keyMock.getKey.mockResolvedValue(null);
const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' };
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
});
it('should work with no cookies', () => {
const cookie: IncomingHttpHeaders = {
cookie: undefined,
};
expect(sut.extractTokenFromHeader(cookie)).toBeNull();
});
it('should work on empty cookies', () => {
const cookie: IncomingHttpHeaders = {
cookie: '',
};
expect(sut.extractTokenFromHeader(cookie)).toBeNull();
});
});
describe('extractTokenFromHeader - Bearer Auth', () => {
it('should extract the access token', () => {
expect(sut.extractTokenFromHeader({ authorization: `Bearer signed-jwt` })).toEqual('signed-jwt');
});
it('should work without the auth header', () => {
expect(sut.extractTokenFromHeader({})).toBeNull();
});
it('should ignore basic auth', () => {
expect(sut.extractTokenFromHeader({ authorization: `Basic stuff` })).toBeNull();
it('should return an auth dto', async () => {
keyMock.getKey.mockResolvedValue(keyStub.admin);
const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual(authStub.admin);
expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
});
});
});

View File

@ -11,12 +11,16 @@ import { IncomingHttpHeaders } from 'http';
import { OAuthCore } from '../oauth/oauth.core';
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
import { IUserRepository, UserCore } from '../user';
import { AuthType } from './auth.constant';
import { AuthType, IMMICH_ACCESS_COOKIE } from './auth.constant';
import { AuthCore } from './auth.core';
import { ICryptoRepository } from './crypto.repository';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from './dto';
import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto';
import { IUserTokenRepository, UserTokenCore } from '@app/domain/user-token';
import { IUserTokenRepository, UserTokenCore } from '../user-token';
import cookieParser from 'cookie';
import { ISharedLinkRepository, ShareCore } from '../share';
import { APIKeyCore } from '../api-key/api-key.core';
import { IKeyRepository } from '../api-key';
@Injectable()
export class AuthService {
@ -24,14 +28,18 @@ export class AuthService {
private authCore: AuthCore;
private oauthCore: OAuthCore;
private userCore: UserCore;
private shareCore: ShareCore;
private keyCore: APIKeyCore;
private logger = new Logger(AuthService.name);
constructor(
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IUserRepository) userRepository: IUserRepository,
@Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
@Inject(ISharedLinkRepository) shareRepository: ISharedLinkRepository,
@Inject(IKeyRepository) keyRepository: IKeyRepository,
@Inject(INITIAL_SYSTEM_CONFIG)
initialConfig: SystemConfig,
) {
@ -39,6 +47,8 @@ export class AuthService {
this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
this.oauthCore = new OAuthCore(configRepository, initialConfig);
this.userCore = new UserCore(userRepository, cryptoRepository);
this.shareCore = new ShareCore(shareRepository, cryptoRepository);
this.keyCore = new APIKeyCore(cryptoRepository, keyRepository);
}
public async login(
@ -115,28 +125,40 @@ export class AuthService {
}
}
public async validate(headers: IncomingHttpHeaders): Promise<AuthUserDto | null> {
const tokenValue = this.extractTokenFromHeader(headers);
if (!tokenValue) {
return null;
public async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthUserDto | null> {
const shareKey = (headers['x-immich-share-key'] || params.key) as string;
const userToken = (headers['x-immich-user-token'] ||
params.userToken ||
this.getBearerToken(headers) ||
this.getCookieToken(headers)) as string;
const apiKey = (headers['x-api-key'] || params.apiKey) as string;
if (shareKey) {
return this.shareCore.validate(shareKey);
}
const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
const user = await this.userTokenCore.getUserByToken(hashedToken);
if (user) {
return {
...user,
isPublicUser: false,
isAllowUpload: true,
isAllowDownload: true,
isShowExif: true,
};
if (userToken) {
return this.userTokenCore.validate(userToken);
}
if (apiKey) {
return this.keyCore.validate(apiKey);
}
throw new UnauthorizedException('Authentication required');
}
private getBearerToken(headers: IncomingHttpHeaders): string | null {
const [type, token] = (headers.authorization || '').split(' ');
if (type.toLowerCase() === 'bearer') {
return token;
}
return null;
}
extractTokenFromHeader(headers: IncomingHttpHeaders) {
return this.authCore.extractTokenFromHeader(headers);
private getCookieToken(headers: IncomingHttpHeaders): string | null {
const cookies = cookieParser.parse(headers.cookie || '');
return cookies[IMMICH_ACCESS_COOKIE] || null;
}
}

View File

@ -1,5 +1,4 @@
export * from './auth.constant';
export * from './auth.service';
export * from './crypto.repository';
export * from './dto';
export * from './response-dto';

View File

@ -0,0 +1 @@
export * from './crypto.repository';

View File

@ -2,6 +2,7 @@ export * from './album';
export * from './api-key';
export * from './asset';
export * from './auth';
export * from './crypto';
export * from './domain.module';
export * from './job';
export * from './oauth';

View File

@ -11,11 +11,11 @@ import {
systemConfigStub,
userTokenEntityStub,
} from '../../test';
import { ICryptoRepository } from '../auth';
import { ICryptoRepository } from '../crypto';
import { OAuthService } from '../oauth';
import { ISystemConfigRepository } from '../system-config';
import { IUserRepository } from '../user';
import { IUserTokenRepository } from '@app/domain';
import { IUserTokenRepository } from '../user-token';
import { newUserTokenRepositoryMock } from '../../test/user-token.repository.mock';
const email = 'user@immich.com';

View File

@ -1,13 +1,14 @@
import { SystemConfig } from '@app/infra/db/entities';
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
import { AuthType, AuthUserDto, ICryptoRepository, LoginResponseDto } from '../auth';
import { AuthType, AuthUserDto, LoginResponseDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { AuthCore } from '../auth/auth.core';
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
import { IUserRepository, UserCore, UserResponseDto } from '../user';
import { OAuthCallbackDto, OAuthConfigDto } from './dto';
import { OAuthCore } from './oauth.core';
import { OAuthConfigResponseDto } from './response-dto';
import { IUserTokenRepository } from '@app/domain/user-token';
import { IUserTokenRepository } from '../user-token';
@Injectable()
export class OAuthService {

View File

@ -1,6 +1,13 @@
import { AssetEntity, SharedLinkEntity } from '@app/infra/db/entities';
import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common';
import { AuthUserDto, ICryptoRepository } from '../auth';
import {
BadRequestException,
ForbiddenException,
InternalServerErrorException,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { CreateSharedLinkDto } from './dto';
import { ISharedLinkRepository } from './shared-link.repository';
@ -17,10 +24,6 @@ export class ShareCore {
return this.repository.get(userId, id);
}
getByKey(key: string): Promise<SharedLinkEntity | null> {
return this.repository.getByKey(key);
}
create(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> {
try {
return this.repository.create({
@ -78,4 +81,26 @@ export class ShareCore {
throw new ForbiddenException();
}
}
async validate(key: string): Promise<AuthUserDto | null> {
const link = await this.repository.getByKey(key);
if (link) {
if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
const user = link.user;
if (user) {
return {
id: user.id,
email: user.email,
isAdmin: user.isAdmin,
isPublicUser: true,
sharedLinkId: link.id,
isAllowUpload: link.allowUpload,
isAllowDownload: link.allowDownload,
isShowExif: link.showExif,
};
}
}
}
throw new UnauthorizedException('Invalid share key');
}
}

View File

@ -1,15 +1,12 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import {
authStub,
userEntityStub,
newCryptoRepositoryMock,
newSharedLinkRepositoryMock,
newUserRepositoryMock,
sharedLinkResponseStub,
sharedLinkStub,
} from '../../test';
import { ICryptoRepository } from '../auth';
import { IUserRepository } from '../user';
import { ICryptoRepository } from '../crypto';
import { ShareService } from './share.service';
import { ISharedLinkRepository } from './shared-link.repository';
@ -17,44 +14,18 @@ describe(ShareService.name, () => {
let sut: ShareService;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let shareMock: jest.Mocked<ISharedLinkRepository>;
let userMock: jest.Mocked<IUserRepository>;
beforeEach(async () => {
cryptoMock = newCryptoRepositoryMock();
shareMock = newSharedLinkRepositoryMock();
userMock = newUserRepositoryMock();
sut = new ShareService(cryptoMock, shareMock, userMock);
sut = new ShareService(cryptoMock, shareMock);
});
it('should work', () => {
expect(sut).toBeDefined();
});
describe('validate', () => {
it('should not accept a non-existant key', async () => {
shareMock.getByKey.mockResolvedValue(null);
await expect(sut.validate('key')).resolves.toBeNull();
});
it('should not accept an expired key', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
await expect(sut.validate('key')).resolves.toBeNull();
});
it('should not accept a key without a user', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
userMock.get.mockResolvedValue(null);
await expect(sut.validate('key')).resolves.toBeNull();
});
it('should accept a valid key', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
userMock.get.mockResolvedValue(userEntityStub.admin);
await expect(sut.validate('key')).resolves.toEqual(authStub.adminSharedLink);
});
});
describe('getAll', () => {
it('should return all keys for a user', async () => {
shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
@ -131,20 +102,6 @@ describe(ShareService.name, () => {
});
});
describe('getByKey', () => {
it('should not work on a missing key', async () => {
shareMock.getByKey.mockResolvedValue(null);
await expect(sut.getByKey('secret-key')).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.getByKey).toHaveBeenCalledWith('secret-key');
});
it('should find a key', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.getByKey('secret-key')).resolves.toEqual(sharedLinkResponseStub.valid);
expect(shareMock.getByKey).toHaveBeenCalledWith('secret-key');
});
});
describe('edit', () => {
it('should not work on a missing key', async () => {
shareMock.get.mockResolvedValue(null);

View File

@ -1,6 +1,6 @@
import { BadRequestException, ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common';
import { AuthUserDto, ICryptoRepository } from '../auth';
import { IUserRepository, UserCore } from '../user';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { EditSharedLinkDto } from './dto';
import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto';
import { ShareCore } from './share.core';
@ -10,37 +10,12 @@ import { ISharedLinkRepository } from './shared-link.repository';
export class ShareService {
readonly logger = new Logger(ShareService.name);
private shareCore: ShareCore;
private userCore: UserCore;
constructor(
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
@Inject(IUserRepository) userRepository: IUserRepository,
) {
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
this.userCore = new UserCore(userRepository, cryptoRepository);
}
async validate(key: string): Promise<AuthUserDto | null> {
const link = await this.shareCore.getByKey(key);
if (link) {
if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
const user = await this.userCore.get(link.userId);
if (user) {
return {
id: user.id,
email: user.email,
isAdmin: user.isAdmin,
isPublicUser: true,
sharedLinkId: link.id,
isAllowUpload: link.allowUpload,
isAllowDownload: link.allowDownload,
isShowExif: link.showExif,
};
}
}
}
return null;
}
async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
@ -74,14 +49,6 @@ export class ShareService {
}
}
async getByKey(key: string): Promise<SharedLinkResponseDto> {
const link = await this.shareCore.getByKey(key);
if (!link) {
throw new BadRequestException('Shared link not found');
}
return mapSharedLink(link);
}
async remove(authUser: AuthUserDto, id: string): Promise<void> {
await this.shareCore.remove(authUser.id, id);
}

View File

@ -6,7 +6,7 @@ export interface ISharedLinkRepository {
getAll(userId: string): Promise<SharedLinkEntity[]>;
get(userId: string, id: string): Promise<SharedLinkEntity | null>;
getByKey(key: string): Promise<SharedLinkEntity | null>;
create(entity: Omit<SharedLinkEntity, 'id'>): Promise<SharedLinkEntity>;
create(entity: Omit<SharedLinkEntity, 'id' | 'user'>): Promise<SharedLinkEntity>;
remove(entity: SharedLinkEntity): Promise<SharedLinkEntity>;
save(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
hasAssetAccess(id: string, assetId: string): Promise<boolean>;

View File

@ -1,12 +1,28 @@
import { UserEntity } from '@app/infra/db/entities';
import { Injectable } from '@nestjs/common';
import { ICryptoRepository } from '../auth';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ICryptoRepository } from '../crypto';
import { IUserTokenRepository } from './user-token.repository';
@Injectable()
export class UserTokenCore {
constructor(private crypto: ICryptoRepository, private repository: IUserTokenRepository) {}
async validate(tokenValue: string) {
const hashedToken = this.crypto.hashSha256(tokenValue);
const user = await this.getUserByToken(hashedToken);
if (user) {
return {
...user,
isPublicUser: false,
isAllowUpload: true,
isAllowDownload: true,
isShowExif: true,
};
}
throw new UnauthorizedException('Invalid user token');
}
public async getUserByToken(tokenValue: string): Promise<UserEntity | null> {
const token = await this.repository.get(tokenValue);
if (token?.user) {

View File

@ -10,7 +10,8 @@ import {
import { hash } from 'bcrypt';
import { constants, createReadStream, ReadStream } from 'fs';
import fs from 'fs/promises';
import { AuthUserDto, ICryptoRepository } from '../auth';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from './dto/create-user.dto';
import { IUserRepository, UserListFilter } from './user.repository';

View File

@ -3,7 +3,8 @@ import { UserEntity } from '@app/infra/db/entities';
import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { when } from 'jest-when';
import { newCryptoRepositoryMock, newUserRepositoryMock } from '../../test';
import { AuthUserDto, ICryptoRepository } from '../auth';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserService } from './user.service';

View File

@ -1,7 +1,8 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { randomBytes } from 'crypto';
import { ReadStream } from 'fs';
import { AuthUserDto, ICryptoRepository } from '../auth';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { IUserRepository } from '../user';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

View File

@ -1,4 +1,5 @@
import {
APIKeyEntity,
AssetType,
SharedLinkEntity,
SharedLinkType,
@ -148,6 +149,16 @@ export const userTokenEntityStub = {
}),
};
export const keyStub = {
admin: Object.freeze({
id: 1,
name: 'My Key',
key: 'my-api-key (hashed)',
userId: authStub.admin.id,
user: userEntityStub.admin,
} as APIKeyEntity),
};
export const systemConfigStub = {
defaults: Object.freeze({
ffmpeg: {
@ -275,6 +286,7 @@ export const sharedLinkStub = {
valid: Object.freeze({
id: '123',
userId: authStub.admin.id,
user: userEntityStub.admin,
key: Buffer.from('secret-key', 'utf8'),
type: SharedLinkType.ALBUM,
createdAt: today.toISOString(),
@ -288,6 +300,7 @@ export const sharedLinkStub = {
expired: Object.freeze({
id: '123',
userId: authStub.admin.id,
user: userEntityStub.admin,
key: Buffer.from('secret-key', 'utf8'),
type: SharedLinkType.ALBUM,
createdAt: today.toISOString(),
@ -300,6 +313,7 @@ export const sharedLinkStub = {
readonly: Object.freeze<SharedLinkEntity>({
id: '123',
userId: authStub.admin.id,
user: userEntityStub.admin,
key: Buffer.from('secret-key', 'utf8'),
type: SharedLinkType.ALBUM,
createdAt: today.toISOString(),

View File

@ -4,4 +4,5 @@ export * from './fixtures';
export * from './job.repository.mock';
export * from './shared-link.repository.mock';
export * from './system-config.repository.mock';
export * from './user-token.repository.mock';
export * from './user.repository.mock';

View File

@ -1,6 +1,7 @@
import { Column, Entity, Index, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { AlbumEntity } from './album.entity';
import { AssetEntity } from './asset.entity';
import { UserEntity } from './user.entity';
@Entity('shared_links')
@Unique('UQ_sharedlink_key', ['key'])
@ -14,6 +15,9 @@ export class SharedLinkEntity {
@Column()
userId!: string;
@ManyToOne(() => UserEntity)
user!: UserEntity;
@Index('IDX_sharedlink_key')
@Column({ type: 'bytea' })
key!: Buffer; // use to access the inidividual asset

View File

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddSharedLinkUserForeignKeyConstraint1674939383309 implements MigrationInterface {
name = 'AddSharedLinkUserForeignKeyConstraint1674939383309';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "shared_links" ALTER COLUMN "userId" TYPE varchar(36)`);
await queryRunner.query(`ALTER TABLE "shared_links" ALTER COLUMN "userId" TYPE uuid using "userId"::uuid`);
await queryRunner.query(
`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340"`);
await queryRunner.query(`ALTER TABLE "shared_links" ALTER COLUMN "userId" TYPE character varying`);
}
}

View File

@ -73,6 +73,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
assetInfo: true,
},
},
user: true,
},
order: {
createdAt: 'DESC',

106
server/package-lock.json generated
View File

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "immich",
"version": "1.42.0",
"version": "1.43.1",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/bull": "^0.6.2",
@ -14,7 +14,6 @@
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.2.1",
"@nestjs/mapped-types": "1.2.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.2.1",
"@nestjs/platform-socket.io": "^9.2.1",
"@nestjs/schedule": "^2.1.0",
@ -46,9 +45,6 @@
"mv": "^2.1.1",
"nest-commander": "^3.3.0",
"openid-client": "^5.2.1",
"passport": "^0.6.0",
"passport-custom": "^1.1.1",
"passport-http-header-strategy": "^1.1.0",
"pg": "^8.8.0",
"redis": "^4.5.1",
"reflect-metadata": "^0.1.13",
@ -1537,15 +1533,6 @@
}
}
},
"node_modules/@nestjs/passport": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz",
"integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==",
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0",
"passport": "^0.4.0 || ^0.5.0 || ^0.6.0"
}
},
"node_modules/@nestjs/platform-express": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.1.tgz",
@ -8869,50 +8856,6 @@
"node": ">= 0.8"
}
},
"node_modules/passport": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-custom": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz",
"integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==",
"dependencies": {
"passport-strategy": "1.x.x"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/passport-http-header-strategy": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz",
"integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==",
"dependencies": {
"passport-strategy": "^1.0.0"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -8964,11 +8907,6 @@
"node": ">=8"
}
},
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
},
"node_modules/pbf": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",
@ -12666,12 +12604,6 @@
"integrity": "sha512-NTFwPZkQWsArQH8QSyFWGZvJ08gR+R4TofglqZoihn/vU+ktHEJjMqsIsADwb7XD97DhiD+TVv5ac+jG33BHrg==",
"requires": {}
},
"@nestjs/passport": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz",
"integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==",
"requires": {}
},
"@nestjs/platform-express": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.1.tgz",
@ -18330,37 +18262,6 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"passport": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
}
},
"passport-custom": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz",
"integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-http-header-strategy": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz",
"integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==",
"requires": {
"passport-strategy": "^1.0.0"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -18400,11 +18301,6 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
},
"pbf": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",

View File

@ -43,7 +43,6 @@
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.2.1",
"@nestjs/mapped-types": "1.2.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.2.1",
"@nestjs/platform-socket.io": "^9.2.1",
"@nestjs/schedule": "^2.1.0",
@ -75,9 +74,6 @@
"mv": "^2.1.1",
"nest-commander": "^3.3.0",
"openid-client": "^5.2.1",
"passport": "^0.6.0",
"passport-custom": "^1.1.1",
"passport-http-header-strategy": "^1.1.0",
"pg": "^8.8.0",
"redis": "^4.5.1",
"reflect-metadata": "^0.1.13",
@ -147,10 +143,10 @@
"statements": 20
},
"./libs/domain/": {
"branches": 75,
"functions": 85,
"lines": 90,
"statements": 90
"branches": 80,
"functions": 90,
"lines": 95,
"statements": 95
}
},
"testEnvironment": "node",