diff --git a/package-lock.json b/package-lock.json index a182c86..4125dfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2109,7 +2109,7 @@ }, "node_modules/@icynet/oauth2-provider": { "version": "1.0.0", - "resolved": "git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git#2877dac9374ac4509615dd0d4df5246b82cea3bd", + "resolved": "git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git#a440d1f4ac53ccb6989dd25221797490611e240a", "license": "MIT", "dependencies": { "express": "^4.17.3", @@ -13956,7 +13956,7 @@ "dev": true }, "@icynet/oauth2-provider": { - "version": "git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git#2877dac9374ac4509615dd0d4df5246b82cea3bd", + "version": "git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git#a440d1f4ac53ccb6989dd25221797490611e240a", "from": "@icynet/oauth2-provider@git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git", "requires": { "express": "^4.17.3", diff --git a/src/middleware/validate-csrf.middleware.ts b/src/middleware/validate-csrf.middleware.ts index 68778e1..6634a92 100644 --- a/src/middleware/validate-csrf.middleware.ts +++ b/src/middleware/validate-csrf.middleware.ts @@ -7,6 +7,11 @@ export class ValidateCSRFMiddleware implements NestMiddleware { constructor(private readonly tokenService: TokenService) {} use(req: Request, res: Response, next: NextFunction) { + // Never try to validate these + if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { + return next(); + } + // Multipart is handeled elsewhere if (req.header('content-type')?.startsWith('multipart/form-data')) { return next(); diff --git a/src/modules/features/login/login.controller.ts b/src/modules/features/login/login.controller.ts index 9a09b08..4cc0986 100644 --- a/src/modules/features/login/login.controller.ts +++ b/src/modules/features/login/login.controller.ts @@ -35,8 +35,15 @@ export class LoginController { @Get() @Render('login/login') - public loginView(@Req() req: Request): Record { - return this.formUtil.populateTemplate(req); + public loginView( + @Req() req: Request, + @Query('redirectTo') redirectTo?: string, + ): Record { + return this.formUtil.populateTemplate(req, { + query: redirectTo + ? new URLSearchParams({ redirectTo }).toString() + : undefined, + }); } @Post() @@ -44,7 +51,7 @@ export class LoginController { @Req() req: Request, @Res() res: Response, @Body() body: { username: string; password: string }, - @Query() query: { redirectTo?: string }, + @Query('redirectTo') redirectTo?: string, ) { const { username, password } = body; const user = await this.userService.getByUsername(username); @@ -69,22 +76,21 @@ export class LoginController { const challenge = { type: 'verify', user: user.uuid }; req.session.challenge = await this.token.encryptChallenge(challenge); res.redirect( - '/login/verify' + - (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''), + '/login/verify' + (redirectTo ? '?redirectTo=' + redirectTo : ''), ); return; } req.session.user = user.uuid; - res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/'); + res.redirect(redirectTo ? decodeURIComponent(redirectTo) : '/'); } @Get('verify') public verifyUserTokenView( @Session() session: SessionData, - @Query() query: { redirectTo?: string }, @Req() req: Request, @Res() res: Response, + @Query('redirectTo') redirectTo?: string, ) { if (!session.challenge) { req.flash('message', { @@ -92,9 +98,7 @@ export class LoginController { text: 'An unexpected error occured, please log in again.', }); - res.redirect( - '/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''), - ); + res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : '')); return; } @@ -107,10 +111,10 @@ export class LoginController { @Post('verify') public async verifyUserToken( @Session() session: SessionData, - @Query() query: { redirectTo?: string }, @Body() body: { totp: string }, @Req() req: Request, @Res() res: Response, + @Query('redirectTo') redirectTo?: string, ) { let user: User; @@ -134,9 +138,7 @@ export class LoginController { text: 'An unexpected error occured, please log in again.', }); - res.redirect( - '/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''), - ); + res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : '')); return; } @@ -157,7 +159,7 @@ export class LoginController { session.challenge = null; session.user = user.uuid; - res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/'); + res.redirect(redirectTo ? decodeURIComponent(redirectTo) : '/'); } @Get('activate') @@ -165,9 +167,14 @@ export class LoginController { @Req() req: Request, @Res() res: Response, @Query() query: { token: string }, + @Query('redirectTo') redirectTo?: string, ) { + const loginPath = `/login${ + redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : '' + }`; let user: User; let token: UserToken; + try { if (!query || !query.token) { throw new Error(); @@ -189,7 +196,7 @@ export class LoginController { text: 'Invalid or expired activation link.', }); - res.redirect('/login'); + res.redirect(loginPath); return; } @@ -202,7 +209,7 @@ export class LoginController { text: 'Account has been activated successfully. You may now log in.', }); - res.redirect('/login'); + res.redirect(loginPath); } @Get('password') diff --git a/src/modules/features/oauth2/oauth2.module.ts b/src/modules/features/oauth2/oauth2.module.ts index 8438a79..6324372 100644 --- a/src/modules/features/oauth2/oauth2.module.ts +++ b/src/modules/features/oauth2/oauth2.module.ts @@ -1,5 +1,6 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { AuthMiddleware } from 'src/middleware/auth.middleware'; +import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware'; import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module'; import { OAuth2TokenModule } from 'src/modules/objects/oauth2-token/oauth2-token.module'; import { UploadModule } from 'src/modules/objects/upload/upload.module'; @@ -19,6 +20,8 @@ export class OAuth2Module implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*'); consumer.apply(this._service.oauth.bearer).forRoutes('oauth2/user'); - consumer.apply(AuthMiddleware).forRoutes('oauth2/authorize'); + consumer + .apply(AuthMiddleware, ValidateCSRFMiddleware) + .forRoutes('oauth2/authorize'); } } diff --git a/src/modules/features/oauth2/oauth2.service.ts b/src/modules/features/oauth2/oauth2.service.ts index a90c5cd..1de7e93 100644 --- a/src/modules/features/oauth2/oauth2.service.ts +++ b/src/modules/features/oauth2/oauth2.service.ts @@ -30,7 +30,7 @@ export class OAuth2Service { public oauth = new OAuth2Provider( this._oauthAdapter, - async (req, res, client, scope, user, redirectUri) => { + async (req, res, client, scope) => { const fullClient = await this.clientService.getById(client.id as string); const allowedScopes = [...ALWAYS_AVAILABLE]; const disallowedScopes = [...ALWAYS_UNAVAILABLE]; @@ -44,7 +44,7 @@ export class OAuth2Service { }); res.render('authorize', { - csrf: req.session.csrf, + csrf: req.csrfToken(), user: req.user, client: fullClient, allowedScopes, diff --git a/src/modules/features/register/register.controller.ts b/src/modules/features/register/register.controller.ts index ff602e5..204239d 100644 --- a/src/modules/features/register/register.controller.ts +++ b/src/modules/features/register/register.controller.ts @@ -35,7 +35,7 @@ export class RegisterController { @Req() req: Request, @Res() res: Response, @Body() body: RegisterDto, - @Query() query: { redirectTo?: string }, + @Query('redirectTo') redirectTo?: string, ) { const { username, display_name, email, password, password_repeat } = body; @@ -77,16 +77,14 @@ export class RegisterController { throw new Error('The passwords do not match!'); } - await this.userService.userRegistration(body); + await this.userService.userRegistration(body, redirectTo); req.flash('message', { error: false, text: `An activation email has been sent to ${email}!`, }); - res.redirect( - '/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''), - ); + res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : '')); } catch (e: any) { req.flash('message', { error: true, text: e.message }); req.flash('form', { ...body, password_repeat: undefined }); diff --git a/src/modules/objects/user/user.service.ts b/src/modules/objects/user/user.service.ts index 0b45aa4..4a51e8c 100644 --- a/src/modules/objects/user/user.service.ts +++ b/src/modules/objects/user/user.service.ts @@ -95,19 +95,27 @@ export class UserService { return bcrypt.hash(password, salt); } - public async sendActivationEmail(user: User): Promise { + public async sendActivationEmail( + user: User, + redirectTo?: string, + ): Promise { const activationToken = await this.userToken.create( user, UserTokenType.ACTIVATION, new Date(Date.now() + 3600 * 1000), ); + const params = new URLSearchParams({ token: activationToken.token }); + if (redirectTo) { + params.append('redirectTo', redirectTo); + } + try { const content = RegistrationEmail( user.username, - `${this.config.get('app.base_url')}/login/activate?token=${ - activationToken.token - }`, + `${this.config.get( + 'app.base_url', + )}/login/activate?${params.toString()}`, ); await this.email.sendEmailTemplate( user.email, @@ -154,12 +162,15 @@ export class UserService { await this.sendPasswordEmail(user); } - public async userRegistration(newUserInfo: { - username: string; - display_name: string; - email: string; - password: string; - }): Promise { + public async userRegistration( + newUserInfo: { + username: string; + display_name: string; + email: string; + password: string; + }, + redirectTo?: string, + ): Promise { if (!!(await this.getByEmail(newUserInfo.email))) { throw new Error('Email is already in use!'); } @@ -179,7 +190,7 @@ export class UserService { await this.userRepository.insert(user); try { - await this.sendActivationEmail(user); + await this.sendActivationEmail(user, redirectTo); } catch (e) { await this.userRepository.remove(user); throw new Error( diff --git a/src/modules/utility/services/token.service.ts b/src/modules/utility/services/token.service.ts index 563b620..aae5793 100644 --- a/src/modules/utility/services/token.service.ts +++ b/src/modules/utility/services/token.service.ts @@ -10,7 +10,10 @@ const ALGORITHM = 'aes-256-cbc'; @Injectable() export class TokenService { - public csrf = new CSRF(); + public csrf = new CSRF({ + saltLength: 16, + secretLength: 32, + }); constructor(private config: ConfigurationService) {} diff --git a/views/login/login.pug b/views/login/login.pug index 5f11c19..f215c9b 100644 --- a/views/login/login.pug +++ b/views/login/login.pug @@ -25,7 +25,7 @@ block body input.form-control#password(type="password", name="password", placeholder="Password") button.btn.btn-primary(type="submit") Log in div.btn-group.align-self-end - a.btn.btn-link(type="button" href="/register") Create a new account + a.btn.btn-link(type="button" href="/register" + (query ? ('?' + query) : '')) Create a new account |• a.btn.btn-link(type="button" href="/login/password") Forgot password? div.center-box-addon