diff --git a/apps/backend/src/authn/authn.controller.ts b/apps/backend/src/authn/authn.controller.ts index 20b75ec6a0..78ded3c19e 100644 --- a/apps/backend/src/authn/authn.controller.ts +++ b/apps/backend/src/authn/authn.controller.ts @@ -17,7 +17,12 @@ import {LocalAuthGuard} from '../guards/local-auth.guard'; import {LoggingInterceptor} from '../interceptors/logging.interceptor'; import {User} from '../users/user.model'; import {AuthnService} from './authn.service'; - +import 'express-session'; +declare module 'express-session' { + interface SessionData { + redirectLogin?: string; + } +} @UseInterceptors(LoggingInterceptor) @Controller('authn') export class AuthnController { @@ -85,7 +90,15 @@ export class AuthnController { this.logger.debug('in the github login callback func'); this.logger.debug(JSON.stringify(req.session, null, 2)); const session = await this.authnService.login(req.user as User); - await this.setSessionCookies(req, session); + const redirectTarget = + typeof req.session.redirectLogin === 'string' && + req.session.redirectLogin.startsWith('/') + ? req.session.redirectLogin + : undefined; + + delete req.session.redirectLogin; + + await this.setSessionCookies(req, session, redirectTarget); } @Get('gitlab') @@ -169,7 +182,15 @@ export class AuthnController { this.logger.debug('in the oidc login callback func'); this.logger.debug(JSON.stringify(req.session, null, 2)); const session = await this.authnService.login(req.user as User); - await this.setSessionCookies(req, session); + const redirectTarget = + typeof req.session.redirectLogin === 'string' && + req.session.redirectLogin.startsWith('/') + ? req.session.redirectLogin + : undefined; + + delete req.session.redirectLogin; + + await this.setSessionCookies(req, session, redirectTarget); } async setSessionCookies( @@ -177,7 +198,8 @@ export class AuthnController { session: { userID: string; accessToken: string; - } + }, + redirectTarget?: string ): Promise { req.res?.cookie('userID', session.userID, { secure: this.configService.isInProductionMode() @@ -185,6 +207,6 @@ export class AuthnController { req.res?.cookie('accessToken', session.accessToken, { secure: this.configService.isInProductionMode() }); - req.res?.redirect('/'); + req.res?.redirect(redirectTarget || '/'); } } diff --git a/apps/backend/src/authn/github.strategy.ts b/apps/backend/src/authn/github.strategy.ts index 3c76d890c3..90c87a973d 100644 --- a/apps/backend/src/authn/github.strategy.ts +++ b/apps/backend/src/authn/github.strategy.ts @@ -5,7 +5,13 @@ import {Strategy} from 'passport-github'; import {ConfigService} from '../config/config.service'; import {User} from '../users/user.model'; import {AuthnService} from './authn.service'; - +import {Request} from 'express'; +import 'express-session'; +declare module 'express-session' { + interface SessionData { + redirectLogin?: string; + } +} interface GithubProfile { name: string | null; login: string; @@ -43,10 +49,32 @@ export class GithubStrategy extends PassportStrategy(Strategy, 'github') { }); } - async validate( - req: Record, - accessToken: string - ): Promise { + authenticate(req: Request, options: Record = {}) { + const redirect = + typeof req.query?.redirect === 'string' && + req.query.redirect.startsWith('/') + ? req.query.redirect + : undefined; + if (redirect) { + super.authenticate(req, { + ...options, + state: encodeURIComponent(redirect) + }); + return; + } + super.authenticate(req); + } + + async validate(req: Request, accessToken: string): Promise { + const redirectLogin = + typeof req.query?.state === 'string' + ? decodeURIComponent(req.query.state as string) + : undefined; + + if (redirectLogin?.startsWith('/')) { + req.session.redirectLogin = redirectLogin; + } + // Get user's linked emails from Github const githubEmails = await axios .get( diff --git a/apps/backend/src/authn/oidc.strategy.ts b/apps/backend/src/authn/oidc.strategy.ts index 1c6d1b5ae3..280f5e60dc 100644 --- a/apps/backend/src/authn/oidc.strategy.ts +++ b/apps/backend/src/authn/oidc.strategy.ts @@ -6,6 +6,13 @@ import winston from 'winston'; import {ConfigService} from '../config/config.service'; import {GroupsService} from '../groups/groups.service'; import {AuthnService} from './authn.service'; +import {Request} from 'express'; +import 'express-session'; +declare module 'express-session' { + interface SessionData { + redirectLogin?: string; + } +} interface OIDCProfile { id: string; @@ -24,6 +31,22 @@ interface OIDCProfile { @Injectable() export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { + authenticate(req: Request, options: Record = {}) { + const redirect = + typeof req.query?.redirect === 'string' && + req.query.redirect.startsWith('/') + ? req.query.redirect + : undefined; + if (redirect) { + super.authenticate(req, { + ...options, + state: encodeURIComponent(redirect) + }); + return; + } + super.authenticate(req); + } + private readonly line = '_______________________________________________\n'; public loggingTimeFormat = 'MMM-DD-YYYY HH:mm:ss Z'; public logger = winston.createLogger({ @@ -73,6 +96,7 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { }, // using the 9-arity function so that we can access the underlying JSON response and extract the 'email_verified' attribute async ( + req: Request, _issuer: string, uiProfile: OIDCProfile, _idProfile: object, @@ -84,6 +108,15 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { //eslint-disable-next-line @typescript-eslint/no-explicit-any done: any ) => { + const redirectLogin = + typeof req.query?.state === 'string' + ? decodeURIComponent(req.query.state as string) + : undefined; + + if (redirectLogin?.startsWith('/')) { + req.session.redirectLogin = redirectLogin; + } + this.logger.debug('in oidc strategy file'); this.logger.debug(JSON.stringify(uiProfile, null, 2)); const userData = uiProfile._json; diff --git a/apps/frontend/src/components/global/login/LDAPLogin.vue b/apps/frontend/src/components/global/login/LDAPLogin.vue index 58722c6e01..d033525283 100644 --- a/apps/frontend/src/components/global/login/LDAPLogin.vue +++ b/apps/frontend/src/components/global/login/LDAPLogin.vue @@ -75,7 +75,19 @@ export default class LDAPLogin extends Vue { password: this.password }; ServerModule.LoginLDAP(creds).then(() => { - this.$router.push('/'); + if (ServerModule.token) { + const redirectQuery = this.$route.query.redirect; + const redirectTarget = Array.isArray(redirectQuery) + ? redirectQuery[0] + : redirectQuery; + + const destination = + typeof redirectTarget === 'string' && redirectTarget.startsWith('/') + ? redirectTarget + : '/'; + + this.$router.push(destination); + } SnackbarModule.notify('You have successfully signed in.'); }); } diff --git a/apps/frontend/src/components/global/login/LocalLogin.vue b/apps/frontend/src/components/global/login/LocalLogin.vue index c9e14fc22b..9fff81196b 100644 --- a/apps/frontend/src/components/global/login/LocalLogin.vue +++ b/apps/frontend/src/components/global/login/LocalLogin.vue @@ -192,7 +192,19 @@ export default class LocalLogin extends Vue { }; ServerModule.Login(creds) .then(() => { - this.$router.push('/'); + if (ServerModule.token) { + const redirectQuery = this.$route.query.redirect; + const redirectTarget = Array.isArray(redirectQuery) + ? redirectQuery[0] + : redirectQuery; + + const destination = + typeof redirectTarget === 'string' && redirectTarget.startsWith('/') + ? redirectTarget + : '/'; + + this.$router.push(destination); + } SnackbarModule.notify('You have successfully signed in.'); }) .finally(() => { @@ -219,7 +231,19 @@ export default class LocalLogin extends Vue { } oauthLogin(site: string) { - window.location.href = `/authn/${site}`; + const redirectQuery = this.$route.query.redirect; + const redirectTarget = Array.isArray(redirectQuery) + ? redirectQuery[0] + : redirectQuery; + + const destination = + typeof redirectTarget === 'string' && redirectTarget.startsWith('/') + ? redirectTarget + : '/'; + + const url = `/authn/${site}?redirect=${encodeURIComponent(destination)}`; + + window.location.href = url; } get oidcName() { diff --git a/apps/frontend/src/router.ts b/apps/frontend/src/router.ts index 7b27e16be6..3365ca60ee 100644 --- a/apps/frontend/src/router.ts +++ b/apps/frontend/src/router.ts @@ -85,7 +85,7 @@ router.beforeEach((to, _, next) => { AppInfoModule.CheckForUpdates(); if (to.matched.some((record) => record.meta.requiresAuth)) { if (ServerModule.serverMode && !ServerModule.token) { - next('/login'); + next({path: '/login', query: {redirect: to.fullPath}}); return; } } diff --git a/apps/frontend/src/views/Login.vue b/apps/frontend/src/views/Login.vue index fffaa7e8f7..e75fb786f7 100644 --- a/apps/frontend/src/views/Login.vue +++ b/apps/frontend/src/views/Login.vue @@ -82,7 +82,17 @@ export default class Login extends Vue { checkLoggedIn() { if (ServerModule.token) { - this.$router.push('/'); + const redirectQuery = this.$route.query.redirect; + const redirectTarget = Array.isArray(redirectQuery) + ? redirectQuery[0] + : redirectQuery; + + const destination = + typeof redirectTarget === 'string' && redirectTarget.startsWith('/') + ? redirectTarget + : '/'; + + this.$router.push(destination); } }