redirect uri follows the user
This commit is contained in:
parent
21a1ceeb2d
commit
abb0e24d99
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||||
|
@ -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();
|
||||||
|
@ -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')
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 });
|
||||||
|
@ -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(
|
||||||
|
newUserInfo: {
|
||||||
username: string;
|
username: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
}): Promise<User> {
|
},
|
||||||
|
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(
|
||||||
|
@ -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) {}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user