diff --git a/src/api/private/private-api.module.ts b/src/api/private/private-api.module.ts index e7eea137c..74dc95fe8 100644 --- a/src/api/private/private-api.module.ts +++ b/src/api/private/private-api.module.ts @@ -5,12 +5,13 @@ */ import { Module } from '@nestjs/common'; -import { UsersModule } from '../../users/users.module'; import { TokensController } from './tokens/tokens.controller'; import { LoggerModule } from '../../logger/logger.module'; +import { UsersModule } from '../../users/users.module'; +import { AuthModule } from '../../auth/auth.module'; @Module({ - imports: [UsersModule, LoggerModule], + imports: [LoggerModule, UsersModule, AuthModule], controllers: [TokensController], }) export class PrivateApiModule {} diff --git a/src/api/private/tokens/tokens.controller.spec.ts b/src/api/private/tokens/tokens.controller.spec.ts index f90278756..287a5f0a2 100644 --- a/src/api/private/tokens/tokens.controller.spec.ts +++ b/src/api/private/tokens/tokens.controller.spec.ts @@ -7,11 +7,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TokensController } from './tokens.controller'; import { LoggerModule } from '../../../logger/logger.module'; -import { UsersModule } from '../../../users/users.module'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Identity } from '../../../users/identity.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', () => { let controller: TokensController; @@ -19,7 +19,7 @@ describe('TokensController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [TokensController], - imports: [LoggerModule, UsersModule], + imports: [LoggerModule, AuthModule], }) .overrideProvider(getRepositoryToken(User)) .useValue({}) diff --git a/src/api/private/tokens/tokens.controller.ts b/src/api/private/tokens/tokens.controller.ts index 86e28141c..89d47cbde 100644 --- a/src/api/private/tokens/tokens.controller.ts +++ b/src/api/private/tokens/tokens.controller.ts @@ -14,15 +14,15 @@ import { Post, } from '@nestjs/common'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; -import { UsersService } from '../../../users/users.service'; -import { AuthTokenDto } from '../../../users/auth-token.dto'; -import { AuthTokenWithSecretDto } from '../../../users/auth-token-with-secret.dto'; +import { AuthTokenDto } from '../../../auth/auth-token.dto'; +import { AuthTokenWithSecretDto } from '../../../auth/auth-token-with-secret.dto'; +import { AuthService } from '../../../auth/auth.service'; @Controller('tokens') export class TokensController { constructor( private readonly logger: ConsoleLoggerService, - private usersService: UsersService, + private authService: AuthService, ) { this.logger.setContext(TokensController.name); } @@ -31,8 +31,8 @@ export class TokensController { async getUserTokens(): Promise { // ToDo: Get real userName return ( - await this.usersService.getTokensByUsername('hardcoded') - ).map((token) => this.usersService.toAuthTokenDto(token)); + await this.authService.getTokensByUsername('hardcoded') + ).map((token) => this.authService.toAuthTokenDto(token)); } @Post() @@ -41,18 +41,13 @@ export class TokensController { @Body('until') until: number, ): Promise { // ToDo: Get real userName - const authToken = await this.usersService.createTokenForUser( - 'hardcoded', - label, - until, - ); - return this.usersService.toAuthTokenWithSecretDto(authToken); + return this.authService.createTokenForUser('hardcoded', label, until); } @Delete('/:keyId') @HttpCode(204) async deleteToken(@Param('keyId') keyId: string) { // ToDo: Get real userName - return this.usersService.removeToken('hardcoded', keyId); + return this.authService.removeToken('hardcoded', keyId); } } diff --git a/src/api/public/me/me.controller.spec.ts b/src/api/public/me/me.controller.spec.ts index b7b26a915..e3f262396 100644 --- a/src/api/public/me/me.controller.spec.ts +++ b/src/api/public/me/me.controller.spec.ts @@ -14,7 +14,7 @@ import { NotesModule } from '../../../notes/notes.module'; import { Tag } from '../../../notes/tag.entity'; import { Authorship } from '../../../revisions/authorship.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 { User } from '../../../users/user.entity'; import { UsersModule } from '../../../users/users.module'; diff --git a/src/api/public/media/media.controller.spec.ts b/src/api/public/media/media.controller.spec.ts index 2e5180993..eb699f1c2 100644 --- a/src/api/public/media/media.controller.spec.ts +++ b/src/api/public/media/media.controller.spec.ts @@ -18,7 +18,7 @@ import { NotesModule } from '../../../notes/notes.module'; import { Tag } from '../../../notes/tag.entity'; import { Authorship } from '../../../revisions/authorship.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 { User } from '../../../users/user.entity'; import { MediaController } from './media.controller'; diff --git a/src/api/public/notes/notes.controller.spec.ts b/src/api/public/notes/notes.controller.spec.ts index 448c9f345..13ae993bc 100644 --- a/src/api/public/notes/notes.controller.spec.ts +++ b/src/api/public/notes/notes.controller.spec.ts @@ -14,7 +14,7 @@ import { Tag } from '../../../notes/tag.entity'; import { Authorship } from '../../../revisions/authorship.entity'; import { Revision } from '../../../revisions/revision.entity'; 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 { User } from '../../../users/user.entity'; import { UsersModule } from '../../../users/users.module'; diff --git a/src/users/auth-token-with-secret.dto.ts b/src/auth/auth-token-with-secret.dto.ts similarity index 100% rename from src/users/auth-token-with-secret.dto.ts rename to src/auth/auth-token-with-secret.dto.ts diff --git a/src/users/auth-token.dto.ts b/src/auth/auth-token.dto.ts similarity index 92% rename from src/users/auth-token.dto.ts rename to src/auth/auth-token.dto.ts index c4fc8ffba..1c3592db3 100644 --- a/src/users/auth-token.dto.ts +++ b/src/auth/auth-token.dto.ts @@ -9,6 +9,8 @@ import { IsNumber, IsString } from 'class-validator'; export class AuthTokenDto { @IsString() label: string; + @IsString() + keyId: string; @IsNumber() created: number; @IsNumber() diff --git a/src/users/auth-token.entity.ts b/src/auth/auth-token.entity.ts similarity index 86% rename from src/users/auth-token.entity.ts rename to src/auth/auth-token.entity.ts index 2dc58d2d0..c0538f52a 100644 --- a/src/users/auth-token.entity.ts +++ b/src/auth/auth-token.entity.ts @@ -11,7 +11,7 @@ import { ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { User } from './user.entity'; +import { User } from '../users/user.entity'; @Entity() export class AuthToken { @@ -31,7 +31,7 @@ export class AuthToken { createdAt: Date; @Column({ unique: true }) - accessToken: string; + accessTokenHash: string; @Column({ nullable: true, @@ -49,12 +49,12 @@ export class AuthToken { keyId: string, accessToken: string, validUntil?: number, - ): Pick { + ): Pick { const newToken = new AuthToken(); newToken.user = user; newToken.identifier = identifier; newToken.keyId = keyId; - newToken.accessToken = accessToken; + newToken.accessTokenHash = accessToken; newToken.createdAt = new Date(); if (validUntil !== undefined) { newToken.validUntil = validUntil; diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 95ae873b6..b39aa89a3 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -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 { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; import { TokenStrategy } from './token.strategy'; +import { LoggerModule } from '../logger/logger.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthToken } from './auth-token.entity'; @Module({ - imports: [UsersModule, PassportModule], + imports: [ + UsersModule, + PassportModule, + LoggerModule, + TypeOrmModule.forFeature([AuthToken]), + ], providers: [AuthService, TokenStrategy], + exports: [AuthService], }) export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index cf9de2666..94c516f41 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -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 { AuthService } from './auth.service'; import { PassportModule } from '@nestjs/passport'; 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 { UsersModule } from '../users/users.module'; import { Identity } from '../users/identity.entity'; +import { LoggerModule } from '../logger/logger.module'; describe('AuthService', () => { let service: AuthService; + let user: User; + let authToken: AuthToken; 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({ - providers: [AuthService], - imports: [PassportModule, UsersModule], + providers: [ + AuthService, + { + provide: getRepositoryToken(AuthToken), + useValue: {}, + }, + ], + imports: [PassportModule, UsersModule, LoggerModule], }) .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)) .useValue({}) .overrideProvider(getRepositoryToken(User)) - .useValue({}) + .useValue({ + findOne: (): User => { + return { + ...user, + authTokens: [authToken], + }; + }, + }) .compile(); service = module.get(AuthService); @@ -29,4 +94,64 @@ describe('AuthService', () => { it('should be defined', () => { 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(); + }); }); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 89565e96c..3354a031b 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -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 { UsersService } from '../users/users.service'; 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() export class AuthService { - constructor(private usersService: UsersService) {} + constructor( + private readonly logger: ConsoleLoggerService, + private usersService: UsersService, + @InjectRepository(AuthToken) + private authTokenRepository: Repository, + ) { + this.logger.setContext(AuthService.name); + } async validateToken(token: string): Promise { 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) { - await this.usersService.setLastUsedToken(parts[0]) + await this.setLastUsedToken(parts[0]); return user; } return null; } + + async hashPassword(cleartext: string): Promise { + // hash the password with bcrypt and 2^16 iterations + return hash(cleartext, 12); + } + + async checkPassword(cleartext: string, password: string): Promise { + // 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 { + 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 { + 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 { + 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, + }; + } } diff --git a/src/errors/errors.ts b/src/errors/errors.ts index b288f5443..1b4821bf7 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -16,6 +16,6 @@ export class PermissionError extends Error { name = 'PermissionError'; } -export class TokenNotValid extends Error { - name = 'TokenNotValid'; +export class TokenNotValidError extends Error { + name = 'TokenNotValidError'; } diff --git a/src/groups/group.entity.ts b/src/groups/group.entity.ts index b96ccdce1..427709718 100644 --- a/src/groups/group.entity.ts +++ b/src/groups/group.entity.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Group { diff --git a/src/media/media.service.spec.ts b/src/media/media.service.spec.ts index 6eb99deb1..7b32af5a6 100644 --- a/src/media/media.service.spec.ts +++ b/src/media/media.service.spec.ts @@ -15,7 +15,7 @@ import { NotesModule } from '../notes/notes.module'; import { Tag } from '../notes/tag.entity'; import { Authorship } from '../revisions/authorship.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 { User } from '../users/user.entity'; import { UsersModule } from '../users/users.module'; diff --git a/src/notes/author-color.entity.ts b/src/notes/author-color.entity.ts index 322667310..965d5e6f9 100644 --- a/src/notes/author-color.entity.ts +++ b/src/notes/author-color.entity.ts @@ -4,7 +4,7 @@ * 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 { Note } from './note.entity'; diff --git a/src/notes/notes.service.spec.ts b/src/notes/notes.service.spec.ts index 41c514a77..2c623796a 100644 --- a/src/notes/notes.service.spec.ts +++ b/src/notes/notes.service.spec.ts @@ -10,7 +10,7 @@ import { LoggerModule } from '../logger/logger.module'; import { Authorship } from '../revisions/authorship.entity'; import { Revision } from '../revisions/revision.entity'; 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 { User } from '../users/user.entity'; import { UsersModule } from '../users/users.module'; diff --git a/src/permissions/note-group-permission.entity.ts b/src/permissions/note-group-permission.entity.ts index 68c774033..79ea2d51d 100644 --- a/src/permissions/note-group-permission.entity.ts +++ b/src/permissions/note-group-permission.entity.ts @@ -4,7 +4,7 @@ * 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 { Note } from '../notes/note.entity'; diff --git a/src/permissions/note-user-permission.entity.ts b/src/permissions/note-user-permission.entity.ts index 62c96d9b9..c0f8f0e83 100644 --- a/src/permissions/note-user-permission.entity.ts +++ b/src/permissions/note-user-permission.entity.ts @@ -4,7 +4,7 @@ * 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 { User } from '../users/user.entity'; diff --git a/src/revisions/authorship.entity.ts b/src/revisions/authorship.entity.ts index 19772f320..24eaa084e 100644 --- a/src/revisions/authorship.entity.ts +++ b/src/revisions/authorship.entity.ts @@ -12,7 +12,7 @@ import { ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn, -} from 'typeorm/index'; +} from 'typeorm'; import { User } from '../users/user.entity'; import { Revision } from './revision.entity'; diff --git a/src/revisions/revision-metadata.dto.ts b/src/revisions/revision-metadata.dto.ts index bcbfa08a5..6f2e6832e 100644 --- a/src/revisions/revision-metadata.dto.ts +++ b/src/revisions/revision-metadata.dto.ts @@ -4,7 +4,7 @@ * 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'; export class RevisionMetadataDto { diff --git a/src/revisions/revision.entity.ts b/src/revisions/revision.entity.ts index e2c9e2f49..967ef26c5 100644 --- a/src/revisions/revision.entity.ts +++ b/src/revisions/revision.entity.ts @@ -11,7 +11,7 @@ import { ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { JoinTable, ManyToMany } from 'typeorm/index'; +import { JoinTable, ManyToMany } from 'typeorm'; import { Note } from '../notes/note.entity'; import { Authorship } from './authorship.entity'; diff --git a/src/revisions/revisions.service.spec.ts b/src/revisions/revisions.service.spec.ts index 50029d698..5c1d9d437 100644 --- a/src/revisions/revisions.service.spec.ts +++ b/src/revisions/revisions.service.spec.ts @@ -10,7 +10,7 @@ import { LoggerModule } from '../logger/logger.module'; import { AuthorColor } from '../notes/author-color.entity'; import { Note } from '../notes/note.entity'; 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 { User } from '../users/user.entity'; import { Authorship } from './authorship.entity'; diff --git a/src/users/identity.entity.ts b/src/users/identity.entity.ts index 73e2d8098..f8999bc82 100644 --- a/src/users/identity.entity.ts +++ b/src/users/identity.entity.ts @@ -11,7 +11,7 @@ import { ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn, -} from 'typeorm/index'; +} from 'typeorm'; import { User } from './user.entity'; @Entity() diff --git a/src/users/session.entity.ts b/src/users/session.entity.ts index aed832e49..9603373e8 100644 --- a/src/users/session.entity.ts +++ b/src/users/session.entity.ts @@ -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 { diff --git a/src/users/user.entity.ts b/src/users/user.entity.ts index c6c04d12a..cd0ab9c84 100644 --- a/src/users/user.entity.ts +++ b/src/users/user.entity.ts @@ -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() diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 9cbe048a1..fe68b2398 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -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], }) diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts index bb2596b21..407114f23 100644 --- a/src/users/users.service.spec.ts +++ b/src/users/users.service.spec.ts @@ -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); diff --git a/src/users/users.service.ts b/src/users/users.service.ts index ecf7ab4ea..cf30e0651 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -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, - @InjectRepository(AuthToken) - private authTokenRepository: Repository, ) { 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 { - 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 { - // hash the password with bcrypt and 2^16 iterations - return hash(cleartext, 16); - } - - async checkPassword(cleartext: string, password: string): Promise { - // 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 { - 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 { - 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'); diff --git a/test/public-api/users.e2e-spec.ts b/test/public-api/users.e2e-spec.ts index ae502d670..fd4689c3d 100644 --- a/test/public-api/users.e2e-spec.ts +++ b/test/public-api/users.e2e-spec.ts @@ -8,7 +8,6 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; -//import { UsersService } from '../../src/users/users.service'; import { UserInfoDto } from '../../src/users/user-info.dto'; import { HistoryService } from '../../src/history/history.service'; import { NotesService } from '../../src/notes/notes.service';