Files
web/auth.ts
2024-08-14 10:44:11 +02:00

278 lines
7.7 KiB
TypeScript

import { encode } from "@auth/core/jwt"
import { cookies } from "next/headers"
import NextAuth from "next-auth"
import { env } from "@/env/server"
import { LoginTypeEnum } from "./types/components/tracking"
import type { NextAuthConfig, User } from "next-auth"
import type { OIDCConfig } from "next-auth/providers"
function getLoginType(user: User) {
// TODO: handle magic link, should be enough to just check for Nonce.
// if (user?.nonce) {
// return LoginTypeEnum.MagicLink
// }
if (user?.login_with.includes("@")) {
return LoginTypeEnum.email
} else {
return LoginTypeEnum["membership number"]
}
}
const sharedConfig = {
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,
}
},
}
const curityProvider = {
...sharedConfig,
id: "curity",
name: "Curity",
type: "oidc",
authorization: {
...sharedConfig.authorization,
params: {
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",
},
},
} satisfies OIDCConfig<User>
const curityMFAProvider = {
...sharedConfig,
id: "curity-mfa",
name: "Curity MFA",
type: "oidc",
authorization: {
...sharedConfig.authorization,
params: {
scope: ["profile_update", "openid"].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 ID and secret
*/
acr_values:
"urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web",
},
},
} satisfies OIDCConfig<User>
export const config = {
debug: env.NEXTAUTH_DEBUG,
providers: [curityProvider, curityMFAProvider],
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 }) {
if (url.startsWith("/")) {
// 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)
) {
// Allows any subdomains on all top level domains above
return url
} else if (parsedUrl.origin === baseUrl) {
// Allows callback URLs on the same origin
return url
}
} catch (e) {
console.error("Error in auth redirect callback")
console.error(e)
}
}
return baseUrl
},
async authorized({ auth, request }) {
return true
},
async jwt({ account, session, token, trigger, user }) {
if (account?.provider == "curity-mfa") {
const cookieStore = cookies()
const value = token.access_token
const secret = env.NEXTAUTH_SECRET
const maxAge = 60 * 15
const name = "_SecureMFA-token"
const mfaCookie = await encode({
secret,
maxAge,
token: value,
salt: name,
})
cookieStore.set("_SecureMFA-token", mfaCookie.toString(), {
maxAge: maxAge,
})
return null
}
const loginType = getLoginType(user)
if (account) {
return {
access_token: account.access_token,
expires_at: account.expires_at
? account.expires_at * 1000
: undefined,
refresh_token: account.refresh_token,
loginType,
}
} else if (Date.now() < token.expires_at) {
return token
} else {
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,
})
return {
...token,
access_token: new_tokens.access_token,
expires_at: new_tokens.expires_at,
refresh_token: new_tokens.refresh_token ?? token.refresh_token,
}
} catch (error) {
console.log("token-debug Error thrown when trying to refresh", {
error,
sub: token.sub,
})
return {
...token,
error: "RefreshAccessTokenError" as const,
}
}
}
},
},
// 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)