@ -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 ( ) = > {
userMock . get . mockResolvedValue ( null ) ;
await expect ( sut . validate ( { email : 'a' , userId : 'test' } ) ) . resolves . toBeNull ( ) ;
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 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 ) ;
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 ) ;
} ) ;
} ) ;
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' ) ;
it ( 'should not accept a key without a user' , async ( ) = > {
shareMock . getByKey . mockResolvedValue ( sharedLinkStub . expired ) ;
userMock . get . mockResolvedValue ( null ) ;
const headers : IncomingHttpHeaders = { 'x-immich-share-key' : 'key' } ;
await expect ( sut . validate ( headers , { } ) ) . rejects . toBeInstanceOf ( UnauthorizedException ) ;
} ) ;
it ( 'should work with no cookies', ( ) = > {
const cookie : IncomingHttpHeaders = {
cookie : undefined ,
} ;
expect ( sut . extractTokenFromHeader ( cookie ) ) . toBeNull ( ) ;
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 ) ;
} ) ;
} ) ;
it ( 'should work on empty cookies ', ( ) = > {
const cookie : IncomingHttpHeaders = {
cookie : '' ,
} ;
expect ( sut . extractTokenFromHeader ( cookie ) ) . toBeNull ( ) ;
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 ) ;
} ) ;
} ) ;
describe ( 'extractTokenFromHeader - Bearer Auth' , ( ) = > {
it ( 'should extract the access token' , ( ) = > {
expect ( sut . extractTokenFromHeader ( { authorization : ` Bearer signed-jwt ` } ) ) . toEqual ( 'signed-jwt' ) ;
it ( 'should return an auth dto' , async ( ) = > {
userTokenMock . get . mockResolvedValue ( userTokenEntityStub . userToken ) ;
const headers : IncomingHttpHeaders = { cookie : 'immich_access_token=auth_token' } ;
await expect ( sut . validate ( headers , { } ) ) . resolves . toEqual ( userEntityStub . user1 ) ;
} ) ;
} ) ;
it ( 'should work without the auth header' , ( ) = > {
expect ( sut . extractTokenFromHeader ( { } ) ) . toBeNull ( ) ;
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 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)' ) ;
} ) ;
} ) ;
} ) ;