diff --git a/Auth.md b/Auth.md new file mode 100644 index 000000000..7bb71b2ef --- /dev/null +++ b/Auth.md @@ -0,0 +1,13 @@ +# Auth + +The web is using OAuth 2.0 to handle auth. We host our own instance of [Curity](https://curity.io), which is our identity and access management solution. + +## Session management in Next +We use [Auth.js](https://authjs.dev) to handle everything regarding auth in the web. We use the JWT session strategy, which means that everything regarding the session is stored in a JWT, which is stored in the browser in an encrypted cookie. + +## Keeping the access token alive +When the user performs a navigation the web app often does multiple requests to Next. If the access token has expired Next will do a request to Curity to renew the tokens. Since we only allow a single refresh token to be used only once only the first request will succeed and the following requests will fail. + +To avoid that we have a component whose only purpose is to keep the access token alive. As long as no other request is happening at the same time this will work fine. + +To avoid a session that keeps on refreshing forever, if the user have the page open in the background e.g., we have a timeout that stops the refreshing if the user is not active. \ No newline at end of file diff --git a/app/[lang]/(live)/(public)/login/route.ts b/app/[lang]/(live)/(public)/login/route.ts index 5ccb67d44..ba8292d89 100644 --- a/app/[lang]/(live)/(public)/login/route.ts +++ b/app/[lang]/(live)/(public)/login/route.ts @@ -17,6 +17,13 @@ export async function GET( const returnUrl = request.headers.get("x-returnurl") const isMFA = request.headers.get("x-mfa-login") + // This is to support seamless login when using magic link login + const isMagicLinkUpdateLogin = !!request.headers.get("x-magic-link") + + if (!env.PUBLIC_URL) { + throw internalServerError("No value for env.PUBLIC_URL") + } + if (returnUrl) { // Seamless login request from Current web redirectTo = returnUrl @@ -29,9 +36,6 @@ export async function GET( // Make relative URL to absolute URL if (redirectTo.startsWith("/")) { - if (!env.PUBLIC_URL) { - throw internalServerError("No value for env.PUBLIC_URL") - } redirectTo = new URL(redirectTo, env.PUBLIC_URL).href } @@ -68,6 +72,14 @@ export async function GET( const redirectUrl = new URL(redirectUrlValue) redirectUrl.searchParams.set("returnurl", redirectTo) redirectTo = redirectUrl.toString() + + /** Set cookie with redirect Url to appropriately redirect user when using magic link login */ + redirectHeaders.append( + "set-cookie", + "magicLinkRedirectTo=" + + redirectTo + + "; Max-Age=300; Path=/; HttpOnly; SameSite=Lax" + ) } catch (e) { console.error( "Unable to create URL for seamless login, proceeding without it." @@ -86,27 +98,37 @@ export async function GET( console.log({ login_env: process.env }) console.log({ login_redirectTo: redirectTo }) - const params = isMFA - ? { - ui_locales: context.params.lang, - scope: ["profile_update", "openid", "profile"].join(" "), - /** - * The below acr value is required as for New Web same Curity Client is used for MFA - * while in current web it is being setup using different Curity Client - */ - acr_values: - "urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web", - } - : { - ui_locales: context.params.lang, - scope: ["openid", "profile"].join(" "), - /** - * The `acr_values` param is used to make Curity display the proper login - * page for Scandic. Without the parameter Curity presents some choices - * to the user which we do not want. - */ - acr_values: "acr", - } + const params = { + ui_locales: context.params.lang, + scope: ["openid", "profile"].join(" "), + /** + * The `acr_values` param is used to make Curity display the proper login + * page for Scandic. Without the parameter Curity presents some choices + * to the user which we do not want. + */ + acr_values: "acr", + + /** + * Both of the below two params are required to send for initiating login as well + * because user might choose to do Email link login. + * */ + // The `for_origin` param is used to make Curity email login functionality working. + for_origin: env.PUBLIC_URL, + // This is new param set for differentiate between the Magic link login of New web and current web + version: "2", + } + if (isMFA) { + // Append profile_update scope for MFA + params.scope = params.scope + " profile_udpate" + /** + * The below acr value is required as for New Web same Curity Client is used for MFA + * while in current web it is being setup using different Curity Client + */ + params.acr_values = + "urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web" + } else if (isMagicLinkUpdateLogin) { + params.acr_values = "abc" + } const redirectUrl = await signIn( "curity", { diff --git a/app/[lang]/(live)/(public)/verifymagiclink/route.ts b/app/[lang]/(live)/(public)/verifymagiclink/route.ts new file mode 100644 index 000000000..4f2a1e86d --- /dev/null +++ b/app/[lang]/(live)/(public)/verifymagiclink/route.ts @@ -0,0 +1,75 @@ +import { NextRequest, NextResponse } from "next/server" +import { AuthError } from "next-auth" + +import { Lang } from "@/constants/languages" +import { login } from "@/constants/routes/handleAuth" +import { env } from "@/env/server" +import { badRequest, internalServerError } from "@/server/errors/next" + +import { signIn } from "@/auth" + +export async function GET( + request: NextRequest, + context: { params: { lang: Lang } } +) { + let redirectTo: string + + // Set redirect url from the magicLinkRedirect Cookie which is set when intiating login + redirectTo = + request.cookies.get("magicLinkRedirectTo")?.value || + "/" + context.params.lang + + if (!env.PUBLIC_URL) { + throw internalServerError("No value for env.PUBLIC_URL") + } + + // Make relative URL to absolute URL + if (redirectTo.startsWith("/")) { + redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + } + + // Update Seamless login url as Magic link login has a different authenticator in Curity + redirectTo = redirectTo.replace("updatelogin", "updateloginemail") + + const loginKey = request.nextUrl.searchParams.get("loginKey") + + if (!loginKey) { + return badRequest() + } + + try { + /** + * Passing `redirect: false` to `signIn` will return the URL instead of + * automatically redirecting to it inside of `signIn`. + * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76 + */ + console.log({ login_redirectTo: redirectTo }) + let redirectUrl = await signIn( + "curity", + { + redirectTo, + redirect: false, + }, + { + ui_locales: context.params.lang, + scope: ["openid", "profile"].join(" "), + loginKey: loginKey, + for_origin: env.PUBLIC_URL, + acr_values: "abc", + version: "2", + } + ) + + if (redirectUrl) { + return NextResponse.redirect(redirectUrl) + } + } catch (error) { + if (error instanceof AuthError) { + console.error({ signInAuthError: error }) + } else { + console.error({ signInError: error }) + } + } + + return internalServerError() +} diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 9f9a9263c..dec5edf44 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -5,6 +5,7 @@ import Script from "next/script" import TrpcProvider from "@/lib/trpc/Provider" +import TokenRefresher from "@/components/Auth/TokenRefresher" import AdobeSDKScript from "@/components/Current/AdobeSDKScript" import Footer from "@/components/Current/Footer" import VwoScript from "@/components/Current/VwoScript" @@ -55,6 +56,7 @@ export default async function RootLayout({ {header} {children}