mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-20 02:05:21 -04:00
feat: add identity service
This service handles all the authentication of the private api. Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
021a0c9440
commit
6ad11e47cc
2 changed files with 214 additions and 0 deletions
120
src/identity/identity.service.spec.ts
Normal file
120
src/identity/identity.service.spec.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import appConfigMock from '../config/mock/app.config.mock';
|
||||||
|
import authConfigMock from '../config/mock/auth.config.mock';
|
||||||
|
import { NotInDBError } from '../errors/errors';
|
||||||
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { checkPassword, hashPassword } from '../utils/password';
|
||||||
|
import { Identity } from './identity.entity';
|
||||||
|
import { IdentityService } from './identity.service';
|
||||||
|
import { ProviderType } from './provider-type.enum';
|
||||||
|
|
||||||
|
describe('IdentityService', () => {
|
||||||
|
let service: IdentityService;
|
||||||
|
let user: User;
|
||||||
|
let identityRepo: Repository<Identity>;
|
||||||
|
const password = 'test123';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
IdentityService,
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(Identity),
|
||||||
|
useClass: Repository,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfigMock, authConfigMock],
|
||||||
|
}),
|
||||||
|
LoggerModule,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<IdentityService>(IdentityService);
|
||||||
|
user = User.create('test', 'Testy') as User;
|
||||||
|
identityRepo = module.get<Repository<Identity>>(
|
||||||
|
getRepositoryToken(Identity),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createLocalIdentity', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(identityRepo, 'save')
|
||||||
|
.mockImplementationOnce(
|
||||||
|
async (identity: Identity): Promise<Identity> => identity,
|
||||||
|
);
|
||||||
|
const identity = await service.createLocalIdentity(user, password);
|
||||||
|
await checkPassword(password, identity.passwordHash ?? '').then(
|
||||||
|
(result) => expect(result).toBeTruthy(),
|
||||||
|
);
|
||||||
|
expect(identity.user).toEqual(user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateLocalPassword', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(identityRepo, 'save')
|
||||||
|
.mockImplementationOnce(
|
||||||
|
async (identity: Identity): Promise<Identity> => identity,
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(
|
||||||
|
async (identity: Identity): Promise<Identity> => identity,
|
||||||
|
);
|
||||||
|
const identity = await service.createLocalIdentity(user, password);
|
||||||
|
user.identities = Promise.resolve([identity]);
|
||||||
|
});
|
||||||
|
it('works', async () => {
|
||||||
|
const newPassword = 'newPassword';
|
||||||
|
const identity = await service.updateLocalPassword(user, newPassword);
|
||||||
|
await checkPassword(newPassword, identity.passwordHash ?? '').then(
|
||||||
|
(result) => expect(result).toBeTruthy(),
|
||||||
|
);
|
||||||
|
expect(identity.user).toEqual(user);
|
||||||
|
});
|
||||||
|
it('fails, when user has no local identity', async () => {
|
||||||
|
user.identities = Promise.resolve([]);
|
||||||
|
await expect(service.updateLocalPassword(user, password)).rejects.toThrow(
|
||||||
|
NotInDBError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loginWithLocalIdentity', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
const identity = Identity.create(user, ProviderType.LOCAL);
|
||||||
|
identity.passwordHash = await hashPassword(password);
|
||||||
|
user.identities = Promise.resolve([identity]);
|
||||||
|
await expect(
|
||||||
|
service.loginWithLocalIdentity(user, password),
|
||||||
|
).resolves.toEqual(undefined);
|
||||||
|
});
|
||||||
|
describe('fails', () => {
|
||||||
|
it('when user has no local identity', async () => {
|
||||||
|
user.identities = Promise.resolve([]);
|
||||||
|
await expect(
|
||||||
|
service.updateLocalPassword(user, password),
|
||||||
|
).rejects.toThrow(NotInDBError);
|
||||||
|
});
|
||||||
|
it('when the password is wrong', async () => {
|
||||||
|
user.identities = Promise.resolve([]);
|
||||||
|
await expect(
|
||||||
|
service.updateLocalPassword(user, 'wrong_password'),
|
||||||
|
).rejects.toThrow(NotInDBError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
94
src/identity/identity.service.ts
Normal file
94
src/identity/identity.service.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import authConfiguration, { AuthConfig } from '../config/auth.config';
|
||||||
|
import { NotInDBError } from '../errors/errors';
|
||||||
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { checkPassword, hashPassword } from '../utils/password';
|
||||||
|
import { Identity } from './identity.entity';
|
||||||
|
import { ProviderType } from './provider-type.enum';
|
||||||
|
import { getFirstIdentityFromUser } from './utils';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IdentityService {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@InjectRepository(Identity)
|
||||||
|
private identityRepository: Repository<Identity>,
|
||||||
|
@Inject(authConfiguration.KEY)
|
||||||
|
private authConfig: AuthConfig,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(IdentityService.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* Create a new identity for internal auth
|
||||||
|
* @param {User} user - the user the identity should be added to
|
||||||
|
* @param {string} password - the password the identity should have
|
||||||
|
* @return {Identity} the new local identity
|
||||||
|
*/
|
||||||
|
async createLocalIdentity(user: User, password: string): Promise<Identity> {
|
||||||
|
const identity = Identity.create(user, ProviderType.LOCAL);
|
||||||
|
identity.passwordHash = await hashPassword(password);
|
||||||
|
return await this.identityRepository.save(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* Update the internal password of the specified the user
|
||||||
|
* @param {User} user - the user, which identity should be updated
|
||||||
|
* @param {string} newPassword - the new password
|
||||||
|
* @throws {NotInDBError} the specified user has no internal identity
|
||||||
|
* @return {Identity} the changed identity
|
||||||
|
*/
|
||||||
|
async updateLocalPassword(
|
||||||
|
user: User,
|
||||||
|
newPassword: string,
|
||||||
|
): Promise<Identity> {
|
||||||
|
const internalIdentity: Identity | undefined =
|
||||||
|
await getFirstIdentityFromUser(user, ProviderType.LOCAL);
|
||||||
|
if (internalIdentity === undefined) {
|
||||||
|
this.logger.debug(
|
||||||
|
`The user with the username ${user.userName} does not have a internal identity.`,
|
||||||
|
'updateLocalPassword',
|
||||||
|
);
|
||||||
|
throw new NotInDBError('This user has no internal identity.');
|
||||||
|
}
|
||||||
|
internalIdentity.passwordHash = await hashPassword(newPassword);
|
||||||
|
return await this.identityRepository.save(internalIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* Login the user with their username and password
|
||||||
|
* @param {User} user - the user to use
|
||||||
|
* @param {string} password - the password to use
|
||||||
|
* @throws {NotInDBError} the specified user can't be logged in
|
||||||
|
*/
|
||||||
|
async loginWithLocalIdentity(user: User, password: string): Promise<void> {
|
||||||
|
const internalIdentity: Identity | undefined =
|
||||||
|
await getFirstIdentityFromUser(user, ProviderType.LOCAL);
|
||||||
|
if (internalIdentity === undefined) {
|
||||||
|
this.logger.debug(
|
||||||
|
`The user with the username ${user.userName} does not have a internal identity.`,
|
||||||
|
'loginWithLocalIdentity',
|
||||||
|
);
|
||||||
|
throw new NotInDBError();
|
||||||
|
}
|
||||||
|
if (!(await checkPassword(password, internalIdentity.passwordHash ?? ''))) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Password check for ${user.userName} did not succeed.`,
|
||||||
|
'loginWithLocalIdentity',
|
||||||
|
);
|
||||||
|
throw new NotInDBError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue