From 1cf4e57bb176b4c0870856f95eea3ec49f30e2e5 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Mon, 9 Oct 2017 00:28:46 +0200 Subject: [PATCH] Redirect user when he has already validated some factors Example 1: The user has validated first factor when accessing a service protected by basic auth. When he tries to access another service protected by second factor, he is redirected to second factor step to complete authentication. Example 2: The user has already validated second factor. When he access auth service, he is redirected either to /loggedin page that displays an "already logged in" page or to the URL provided in the "redirect" query parameter. --- server/src/lib/RestApi.ts | 4 ++ server/src/lib/routes/firstfactor/get.ts | 45 ++++++++++++++++++- server/src/lib/routes/firstfactor/post.ts | 5 ++- server/src/lib/routes/loggedin/get.ts | 8 ++++ server/src/lib/routes/secondfactor/get.ts | 19 +++++--- server/src/lib/utils/DomainExtractor.ts | 2 + shared/api.ts | 2 + test/features/auth-portal-redirection.feature | 32 +++++++++++++ test/features/authentication.feature | 2 +- 9 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 server/src/lib/routes/loggedin/get.ts create mode 100644 test/features/auth-portal-redirection.feature diff --git a/server/src/lib/RestApi.ts b/server/src/lib/RestApi.ts index 0006fffa7..34686c719 100644 --- a/server/src/lib/RestApi.ts +++ b/server/src/lib/RestApi.ts @@ -29,6 +29,9 @@ import ResetPasswordRequestPost = require("./routes/password-reset/request/get") import Error401Get = require("./routes/error/401/get"); import Error403Get = require("./routes/error/403/get"); import Error404Get = require("./routes/error/404/get"); + +import LoggedIn = require("./routes/loggedin/get"); + import { ServerVariablesHandler } from "./ServerVariablesHandler"; import Endpoints = require("../../../shared/api"); @@ -72,5 +75,6 @@ export class RestApi { app.get(Endpoints.ERROR_401_GET, withLog(Error401Get.default)); app.get(Endpoints.ERROR_403_GET, withLog(Error403Get.default)); app.get(Endpoints.ERROR_404_GET, withLog(Error404Get.default)); + app.get(Endpoints.LOGGED_IN, withLog(LoggedIn.default)); } } diff --git a/server/src/lib/routes/firstfactor/get.ts b/server/src/lib/routes/firstfactor/get.ts index 249f9dc37..22972ae44 100644 --- a/server/src/lib/routes/firstfactor/get.ts +++ b/server/src/lib/routes/firstfactor/get.ts @@ -6,11 +6,52 @@ import Endpoints = require("../../../../../shared/api"); import AuthenticationValidator = require("../../AuthenticationValidator"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import BluebirdPromise = require("bluebird"); +import AuthenticationSession = require("../../AuthenticationSession"); +import Constants = require("../../../../../shared/constants"); +import Util = require("util"); -export default function (req: express.Request, res: express.Response): BluebirdPromise { +function getRedirectParam(req: express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + +function redirectToSecondFactorPage(req: express.Request, res: express.Response) { + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) + res.redirect(Endpoints.SECOND_FACTOR_GET); + else + res.redirect(Util.format("%s?redirect=%s", Endpoints.SECOND_FACTOR_GET, + encodeURIComponent(redirectUrl))); +} + +function redirectToService(req: express.Request, res: express.Response) { + const redirectUrl = getRedirectParam(req); + if (!redirectUrl) + res.redirect(Endpoints.LOGGED_IN); + else + res.redirect(redirectUrl); +} + +function renderFirstFactor(res: express.Response) { res.render("firstfactor", { first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET }); - return BluebirdPromise.resolve(); +} + +export default function (req: express.Request, res: express.Response): BluebirdPromise { + return AuthenticationSession.get(req) + .then(function (authSession) { + if (authSession.first_factor) { + if (authSession.second_factor) + redirectToService(req, res); + else + redirectToSecondFactorPage(req, res); + return BluebirdPromise.resolve(); + } + + renderFirstFactor(res); + return BluebirdPromise.resolve(); + }); } \ No newline at end of file diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index d1b82bb90..ae772f4ac 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -48,7 +48,10 @@ export default function (req: express.Request, res: express.Response): BluebirdP JSON.stringify(groupsAndEmails)); authSession.userid = username; authSession.first_factor = true; - const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM]; + const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + // Fuck, don't know why it is a string! + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; const emails: string[] = groupsAndEmails.emails; const groups: string[] = groupsAndEmails.groups; diff --git a/server/src/lib/routes/loggedin/get.ts b/server/src/lib/routes/loggedin/get.ts new file mode 100644 index 000000000..0a9910a92 --- /dev/null +++ b/server/src/lib/routes/loggedin/get.ts @@ -0,0 +1,8 @@ +import Express = require("express"); +import Endpoints = require("../../../../../shared/api"); + +export default function(req: Express.Request, res: Express.Response) { + res.render("already-logged-in", { + logout_endpoint: Endpoints.LOGOUT_GET + }); +} \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/get.ts b/server/src/lib/routes/secondfactor/get.ts index 172e1d6e6..91d68e1ec 100644 --- a/server/src/lib/routes/secondfactor/get.ts +++ b/server/src/lib/routes/secondfactor/get.ts @@ -4,15 +4,24 @@ import Endpoints = require("../../../../../shared/api"); import FirstFactorBlocker = require("../FirstFactorBlocker"); import BluebirdPromise = require("bluebird"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; +import AuthenticationSession = require("../../AuthenticationSession"); const TEMPLATE_NAME = "secondfactor"; export default FirstFactorBlocker.default(handler); function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - res.render(TEMPLATE_NAME, { - totp_identity_start_endpoint: Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - u2f_identity_start_endpoint: Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET - }); - return BluebirdPromise.resolve(); + return AuthenticationSession.get(req) + .then(function (authSession) { + if (authSession.first_factor && authSession.second_factor) { + res.redirect(Endpoints.LOGGED_IN); + return BluebirdPromise.resolve(); + } + + res.render(TEMPLATE_NAME, { + totp_identity_start_endpoint: Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + u2f_identity_start_endpoint: Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET + }); + return BluebirdPromise.resolve(); + }); } \ No newline at end of file diff --git a/server/src/lib/utils/DomainExtractor.ts b/server/src/lib/utils/DomainExtractor.ts index 2aa55b61b..f2e8b8886 100644 --- a/server/src/lib/utils/DomainExtractor.ts +++ b/server/src/lib/utils/DomainExtractor.ts @@ -1,9 +1,11 @@ export class DomainExtractor { static fromUrl(url: string): string { + if (!url) return ""; return url.match(/https?:\/\/([^\/:]+).*/)[1]; } static fromHostHeader(host: string): string { + if (!host) return ""; return host.split(":")[0]; } } \ No newline at end of file diff --git a/shared/api.ts b/shared/api.ts index 852c9a79d..32ba220a7 100644 --- a/shared/api.ts +++ b/shared/api.ts @@ -297,3 +297,5 @@ export const LOGOUT_GET = "/logout"; export const ERROR_401_GET = "/error/401"; export const ERROR_403_GET = "/error/403"; export const ERROR_404_GET = "/error/404"; + +export const LOGGED_IN = "/loggedin"; diff --git a/test/features/auth-portal-redirection.feature b/test/features/auth-portal-redirection.feature new file mode 100644 index 000000000..f4b7a2475 --- /dev/null +++ b/test/features/auth-portal-redirection.feature @@ -0,0 +1,32 @@ +Feature: User is redirected when factors are already validated + + @need-registered-user-john + Scenario: User has validated first factor and tries to access service protected by second factor. He is then redirect to second factor step. + When I visit "https://basicauth.test.local:8080/secret.html" + And I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fbasicauth.test.local%3A8080%2Fsecret.html" + And I login with user "john" and password "password" + And I'm redirected to "https://basicauth.test.local:8080/secret.html" + And I visit "https://public.test.local:8080/secret.html" + Then I'm redirected to "https://auth.test.local:8080/secondfactor?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html" + + @need-registered-user-john + Scenario: User who has validated second factor and access auth portal should be redirected to "Already logged in page" + When I visit "https://public.test.local:8080/secret.html" + And I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html" + And I login with user "john" and password "password" + And I use "REGISTERED" as TOTP token handle + And I click on "TOTP" + And I'm redirected to "https://public.test.local:8080/secret.html" + And I visit "https://auth.test.local:8080" + Then I'm redirected to "https://auth.test.local:8080/loggedin" + + @need-registered-user-john + Scenario: User who has validated second factor and access auth portal with rediction param should be redirected to that URL + When I visit "https://public.test.local:8080/secret.html" + And I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html" + And I login with user "john" and password "password" + And I use "REGISTERED" as TOTP token handle + And I click on "TOTP" + And I'm redirected to "https://public.test.local:8080/secret.html" + And I visit "https://auth.test.local:8080?redirect=https://public.test.local:8080/secret.html" + Then I'm redirected to "https://public.test.local:8080/secret.html" \ No newline at end of file diff --git a/test/features/authentication.feature b/test/features/authentication.feature index 3beed8045..69ea88843 100644 --- a/test/features/authentication.feature +++ b/test/features/authentication.feature @@ -1,4 +1,4 @@ -Feature: User validate first factor +Feature: Authentication scenarii Scenario: User succeeds first factor Given I visit "https://auth.test.local:8080/"