Files
web/auth.ts
Michael Zetterberg 4a846540c3 feat: improve handling of deployment env vars
These are now defined in Netlify UI for dedicated environments (test, stage, production):

AUTH_URL
NEXTAUTH_URL
PUBLIC_URL

Code now falls back to incoming request host. Mainly used for
deployment previews which do not have Akamai in front, meaning
we do not need the above workaround as incoming request host
matches the actual public facing host. When Akamai is in front,
we lose the public facing host in Netlify's routing layer as they
internally use `x-forwarded-for` and we can't claim it for our usage.
2024-10-15 17:03:36 +02:00

238 lines
6.8 KiB
TypeScript

import NextAuth from "next-auth"
import { PRE_REFRESH_TIME_IN_SECONDS } from "@/constants/auth"
import { env } from "@/env/server"
import { LoginTypeEnum } from "./types/components/tracking"
import type { NextAuthConfig, User } from "next-auth"
import type { JWT } from "next-auth/jwt"
import type { OIDCConfig } from "next-auth/providers"
function getLoginType(user: User) {
if (user?.login_with.includes("@")) {
return LoginTypeEnum.email
} else if (user?.login_with.toLowerCase() == LoginTypeEnum["email link"]) {
return LoginTypeEnum["email link"]
} else {
return LoginTypeEnum["membership number"]
}
}
async function refreshTokens(token: JWT) {
try {
console.log("token-debug Access token expired, trying to refresh it.", {
expires_at: token.expires_at,
sub: token.sub,
token: token.access_token,
})
const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
body: new URLSearchParams({
client_id: env.CURITY_CLIENT_ID_USER,
client_secret: env.CURITY_CLIENT_SECRET_USER,
grant_type: "refresh_token",
refresh_token: token.refresh_token,
}),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
})
const new_tokens = await response.json()
if (!response.ok) {
console.log("token-debug Token response was not ok", {
status: response.status,
statusText: response.statusText,
sub: token.sub,
})
throw new_tokens
}
console.log("token-debug Successfully got new token(s)", {
expires_at: new_tokens.expires_at,
got_new_refresh_token: new_tokens.refresh_token !== token.refresh_token,
got_new_access_token: new_tokens.access_token !== token.access_token,
sub: token.sub,
})
const expiresAt = new_tokens.expires_in
? Date.now() + new_tokens.expires_in * 1000
: undefined
return {
...token,
access_token: new_tokens.access_token,
expires_at: expiresAt,
refresh_token: new_tokens.refresh_token,
}
} catch (error) {
console.log("token-debug Error thrown when trying to refresh", {
error,
sub: token.sub,
})
return {
...token,
error: "RefreshAccessTokenError" as const,
}
}
}
const curityProvider = {
id: "curity",
name: "Curity",
type: "oidc",
clientId: env.CURITY_CLIENT_ID_USER,
clientSecret: env.CURITY_CLIENT_SECRET_USER,
// FIXME: This is incorrect. We should not hard code this.
// It should be ${env.CURITY_ISSUER_USER}.
// This change requires sync between Curity deploy and CurrentWeb and NewWeb.
issuer: "https://scandichotels.com",
authorization: {
url: `${env.CURITY_ISSUER_USER}/oauth/v2/authorize`,
},
token: {
url: `${env.CURITY_ISSUER_USER}/oauth/v2/token`,
},
userinfo: {
url: `${env.CURITY_ISSUER_USER}/oauth/v2/userinfo`,
},
profile(profile: User) {
return {
id: profile.id,
sub: profile.sub,
given_name: profile.given_name,
login_with: profile.login_with,
}
},
} satisfies OIDCConfig<User>
export const config = {
basePath: "/api/web/auth",
debug: env.NEXTAUTH_DEBUG,
providers: [curityProvider],
redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL,
trustHost: true,
session: {
strategy: "jwt",
},
callbacks: {
async signIn() {
return true
},
async session({ session, token }) {
session.error = token.error
if (session.user) {
return {
...session,
token,
user: {
...session.user,
id: token.sub,
},
}
}
return session
},
async redirect({ baseUrl, url }) {
console.log(`[auth] deciding redirect URL`, { baseUrl, url })
if (url.startsWith("/")) {
console.log(`[auth] relative URL accepted, returning: ${baseUrl}${url}`)
// Allows relative callback URLs
return `${baseUrl}${url}`
} else {
// Assume absolute URL
try {
const parsedUrl = new URL(url)
if (
/\.scandichotels\.(dk|de|com|fi|no|se)$/.test(parsedUrl.hostname)
) {
console.log(`[auth] subdomain URL accepted, returning: ${url}`)
// Allows any subdomains on all top level domains above
return url
} else if (parsedUrl.origin === baseUrl) {
// Allows callback URLs on the same origin
console.log(`[auth] origin URL accepted, returning: ${url}`)
return url
}
} catch (e) {
console.error(`[auth] error parsing incoming URL for redirection`, e)
}
}
console.log(`[auth] URL denied, returning base URL: ${baseUrl}`)
return baseUrl
},
async authorized({ auth, request }) {
return true
},
async jwt({ account, session, token, trigger, user, profile }) {
const loginType = getLoginType(user)
if (trigger === "signIn" && account) {
const mfa_scope =
profile?.amr ==
"urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web"
const tokenExpiry = account.expires_at
? account.expires_at * 1000
: undefined
const mfa_expires_at = mfa_scope && tokenExpiry ? tokenExpiry : 0
return {
access_token: account.access_token,
expires_at: tokenExpiry,
refresh_token: account.refresh_token,
loginType,
mfa_scope: mfa_scope,
mfa_expires_at: mfa_expires_at,
}
} else if (
token.expires_at &&
Date.now() > token.expires_at - PRE_REFRESH_TIME_IN_SECONDS * 1000 &&
session?.doRefresh
) {
return refreshTokens(token)
}
return token
},
},
// events: {
// async signIn() {
// console.log("#### SIGNIN EVENT ARGS ######")
// console.log(arguments)
// console.log("#### END - SIGNIN EVENT ARGS ######")
// },
// async signOut() {
// console.log("#### SIGNOUT EVENT ARGS ######")
// console.log(arguments)
// console.log("#### END - SIGNOUT EVENT ARGS ######")
// },
// async session() {
// console.log("#### SESSION EVENT ARGS ######")
// console.log(arguments)
// console.log("#### END - SESSION EVENT ARGS ######")
// },
// },
// logger: {
// error(code, ...message) {
// console.info("ERROR LOGGER")
// console.error(code, message)
// },
// warn(code, ...message) {
// console.info("WARN LOGGER")
// console.warn(code, message)
// },
// debug(code, ...message) {
// console.info("DEBUG LOGGER")
// console.debug(code, message)
// },
// },
} satisfies NextAuthConfig
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth(config)