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": {
"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",

View File

@ -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();

View File

@ -35,8 +35,15 @@ export class LoginController {
@Get()
@Render('login/login')
public loginView(@Req() req: Request): Record<string, any> {
return this.formUtil.populateTemplate(req);
public loginView(
@Req() req: Request,
@Query('redirectTo') redirectTo?: string,
): Record<string, any> {
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')

View File

@ -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');
}
}

View File

@ -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,

View File

@ -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 });

View File

@ -95,19 +95,27 @@ export class UserService {
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(
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<string>('app.base_url')}/login/activate?token=${
activationToken.token
}`,
`${this.config.get<string>(
'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<User> {
public async userRegistration(
newUserInfo: {
username: string;
display_name: string;
email: string;
password: string;
},
redirectTo?: string,
): Promise<User> {
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(

View File

@ -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) {}

View File

@ -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