mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-26 19:53:59 -05:00
Merge pull request #1517 from hedgedoc/privateApi/auth/email
Add local login with username and password
This commit is contained in:
commit
83f0bbb986
51 changed files with 1352 additions and 178 deletions
|
@ -40,8 +40,9 @@ entity "identity" {
|
||||||
*id : number
|
*id : number
|
||||||
--
|
--
|
||||||
*userId : uuid <<FK user>>
|
*userId : uuid <<FK user>>
|
||||||
|
*providerType: text
|
||||||
' Identifies the external login provider and is set in the config
|
' Identifies the external login provider and is set in the config
|
||||||
*providerName : text
|
providerName : text
|
||||||
*syncSource : boolean
|
*syncSource : boolean
|
||||||
*createdAt : date
|
*createdAt : date
|
||||||
*updatedAt : date
|
*updatedAt : date
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"connect-typeorm": "1.1.4",
|
"connect-typeorm": "1.1.4",
|
||||||
"eslint-plugin-jest": "24.4.0",
|
"eslint-plugin-jest": "24.4.0",
|
||||||
"eslint-plugin-local-rules": "1.1.0",
|
"eslint-plugin-local-rules": "1.1.0",
|
||||||
|
"express-session": "1.17.2",
|
||||||
"file-type": "16.5.3",
|
"file-type": "16.5.3",
|
||||||
"joi": "17.4.2",
|
"joi": "17.4.2",
|
||||||
"minio": "7.0.19",
|
"minio": "7.0.19",
|
||||||
|
@ -54,6 +55,7 @@
|
||||||
"node-fetch": "2.6.2",
|
"node-fetch": "2.6.2",
|
||||||
"passport": "0.4.1",
|
"passport": "0.4.1",
|
||||||
"passport-http-bearer": "1.0.1",
|
"passport-http-bearer": "1.0.1",
|
||||||
|
"passport-local": "1.0.0",
|
||||||
"raw-body": "2.4.1",
|
"raw-body": "2.4.1",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
|
@ -70,8 +72,10 @@
|
||||||
"@tsconfig/node12": "1.0.9",
|
"@tsconfig/node12": "1.0.9",
|
||||||
"@types/cli-color": "2.0.1",
|
"@types/cli-color": "2.0.1",
|
||||||
"@types/express": "4.17.13",
|
"@types/express": "4.17.13",
|
||||||
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/jest": "27.0.1",
|
"@types/jest": "27.0.1",
|
||||||
"@types/node": "14.17.16",
|
"@types/node": "14.17.16",
|
||||||
|
"@types/passport-local": "^1.0.34",
|
||||||
"@types/supertest": "2.0.11",
|
"@types/supertest": "2.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "4.31.1",
|
"@typescript-eslint/eslint-plugin": "4.31.1",
|
||||||
"@typescript-eslint/parser": "4.31.1",
|
"@typescript-eslint/parser": "4.31.1",
|
||||||
|
|
71
src/api/private/auth/auth.controller.spec.ts
Normal file
71
src/api/private/auth/auth.controller.spec.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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 { getConnectionToken, 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 { Identity } from '../../../identity/identity.entity';
|
||||||
|
import { IdentityModule } from '../../../identity/identity.module';
|
||||||
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
|
import { Session } from '../../../users/session.entity';
|
||||||
|
import { User } from '../../../users/user.entity';
|
||||||
|
import { UsersModule } from '../../../users/users.module';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
|
||||||
|
describe('AuthController', () => {
|
||||||
|
let controller: AuthController;
|
||||||
|
|
||||||
|
type MockConnection = {
|
||||||
|
transaction: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function mockConnection(): MockConnection {
|
||||||
|
return {
|
||||||
|
transaction: jest.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(Identity),
|
||||||
|
useClass: Repository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: getConnectionToken(),
|
||||||
|
useFactory: mockConnection,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfigMock, authConfigMock],
|
||||||
|
}),
|
||||||
|
LoggerModule,
|
||||||
|
UsersModule,
|
||||||
|
IdentityModule,
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
})
|
||||||
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
|
.useClass(Repository)
|
||||||
|
.overrideProvider(getRepositoryToken(Session))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(User))
|
||||||
|
.useValue({})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
controller = module.get<AuthController>(AuthController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
105
src/api/private/auth/auth.controller.ts
Normal file
105
src/api/private/auth/auth.controller.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
NotFoundException,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Req,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Session } from 'express-session';
|
||||||
|
|
||||||
|
import { AlreadyInDBError, NotInDBError } from '../../../errors/errors';
|
||||||
|
import { IdentityService } from '../../../identity/identity.service';
|
||||||
|
import { LocalAuthGuard } from '../../../identity/local/local.strategy';
|
||||||
|
import { LoginDto } from '../../../identity/local/login.dto';
|
||||||
|
import { RegisterDto } from '../../../identity/local/register.dto';
|
||||||
|
import { UpdatePasswordDto } from '../../../identity/local/update-password.dto';
|
||||||
|
import { SessionGuard } from '../../../identity/session.guard';
|
||||||
|
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
|
||||||
|
import { User } from '../../../users/user.entity';
|
||||||
|
import { UsersService } from '../../../users/users.service';
|
||||||
|
import { LoginEnabledGuard } from '../../utils/login-enabled.guard';
|
||||||
|
import { RegistrationEnabledGuard } from '../../utils/registration-enabled.guard';
|
||||||
|
import { RequestUser } from '../../utils/request-user.decorator';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
private usersService: UsersService,
|
||||||
|
private identityService: IdentityService,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(AuthController.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(RegistrationEnabledGuard)
|
||||||
|
@Post('local')
|
||||||
|
async registerUser(@Body() registerDto: RegisterDto): Promise<void> {
|
||||||
|
try {
|
||||||
|
const user = await this.usersService.createUser(
|
||||||
|
registerDto.username,
|
||||||
|
registerDto.displayname,
|
||||||
|
);
|
||||||
|
// ToDo: Figure out how to rollback user if anything with this calls goes wrong
|
||||||
|
await this.identityService.createLocalIdentity(
|
||||||
|
user,
|
||||||
|
registerDto.password,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof AlreadyInDBError) {
|
||||||
|
throw new BadRequestException(e.message);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(LoginEnabledGuard, SessionGuard)
|
||||||
|
@Put('local')
|
||||||
|
async updatePassword(
|
||||||
|
@RequestUser() user: User,
|
||||||
|
@Body() changePasswordDto: UpdatePasswordDto,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.identityService.updateLocalPassword(
|
||||||
|
user,
|
||||||
|
changePasswordDto.newPassword,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotInDBError) {
|
||||||
|
throw new NotFoundException(e.message);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(LoginEnabledGuard, LocalAuthGuard)
|
||||||
|
@Post('local/login')
|
||||||
|
login(
|
||||||
|
@Req() request: Request & { session: { user: string } },
|
||||||
|
@Body() loginDto: LoginDto,
|
||||||
|
): void {
|
||||||
|
// There is no further testing needed as we only get to this point if LocalAuthGuard was successful
|
||||||
|
request.session.user = loginDto.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(SessionGuard)
|
||||||
|
@Delete('logout')
|
||||||
|
logout(@Req() request: Request & { session: Session }): void {
|
||||||
|
request.session.destroy((err) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error('Encountered an error while logging out: ${err}');
|
||||||
|
throw new BadRequestException('Unable to log out');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import appConfigMock from '../../../../config/mock/app.config.mock';
|
||||||
import { Group } from '../../../../groups/group.entity';
|
import { Group } from '../../../../groups/group.entity';
|
||||||
import { HistoryEntry } from '../../../../history/history-entry.entity';
|
import { HistoryEntry } from '../../../../history/history-entry.entity';
|
||||||
import { HistoryModule } from '../../../../history/history.module';
|
import { HistoryModule } from '../../../../history/history.module';
|
||||||
|
import { Identity } from '../../../../identity/identity.entity';
|
||||||
import { LoggerModule } from '../../../../logger/logger.module';
|
import { LoggerModule } from '../../../../logger/logger.module';
|
||||||
import { Note } from '../../../../notes/note.entity';
|
import { Note } from '../../../../notes/note.entity';
|
||||||
import { NotesModule } from '../../../../notes/notes.module';
|
import { NotesModule } from '../../../../notes/notes.module';
|
||||||
|
@ -25,7 +26,6 @@ import { NoteGroupPermission } from '../../../../permissions/note-group-permissi
|
||||||
import { NoteUserPermission } from '../../../../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../../../../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../../../../revisions/edit.entity';
|
import { Edit } from '../../../../revisions/edit.entity';
|
||||||
import { Revision } from '../../../../revisions/revision.entity';
|
import { Revision } from '../../../../revisions/revision.entity';
|
||||||
import { Identity } from '../../../../users/identity.entity';
|
|
||||||
import { Session } from '../../../../users/session.entity';
|
import { Session } from '../../../../users/session.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';
|
||||||
|
|
|
@ -14,6 +14,7 @@ import customizationConfigMock from '../../../config/mock/customization.config.m
|
||||||
import externalServicesConfigMock from '../../../config/mock/external-services.config.mock';
|
import externalServicesConfigMock from '../../../config/mock/external-services.config.mock';
|
||||||
import mediaConfigMock from '../../../config/mock/media.config.mock';
|
import mediaConfigMock from '../../../config/mock/media.config.mock';
|
||||||
import { Group } from '../../../groups/group.entity';
|
import { Group } from '../../../groups/group.entity';
|
||||||
|
import { Identity } from '../../../identity/identity.entity';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
import { MediaUpload } from '../../../media/media-upload.entity';
|
import { MediaUpload } from '../../../media/media-upload.entity';
|
||||||
import { MediaModule } from '../../../media/media.module';
|
import { MediaModule } from '../../../media/media.module';
|
||||||
|
@ -23,7 +24,6 @@ import { NoteGroupPermission } from '../../../permissions/note-group-permission.
|
||||||
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../../../revisions/edit.entity';
|
import { Edit } from '../../../revisions/edit.entity';
|
||||||
import { Revision } from '../../../revisions/revision.entity';
|
import { Revision } from '../../../revisions/revision.entity';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
|
||||||
import { Session } from '../../../users/session.entity';
|
import { Session } from '../../../users/session.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';
|
||||||
|
|
|
@ -15,6 +15,7 @@ import customizationConfigMock from '../../../config/mock/customization.config.m
|
||||||
import externalConfigMock from '../../../config/mock/external-services.config.mock';
|
import externalConfigMock from '../../../config/mock/external-services.config.mock';
|
||||||
import mediaConfigMock from '../../../config/mock/media.config.mock';
|
import mediaConfigMock from '../../../config/mock/media.config.mock';
|
||||||
import { Group } from '../../../groups/group.entity';
|
import { Group } from '../../../groups/group.entity';
|
||||||
|
import { Identity } from '../../../identity/identity.entity';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
import { MediaUpload } from '../../../media/media-upload.entity';
|
import { MediaUpload } from '../../../media/media-upload.entity';
|
||||||
import { MediaModule } from '../../../media/media.module';
|
import { MediaModule } from '../../../media/media.module';
|
||||||
|
@ -25,7 +26,6 @@ import { NoteGroupPermission } from '../../../permissions/note-group-permission.
|
||||||
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../../../revisions/edit.entity';
|
import { Edit } from '../../../revisions/edit.entity';
|
||||||
import { Revision } from '../../../revisions/revision.entity';
|
import { Revision } from '../../../revisions/revision.entity';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
|
||||||
import { Session } from '../../../users/session.entity';
|
import { Session } from '../../../users/session.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';
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { Group } from '../../../groups/group.entity';
|
||||||
import { GroupsModule } from '../../../groups/groups.module';
|
import { GroupsModule } from '../../../groups/groups.module';
|
||||||
import { HistoryEntry } from '../../../history/history-entry.entity';
|
import { HistoryEntry } from '../../../history/history-entry.entity';
|
||||||
import { HistoryModule } from '../../../history/history.module';
|
import { HistoryModule } from '../../../history/history.module';
|
||||||
|
import { Identity } from '../../../identity/identity.entity';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
import { MediaUpload } from '../../../media/media-upload.entity';
|
import { MediaUpload } from '../../../media/media-upload.entity';
|
||||||
import { MediaModule } from '../../../media/media.module';
|
import { MediaModule } from '../../../media/media.module';
|
||||||
|
@ -31,7 +32,6 @@ import { PermissionsModule } from '../../../permissions/permissions.module';
|
||||||
import { Edit } from '../../../revisions/edit.entity';
|
import { Edit } from '../../../revisions/edit.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 { Identity } from '../../../users/identity.entity';
|
|
||||||
import { Session } from '../../../users/session.entity';
|
import { Session } from '../../../users/session.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';
|
||||||
|
|
|
@ -8,12 +8,14 @@ import { Module } from '@nestjs/common';
|
||||||
import { AuthModule } from '../../auth/auth.module';
|
import { AuthModule } from '../../auth/auth.module';
|
||||||
import { FrontendConfigModule } from '../../frontend-config/frontend-config.module';
|
import { FrontendConfigModule } from '../../frontend-config/frontend-config.module';
|
||||||
import { HistoryModule } from '../../history/history.module';
|
import { HistoryModule } from '../../history/history.module';
|
||||||
|
import { IdentityModule } from '../../identity/identity.module';
|
||||||
import { LoggerModule } from '../../logger/logger.module';
|
import { LoggerModule } from '../../logger/logger.module';
|
||||||
import { MediaModule } from '../../media/media.module';
|
import { MediaModule } from '../../media/media.module';
|
||||||
import { NotesModule } from '../../notes/notes.module';
|
import { NotesModule } from '../../notes/notes.module';
|
||||||
import { PermissionsModule } from '../../permissions/permissions.module';
|
import { PermissionsModule } from '../../permissions/permissions.module';
|
||||||
import { RevisionsModule } from '../../revisions/revisions.module';
|
import { RevisionsModule } from '../../revisions/revisions.module';
|
||||||
import { UsersModule } from '../../users/users.module';
|
import { UsersModule } from '../../users/users.module';
|
||||||
|
import { AuthController } from './auth/auth.controller';
|
||||||
import { ConfigController } from './config/config.controller';
|
import { ConfigController } from './config/config.controller';
|
||||||
import { HistoryController } from './me/history/history.controller';
|
import { HistoryController } from './me/history/history.controller';
|
||||||
import { MeController } from './me/me.controller';
|
import { MeController } from './me/me.controller';
|
||||||
|
@ -32,6 +34,7 @@ import { TokensController } from './tokens/tokens.controller';
|
||||||
NotesModule,
|
NotesModule,
|
||||||
MediaModule,
|
MediaModule,
|
||||||
RevisionsModule,
|
RevisionsModule,
|
||||||
|
IdentityModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
TokensController,
|
TokensController,
|
||||||
|
@ -40,6 +43,7 @@ import { TokensController } from './tokens/tokens.controller';
|
||||||
HistoryController,
|
HistoryController,
|
||||||
MeController,
|
MeController,
|
||||||
NotesController,
|
NotesController,
|
||||||
|
AuthController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PrivateApiModule {}
|
export class PrivateApiModule {}
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { AuthToken } from '../../../auth/auth-token.entity';
|
import { AuthToken } from '../../../auth/auth-token.entity';
|
||||||
import { AuthModule } from '../../../auth/auth.module';
|
import { AuthModule } from '../../../auth/auth.module';
|
||||||
import appConfigMock from '../../../config/mock/app.config.mock';
|
import appConfigMock from '../../../config/mock/app.config.mock';
|
||||||
|
import { Identity } from '../../../identity/identity.entity';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
|
||||||
import { Session } from '../../../users/session.entity';
|
import { Session } from '../../../users/session.entity';
|
||||||
import { User } from '../../../users/user.entity';
|
import { User } from '../../../users/user.entity';
|
||||||
import { TokensController } from './tokens.controller';
|
import { TokensController } from './tokens.controller';
|
||||||
|
|
|
@ -18,6 +18,7 @@ import mediaConfigMock from '../../../config/mock/media.config.mock';
|
||||||
import { Group } from '../../../groups/group.entity';
|
import { Group } from '../../../groups/group.entity';
|
||||||
import { HistoryEntry } from '../../../history/history-entry.entity';
|
import { HistoryEntry } from '../../../history/history-entry.entity';
|
||||||
import { HistoryModule } from '../../../history/history.module';
|
import { HistoryModule } from '../../../history/history.module';
|
||||||
|
import { Identity } from '../../../identity/identity.entity';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
import { MediaUpload } from '../../../media/media-upload.entity';
|
import { MediaUpload } from '../../../media/media-upload.entity';
|
||||||
import { MediaModule } from '../../../media/media.module';
|
import { MediaModule } from '../../../media/media.module';
|
||||||
|
@ -28,7 +29,6 @@ import { NoteGroupPermission } from '../../../permissions/note-group-permission.
|
||||||
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../../../revisions/edit.entity';
|
import { Edit } from '../../../revisions/edit.entity';
|
||||||
import { Revision } from '../../../revisions/revision.entity';
|
import { Revision } from '../../../revisions/revision.entity';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
|
||||||
import { Session } from '../../../users/session.entity';
|
import { Session } from '../../../users/session.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';
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Author } from '../../../authors/author.entity';
|
||||||
import appConfigMock from '../../../config/mock/app.config.mock';
|
import appConfigMock from '../../../config/mock/app.config.mock';
|
||||||
import mediaConfigMock from '../../../config/mock/media.config.mock';
|
import mediaConfigMock from '../../../config/mock/media.config.mock';
|
||||||
import { Group } from '../../../groups/group.entity';
|
import { Group } from '../../../groups/group.entity';
|
||||||
|
import { Identity } from '../../../identity/identity.entity';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
import { MediaUpload } from '../../../media/media-upload.entity';
|
import { MediaUpload } from '../../../media/media-upload.entity';
|
||||||
import { MediaModule } from '../../../media/media.module';
|
import { MediaModule } from '../../../media/media.module';
|
||||||
|
@ -22,7 +23,6 @@ import { NoteGroupPermission } from '../../../permissions/note-group-permission.
|
||||||
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../../../revisions/edit.entity';
|
import { Edit } from '../../../revisions/edit.entity';
|
||||||
import { Revision } from '../../../revisions/revision.entity';
|
import { Revision } from '../../../revisions/revision.entity';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
|
||||||
import { Session } from '../../../users/session.entity';
|
import { Session } from '../../../users/session.entity';
|
||||||
import { User } from '../../../users/user.entity';
|
import { User } from '../../../users/user.entity';
|
||||||
import { MediaController } from './media.controller';
|
import { MediaController } from './media.controller';
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { Group } from '../../../groups/group.entity';
|
||||||
import { GroupsModule } from '../../../groups/groups.module';
|
import { GroupsModule } from '../../../groups/groups.module';
|
||||||
import { HistoryEntry } from '../../../history/history-entry.entity';
|
import { HistoryEntry } from '../../../history/history-entry.entity';
|
||||||
import { HistoryModule } from '../../../history/history.module';
|
import { HistoryModule } from '../../../history/history.module';
|
||||||
|
import { Identity } from '../../../identity/identity.entity';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
import { MediaUpload } from '../../../media/media-upload.entity';
|
import { MediaUpload } from '../../../media/media-upload.entity';
|
||||||
import { MediaModule } from '../../../media/media.module';
|
import { MediaModule } from '../../../media/media.module';
|
||||||
|
@ -31,7 +32,6 @@ import { PermissionsModule } from '../../../permissions/permissions.module';
|
||||||
import { Edit } from '../../../revisions/edit.entity';
|
import { Edit } from '../../../revisions/edit.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 { Identity } from '../../../users/identity.entity';
|
|
||||||
import { Session } from '../../../users/session.entity';
|
import { Session } from '../../../users/session.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';
|
||||||
|
|
33
src/api/utils/login-enabled.guard.ts
Normal file
33
src/api/utils/login-enabled.guard.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
CanActivate,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import authConfiguration, { AuthConfig } from '../../config/auth.config';
|
||||||
|
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoginEnabledGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@Inject(authConfiguration.KEY)
|
||||||
|
private authConfig: AuthConfig,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(LoginEnabledGuard.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(): boolean {
|
||||||
|
if (!this.authConfig.local.enableLogin) {
|
||||||
|
this.logger.debug('Local auth is disabled.', 'canActivate');
|
||||||
|
throw new BadRequestException('Local auth is disabled.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
33
src/api/utils/registration-enabled.guard.ts
Normal file
33
src/api/utils/registration-enabled.guard.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
CanActivate,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import authConfiguration, { AuthConfig } from '../../config/auth.config';
|
||||||
|
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RegistrationEnabledGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@Inject(authConfiguration.KEY)
|
||||||
|
private authConfig: AuthConfig,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(RegistrationEnabledGuard.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(): boolean {
|
||||||
|
if (!this.authConfig.local.enableRegister) {
|
||||||
|
this.logger.debug('User registration is disabled.', 'canActivate');
|
||||||
|
throw new BadRequestException('User registration is disabled.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import { FrontendConfigModule } from './frontend-config/frontend-config.module';
|
||||||
import { FrontendConfigService } from './frontend-config/frontend-config.service';
|
import { FrontendConfigService } from './frontend-config/frontend-config.service';
|
||||||
import { GroupsModule } from './groups/groups.module';
|
import { GroupsModule } from './groups/groups.module';
|
||||||
import { HistoryModule } from './history/history.module';
|
import { HistoryModule } from './history/history.module';
|
||||||
|
import { IdentityModule } from './identity/identity.module';
|
||||||
import { LoggerModule } from './logger/logger.module';
|
import { LoggerModule } from './logger/logger.module';
|
||||||
import { MediaModule } from './media/media.module';
|
import { MediaModule } from './media/media.module';
|
||||||
import { MonitoringModule } from './monitoring/monitoring.module';
|
import { MonitoringModule } from './monitoring/monitoring.module';
|
||||||
|
@ -81,6 +82,7 @@ const routes: Routes = [
|
||||||
MediaModule,
|
MediaModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
FrontendConfigModule,
|
FrontendConfigModule,
|
||||||
|
IdentityModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [FrontendConfigService],
|
providers: [FrontendConfigService],
|
||||||
|
|
|
@ -7,16 +7,16 @@ import { ConfigModule } from '@nestjs/config';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import appConfigMock from '../config/mock/app.config.mock';
|
import appConfigMock from '../config/mock/app.config.mock';
|
||||||
import { NotInDBError, TokenNotValidError } from '../errors/errors';
|
import { NotInDBError, TokenNotValidError } from '../errors/errors';
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { Identity } from '../users/identity.entity';
|
|
||||||
import { Session } from '../users/session.entity';
|
import { Session } from '../users/session.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 { hashPassword } from '../utils/password';
|
||||||
import { AuthToken } from './auth-token.entity';
|
import { AuthToken } from './auth-token.entity';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
@ -74,26 +74,6 @@ describe('AuthService', () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkPassword', () => {
|
|
||||||
it('works', async () => {
|
|
||||||
const testPassword = 'thisIsATestPassword';
|
|
||||||
const hash = await service.hashPassword(testPassword);
|
|
||||||
await service
|
|
||||||
.checkPassword(testPassword, hash)
|
|
||||||
.then((result) => expect(result).toBeTruthy());
|
|
||||||
});
|
|
||||||
it('fails, if secret is too short', async () => {
|
|
||||||
const secret = service.bufferToBase64Url(randomBytes(54));
|
|
||||||
const hash = await service.hashPassword(secret);
|
|
||||||
await service
|
|
||||||
.checkPassword(secret, hash)
|
|
||||||
.then((result) => expect(result).toBeTruthy());
|
|
||||||
await service
|
|
||||||
.checkPassword(secret.substr(0, secret.length - 1), hash)
|
|
||||||
.then((result) => expect(result).toBeFalsy());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTokensByUsername', () => {
|
describe('getTokensByUsername', () => {
|
||||||
it('works', async () => {
|
it('works', async () => {
|
||||||
jest
|
jest
|
||||||
|
@ -108,7 +88,7 @@ describe('AuthService', () => {
|
||||||
describe('getAuthToken', () => {
|
describe('getAuthToken', () => {
|
||||||
const token = 'testToken';
|
const token = 'testToken';
|
||||||
it('works', async () => {
|
it('works', async () => {
|
||||||
const accessTokenHash = await service.hashPassword(token);
|
const accessTokenHash = await hashPassword(token);
|
||||||
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({
|
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({
|
||||||
...authToken,
|
...authToken,
|
||||||
user: user,
|
user: user,
|
||||||
|
@ -142,7 +122,7 @@ describe('AuthService', () => {
|
||||||
).rejects.toThrow(TokenNotValidError);
|
).rejects.toThrow(TokenNotValidError);
|
||||||
});
|
});
|
||||||
it('AuthToken has wrong validUntil Date', async () => {
|
it('AuthToken has wrong validUntil Date', async () => {
|
||||||
const accessTokenHash = await service.hashPassword(token);
|
const accessTokenHash = await hashPassword(token);
|
||||||
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({
|
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({
|
||||||
...authToken,
|
...authToken,
|
||||||
user: user,
|
user: user,
|
||||||
|
@ -185,7 +165,7 @@ describe('AuthService', () => {
|
||||||
describe('validateToken', () => {
|
describe('validateToken', () => {
|
||||||
it('works', async () => {
|
it('works', async () => {
|
||||||
const token = 'testToken';
|
const token = 'testToken';
|
||||||
const accessTokenHash = await service.hashPassword(token);
|
const accessTokenHash = await hashPassword(token);
|
||||||
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce({
|
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce({
|
||||||
...user,
|
...user,
|
||||||
authTokens: [authToken],
|
authTokens: [authToken],
|
||||||
|
@ -303,16 +283,6 @@ describe('AuthService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('bufferToBase64Url', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(
|
|
||||||
service.bufferToBase64Url(
|
|
||||||
Buffer.from('testsentence is a test sentence'),
|
|
||||||
),
|
|
||||||
).toEqual('dGVzdHNlbnRlbmNlIGlzIGEgdGVzdCBzZW50ZW5jZQ');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('toAuthTokenDto', () => {
|
describe('toAuthTokenDto', () => {
|
||||||
it('works', () => {
|
it('works', () => {
|
||||||
const authToken = new AuthToken();
|
const authToken = new AuthToken();
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Cron, Timeout } from '@nestjs/schedule';
|
import { Cron, Timeout } from '@nestjs/schedule';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { compare, hash } from 'bcrypt';
|
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@ -16,8 +15,14 @@ import {
|
||||||
TooManyTokensError,
|
TooManyTokensError,
|
||||||
} from '../errors/errors';
|
} from '../errors/errors';
|
||||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
|
import { UserRelationEnum } from '../users/user-relation.enum';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
|
import {
|
||||||
|
bufferToBase64Url,
|
||||||
|
checkPassword,
|
||||||
|
hashPassword,
|
||||||
|
} from '../utils/password';
|
||||||
import { TimestampMillis } from '../utils/timestamp';
|
import { TimestampMillis } from '../utils/timestamp';
|
||||||
import { AuthTokenWithSecretDto } from './auth-token-with-secret.dto';
|
import { AuthTokenWithSecretDto } from './auth-token-with-secret.dto';
|
||||||
import { AuthTokenDto } from './auth-token.dto';
|
import { AuthTokenDto } from './auth-token.dto';
|
||||||
|
@ -52,33 +57,14 @@ export class AuthService {
|
||||||
return await this.usersService.getUserByUsername(accessToken.user.userName);
|
return await this.usersService.getUserByUsername(accessToken.user.userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hashPassword(cleartext: string): Promise<string> {
|
|
||||||
// hash the password with bcrypt and 2^12 iterations
|
|
||||||
// this was decided on the basis of https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#bcrypt
|
|
||||||
return await hash(cleartext, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkPassword(cleartext: string, password: string): Promise<boolean> {
|
|
||||||
return await compare(cleartext, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferToBase64Url(text: Buffer): 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 text
|
|
||||||
.toString('base64')
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTokenForUser(
|
async createTokenForUser(
|
||||||
userName: string,
|
userName: string,
|
||||||
identifier: string,
|
identifier: string,
|
||||||
validUntil: TimestampMillis,
|
validUntil: TimestampMillis,
|
||||||
): Promise<AuthTokenWithSecretDto> {
|
): Promise<AuthTokenWithSecretDto> {
|
||||||
const user = await this.usersService.getUserByUsername(userName, true);
|
const user = await this.usersService.getUserByUsername(userName, [
|
||||||
|
UserRelationEnum.AUTHTOKENS,
|
||||||
|
]);
|
||||||
if (user.authTokens.length >= 200) {
|
if (user.authTokens.length >= 200) {
|
||||||
// This is a very high ceiling unlikely to hinder legitimate usage,
|
// This is a very high ceiling unlikely to hinder legitimate usage,
|
||||||
// but should prevent possible attack vectors
|
// but should prevent possible attack vectors
|
||||||
|
@ -86,9 +72,9 @@ export class AuthService {
|
||||||
`User '${user.userName}' has already 200 tokens and can't have anymore`,
|
`User '${user.userName}' has already 200 tokens and can't have anymore`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const secret = this.bufferToBase64Url(randomBytes(54));
|
const secret = bufferToBase64Url(randomBytes(54));
|
||||||
const keyId = this.bufferToBase64Url(randomBytes(8));
|
const keyId = bufferToBase64Url(randomBytes(8));
|
||||||
const accessToken = await this.hashPassword(secret);
|
const accessToken = await hashPassword(secret);
|
||||||
let token;
|
let token;
|
||||||
// Tokens can only be valid for a maximum of 2 years
|
// Tokens can only be valid for a maximum of 2 years
|
||||||
const maximumTokenValidity =
|
const maximumTokenValidity =
|
||||||
|
@ -138,7 +124,7 @@ export class AuthService {
|
||||||
if (accessToken === undefined) {
|
if (accessToken === undefined) {
|
||||||
throw new NotInDBError(`AuthToken '${token}' not found`);
|
throw new NotInDBError(`AuthToken '${token}' not found`);
|
||||||
}
|
}
|
||||||
if (!(await this.checkPassword(token, accessToken.accessTokenHash))) {
|
if (!(await checkPassword(token, accessToken.accessTokenHash))) {
|
||||||
// hashes are not the same
|
// hashes are not the same
|
||||||
throw new TokenNotValidError(`AuthToken '${token}' is not valid.`);
|
throw new TokenNotValidError(`AuthToken '${token}' is not valid.`);
|
||||||
}
|
}
|
||||||
|
@ -155,7 +141,9 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTokensByUsername(userName: string): Promise<AuthToken[]> {
|
async getTokensByUsername(userName: string): Promise<AuthToken[]> {
|
||||||
const user = await this.usersService.getUserByUsername(userName, true);
|
const user = await this.usersService.getUserByUsername(userName, [
|
||||||
|
UserRelationEnum.AUTHTOKENS,
|
||||||
|
]);
|
||||||
if (user.authTokens === undefined) {
|
if (user.authTokens === undefined) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,17 @@ import * as Joi from 'joi';
|
||||||
import { GitlabScope, GitlabVersion } from './gitlab.enum';
|
import { GitlabScope, GitlabVersion } from './gitlab.enum';
|
||||||
import {
|
import {
|
||||||
buildErrorMessage,
|
buildErrorMessage,
|
||||||
|
parseOptionalInt,
|
||||||
replaceAuthErrorsWithEnvironmentVariables,
|
replaceAuthErrorsWithEnvironmentVariables,
|
||||||
toArrayConfig,
|
toArrayConfig,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
export interface AuthConfig {
|
export interface AuthConfig {
|
||||||
email: {
|
session: {
|
||||||
|
secret: string;
|
||||||
|
lifetime: number;
|
||||||
|
};
|
||||||
|
local: {
|
||||||
enableLogin: boolean;
|
enableLogin: boolean;
|
||||||
enableRegister: boolean;
|
enableRegister: boolean;
|
||||||
};
|
};
|
||||||
|
@ -101,15 +106,22 @@ export interface AuthConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
const authSchema = Joi.object({
|
const authSchema = Joi.object({
|
||||||
email: {
|
session: {
|
||||||
|
secret: Joi.string().label('HD_SESSION_SECRET'),
|
||||||
|
lifetime: Joi.number()
|
||||||
|
.default(1209600000) // 14 * 24 * 60 * 60 * 1000ms = 14 days
|
||||||
|
.optional()
|
||||||
|
.label('HD_SESSION_LIFETIME'),
|
||||||
|
},
|
||||||
|
local: {
|
||||||
enableLogin: Joi.boolean()
|
enableLogin: Joi.boolean()
|
||||||
.default(false)
|
.default(false)
|
||||||
.optional()
|
.optional()
|
||||||
.label('HD_AUTH_EMAIL_ENABLE_LOGIN'),
|
.label('HD_AUTH_LOCAL_ENABLE_LOGIN'),
|
||||||
enableRegister: Joi.boolean()
|
enableRegister: Joi.boolean()
|
||||||
.default(false)
|
.default(false)
|
||||||
.optional()
|
.optional()
|
||||||
.label('HD_AUTH_EMAIL_ENABLE_REGISTER'),
|
.label('HD_AUTH_LOCAL_ENABLE_REGISTER'),
|
||||||
},
|
},
|
||||||
facebook: {
|
facebook: {
|
||||||
clientID: Joi.string().optional().label('HD_AUTH_FACEBOOK_CLIENT_ID'),
|
clientID: Joi.string().optional().label('HD_AUTH_FACEBOOK_CLIENT_ID'),
|
||||||
|
@ -199,7 +211,7 @@ const authSchema = Joi.object({
|
||||||
attribute: {
|
attribute: {
|
||||||
id: Joi.string().default('NameId').optional(),
|
id: Joi.string().default('NameId').optional(),
|
||||||
username: Joi.string().default('NameId').optional(),
|
username: Joi.string().default('NameId').optional(),
|
||||||
email: Joi.string().default('NameId').optional(),
|
local: Joi.string().default('NameId').optional(),
|
||||||
},
|
},
|
||||||
}).optional(),
|
}).optional(),
|
||||||
)
|
)
|
||||||
|
@ -297,7 +309,7 @@ export default registerAs('authConfig', () => {
|
||||||
attribute: {
|
attribute: {
|
||||||
id: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_ID`],
|
id: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_ID`],
|
||||||
username: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
|
username: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
|
||||||
email: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
|
local: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -332,9 +344,13 @@ export default registerAs('authConfig', () => {
|
||||||
|
|
||||||
const authConfig = authSchema.validate(
|
const authConfig = authSchema.validate(
|
||||||
{
|
{
|
||||||
email: {
|
session: {
|
||||||
enableLogin: process.env.HD_AUTH_EMAIL_ENABLE_LOGIN,
|
secret: process.env.HD_SESSION_SECRET,
|
||||||
enableRegister: process.env.HD_AUTH_EMAIL_ENABLE_REGISTER,
|
lifetime: parseOptionalInt(process.env.HD_SESSION_LIFETIME),
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
enableLogin: process.env.HD_AUTH_LOCAL_ENABLE_LOGIN,
|
||||||
|
enableRegister: process.env.HD_AUTH_LOCAL_ENABLE_REGISTER,
|
||||||
},
|
},
|
||||||
facebook: {
|
facebook: {
|
||||||
clientID: process.env.HD_AUTH_FACEBOOK_CLIENT_ID,
|
clientID: process.env.HD_AUTH_FACEBOOK_CLIENT_ID,
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
export default registerAs('authConfig', () => ({
|
export default registerAs('authConfig', () => ({
|
||||||
email: {
|
session: {
|
||||||
|
secret: 'my_secret',
|
||||||
|
lifetime: 1209600000,
|
||||||
|
},
|
||||||
|
local: {
|
||||||
enableLogin: true,
|
enableLogin: true,
|
||||||
enableRegister: true,
|
enableRegister: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -71,10 +71,10 @@ export class AuthProviders {
|
||||||
oauth2: boolean;
|
oauth2: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is internal auth available?
|
* Is local auth available?
|
||||||
*/
|
*/
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
internal: boolean;
|
local: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BrandingDto {
|
export class BrandingDto {
|
||||||
|
|
|
@ -23,7 +23,11 @@ import { FrontendConfigService } from './frontend-config.service';
|
||||||
describe('FrontendConfigService', () => {
|
describe('FrontendConfigService', () => {
|
||||||
const domain = 'http://md.example.com';
|
const domain = 'http://md.example.com';
|
||||||
const emptyAuthConfig: AuthConfig = {
|
const emptyAuthConfig: AuthConfig = {
|
||||||
email: {
|
session: {
|
||||||
|
secret: 'my-secret',
|
||||||
|
lifetime: 1209600000,
|
||||||
|
},
|
||||||
|
local: {
|
||||||
enableLogin: false,
|
enableLogin: false,
|
||||||
enableRegister: false,
|
enableRegister: false,
|
||||||
},
|
},
|
||||||
|
@ -193,7 +197,7 @@ describe('FrontendConfigService', () => {
|
||||||
};
|
};
|
||||||
const authConfig: AuthConfig = {
|
const authConfig: AuthConfig = {
|
||||||
...emptyAuthConfig,
|
...emptyAuthConfig,
|
||||||
email: {
|
local: {
|
||||||
enableLogin,
|
enableLogin,
|
||||||
enableRegister,
|
enableRegister,
|
||||||
},
|
},
|
||||||
|
@ -258,7 +262,7 @@ describe('FrontendConfigService', () => {
|
||||||
expect(config.authProviders.google).toEqual(
|
expect(config.authProviders.google).toEqual(
|
||||||
!!authConfig.google.clientID,
|
!!authConfig.google.clientID,
|
||||||
);
|
);
|
||||||
expect(config.authProviders.internal).toEqual(
|
expect(config.authProviders.local).toEqual(
|
||||||
enableLogin,
|
enableLogin,
|
||||||
);
|
);
|
||||||
expect(config.authProviders.twitter).toEqual(
|
expect(config.authProviders.twitter).toEqual(
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class FrontendConfigService {
|
||||||
return {
|
return {
|
||||||
// ToDo: use actual value here
|
// ToDo: use actual value here
|
||||||
allowAnonymous: false,
|
allowAnonymous: false,
|
||||||
allowRegister: this.authConfig.email.enableRegister,
|
allowRegister: this.authConfig.local.enableRegister,
|
||||||
authProviders: this.getAuthProviders(),
|
authProviders: this.getAuthProviders(),
|
||||||
branding: this.getBranding(),
|
branding: this.getBranding(),
|
||||||
customAuthNames: this.getCustomAuthNames(),
|
customAuthNames: this.getCustomAuthNames(),
|
||||||
|
@ -66,7 +66,7 @@ export class FrontendConfigService {
|
||||||
github: !!this.authConfig.github.clientID,
|
github: !!this.authConfig.github.clientID,
|
||||||
gitlab: this.authConfig.gitlab.length !== 0,
|
gitlab: this.authConfig.gitlab.length !== 0,
|
||||||
google: !!this.authConfig.google.clientID,
|
google: !!this.authConfig.google.clientID,
|
||||||
internal: this.authConfig.email.enableLogin,
|
local: this.authConfig.local.enableLogin,
|
||||||
ldap: this.authConfig.ldap.length !== 0,
|
ldap: this.authConfig.ldap.length !== 0,
|
||||||
oauth2: this.authConfig.oauth2.length !== 0,
|
oauth2: this.authConfig.oauth2.length !== 0,
|
||||||
saml: this.authConfig.saml.length !== 0,
|
saml: this.authConfig.saml.length !== 0,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Author } from '../authors/author.entity';
|
||||||
import appConfigMock from '../config/mock/app.config.mock';
|
import appConfigMock from '../config/mock/app.config.mock';
|
||||||
import { NotInDBError } from '../errors/errors';
|
import { NotInDBError } from '../errors/errors';
|
||||||
import { Group } from '../groups/group.entity';
|
import { Group } from '../groups/group.entity';
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { NotesModule } from '../notes/notes.module';
|
import { NotesModule } from '../notes/notes.module';
|
||||||
|
@ -21,7 +22,6 @@ import { NoteGroupPermission } from '../permissions/note-group-permission.entity
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { Edit } from '../revisions/edit.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { Identity } from '../users/identity.entity';
|
|
||||||
import { Session } from '../users/session.entity';
|
import { Session } from '../users/session.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';
|
||||||
|
|
111
src/identity/identity.entity.ts
Normal file
111
src/identity/identity.entity.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { ProviderType } from './provider-type.enum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identity represents a single way for a user to login.
|
||||||
|
* A 'user' can have any number of these.
|
||||||
|
* Each one holds a type (local, github, twitter, etc.), if this type can have multiple instances (e.g. gitlab),
|
||||||
|
* it also saves the name of the instance. Also if this identity shall be the syncSource is saved.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export class Identity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User that this identity corresponds to
|
||||||
|
*/
|
||||||
|
@ManyToOne((_) => User, (user) => user.identities, {
|
||||||
|
onDelete: 'CASCADE', // This deletes the Identity, when the associated User is deleted
|
||||||
|
})
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ProviderType of the identity
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
providerType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the provider.
|
||||||
|
* Only set if there are multiple provider of that type (e.g. gitlab)
|
||||||
|
*/
|
||||||
|
@Column({
|
||||||
|
nullable: true,
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
providerName: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the identity should be used as the sync source.
|
||||||
|
* See [authentication doc](../../docs/content/dev/authentication.md) for clarification
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
syncSource: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the identity was created.
|
||||||
|
*/
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the identity was last updated.
|
||||||
|
*/
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique identifier of a user from the login provider
|
||||||
|
*/
|
||||||
|
@Column({
|
||||||
|
nullable: true,
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
providerUserId: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token used to access the OAuth provider in the users name.
|
||||||
|
*/
|
||||||
|
@Column({
|
||||||
|
nullable: true,
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
oAuthAccessToken: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hash of the password
|
||||||
|
* Only set when the type of the identity is local
|
||||||
|
*/
|
||||||
|
@Column({
|
||||||
|
nullable: true,
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
passwordHash: string | null;
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
user: User,
|
||||||
|
providerType: ProviderType,
|
||||||
|
syncSource = false,
|
||||||
|
): Identity {
|
||||||
|
const newIdentity = new Identity();
|
||||||
|
newIdentity.user = user;
|
||||||
|
newIdentity.providerType = providerType;
|
||||||
|
newIdentity.syncSource = syncSource;
|
||||||
|
return newIdentity;
|
||||||
|
}
|
||||||
|
}
|
28
src/identity/identity.module.ts
Normal file
28
src/identity/identity.module.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { Identity } from './identity.entity';
|
||||||
|
import { IdentityService } from './identity.service';
|
||||||
|
import { LocalStrategy } from './local/local.strategy';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Identity, User]),
|
||||||
|
UsersModule,
|
||||||
|
PassportModule,
|
||||||
|
LoggerModule,
|
||||||
|
],
|
||||||
|
controllers: [],
|
||||||
|
providers: [IdentityService, LocalStrategy],
|
||||||
|
exports: [IdentityService, LocalStrategy],
|
||||||
|
})
|
||||||
|
export class IdentityModule {}
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
src/identity/local/local.strategy.ts
Normal file
44
src/identity/local/local.strategy.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { AuthGuard, PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Strategy } from 'passport-local';
|
||||||
|
|
||||||
|
import { NotInDBError } from '../../errors/errors';
|
||||||
|
import { UserRelationEnum } from '../../users/user-relation.enum';
|
||||||
|
import { User } from '../../users/user.entity';
|
||||||
|
import { UsersService } from '../../users/users.service';
|
||||||
|
import { IdentityService } from '../identity.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
|
||||||
|
constructor(
|
||||||
|
private userService: UsersService,
|
||||||
|
private identityService: IdentityService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(username: string, password: string): Promise<User> {
|
||||||
|
try {
|
||||||
|
const user = await this.userService.getUserByUsername(username, [
|
||||||
|
UserRelationEnum.IDENTITIES,
|
||||||
|
]);
|
||||||
|
await this.identityService.loginWithLocalIdentity(user, password);
|
||||||
|
return user;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotInDBError) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
'This username and password combination did not work.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src/identity/local/login.dto.ts
Normal file
13
src/identity/local/login.dto.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@IsString()
|
||||||
|
username: string;
|
||||||
|
@IsString()
|
||||||
|
password: string;
|
||||||
|
}
|
17
src/identity/local/register.dto.ts
Normal file
17
src/identity/local/register.dto.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class RegisterDto {
|
||||||
|
@IsString()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
displayname: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
password: string;
|
||||||
|
}
|
11
src/identity/local/update-password.dto.ts
Normal file
11
src/identity/local/update-password.dto.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdatePasswordDto {
|
||||||
|
@IsString()
|
||||||
|
newPassword: string;
|
||||||
|
}
|
18
src/identity/provider-type.enum.ts
Normal file
18
src/identity/provider-type.enum.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum ProviderType {
|
||||||
|
LOCAL = 'local',
|
||||||
|
LDAP = 'ldap',
|
||||||
|
SAML = 'saml',
|
||||||
|
OAUTH2 = 'oauth2',
|
||||||
|
GITLAB = 'gitlab',
|
||||||
|
GITHUB = 'github',
|
||||||
|
FACEBOOK = 'facebook',
|
||||||
|
TWITTER = 'twitter',
|
||||||
|
DROPBOX = 'dropbox',
|
||||||
|
GOOGLE = 'google',
|
||||||
|
}
|
49
src/identity/session.guard.ts
Normal file
49
src/identity/session.guard.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { NotInDBError } from '../errors/errors';
|
||||||
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { UsersService } from '../users/users.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
private userService: UsersService,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(SessionGuard.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request: Request & { session?: { user: string }; user?: User } =
|
||||||
|
context.switchToHttp().getRequest();
|
||||||
|
if (!request.session) {
|
||||||
|
this.logger.debug('The user has no session.');
|
||||||
|
throw new UnauthorizedException("You're not logged in");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
request.user = await this.userService.getUserByUsername(
|
||||||
|
request.session.user,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotInDBError) {
|
||||||
|
this.logger.debug(
|
||||||
|
`The user '${request.session.user}' does not exist, but has a session.`,
|
||||||
|
);
|
||||||
|
throw new UnauthorizedException("You're not logged in");
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/identity/utils.ts
Normal file
27
src/identity/utils.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { Identity } from './identity.entity';
|
||||||
|
import { ProviderType } from './provider-type.enum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first identity of a given type from the user
|
||||||
|
* @param {User} user - the user to get the identity from
|
||||||
|
* @param {ProviderType} providerType - the type of the identity
|
||||||
|
* @return {Identity | undefined} the first identity of the user or undefined, if such an identity can not be found
|
||||||
|
*/
|
||||||
|
export async function getFirstIdentityFromUser(
|
||||||
|
user: User,
|
||||||
|
providerType: ProviderType,
|
||||||
|
): Promise<Identity | undefined> {
|
||||||
|
const identities = await user.identities;
|
||||||
|
if (identities === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return identities.find(
|
||||||
|
(aIdentity) => aIdentity.providerType === providerType,
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,9 +10,11 @@ import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { AppConfig } from './config/app.config';
|
import { AppConfig } from './config/app.config';
|
||||||
|
import { AuthConfig } from './config/auth.config';
|
||||||
import { MediaConfig } from './config/media.config';
|
import { MediaConfig } from './config/media.config';
|
||||||
import { ConsoleLoggerService } from './logger/console-logger.service';
|
import { ConsoleLoggerService } from './logger/console-logger.service';
|
||||||
import { BackendType } from './media/backends/backend-type.enum';
|
import { BackendType } from './media/backends/backend-type.enum';
|
||||||
|
import { setupSessionMiddleware } from './utils/session';
|
||||||
import { setupPrivateApiDocs, setupPublicApiDocs } from './utils/swagger';
|
import { setupPrivateApiDocs, setupPublicApiDocs } from './utils/swagger';
|
||||||
|
|
||||||
async function bootstrap(): Promise<void> {
|
async function bootstrap(): Promise<void> {
|
||||||
|
@ -25,9 +27,10 @@ async function bootstrap(): Promise<void> {
|
||||||
app.useLogger(logger);
|
app.useLogger(logger);
|
||||||
const configService = app.get(ConfigService);
|
const configService = app.get(ConfigService);
|
||||||
const appConfig = configService.get<AppConfig>('appConfig');
|
const appConfig = configService.get<AppConfig>('appConfig');
|
||||||
|
const authConfig = configService.get<AuthConfig>('authConfig');
|
||||||
const mediaConfig = configService.get<MediaConfig>('mediaConfig');
|
const mediaConfig = configService.get<MediaConfig>('mediaConfig');
|
||||||
|
|
||||||
if (!appConfig || !mediaConfig) {
|
if (!appConfig || !authConfig || !mediaConfig) {
|
||||||
logger.error('Could not initialize config, aborting.', 'AppBootstrap');
|
logger.error('Could not initialize config, aborting.', 'AppBootstrap');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
@ -45,6 +48,8 @@ async function bootstrap(): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupSessionMiddleware(app, authConfig);
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: appConfig.rendererOrigin,
|
origin: appConfig.rendererOrigin,
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { Author } from '../authors/author.entity';
|
||||||
import mediaConfigMock from '../config/mock/media.config.mock';
|
import mediaConfigMock from '../config/mock/media.config.mock';
|
||||||
import { ClientError, NotInDBError } from '../errors/errors';
|
import { ClientError, NotInDBError } from '../errors/errors';
|
||||||
import { Group } from '../groups/group.entity';
|
import { Group } from '../groups/group.entity';
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { NotesModule } from '../notes/notes.module';
|
import { NotesModule } from '../notes/notes.module';
|
||||||
|
@ -23,7 +24,6 @@ import { NoteGroupPermission } from '../permissions/note-group-permission.entity
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { Edit } from '../revisions/edit.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { Identity } from '../users/identity.entity';
|
|
||||||
import { Session } from '../users/session.entity';
|
import { Session } from '../users/session.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';
|
||||||
|
|
|
@ -19,13 +19,13 @@ import {
|
||||||
} from '../errors/errors';
|
} from '../errors/errors';
|
||||||
import { Group } from '../groups/group.entity';
|
import { Group } from '../groups/group.entity';
|
||||||
import { GroupsModule } from '../groups/groups.module';
|
import { GroupsModule } from '../groups/groups.module';
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { Edit } from '../revisions/edit.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 { Identity } from '../users/identity.entity';
|
|
||||||
import { Session } from '../users/session.entity';
|
import { Session } from '../users/session.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';
|
||||||
|
|
|
@ -11,13 +11,13 @@ import { AuthToken } from '../auth/auth-token.entity';
|
||||||
import { Author } from '../authors/author.entity';
|
import { Author } from '../authors/author.entity';
|
||||||
import appConfigMock from '../config/mock/app.config.mock';
|
import appConfigMock from '../config/mock/app.config.mock';
|
||||||
import { Group } from '../groups/group.entity';
|
import { Group } from '../groups/group.entity';
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
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 { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { Edit } from '../revisions/edit.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { Identity } from '../users/identity.entity';
|
|
||||||
import { Session } from '../users/session.entity';
|
import { Session } from '../users/session.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';
|
||||||
|
|
|
@ -13,13 +13,13 @@ import { Author } from '../authors/author.entity';
|
||||||
import appConfigMock from '../config/mock/app.config.mock';
|
import appConfigMock from '../config/mock/app.config.mock';
|
||||||
import { NotInDBError } from '../errors/errors';
|
import { NotInDBError } from '../errors/errors';
|
||||||
import { Group } from '../groups/group.entity';
|
import { Group } from '../groups/group.entity';
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
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 { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { Identity } from '../users/identity.entity';
|
|
||||||
import { Session } from '../users/session.entity';
|
import { Session } from '../users/session.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { Edit } from './edit.entity';
|
import { Edit } from './edit.entity';
|
||||||
|
|
61
src/seed.ts
61
src/seed.ts
|
@ -9,6 +9,8 @@ import { AuthToken } from './auth/auth-token.entity';
|
||||||
import { Author } from './authors/author.entity';
|
import { Author } from './authors/author.entity';
|
||||||
import { Group } from './groups/group.entity';
|
import { Group } from './groups/group.entity';
|
||||||
import { HistoryEntry } from './history/history-entry.entity';
|
import { HistoryEntry } from './history/history-entry.entity';
|
||||||
|
import { Identity } from './identity/identity.entity';
|
||||||
|
import { ProviderType } from './identity/provider-type.enum';
|
||||||
import { MediaUpload } from './media/media-upload.entity';
|
import { MediaUpload } from './media/media-upload.entity';
|
||||||
import { Note } from './notes/note.entity';
|
import { Note } from './notes/note.entity';
|
||||||
import { Tag } from './notes/tag.entity';
|
import { Tag } from './notes/tag.entity';
|
||||||
|
@ -16,9 +18,9 @@ import { NoteGroupPermission } from './permissions/note-group-permission.entity'
|
||||||
import { NoteUserPermission } from './permissions/note-user-permission.entity';
|
import { NoteUserPermission } from './permissions/note-user-permission.entity';
|
||||||
import { Edit } from './revisions/edit.entity';
|
import { Edit } from './revisions/edit.entity';
|
||||||
import { Revision } from './revisions/revision.entity';
|
import { Revision } from './revisions/revision.entity';
|
||||||
import { Identity } from './users/identity.entity';
|
|
||||||
import { Session } from './users/session.entity';
|
import { Session } from './users/session.entity';
|
||||||
import { User } from './users/user.entity';
|
import { User } from './users/user.entity';
|
||||||
|
import { hashPassword } from './utils/password';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function creates and populates a sqlite db for manual testing
|
* This function creates and populates a sqlite db for manual testing
|
||||||
|
@ -47,6 +49,7 @@ createConnection({
|
||||||
dropSchema: true,
|
dropSchema: true,
|
||||||
})
|
})
|
||||||
.then(async (connection) => {
|
.then(async (connection) => {
|
||||||
|
const password = 'test_password';
|
||||||
const users = [];
|
const users = [];
|
||||||
users.push(User.create('hardcoded', 'Test User 1'));
|
users.push(User.create('hardcoded', 'Test User 1'));
|
||||||
users.push(User.create('hardcoded_2', 'Test User 2'));
|
users.push(User.create('hardcoded_2', 'Test User 2'));
|
||||||
|
@ -59,6 +62,9 @@ createConnection({
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const author = connection.manager.create(Author, Author.create(1));
|
const author = connection.manager.create(Author, Author.create(1));
|
||||||
const user = connection.manager.create(User, users[i]);
|
const user = connection.manager.create(User, users[i]);
|
||||||
|
const identity = Identity.create(user, ProviderType.LOCAL);
|
||||||
|
identity.passwordHash = await hashPassword(password);
|
||||||
|
connection.manager.create(Identity, identity);
|
||||||
author.user = user;
|
author.user = user;
|
||||||
const revision = Revision.create(
|
const revision = Revision.create(
|
||||||
'This is a test note',
|
'This is a test note',
|
||||||
|
@ -70,23 +76,48 @@ createConnection({
|
||||||
notes[i].userPermissions = [];
|
notes[i].userPermissions = [];
|
||||||
notes[i].groupPermissions = [];
|
notes[i].groupPermissions = [];
|
||||||
user.ownedNotes = [notes[i]];
|
user.ownedNotes = [notes[i]];
|
||||||
await connection.manager.save([notes[i], user, revision, edit, author]);
|
await connection.manager.save([
|
||||||
|
notes[i],
|
||||||
|
user,
|
||||||
|
revision,
|
||||||
|
edit,
|
||||||
|
author,
|
||||||
|
identity,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
const foundUser = await connection.manager.findOne(User);
|
const foundUsers = await connection.manager.find(User);
|
||||||
if (!foundUser) {
|
if (!foundUsers) {
|
||||||
throw new Error('Could not find freshly seeded user. Aborting.');
|
throw new Error('Could not find freshly seeded users. Aborting.');
|
||||||
}
|
}
|
||||||
const foundNote = await connection.manager.findOne(Note);
|
const foundNotes = await connection.manager.find(Note);
|
||||||
if (!foundNote) {
|
if (!foundNotes) {
|
||||||
throw new Error('Could not find freshly seeded note. Aborting.');
|
throw new Error('Could not find freshly seeded notes. Aborting.');
|
||||||
}
|
}
|
||||||
if (!foundNote.alias) {
|
for (const note of foundNotes) {
|
||||||
throw new Error('Could not find alias of freshly seeded note. Aborting.');
|
if (!note.alias) {
|
||||||
|
throw new Error(
|
||||||
|
'Could not find alias of freshly seeded notes. Aborting.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const user of foundUsers) {
|
||||||
|
console.log(
|
||||||
|
`Created User '${user.userName}' with password '${password}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const note of foundNotes) {
|
||||||
|
console.log(`Created Note '${note.alias ?? ''}'`);
|
||||||
|
}
|
||||||
|
for (const user of foundUsers) {
|
||||||
|
for (const note of foundNotes) {
|
||||||
|
const historyEntry = HistoryEntry.create(user, note);
|
||||||
|
await connection.manager.save(historyEntry);
|
||||||
|
console.log(
|
||||||
|
`Created HistoryEntry for user '${user.userName}' and note '${
|
||||||
|
note.alias ?? ''
|
||||||
|
}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const historyEntry = HistoryEntry.create(foundUser, foundNote);
|
|
||||||
await connection.manager.save(historyEntry);
|
|
||||||
console.log(`Created User '${foundUser.userName}'`);
|
|
||||||
console.log(`Created Note '${foundNote.alias}'`);
|
|
||||||
console.log(`Created HistoryEntry`);
|
|
||||||
})
|
})
|
||||||
.catch((error) => console.log(error));
|
.catch((error) => console.log(error));
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
Entity,
|
|
||||||
ManyToOne,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { User } from './user.entity';
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
export class Identity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ManyToOne((_) => User, (user) => user.identities, {
|
|
||||||
onDelete: 'CASCADE', // This deletes the Identity, when the associated User is deleted
|
|
||||||
})
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
providerName: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
syncSource: boolean;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
nullable: true,
|
|
||||||
type: 'text',
|
|
||||||
})
|
|
||||||
providerUserId: string | null;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
nullable: true,
|
|
||||||
type: 'text',
|
|
||||||
})
|
|
||||||
oAuthAccessToken: string | null;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
nullable: true,
|
|
||||||
type: 'text',
|
|
||||||
})
|
|
||||||
passwordHash: string | null;
|
|
||||||
}
|
|
10
src/users/user-relation.enum.ts
Normal file
10
src/users/user-relation.enum.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum UserRelationEnum {
|
||||||
|
AUTHTOKENS = 'authTokens',
|
||||||
|
IDENTITIES = 'identities',
|
||||||
|
}
|
|
@ -17,9 +17,9 @@ import { AuthToken } from '../auth/auth-token.entity';
|
||||||
import { Author } from '../authors/author.entity';
|
import { Author } from '../authors/author.entity';
|
||||||
import { Group } from '../groups/group.entity';
|
import { Group } from '../groups/group.entity';
|
||||||
import { HistoryEntry } from '../history/history-entry.entity';
|
import { HistoryEntry } from '../history/history-entry.entity';
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
import { MediaUpload } from '../media/media-upload.entity';
|
import { MediaUpload } from '../media/media-upload.entity';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { Identity } from './identity.entity';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User {
|
export class User {
|
||||||
|
@ -59,7 +59,7 @@ export class User {
|
||||||
authTokens: AuthToken[];
|
authTokens: AuthToken[];
|
||||||
|
|
||||||
@OneToMany((_) => Identity, (identity) => identity.user)
|
@OneToMany((_) => Identity, (identity) => identity.user)
|
||||||
identities: Identity[];
|
identities: Promise<Identity[]>;
|
||||||
|
|
||||||
@ManyToMany((_) => Group, (group) => group.members)
|
@ManyToMany((_) => Group, (group) => group.members)
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { Identity } from './identity.entity';
|
|
||||||
import { Session } from './session.entity';
|
import { Session } from './session.entity';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Repository } from 'typeorm';
|
||||||
import { AlreadyInDBError, NotInDBError } from '../errors/errors';
|
import { AlreadyInDBError, 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 { UserRelationEnum } from './user-relation.enum';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -73,17 +74,16 @@ export class UsersService {
|
||||||
* @async
|
* @async
|
||||||
* Get the user specified by the username
|
* Get the user specified by the username
|
||||||
* @param {string} userName the username by which the user is specified
|
* @param {string} userName the username by which the user is specified
|
||||||
* @param {boolean} [withTokens=false] if the returned user object should contain authTokens
|
* @param {UserRelationEnum[]} [withRelations=[]] if the returned user object should contain certain relations
|
||||||
* @return {User} the specified user
|
* @return {User} the specified user
|
||||||
*/
|
*/
|
||||||
async getUserByUsername(userName: string, withTokens = false): Promise<User> {
|
async getUserByUsername(
|
||||||
const relations: string[] = [];
|
userName: string,
|
||||||
if (withTokens) {
|
withRelations: UserRelationEnum[] = [],
|
||||||
relations.push('authTokens');
|
): Promise<User> {
|
||||||
}
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userName: userName },
|
where: { userName: userName },
|
||||||
relations: relations,
|
relations: withRelations,
|
||||||
});
|
});
|
||||||
if (user === undefined) {
|
if (user === undefined) {
|
||||||
throw new NotInDBError(`User with username '${userName}' not found`);
|
throw new NotInDBError(`User with username '${userName}' not found`);
|
||||||
|
|
59
src/utils/password.spec.ts
Normal file
59
src/utils/password.spec.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
import { bufferToBase64Url, checkPassword, hashPassword } from './password';
|
||||||
|
|
||||||
|
const testPassword = 'thisIsATestPassword';
|
||||||
|
|
||||||
|
describe('hashPassword', () => {
|
||||||
|
it('output looks like a bcrypt hash with 2^12 rounds of hashing', async () => {
|
||||||
|
/*
|
||||||
|
* a bcrypt hash example with the different parts highlighted:
|
||||||
|
* $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
|
||||||
|
* \__/\/ \____________________/\_____________________________/
|
||||||
|
* Alg Cost Salt Hash
|
||||||
|
* from https://en.wikipedia.org/wiki/Bcrypt#Description
|
||||||
|
*/
|
||||||
|
const regexBcrypt = /^\$2[abxy]\$12\$[A-Za-z0-9/.]{53}$/;
|
||||||
|
const hash = await hashPassword(testPassword);
|
||||||
|
expect(regexBcrypt.test(hash)).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('calls bcrypt.hash with the correct parameters', async () => {
|
||||||
|
const spy = jest.spyOn(bcrypt, 'hash');
|
||||||
|
await hashPassword(testPassword);
|
||||||
|
expect(spy).toHaveBeenCalledWith(testPassword, 12);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkPassword', () => {
|
||||||
|
it("is returning true if the inputs are a plaintext password and it's bcrypt-hashed version", async () => {
|
||||||
|
const hashOfTestPassword =
|
||||||
|
'$2a$12$WHKCq4c0rg19zyx5WgX0p.or0rjSKYpIBcHhQQGLrxrr6FfMPylIW';
|
||||||
|
await checkPassword(testPassword, hashOfTestPassword).then((result) =>
|
||||||
|
expect(result).toBeTruthy(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('fails, if secret is too short', async () => {
|
||||||
|
const secret = bufferToBase64Url(randomBytes(54));
|
||||||
|
const hash = await hashPassword(secret);
|
||||||
|
await checkPassword(secret, hash).then((result) =>
|
||||||
|
expect(result).toBeTruthy(),
|
||||||
|
);
|
||||||
|
await checkPassword(secret.substr(0, secret.length - 1), hash).then(
|
||||||
|
(result) => expect(result).toBeFalsy(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bufferToBase64Url', () => {
|
||||||
|
it('transforms a buffer to the correct base64url encoded string', () => {
|
||||||
|
expect(
|
||||||
|
bufferToBase64Url(Buffer.from('testsentence is a test sentence')),
|
||||||
|
).toEqual('dGVzdHNlbnRlbmNlIGlzIGEgdGVzdCBzZW50ZW5jZQ');
|
||||||
|
});
|
||||||
|
});
|
30
src/utils/password.ts
Normal file
30
src/utils/password.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { compare, hash } from 'bcrypt';
|
||||||
|
|
||||||
|
export async function hashPassword(cleartext: string): Promise<string> {
|
||||||
|
// hash the password with bcrypt and 2^12 iterations
|
||||||
|
// this was decided on the basis of https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#bcrypt
|
||||||
|
return await hash(cleartext, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkPassword(
|
||||||
|
cleartext: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return await compare(cleartext, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bufferToBase64Url(text: Buffer): 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 text
|
||||||
|
.toString('base64')
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=+$/, '');
|
||||||
|
}
|
39
src/utils/session.ts
Normal file
39
src/utils/session.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { TypeormStore } from 'connect-typeorm';
|
||||||
|
import session from 'express-session';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { AuthConfig } from '../config/auth.config';
|
||||||
|
import { Session } from '../users/session.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the session middleware via the given authConfig.
|
||||||
|
* @param {INestApplication} app - the nest application to configure the middleware for.
|
||||||
|
* @param {AuthConfig} authConfig - the authConfig to configure the middleware with.
|
||||||
|
*/
|
||||||
|
export function setupSessionMiddleware(
|
||||||
|
app: INestApplication,
|
||||||
|
authConfig: AuthConfig,
|
||||||
|
): void {
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
name: 'hedgedoc-session',
|
||||||
|
secret: authConfig.session.secret,
|
||||||
|
cookie: {
|
||||||
|
maxAge: authConfig.session.lifetime,
|
||||||
|
},
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: new TypeormStore({
|
||||||
|
cleanupLimit: 2,
|
||||||
|
ttl: 86400,
|
||||||
|
}).connect(app.get<Repository<Session>>(getRepositoryToken(Session))),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
265
test/private-api/auth.e2e-spec.ts
Normal file
265
test/private-api/auth.e2e-spec.ts
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable
|
||||||
|
@typescript-eslint/no-unsafe-assignment,
|
||||||
|
@typescript-eslint/no-unsafe-member-access
|
||||||
|
*/
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { PrivateApiModule } from '../../src/api/private/private-api.module';
|
||||||
|
import { AuthModule } from '../../src/auth/auth.module';
|
||||||
|
import { AuthConfig } from '../../src/config/auth.config';
|
||||||
|
import appConfigMock from '../../src/config/mock/app.config.mock';
|
||||||
|
import authConfigMock from '../../src/config/mock/auth.config.mock';
|
||||||
|
import customizationConfigMock from '../../src/config/mock/customization.config.mock';
|
||||||
|
import externalServicesConfigMock from '../../src/config/mock/external-services.config.mock';
|
||||||
|
import mediaConfigMock from '../../src/config/mock/media.config.mock';
|
||||||
|
import { GroupsModule } from '../../src/groups/groups.module';
|
||||||
|
import { HistoryModule } from '../../src/history/history.module';
|
||||||
|
import { LoginDto } from '../../src/identity/local/login.dto';
|
||||||
|
import { RegisterDto } from '../../src/identity/local/register.dto';
|
||||||
|
import { UpdatePasswordDto } from '../../src/identity/local/update-password.dto';
|
||||||
|
import { LoggerModule } from '../../src/logger/logger.module';
|
||||||
|
import { MediaModule } from '../../src/media/media.module';
|
||||||
|
import { NotesModule } from '../../src/notes/notes.module';
|
||||||
|
import { PermissionsModule } from '../../src/permissions/permissions.module';
|
||||||
|
import { UserRelationEnum } from '../../src/users/user-relation.enum';
|
||||||
|
import { UsersModule } from '../../src/users/users.module';
|
||||||
|
import { UsersService } from '../../src/users/users.service';
|
||||||
|
import { checkPassword } from '../../src/utils/password';
|
||||||
|
import { setupSessionMiddleware } from '../../src/utils/session';
|
||||||
|
|
||||||
|
describe('Auth', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let userService: UsersService;
|
||||||
|
let username: string;
|
||||||
|
let displayname: string;
|
||||||
|
let password: string;
|
||||||
|
let config: ConfigService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [
|
||||||
|
appConfigMock,
|
||||||
|
authConfigMock,
|
||||||
|
mediaConfigMock,
|
||||||
|
customizationConfigMock,
|
||||||
|
externalServicesConfigMock,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
PrivateApiModule,
|
||||||
|
NotesModule,
|
||||||
|
PermissionsModule,
|
||||||
|
GroupsModule,
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type: 'sqlite',
|
||||||
|
database: './hedgedoc-e2e-private-auth.sqlite',
|
||||||
|
autoLoadEntities: true,
|
||||||
|
synchronize: true,
|
||||||
|
dropSchema: true,
|
||||||
|
}),
|
||||||
|
LoggerModule,
|
||||||
|
AuthModule,
|
||||||
|
UsersModule,
|
||||||
|
MediaModule,
|
||||||
|
HistoryModule,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
config = moduleRef.get<ConfigService>(ConfigService);
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
const authConfig = config.get('authConfig') as AuthConfig;
|
||||||
|
setupSessionMiddleware(app, authConfig);
|
||||||
|
await app.init();
|
||||||
|
userService = moduleRef.get(UsersService);
|
||||||
|
username = 'hardcoded';
|
||||||
|
displayname = 'Testy';
|
||||||
|
password = 'test_password';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /auth/local', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
const registrationDto: RegisterDto = {
|
||||||
|
displayname: displayname,
|
||||||
|
password: password,
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/auth/local')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(registrationDto))
|
||||||
|
.expect(201);
|
||||||
|
const newUser = await userService.getUserByUsername(username, [
|
||||||
|
UserRelationEnum.IDENTITIES,
|
||||||
|
]);
|
||||||
|
expect(newUser.displayName).toEqual(displayname);
|
||||||
|
await expect(newUser.identities).resolves.toHaveLength(1);
|
||||||
|
await expect(
|
||||||
|
checkPassword(
|
||||||
|
password,
|
||||||
|
(await newUser.identities)[0].passwordHash ?? '',
|
||||||
|
),
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
describe('fails', () => {
|
||||||
|
it('when the user already exits', async () => {
|
||||||
|
const username2 = 'already_existing';
|
||||||
|
await userService.createUser(username2, displayname);
|
||||||
|
const registrationDto: RegisterDto = {
|
||||||
|
displayname: displayname,
|
||||||
|
password: password,
|
||||||
|
username: username2,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/auth/local')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(registrationDto))
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
it('when registration is disabled', async () => {
|
||||||
|
config.get('authConfig').local.enableRegister = false;
|
||||||
|
const registrationDto: RegisterDto = {
|
||||||
|
displayname: displayname,
|
||||||
|
password: password,
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/auth/local')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(registrationDto))
|
||||||
|
.expect(400);
|
||||||
|
config.get('authConfig').local.enableRegister = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /auth/local', () => {
|
||||||
|
const newPassword = 'new_password';
|
||||||
|
let cookie = '';
|
||||||
|
beforeEach(async () => {
|
||||||
|
const loginDto: LoginDto = {
|
||||||
|
password: password,
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post('/auth/local/login')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(loginDto))
|
||||||
|
.expect(201);
|
||||||
|
cookie = response.get('Set-Cookie')[0];
|
||||||
|
});
|
||||||
|
it('works', async () => {
|
||||||
|
// Change password
|
||||||
|
const changePasswordDto: UpdatePasswordDto = {
|
||||||
|
newPassword: newPassword,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.put('/auth/local')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.send(JSON.stringify(changePasswordDto))
|
||||||
|
.expect(200);
|
||||||
|
// Successfully login with new password
|
||||||
|
const loginDto: LoginDto = {
|
||||||
|
password: newPassword,
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post('/auth/local/login')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(loginDto))
|
||||||
|
.expect(201);
|
||||||
|
cookie = response.get('Set-Cookie')[0];
|
||||||
|
// Reset password
|
||||||
|
const changePasswordBackDto: UpdatePasswordDto = {
|
||||||
|
newPassword: password,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.put('/auth/local')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.send(JSON.stringify(changePasswordBackDto))
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
it('fails, when registration is disabled', async () => {
|
||||||
|
config.get('authConfig').local.enableLogin = false;
|
||||||
|
// Try to change password
|
||||||
|
const changePasswordDto: UpdatePasswordDto = {
|
||||||
|
newPassword: newPassword,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.put('/auth/local')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.send(JSON.stringify(changePasswordDto))
|
||||||
|
.expect(400);
|
||||||
|
// enable login again
|
||||||
|
config.get('authConfig').local.enableLogin = true;
|
||||||
|
// new password doesn't work for login
|
||||||
|
const loginNewPasswordDto: LoginDto = {
|
||||||
|
password: newPassword,
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/auth/local/login')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(loginNewPasswordDto))
|
||||||
|
.expect(401);
|
||||||
|
// old password does work for login
|
||||||
|
const loginOldPasswordDto: LoginDto = {
|
||||||
|
password: password,
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/auth/local/login')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(loginOldPasswordDto))
|
||||||
|
.expect(201);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /auth/local/login', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
config.get('authConfig').local.enableLogin = true;
|
||||||
|
const loginDto: LoginDto = {
|
||||||
|
password: password,
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/auth/local/login')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(loginDto))
|
||||||
|
.expect(201);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /auth/logout', () => {
|
||||||
|
it('works', async () => {
|
||||||
|
config.get('authConfig').local.enableLogin = true;
|
||||||
|
const loginDto: LoginDto = {
|
||||||
|
password: password,
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post('/auth/local/login')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify(loginDto))
|
||||||
|
.expect(201);
|
||||||
|
const cookie = response.get('Set-Cookie')[0];
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.delete('/auth/logout')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
28
yarn.lock
28
yarn.lock
|
@ -1147,7 +1147,7 @@
|
||||||
"@types/qs" "*"
|
"@types/qs" "*"
|
||||||
"@types/range-parser" "*"
|
"@types/range-parser" "*"
|
||||||
|
|
||||||
"@types/express-session@^1.15.5":
|
"@types/express-session@^1.15.5", "@types/express-session@^1.17.4":
|
||||||
version "1.17.4"
|
version "1.17.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.4.tgz#97a30a35e853a61bdd26e727453b8ed314d6166b"
|
resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.4.tgz#97a30a35e853a61bdd26e727453b8ed314d6166b"
|
||||||
integrity sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==
|
integrity sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==
|
||||||
|
@ -1293,6 +1293,23 @@
|
||||||
"@types/koa" "*"
|
"@types/koa" "*"
|
||||||
"@types/passport" "*"
|
"@types/passport" "*"
|
||||||
|
|
||||||
|
"@types/passport-local@^1.0.34":
|
||||||
|
version "1.0.34"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/passport-local/-/passport-local-1.0.34.tgz#84d3b35b2fd4d36295039ded17fe5f3eaa62f4f6"
|
||||||
|
integrity sha512-PSc07UdYx+jhadySxxIYWuv6sAnY5e+gesn/5lkPKfBeGuIYn9OPR+AAEDq73VRUh6NBTpvE/iPE62rzZUslog==
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "*"
|
||||||
|
"@types/passport" "*"
|
||||||
|
"@types/passport-strategy" "*"
|
||||||
|
|
||||||
|
"@types/passport-strategy@*":
|
||||||
|
version "0.2.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.35.tgz#e52f5212279ea73f02d9b06af67efe9cefce2d0c"
|
||||||
|
integrity sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "*"
|
||||||
|
"@types/passport" "*"
|
||||||
|
|
||||||
"@types/passport@*":
|
"@types/passport@*":
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.7.tgz#85892f14932168158c86aecafd06b12f5439467a"
|
resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.7.tgz#85892f14932168158c86aecafd06b12f5439467a"
|
||||||
|
@ -3158,7 +3175,7 @@ expect@^27.2.0:
|
||||||
jest-message-util "^27.2.0"
|
jest-message-util "^27.2.0"
|
||||||
jest-regex-util "^27.0.6"
|
jest-regex-util "^27.0.6"
|
||||||
|
|
||||||
express-session@^1.15.6:
|
express-session@1.17.2, express-session@^1.15.6:
|
||||||
version "1.17.2"
|
version "1.17.2"
|
||||||
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.2.tgz#397020374f9bf7997f891b85ea338767b30d0efd"
|
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.2.tgz#397020374f9bf7997f891b85ea338767b30d0efd"
|
||||||
integrity sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==
|
integrity sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==
|
||||||
|
@ -5573,6 +5590,13 @@ passport-http-bearer@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
passport-strategy "1.x.x"
|
passport-strategy "1.x.x"
|
||||||
|
|
||||||
|
passport-local@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee"
|
||||||
|
integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=
|
||||||
|
dependencies:
|
||||||
|
passport-strategy "1.x.x"
|
||||||
|
|
||||||
passport-strategy@1.x.x:
|
passport-strategy@1.x.x:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
|
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
|
||||||
|
|
Loading…
Reference in a new issue