Counter name is now searchable and add counter for redirects * refactor: createCounter() only takes one argument, the name of the counter. Makes it easier to search for * feat: add counter when we do a redirect from redirect-service Approved-by: Linus Flood
272 lines
7.5 KiB
TypeScript
272 lines
7.5 KiB
TypeScript
import NextAuth, { type NextAuthConfig, type User } from "next-auth"
|
|
|
|
import { LoginTypeEnum } from "@scandic-hotels/common/constants/loginType"
|
|
import { logger } from "@scandic-hotels/common/logger"
|
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
|
|
|
import { PRE_REFRESH_TIME_IN_SECONDS } from "@/constants/auth"
|
|
import { env } from "@/env/server"
|
|
|
|
import type { JWT } from "next-auth/jwt"
|
|
import type { OIDCConfig } from "next-auth/providers"
|
|
|
|
export const signInCounter = createCounter("auth.signIn")
|
|
|
|
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 {
|
|
if (!token.refresh_token) {
|
|
throw "Refresh token missing."
|
|
}
|
|
|
|
logger.debug("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",
|
|
signal: AbortSignal.timeout(15_000),
|
|
})
|
|
|
|
const new_tokens = await response.json()
|
|
|
|
if (!response.ok) {
|
|
logger.debug("token-debug Token response was not ok", {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
sub: token.sub,
|
|
})
|
|
throw new_tokens
|
|
}
|
|
|
|
logger.debug("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) {
|
|
logger.error("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,
|
|
issuer: env.CURITY_ISSUER_SERVICE,
|
|
authorization: {
|
|
url: `${env.CURITY_ISSUER_USER}/oauth/v2/authorize?allow=local`,
|
|
},
|
|
token: {
|
|
url: `${env.CURITY_ISSUER_USER}/oauth/v2/token?allow=local`,
|
|
},
|
|
userinfo: {
|
|
url: `${env.CURITY_ISSUER_USER}/oauth/v2/userinfo?allow=local`,
|
|
},
|
|
profile(profile: User) {
|
|
return {
|
|
id: profile.id,
|
|
sub: profile.sub,
|
|
given_name: profile.given_name,
|
|
login_with: profile.login_with,
|
|
}
|
|
},
|
|
} satisfies OIDCConfig<User>
|
|
|
|
const baseConfig = {
|
|
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: {
|
|
expires_at: token.expires_at,
|
|
error: token.error,
|
|
},
|
|
user: {
|
|
...session.user,
|
|
id: token.sub,
|
|
},
|
|
}
|
|
}
|
|
|
|
return session
|
|
},
|
|
async redirect({ baseUrl, url }) {
|
|
logger.debug(`[auth] deciding redirect URL`, { baseUrl, url })
|
|
if (url.startsWith("/")) {
|
|
logger.debug(
|
|
`[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)
|
|
) {
|
|
logger.debug(`[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
|
|
logger.debug(`[auth] origin URL accepted, returning: ${url}`)
|
|
return url
|
|
}
|
|
} catch (e) {
|
|
logger.error(`[auth] error parsing incoming URL for redirection`, e)
|
|
}
|
|
}
|
|
logger.debug(`[auth] URL denied, returning base URL: ${baseUrl}`)
|
|
return baseUrl
|
|
},
|
|
async authorized() {
|
|
return true
|
|
},
|
|
async jwt({ account, session, token, trigger, user, profile }) {
|
|
const loginType = getLoginType(user)
|
|
if (trigger === "signIn" && account) {
|
|
const counter = signInCounter.init({ type: "curity" })
|
|
counter.success()
|
|
|
|
const mfa_scope = profile?.amr == "urn:com:scandichotels:scandic-otp"
|
|
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
|
|
|
|
const serverConfig = {
|
|
...baseConfig,
|
|
callbacks: {
|
|
...baseConfig.callbacks,
|
|
async session({ session, token }) {
|
|
session.error = token.error
|
|
if (session.user) {
|
|
return {
|
|
...session,
|
|
token,
|
|
user: {
|
|
...session.user,
|
|
id: token.sub,
|
|
},
|
|
}
|
|
}
|
|
|
|
return session
|
|
},
|
|
},
|
|
} satisfies NextAuthConfig
|
|
|
|
export const {
|
|
handlers: { GET, POST },
|
|
signIn,
|
|
signOut,
|
|
} = NextAuth(baseConfig)
|
|
|
|
export const { auth } = NextAuth(serverConfig)
|