mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(server): move authentication to tokens stored in the database (#1381)
* chore: add typeorm commands to npm and set default database config values * feat: move to server side authentication tokens * fix: websocket should emit error and disconnect on error thrown by the server * refactor: rename cookie-auth-strategy to user-auth-strategy * feat: user tokens and API keys now use SHA256 hash for performance improvements * test: album e2e test remove unneeded module import * infra: truncate api key table as old keys will no longer work with new hash algorithm * fix(server): e2e tests (#1435) * fix: root module paths * chore: linting * chore: rename user-auth to strategy.ts and make validate return AuthUserDto * fix: we should always send HttpOnly for our auth cookies * chore: remove now unused crypto functions and jwt dependencies * fix: return the extra fields for AuthUserDto in auth service validate --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
9be71f603e
commit
3f2513a717
61 changed files with 373 additions and 517 deletions
|
|
@ -7,20 +7,20 @@ import {
|
|||
Logger,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import * as cookieParser from 'cookie';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { Socket } from 'socket.io';
|
||||
import { OAuthCore } from '../oauth/oauth.core';
|
||||
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
|
||||
import { IUserRepository, UserCore, UserResponseDto } from '../user';
|
||||
import { AuthType, jwtSecret } from './auth.constant';
|
||||
import { IUserRepository, UserCore } from '../user';
|
||||
import { AuthType } from './auth.constant';
|
||||
import { AuthCore } from './auth.core';
|
||||
import { ICryptoRepository } from './crypto.repository';
|
||||
import { AuthUserDto, ChangePasswordDto, JwtPayloadDto, LoginCredentialDto, SignUpDto } from './dto';
|
||||
import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from './dto';
|
||||
import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto';
|
||||
import { IUserTokenRepository, UserTokenCore } from '@app/domain/user-token';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private userTokenCore: UserTokenCore;
|
||||
private authCore: AuthCore;
|
||||
private oauthCore: OAuthCore;
|
||||
private userCore: UserCore;
|
||||
|
|
@ -31,11 +31,14 @@ export class AuthService {
|
|||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(IUserRepository) userRepository: IUserRepository,
|
||||
@Inject(INITIAL_SYSTEM_CONFIG) initialConfig: SystemConfig,
|
||||
@Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
|
||||
@Inject(INITIAL_SYSTEM_CONFIG)
|
||||
initialConfig: SystemConfig,
|
||||
) {
|
||||
this.authCore = new AuthCore(cryptoRepository, configRepository, initialConfig);
|
||||
this.userTokenCore = new UserTokenCore(cryptoRepository, userTokenRepository);
|
||||
this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
|
||||
this.oauthCore = new OAuthCore(configRepository, initialConfig);
|
||||
this.userCore = new UserCore(userRepository);
|
||||
this.userCore = new UserCore(userRepository, cryptoRepository);
|
||||
}
|
||||
|
||||
public async login(
|
||||
|
|
@ -49,7 +52,7 @@ export class AuthService {
|
|||
|
||||
let user = await this.userCore.getByEmail(loginCredential.email, true);
|
||||
if (user) {
|
||||
const isAuthenticated = await this.authCore.validatePassword(loginCredential.password, user);
|
||||
const isAuthenticated = this.authCore.validatePassword(loginCredential.password, user);
|
||||
if (!isAuthenticated) {
|
||||
user = null;
|
||||
}
|
||||
|
|
@ -81,7 +84,7 @@ export class AuthService {
|
|||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const valid = await this.authCore.validatePassword(password, user);
|
||||
const valid = this.authCore.validatePassword(password, user);
|
||||
if (!valid) {
|
||||
throw new BadRequestException('Wrong password');
|
||||
}
|
||||
|
|
@ -112,49 +115,28 @@ export class AuthService {
|
|||
}
|
||||
}
|
||||
|
||||
async validateSocket(client: Socket): Promise<UserResponseDto | null> {
|
||||
try {
|
||||
const headers = client.handshake.headers;
|
||||
const accessToken =
|
||||
this.extractJwtFromCookie(cookieParser.parse(headers.cookie || '')) || this.extractJwtFromHeader(headers);
|
||||
|
||||
if (accessToken) {
|
||||
const payload = await this.cryptoRepository.verifyJwtAsync<JwtPayloadDto>(accessToken, { secret: jwtSecret });
|
||||
if (payload?.userId && payload?.email) {
|
||||
const user = await this.userCore.get(payload.userId);
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async validatePayload(payload: JwtPayloadDto) {
|
||||
const { userId } = payload;
|
||||
const user = await this.userCore.get(userId);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Failure to validate JWT payload');
|
||||
public async validate(headers: IncomingHttpHeaders): Promise<AuthUserDto> {
|
||||
const tokenValue = this.extractTokenFromHeader(headers);
|
||||
if (!tokenValue) {
|
||||
throw new UnauthorizedException('No access token provided in request');
|
||||
}
|
||||
|
||||
const authUser = new AuthUserDto();
|
||||
authUser.id = user.id;
|
||||
authUser.email = user.email;
|
||||
authUser.isAdmin = user.isAdmin;
|
||||
authUser.isPublicUser = false;
|
||||
authUser.isAllowUpload = true;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
return authUser;
|
||||
throw new UnauthorizedException('Invalid access token provided');
|
||||
}
|
||||
|
||||
extractJwtFromCookie(cookies: Record<string, string>) {
|
||||
return this.authCore.extractJwtFromCookie(cookies);
|
||||
}
|
||||
|
||||
extractJwtFromHeader(headers: IncomingHttpHeaders) {
|
||||
return this.authCore.extractJwtFromHeader(headers);
|
||||
extractTokenFromHeader(headers: IncomingHttpHeaders) {
|
||||
return this.authCore.extractTokenFromHeader(headers);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue