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:
Zack Pollard 2023-01-27 20:50:07 +00:00 committed by GitHub
parent 9be71f603e
commit 3f2513a717
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 373 additions and 517 deletions

View file

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