auth: Add tests for AuthService

Move AuthTokens to auth folder

Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2021-01-22 15:29:10 +01:00 committed by David Mehren
parent c9751404f7
commit 508ad26771
No known key found for this signature in database
GPG key ID: 185982BA4C42B7C3
30 changed files with 329 additions and 186 deletions

View file

@ -1,13 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsString } from 'class-validator';
import { AuthTokenDto } from './auth-token.dto';
export class AuthTokenWithSecretDto extends AuthTokenDto {
@IsString()
secret: string;
}

View file

@ -1,18 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsNumber, IsString } from 'class-validator';
export class AuthTokenDto {
@IsString()
label: string;
@IsNumber()
created: number;
@IsNumber()
validUntil: number | null;
@IsNumber()
lastUsed: number | null;
}

View file

@ -1,64 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './user.entity';
@Entity()
export class AuthToken {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
keyId: string;
@ManyToOne((_) => User, (user) => user.authTokens)
user: User;
@Column()
identifier: string;
@CreateDateColumn()
createdAt: Date;
@Column({ unique: true })
accessToken: string;
@Column({
nullable: true,
})
validUntil: number;
@Column({
nullable: true,
})
lastUsed: number;
public static create(
user: User,
identifier: string,
keyId: string,
accessToken: string,
validUntil?: number,
): Pick<AuthToken, 'user' | 'accessToken'> {
const newToken = new AuthToken();
newToken.user = user;
newToken.identifier = identifier;
newToken.keyId = keyId;
newToken.accessToken = accessToken;
newToken.createdAt = new Date();
if (validUntil !== undefined) {
newToken.validUntil = validUntil;
}
return newToken;
}
}

View file

@ -11,7 +11,7 @@ import {
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm/index';
} from 'typeorm';
import { User } from './user.entity';
@Entity()

View file

@ -5,7 +5,7 @@
*/
import { ISession } from 'connect-typeorm';
import { Column, Entity, Index, PrimaryColumn } from 'typeorm/index';
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity()
export class Session implements ISession {

View file

@ -12,7 +12,7 @@ import {
} from 'typeorm';
import { Column, OneToMany } from 'typeorm';
import { Note } from '../notes/note.entity';
import { AuthToken } from './auth-token.entity';
import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from './identity.entity';
@Entity()

View file

@ -7,16 +7,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggerModule } from '../logger/logger.module';
import { AuthToken } from './auth-token.entity';
import { Identity } from './identity.entity';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Module({
imports: [
TypeOrmModule.forFeature([User, AuthToken, Identity]),
LoggerModule,
],
imports: [TypeOrmModule.forFeature([User, Identity]), LoggerModule],
providers: [UsersService],
exports: [UsersService],
})

View file

@ -9,7 +9,6 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { LoggerModule } from '../logger/logger.module';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { AuthToken } from './auth-token.entity';
describe('UsersService', () => {
let service: UsersService;
@ -22,17 +21,11 @@ describe('UsersService', () => {
provide: getRepositoryToken(User),
useValue: {},
},
{
provide: getRepositoryToken(AuthToken),
useValue: {},
},
],
imports: [LoggerModule],
})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
.useValue({})
.compile();
service = module.get<UsersService>(UsersService);

View file

@ -7,23 +7,16 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotInDBError, TokenNotValid } from '../errors/errors';
import { NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { UserInfoDto } from './user-info.dto';
import { User } from './user.entity';
import { AuthToken } from './auth-token.entity';
import { hash, compare } from 'bcrypt';
import { randomBytes } from 'crypto';
import { AuthTokenDto } from './auth-token.dto';
import { AuthTokenWithSecretDto } from './auth-token-with-secret.dto';
@Injectable()
export class UsersService {
constructor(
private readonly logger: ConsoleLoggerService,
@InjectRepository(User) private userRepository: Repository<User>,
@InjectRepository(AuthToken)
private authTokenRepository: Repository<AuthToken>,
) {
this.logger.setContext(UsersService.name);
}
@ -33,39 +26,6 @@ export class UsersService {
return this.userRepository.save(user);
}
randomBase64UrlString(): string {
// This is necessary as the is no base64url encoding in the toString method
// but as can be seen on https://tools.ietf.org/html/rfc4648#page-7
// base64url is quite easy buildable from base64
return randomBytes(64)
.toString('base64')
.replace('+', '-')
.replace('/', '_')
.replace(/=+$/, '');
}
async createTokenForUser(
userName: string,
identifier: string,
until: number,
): Promise<AuthToken> {
const user = await this.getUserByUsername(userName);
const secret = this.randomBase64UrlString();
const keyId = this.randomBase64UrlString();
const accessToken = await this.hashPassword(secret);
let token;
if (until === 0) {
token = AuthToken.create(user, identifier, keyId, accessToken);
} else {
token = AuthToken.create(user, identifier, keyId, accessToken, until);
}
const createdToken = await this.authTokenRepository.save(token);
return {
...createdToken,
accessToken: `${keyId}.${secret}`,
};
}
async deleteUser(userName: string) {
// TODO: Handle owned notes and edits
const user = await this.userRepository.findOne({
@ -85,50 +45,6 @@ export class UsersService {
return user;
}
async hashPassword(cleartext: string): Promise<string> {
// hash the password with bcrypt and 2^16 iterations
return hash(cleartext, 16);
}
async checkPassword(cleartext: string, password: string): Promise<boolean> {
// hash the password with bcrypt and 2^16 iterations
return compare(cleartext, password);
}
async setLastUsedToken(keyId: string) {
const accessToken = await this.authTokenRepository.findOne({
where: { keyId: keyId },
});
accessToken.lastUsed = new Date().getTime();
await this.authTokenRepository.save(accessToken);
}
async getUserByAuthToken(keyId: string, token: string): Promise<User> {
const accessToken = await this.authTokenRepository.findOne({
where: { keyId: keyId },
relations: ['user'],
});
if (accessToken === undefined) {
throw new NotInDBError(`AuthToken '${token}' not found`);
}
if (!(await this.checkPassword(token, accessToken.accessToken))) {
// hashes are not the same
throw new TokenNotValid(`AuthToken '${token}' is not valid.`);
}
if (
accessToken.validUntil &&
accessToken.validUntil < new Date().getTime()
) {
// tokens validUntil Date lies in the past
throw new TokenNotValid(
`AuthToken '${token}' is not valid since ${new Date(
accessToken.validUntil,
)}.`,
);
}
return this.getUserByUsername(accessToken.user.userName);
}
getPhotoUrl(user: User): string {
if (user.photo) {
return user.photo;
@ -138,45 +54,6 @@ export class UsersService {
}
}
async getTokensByUsername(userName: string): Promise<AuthToken[]> {
const user = await this.getUserByUsername(userName, true);
if (user.authTokens === undefined) {
return [];
}
return user.authTokens;
}
async removeToken(userName: string, keyId: string) {
const user = await this.getUserByUsername(userName);
const token = await this.authTokenRepository.findOne({
where: { keyId: keyId, user: user },
});
await this.authTokenRepository.remove(token);
}
toAuthTokenDto(authToken: AuthToken | null | undefined): AuthTokenDto | null {
if (!authToken) {
this.logger.warn(`Recieved ${authToken} argument!`, 'toAuthTokenDto');
return null;
}
return {
label: authToken.identifier,
created: authToken.createdAt.getTime(),
validUntil: authToken.validUntil,
lastUsed: authToken.lastUsed,
};
}
toAuthTokenWithSecretDto(
authToken: AuthToken | null | undefined,
): AuthTokenWithSecretDto | null {
const tokeDto = this.toAuthTokenDto(authToken)
return {
...tokeDto,
secret: authToken.accessToken,
};
}
toUserDto(user: User | null | undefined): UserInfoDto | null {
if (!user) {
this.logger.warn(`Recieved ${user} argument!`, 'toUserDto');