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
parent 599fe57ec6
commit 84ec528d14
30 changed files with 329 additions and 186 deletions

View file

@ -5,12 +5,13 @@
*/ */
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { UsersModule } from '../../users/users.module';
import { TokensController } from './tokens/tokens.controller'; import { TokensController } from './tokens/tokens.controller';
import { LoggerModule } from '../../logger/logger.module'; import { LoggerModule } from '../../logger/logger.module';
import { UsersModule } from '../../users/users.module';
import { AuthModule } from '../../auth/auth.module';
@Module({ @Module({
imports: [UsersModule, LoggerModule], imports: [LoggerModule, UsersModule, AuthModule],
controllers: [TokensController], controllers: [TokensController],
}) })
export class PrivateApiModule {} export class PrivateApiModule {}

View file

@ -7,11 +7,11 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { TokensController } from './tokens.controller'; import { TokensController } from './tokens.controller';
import { LoggerModule } from '../../../logger/logger.module'; import { LoggerModule } from '../../../logger/logger.module';
import { UsersModule } from '../../../users/users.module';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import { Identity } from '../../../users/identity.entity'; import { Identity } from '../../../users/identity.entity';
import { User } from '../../../users/user.entity'; import { User } from '../../../users/user.entity';
import { AuthToken } from '../../../users/auth-token.entity'; import { AuthToken } from '../../../auth/auth-token.entity';
import { AuthModule } from '../../../auth/auth.module';
describe('TokensController', () => { describe('TokensController', () => {
let controller: TokensController; let controller: TokensController;
@ -19,7 +19,7 @@ describe('TokensController', () => {
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [TokensController], controllers: [TokensController],
imports: [LoggerModule, UsersModule], imports: [LoggerModule, AuthModule],
}) })
.overrideProvider(getRepositoryToken(User)) .overrideProvider(getRepositoryToken(User))
.useValue({}) .useValue({})

View file

@ -14,15 +14,15 @@ import {
Post, Post,
} from '@nestjs/common'; } from '@nestjs/common';
import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { UsersService } from '../../../users/users.service'; import { AuthTokenDto } from '../../../auth/auth-token.dto';
import { AuthTokenDto } from '../../../users/auth-token.dto'; import { AuthTokenWithSecretDto } from '../../../auth/auth-token-with-secret.dto';
import { AuthTokenWithSecretDto } from '../../../users/auth-token-with-secret.dto'; import { AuthService } from '../../../auth/auth.service';
@Controller('tokens') @Controller('tokens')
export class TokensController { export class TokensController {
constructor( constructor(
private readonly logger: ConsoleLoggerService, private readonly logger: ConsoleLoggerService,
private usersService: UsersService, private authService: AuthService,
) { ) {
this.logger.setContext(TokensController.name); this.logger.setContext(TokensController.name);
} }
@ -31,8 +31,8 @@ export class TokensController {
async getUserTokens(): Promise<AuthTokenDto[]> { async getUserTokens(): Promise<AuthTokenDto[]> {
// ToDo: Get real userName // ToDo: Get real userName
return ( return (
await this.usersService.getTokensByUsername('hardcoded') await this.authService.getTokensByUsername('hardcoded')
).map((token) => this.usersService.toAuthTokenDto(token)); ).map((token) => this.authService.toAuthTokenDto(token));
} }
@Post() @Post()
@ -41,18 +41,13 @@ export class TokensController {
@Body('until') until: number, @Body('until') until: number,
): Promise<AuthTokenWithSecretDto> { ): Promise<AuthTokenWithSecretDto> {
// ToDo: Get real userName // ToDo: Get real userName
const authToken = await this.usersService.createTokenForUser( return this.authService.createTokenForUser('hardcoded', label, until);
'hardcoded',
label,
until,
);
return this.usersService.toAuthTokenWithSecretDto(authToken);
} }
@Delete('/:keyId') @Delete('/:keyId')
@HttpCode(204) @HttpCode(204)
async deleteToken(@Param('keyId') keyId: string) { async deleteToken(@Param('keyId') keyId: string) {
// ToDo: Get real userName // ToDo: Get real userName
return this.usersService.removeToken('hardcoded', keyId); return this.authService.removeToken('hardcoded', keyId);
} }
} }

View file

@ -14,7 +14,7 @@ import { NotesModule } from '../../../notes/notes.module';
import { Tag } from '../../../notes/tag.entity'; import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity'; import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity'; import { Revision } from '../../../revisions/revision.entity';
import { AuthToken } from '../../../users/auth-token.entity'; import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity'; import { Identity } from '../../../users/identity.entity';
import { User } from '../../../users/user.entity'; import { User } from '../../../users/user.entity';
import { UsersModule } from '../../../users/users.module'; import { UsersModule } from '../../../users/users.module';

View file

@ -18,7 +18,7 @@ import { NotesModule } from '../../../notes/notes.module';
import { Tag } from '../../../notes/tag.entity'; import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity'; import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity'; import { Revision } from '../../../revisions/revision.entity';
import { AuthToken } from '../../../users/auth-token.entity'; import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity'; import { Identity } from '../../../users/identity.entity';
import { User } from '../../../users/user.entity'; import { User } from '../../../users/user.entity';
import { MediaController } from './media.controller'; import { MediaController } from './media.controller';

View file

@ -14,7 +14,7 @@ import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity'; import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity'; import { Revision } from '../../../revisions/revision.entity';
import { RevisionsModule } from '../../../revisions/revisions.module'; import { RevisionsModule } from '../../../revisions/revisions.module';
import { AuthToken } from '../../../users/auth-token.entity'; import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity'; import { Identity } from '../../../users/identity.entity';
import { User } from '../../../users/user.entity'; import { User } from '../../../users/user.entity';
import { UsersModule } from '../../../users/users.module'; import { UsersModule } from '../../../users/users.module';

View file

@ -9,6 +9,8 @@ import { IsNumber, IsString } from 'class-validator';
export class AuthTokenDto { export class AuthTokenDto {
@IsString() @IsString()
label: string; label: string;
@IsString()
keyId: string;
@IsNumber() @IsNumber()
created: number; created: number;
@IsNumber() @IsNumber()

View file

@ -11,7 +11,7 @@ import {
ManyToOne, ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
} from 'typeorm'; } from 'typeorm';
import { User } from './user.entity'; import { User } from '../users/user.entity';
@Entity() @Entity()
export class AuthToken { export class AuthToken {
@ -31,7 +31,7 @@ export class AuthToken {
createdAt: Date; createdAt: Date;
@Column({ unique: true }) @Column({ unique: true })
accessToken: string; accessTokenHash: string;
@Column({ @Column({
nullable: true, nullable: true,
@ -49,12 +49,12 @@ export class AuthToken {
keyId: string, keyId: string,
accessToken: string, accessToken: string,
validUntil?: number, validUntil?: number,
): Pick<AuthToken, 'user' | 'accessToken'> { ): Pick<AuthToken, 'user' | 'accessTokenHash'> {
const newToken = new AuthToken(); const newToken = new AuthToken();
newToken.user = user; newToken.user = user;
newToken.identifier = identifier; newToken.identifier = identifier;
newToken.keyId = keyId; newToken.keyId = keyId;
newToken.accessToken = accessToken; newToken.accessTokenHash = accessToken;
newToken.createdAt = new Date(); newToken.createdAt = new Date();
if (validUntil !== undefined) { if (validUntil !== undefined) {
newToken.validUntil = validUntil; newToken.validUntil = validUntil;

View file

@ -1,11 +1,26 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { TokenStrategy } from './token.strategy'; import { TokenStrategy } from './token.strategy';
import { LoggerModule } from '../logger/logger.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthToken } from './auth-token.entity';
@Module({ @Module({
imports: [UsersModule, PassportModule], imports: [
UsersModule,
PassportModule,
LoggerModule,
TypeOrmModule.forFeature([AuthToken]),
],
providers: [AuthService, TokenStrategy], providers: [AuthService, TokenStrategy],
exports: [AuthService],
}) })
export class AuthModule {} export class AuthModule {}

View file

@ -1,26 +1,91 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import { AuthToken } from '../users/auth-token.entity'; import { AuthToken } from './auth-token.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
import { Identity } from '../users/identity.entity'; import { Identity } from '../users/identity.entity';
import { LoggerModule } from '../logger/logger.module';
describe('AuthService', () => { describe('AuthService', () => {
let service: AuthService; let service: AuthService;
let user: User;
let authToken: AuthToken;
beforeEach(async () => { beforeEach(async () => {
user = {
authTokens: [],
createdAt: new Date(),
displayName: 'hardcoded',
id: '1',
identities: [],
ownedNotes: [],
updatedAt: new Date(),
userName: 'Testy',
};
authToken = {
accessTokenHash: '',
createdAt: new Date(),
id: 1,
identifier: 'testIdentifier',
keyId: 'abc',
lastUsed: null,
user: null,
validUntil: null,
};
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [AuthService], providers: [
imports: [PassportModule, UsersModule], AuthService,
{
provide: getRepositoryToken(AuthToken),
useValue: {},
},
],
imports: [PassportModule, UsersModule, LoggerModule],
}) })
.overrideProvider(getRepositoryToken(AuthToken)) .overrideProvider(getRepositoryToken(AuthToken))
.useValue({}) .useValue({
findOne: (): AuthToken => {
return {
...authToken,
user: user,
};
},
save: async (entity: AuthToken) => {
if (entity.lastUsed === undefined) {
expect(entity.lastUsed).toBeUndefined();
} else {
expect(entity.lastUsed).toBeLessThanOrEqual(new Date().getTime());
}
return entity;
},
remove: async (entity: AuthToken) => {
expect(entity).toEqual({
...authToken,
user: user,
});
},
})
.overrideProvider(getRepositoryToken(Identity)) .overrideProvider(getRepositoryToken(Identity))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(User)) .overrideProvider(getRepositoryToken(User))
.useValue({}) .useValue({
findOne: (): User => {
return {
...user,
authTokens: [authToken],
};
},
})
.compile(); .compile();
service = module.get<AuthService>(AuthService); service = module.get<AuthService>(AuthService);
@ -29,4 +94,64 @@ describe('AuthService', () => {
it('should be defined', () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
it('checkPassword', async () => {
const testPassword = 'thisIsATestPassword';
const hash = await service.hashPassword(testPassword);
service
.checkPassword(testPassword, hash)
.then((result) => expect(result).toBeTruthy());
});
it('getTokensByUsername', async () => {
const tokens = await service.getTokensByUsername(user.userName);
expect(tokens).toHaveLength(1);
expect(tokens).toEqual([authToken]);
});
it('getAuthToken', async () => {
const token = 'testToken';
authToken.accessTokenHash = await service.hashPassword(token);
const authTokenFromCall = await service.getAuthToken(
authToken.keyId,
token,
);
expect(authTokenFromCall).toEqual({
...authToken,
user: user,
});
});
it('setLastUsedToken', async () => {
await service.setLastUsedToken(authToken.keyId);
});
it('validateToken', async () => {
const token = 'testToken';
authToken.accessTokenHash = await service.hashPassword(token);
const userByToken = await service.validateToken(
`${authToken.keyId}.${token}`,
);
expect(userByToken).toEqual({
...user,
authTokens: [authToken],
});
});
it('removeToken', async () => {
await service.removeToken(user.userName, authToken.keyId);
});
it('createTokenForUser', async () => {
const identifier = 'identifier2';
const token = await service.createTokenForUser(
user.userName,
identifier,
0,
);
expect(token.label).toEqual(identifier);
expect(token.validUntil).toBeUndefined();
expect(token.lastUsed).toBeUndefined();
expect(token.secret.startsWith(token.keyId)).toBeTruthy();
});
}); });

View file

@ -1,18 +1,158 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { AuthToken } from './auth-token.entity';
import { AuthTokenDto } from './auth-token.dto';
import { AuthTokenWithSecretDto } from './auth-token-with-secret.dto';
import { compare, hash } from 'bcrypt';
import { NotInDBError, TokenNotValidError } from '../errors/errors';
import { randomBytes } from 'crypto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConsoleLoggerService } from '../logger/console-logger.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor(private usersService: UsersService) {} constructor(
private readonly logger: ConsoleLoggerService,
private usersService: UsersService,
@InjectRepository(AuthToken)
private authTokenRepository: Repository<AuthToken>,
) {
this.logger.setContext(AuthService.name);
}
async validateToken(token: string): Promise<User> { async validateToken(token: string): Promise<User> {
const parts = token.split('.'); const parts = token.split('.');
const user = await this.usersService.getUserByAuthToken(parts[0], parts[1]); const accessToken = await this.getAuthToken(parts[0], parts[1]);
const user = await this.usersService.getUserByUsername(
accessToken.user.userName,
);
if (user) { if (user) {
await this.usersService.setLastUsedToken(parts[0]) await this.setLastUsedToken(parts[0]);
return user; return user;
} }
return null; return null;
} }
async hashPassword(cleartext: string): Promise<string> {
// hash the password with bcrypt and 2^16 iterations
return hash(cleartext, 12);
}
async checkPassword(cleartext: string, password: string): Promise<boolean> {
// hash the password with bcrypt and 2^16 iterations
return compare(cleartext, password);
}
randomBase64UrlString(length = 64): 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(length)
.toString('base64')
.replace('+', '-')
.replace('/', '_')
.replace(/=+$/, '');
}
async createTokenForUser(
userName: string,
identifier: string,
until: number,
): Promise<AuthTokenWithSecretDto> {
const user = await this.usersService.getUserByUsername(userName);
const secret = this.randomBase64UrlString();
const keyId = this.randomBase64UrlString(8);
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 this.toAuthTokenWithSecretDto(createdToken, `${keyId}.${secret}`);
}
async setLastUsedToken(keyId: string) {
const accessToken = await this.authTokenRepository.findOne({
where: { keyId: keyId },
});
accessToken.lastUsed = new Date().getTime();
await this.authTokenRepository.save(accessToken);
}
async getAuthToken(keyId: string, token: string): Promise<AuthToken> {
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.accessTokenHash))) {
// hashes are not the same
throw new TokenNotValidError(`AuthToken '${token}' is not valid.`);
}
if (
accessToken.validUntil &&
accessToken.validUntil < new Date().getTime()
) {
// tokens validUntil Date lies in the past
throw new TokenNotValidError(
`AuthToken '${token}' is not valid since ${new Date(
accessToken.validUntil,
)}.`,
);
}
return accessToken;
}
async getTokensByUsername(userName: string): Promise<AuthToken[]> {
const user = await this.usersService.getUserByUsername(userName, true);
if (user.authTokens === undefined) {
return [];
}
return user.authTokens;
}
async removeToken(userName: string, keyId: string) {
const user = await this.usersService.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,
keyId: authToken.keyId,
created: authToken.createdAt.getTime(),
validUntil: authToken.validUntil,
lastUsed: authToken.lastUsed,
};
}
toAuthTokenWithSecretDto(
authToken: AuthToken | null | undefined,
secret: string,
): AuthTokenWithSecretDto | null {
const tokeDto = this.toAuthTokenDto(authToken);
return {
...tokeDto,
secret: secret,
};
}
} }

View file

@ -16,6 +16,6 @@ export class PermissionError extends Error {
name = 'PermissionError'; name = 'PermissionError';
} }
export class TokenNotValid extends Error { export class TokenNotValidError extends Error {
name = 'TokenNotValid'; name = 'TokenNotValidError';
} }

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index'; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity() @Entity()
export class Group { export class Group {

View file

@ -15,7 +15,7 @@ import { NotesModule } from '../notes/notes.module';
import { Tag } from '../notes/tag.entity'; import { Tag } from '../notes/tag.entity';
import { Authorship } from '../revisions/authorship.entity'; import { Authorship } from '../revisions/authorship.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { AuthToken } from '../users/auth-token.entity'; import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from '../users/identity.entity'; import { Identity } from '../users/identity.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Column, Entity, ManyToOne } from 'typeorm/index'; import { Column, Entity, ManyToOne } from 'typeorm';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { Note } from './note.entity'; import { Note } from './note.entity';

View file

@ -10,7 +10,7 @@ import { LoggerModule } from '../logger/logger.module';
import { Authorship } from '../revisions/authorship.entity'; import { Authorship } from '../revisions/authorship.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { RevisionsModule } from '../revisions/revisions.module'; import { RevisionsModule } from '../revisions/revisions.module';
import { AuthToken } from '../users/auth-token.entity'; import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from '../users/identity.entity'; import { Identity } from '../users/identity.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Column, Entity, ManyToOne } from 'typeorm/index'; import { Column, Entity, ManyToOne } from 'typeorm';
import { Group } from '../groups/group.entity'; import { Group } from '../groups/group.entity';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Column, Entity, ManyToOne } from 'typeorm/index'; import { Column, Entity, ManyToOne } from 'typeorm';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';

View file

@ -12,7 +12,7 @@ import {
ManyToOne, ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm/index'; } from 'typeorm';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { Revision } from './revision.entity'; import { Revision } from './revision.entity';

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { IsDate, IsNumber, IsString } from 'class-validator'; import { IsDate, IsNumber } from 'class-validator';
import { Revision } from './revision.entity'; import { Revision } from './revision.entity';
export class RevisionMetadataDto { export class RevisionMetadataDto {

View file

@ -11,7 +11,7 @@ import {
ManyToOne, ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
} from 'typeorm'; } from 'typeorm';
import { JoinTable, ManyToMany } from 'typeorm/index'; import { JoinTable, ManyToMany } from 'typeorm';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { Authorship } from './authorship.entity'; import { Authorship } from './authorship.entity';

View file

@ -10,7 +10,7 @@ import { LoggerModule } from '../logger/logger.module';
import { AuthorColor } from '../notes/author-color.entity'; import { AuthorColor } from '../notes/author-color.entity';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { NotesModule } from '../notes/notes.module'; import { NotesModule } from '../notes/notes.module';
import { AuthToken } from '../users/auth-token.entity'; import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from '../users/identity.entity'; import { Identity } from '../users/identity.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { Authorship } from './authorship.entity'; import { Authorship } from './authorship.entity';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,23 +7,16 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { NotInDBError, TokenNotValid } from '../errors/errors'; import { NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { UserInfoDto } from './user-info.dto'; import { UserInfoDto } from './user-info.dto';
import { User } from './user.entity'; 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() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(
private readonly logger: ConsoleLoggerService, private readonly logger: ConsoleLoggerService,
@InjectRepository(User) private userRepository: Repository<User>, @InjectRepository(User) private userRepository: Repository<User>,
@InjectRepository(AuthToken)
private authTokenRepository: Repository<AuthToken>,
) { ) {
this.logger.setContext(UsersService.name); this.logger.setContext(UsersService.name);
} }
@ -33,39 +26,6 @@ export class UsersService {
return this.userRepository.save(user); 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) { async deleteUser(userName: string) {
// TODO: Handle owned notes and edits // TODO: Handle owned notes and edits
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
@ -85,50 +45,6 @@ export class UsersService {
return user; 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 { getPhotoUrl(user: User): string {
if (user.photo) { if (user.photo) {
return 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 { toUserDto(user: User | null | undefined): UserInfoDto | null {
if (!user) { if (!user) {
this.logger.warn(`Recieved ${user} argument!`, 'toUserDto'); this.logger.warn(`Recieved ${user} argument!`, 'toUserDto');

View file

@ -8,7 +8,6 @@ import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import * as request from 'supertest'; import * as request from 'supertest';
import { AppModule } from '../../src/app.module'; import { AppModule } from '../../src/app.module';
//import { UsersService } from '../../src/users/users.service';
import { UserInfoDto } from '../../src/users/user-info.dto'; import { UserInfoDto } from '../../src/users/user-info.dto';
import { HistoryService } from '../../src/history/history.service'; import { HistoryService } from '../../src/history/history.service';
import { NotesService } from '../../src/notes/notes.service'; import { NotesService } from '../../src/notes/notes.service';