redirect uri follows the user

This commit is contained in:
Evert Prants 2022-03-20 19:05:21 +02:00
parent 21a1ceeb2d
commit abb0e24d99
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
9 changed files with 67 additions and 40 deletions

4
package-lock.json generated
View File

@ -2109,7 +2109,7 @@
}, },
"node_modules/@icynet/oauth2-provider": { "node_modules/@icynet/oauth2-provider": {
"version": "1.0.0", "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", "license": "MIT",
"dependencies": { "dependencies": {
"express": "^4.17.3", "express": "^4.17.3",
@ -13956,7 +13956,7 @@
"dev": true "dev": true
}, },
"@icynet/oauth2-provider": { "@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", "from": "@icynet/oauth2-provider@git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git",
"requires": { "requires": {
"express": "^4.17.3", "express": "^4.17.3",

View File

@ -7,6 +7,11 @@ export class ValidateCSRFMiddleware implements NestMiddleware {
constructor(private readonly tokenService: TokenService) {} constructor(private readonly tokenService: TokenService) {}
use(req: Request, res: Response, next: NextFunction) { 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 // Multipart is handeled elsewhere
if (req.header('content-type')?.startsWith('multipart/form-data')) { if (req.header('content-type')?.startsWith('multipart/form-data')) {
return next(); return next();

View File

@ -35,8 +35,15 @@ export class LoginController {
@Get() @Get()
@Render('login/login') @Render('login/login')
public loginView(@Req() req: Request): Record<string, any> { public loginView(
return this.formUtil.populateTemplate(req); @Req() req: Request,
@Query('redirectTo') redirectTo?: string,
): Record<string, any> {
return this.formUtil.populateTemplate(req, {
query: redirectTo
? new URLSearchParams({ redirectTo }).toString()
: undefined,
});
} }
@Post() @Post()
@ -44,7 +51,7 @@ export class LoginController {
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Body() body: { username: string; password: string }, @Body() body: { username: string; password: string },
@Query() query: { redirectTo?: string }, @Query('redirectTo') redirectTo?: string,
) { ) {
const { username, password } = body; const { username, password } = body;
const user = await this.userService.getByUsername(username); const user = await this.userService.getByUsername(username);
@ -69,22 +76,21 @@ export class LoginController {
const challenge = { type: 'verify', user: user.uuid }; const challenge = { type: 'verify', user: user.uuid };
req.session.challenge = await this.token.encryptChallenge(challenge); req.session.challenge = await this.token.encryptChallenge(challenge);
res.redirect( res.redirect(
'/login/verify' + '/login/verify' + (redirectTo ? '?redirectTo=' + redirectTo : ''),
(query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
); );
return; return;
} }
req.session.user = user.uuid; req.session.user = user.uuid;
res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/'); res.redirect(redirectTo ? decodeURIComponent(redirectTo) : '/');
} }
@Get('verify') @Get('verify')
public verifyUserTokenView( public verifyUserTokenView(
@Session() session: SessionData, @Session() session: SessionData,
@Query() query: { redirectTo?: string },
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Query('redirectTo') redirectTo?: string,
) { ) {
if (!session.challenge) { if (!session.challenge) {
req.flash('message', { req.flash('message', {
@ -92,9 +98,7 @@ export class LoginController {
text: 'An unexpected error occured, please log in again.', text: 'An unexpected error occured, please log in again.',
}); });
res.redirect( res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : ''));
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
);
return; return;
} }
@ -107,10 +111,10 @@ export class LoginController {
@Post('verify') @Post('verify')
public async verifyUserToken( public async verifyUserToken(
@Session() session: SessionData, @Session() session: SessionData,
@Query() query: { redirectTo?: string },
@Body() body: { totp: string }, @Body() body: { totp: string },
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Query('redirectTo') redirectTo?: string,
) { ) {
let user: User; let user: User;
@ -134,9 +138,7 @@ export class LoginController {
text: 'An unexpected error occured, please log in again.', text: 'An unexpected error occured, please log in again.',
}); });
res.redirect( res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : ''));
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
);
return; return;
} }
@ -157,7 +159,7 @@ export class LoginController {
session.challenge = null; session.challenge = null;
session.user = user.uuid; session.user = user.uuid;
res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/'); res.redirect(redirectTo ? decodeURIComponent(redirectTo) : '/');
} }
@Get('activate') @Get('activate')
@ -165,9 +167,14 @@ export class LoginController {
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Query() query: { token: string }, @Query() query: { token: string },
@Query('redirectTo') redirectTo?: string,
) { ) {
const loginPath = `/login${
redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''
}`;
let user: User; let user: User;
let token: UserToken; let token: UserToken;
try { try {
if (!query || !query.token) { if (!query || !query.token) {
throw new Error(); throw new Error();
@ -189,7 +196,7 @@ export class LoginController {
text: 'Invalid or expired activation link.', text: 'Invalid or expired activation link.',
}); });
res.redirect('/login'); res.redirect(loginPath);
return; return;
} }
@ -202,7 +209,7 @@ export class LoginController {
text: 'Account has been activated successfully. You may now log in.', text: 'Account has been activated successfully. You may now log in.',
}); });
res.redirect('/login'); res.redirect(loginPath);
} }
@Get('password') @Get('password')

View File

@ -1,5 +1,6 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AuthMiddleware } from 'src/middleware/auth.middleware'; 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 { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module';
import { OAuth2TokenModule } from 'src/modules/objects/oauth2-token/oauth2-token.module'; import { OAuth2TokenModule } from 'src/modules/objects/oauth2-token/oauth2-token.module';
import { UploadModule } from 'src/modules/objects/upload/upload.module'; import { UploadModule } from 'src/modules/objects/upload/upload.module';
@ -19,6 +20,8 @@ export class OAuth2Module implements NestModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {
consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*'); consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*');
consumer.apply(this._service.oauth.bearer).forRoutes('oauth2/user'); consumer.apply(this._service.oauth.bearer).forRoutes('oauth2/user');
consumer.apply(AuthMiddleware).forRoutes('oauth2/authorize'); consumer
.apply(AuthMiddleware, ValidateCSRFMiddleware)
.forRoutes('oauth2/authorize');
} }
} }

View File

@ -30,7 +30,7 @@ export class OAuth2Service {
public oauth = new OAuth2Provider( public oauth = new OAuth2Provider(
this._oauthAdapter, 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 fullClient = await this.clientService.getById(client.id as string);
const allowedScopes = [...ALWAYS_AVAILABLE]; const allowedScopes = [...ALWAYS_AVAILABLE];
const disallowedScopes = [...ALWAYS_UNAVAILABLE]; const disallowedScopes = [...ALWAYS_UNAVAILABLE];
@ -44,7 +44,7 @@ export class OAuth2Service {
}); });
res.render('authorize', { res.render('authorize', {
csrf: req.session.csrf, csrf: req.csrfToken(),
user: req.user, user: req.user,
client: fullClient, client: fullClient,
allowedScopes, allowedScopes,

View File

@ -35,7 +35,7 @@ export class RegisterController {
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Body() body: RegisterDto, @Body() body: RegisterDto,
@Query() query: { redirectTo?: string }, @Query('redirectTo') redirectTo?: string,
) { ) {
const { username, display_name, email, password, password_repeat } = body; const { username, display_name, email, password, password_repeat } = body;
@ -77,16 +77,14 @@ export class RegisterController {
throw new Error('The passwords do not match!'); throw new Error('The passwords do not match!');
} }
await this.userService.userRegistration(body); await this.userService.userRegistration(body, redirectTo);
req.flash('message', { req.flash('message', {
error: false, error: false,
text: `An activation email has been sent to ${email}!`, text: `An activation email has been sent to ${email}!`,
}); });
res.redirect( res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : ''));
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
);
} catch (e: any) { } catch (e: any) {
req.flash('message', { error: true, text: e.message }); req.flash('message', { error: true, text: e.message });
req.flash('form', { ...body, password_repeat: undefined }); req.flash('form', { ...body, password_repeat: undefined });

View File

@ -95,19 +95,27 @@ export class UserService {
return bcrypt.hash(password, salt); return bcrypt.hash(password, salt);
} }
public async sendActivationEmail(user: User): Promise<void> { public async sendActivationEmail(
user: User,
redirectTo?: string,
): Promise<void> {
const activationToken = await this.userToken.create( const activationToken = await this.userToken.create(
user, user,
UserTokenType.ACTIVATION, UserTokenType.ACTIVATION,
new Date(Date.now() + 3600 * 1000), new Date(Date.now() + 3600 * 1000),
); );
const params = new URLSearchParams({ token: activationToken.token });
if (redirectTo) {
params.append('redirectTo', redirectTo);
}
try { try {
const content = RegistrationEmail( const content = RegistrationEmail(
user.username, user.username,
`${this.config.get<string>('app.base_url')}/login/activate?token=${ `${this.config.get<string>(
activationToken.token 'app.base_url',
}`, )}/login/activate?${params.toString()}`,
); );
await this.email.sendEmailTemplate( await this.email.sendEmailTemplate(
user.email, user.email,
@ -154,12 +162,15 @@ export class UserService {
await this.sendPasswordEmail(user); await this.sendPasswordEmail(user);
} }
public async userRegistration(newUserInfo: { public async userRegistration(
username: string; newUserInfo: {
display_name: string; username: string;
email: string; display_name: string;
password: string; email: string;
}): Promise<User> { password: string;
},
redirectTo?: string,
): Promise<User> {
if (!!(await this.getByEmail(newUserInfo.email))) { if (!!(await this.getByEmail(newUserInfo.email))) {
throw new Error('Email is already in use!'); throw new Error('Email is already in use!');
} }
@ -179,7 +190,7 @@ export class UserService {
await this.userRepository.insert(user); await this.userRepository.insert(user);
try { try {
await this.sendActivationEmail(user); await this.sendActivationEmail(user, redirectTo);
} catch (e) { } catch (e) {
await this.userRepository.remove(user); await this.userRepository.remove(user);
throw new Error( throw new Error(

View File

@ -10,7 +10,10 @@ const ALGORITHM = 'aes-256-cbc';
@Injectable() @Injectable()
export class TokenService { export class TokenService {
public csrf = new CSRF(); public csrf = new CSRF({
saltLength: 16,
secretLength: 32,
});
constructor(private config: ConfigurationService) {} constructor(private config: ConfigurationService) {}

View File

@ -25,7 +25,7 @@ block body
input.form-control#password(type="password", name="password", placeholder="Password") input.form-control#password(type="password", name="password", placeholder="Password")
button.btn.btn-primary(type="submit") Log in button.btn.btn-primary(type="submit") Log in
div.btn-group.align-self-end 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? a.btn.btn-link(type="button" href="/login/password") Forgot password?
div.center-box-addon div.center-box-addon