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}
+