Merged in feature/SW-3506-refresh-tokens (pull request #3064)
feature(SW-3506): refresh-tokens setup for SAS Eurobonus * feature(SW-3506): refresh-tokens setup for SAS Eurobonus * Merge branch 'master' of bitbucket.org:scandic-swap/web into feature/SW-3506-refresh-tokens Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -62,7 +62,7 @@ export async function GET(
|
|||||||
/** Record<string, any> is next-auth typings */
|
/** Record<string, any> is next-auth typings */
|
||||||
const params = {
|
const params = {
|
||||||
ui_locales: SAS_LANGUAGE_MAP[contextParams.lang],
|
ui_locales: SAS_LANGUAGE_MAP[contextParams.lang],
|
||||||
scope: ["openid", "profile", "email"].join(" "),
|
scope: ["openid", "profile", "email", "offline_access"].join(" "),
|
||||||
} satisfies Record<string, string>
|
} satisfies Record<string, string>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,32 +1,100 @@
|
|||||||
import NextAuth, { type NextAuthConfig } from "next-auth"
|
import NextAuth, { type NextAuthConfig } from "next-auth"
|
||||||
import Auth0Provider from "next-auth/providers/auth0"
|
import Auth0Provider from "next-auth/providers/auth0"
|
||||||
|
|
||||||
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||||
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
|
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
|
||||||
import { getEuroBonusProfileData } from "@scandic-hotels/trpc/routers/partners/sas/getEuroBonusProfile"
|
import { getEuroBonusProfileData } from "@scandic-hotels/trpc/routers/partners/sas/getEuroBonusProfile"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
|
import type { JWT } from "next-auth/jwt"
|
||||||
|
|
||||||
const authLogger = createLogger("auth")
|
const authLogger = createLogger("auth")
|
||||||
/*
|
export const PRE_REFRESH_TIME_IN_SECONDS = 180
|
||||||
TODO: Get info for SAS token timeout and accordingly adjust pre-refresh time move to common/contants
|
|
||||||
Do we need to handle refresh tokens at all, isn't that handled by the Auth0 provider?
|
async function refreshTokens(token: JWT) {
|
||||||
Needs to be verified
|
try {
|
||||||
*/
|
if (!token.refresh_token) {
|
||||||
|
throw "Refresh token missing."
|
||||||
|
}
|
||||||
|
|
||||||
|
authLogger.debug("Access token expired, trying to refresh it.", {
|
||||||
|
expires_at: token.expires_at,
|
||||||
|
sub: token.sub,
|
||||||
|
token: token.access_token,
|
||||||
|
refreshToken: token.refresh_token,
|
||||||
|
})
|
||||||
|
const response = await fetch(`${env.SAS_AUTH_ENDPOINT}/oauth/token`, {
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: env.SAS_AUTH_CLIENTID,
|
||||||
|
client_secret: env.SAS_AUTH_CLIENT_SECRET,
|
||||||
|
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) {
|
||||||
|
authLogger.debug("Token response was not ok", {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
sub: token.sub,
|
||||||
|
resopnse: new_tokens,
|
||||||
|
})
|
||||||
|
throw new_tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
authLogger.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) {
|
||||||
|
authLogger.error("Error thrown when trying to refresh", {
|
||||||
|
error,
|
||||||
|
sub: token.sub,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
error: "RefreshAccessTokenError" as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sasProvider = Auth0Provider({
|
const sasProvider = Auth0Provider({
|
||||||
id: "sas",
|
id: "sas",
|
||||||
name: "SAS",
|
name: "SAS",
|
||||||
clientId: env.SAS_AUTH_CLIENTID,
|
clientId: env.SAS_AUTH_CLIENTID,
|
||||||
|
clientSecret: env.SAS_AUTH_CLIENT_SECRET,
|
||||||
issuer: env.SAS_AUTH_ENDPOINT,
|
issuer: env.SAS_AUTH_ENDPOINT,
|
||||||
checks: ["state"],
|
checks: ["state", "pkce"],
|
||||||
authorization: {
|
authorization: {
|
||||||
|
url: `${env.SAS_AUTH_ENDPOINT}/authorize`,
|
||||||
params: {
|
params: {
|
||||||
audience: "eb-partner-api",
|
audience: "eb-partner-api",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
token_endpoint_auth_method: "none",
|
token_endpoint_auth_method: "client_secret_post",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -62,6 +130,13 @@ const config: NextAuthConfig = {
|
|||||||
if (!expiresAt) {
|
if (!expiresAt) {
|
||||||
throw new Error("AuthError: Missing expiry time")
|
throw new Error("AuthError: Missing expiry time")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshToken = params.account?.refresh_token
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
authLogger.warn("⚠️ refreshToken missing")
|
||||||
|
}
|
||||||
|
|
||||||
const [eurobonusProfile, error] = await safeTry(
|
const [eurobonusProfile, error] = await safeTry(
|
||||||
getEuroBonusProfileData({ accessToken, loginType: "eurobonus" })
|
getEuroBonusProfileData({ accessToken, loginType: "eurobonus" })
|
||||||
)
|
)
|
||||||
@@ -75,10 +150,21 @@ const config: NextAuthConfig = {
|
|||||||
isLinked: eurobonusProfile?.linkStatus === "LINKED",
|
isLinked: eurobonusProfile?.linkStatus === "LINKED",
|
||||||
loginType: "eurobonus",
|
loginType: "eurobonus",
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
|
refresh_token: refreshToken,
|
||||||
expires_at: expiresAt,
|
expires_at: expiresAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expiresAt = dt(params.token.expires_at!)
|
||||||
|
|
||||||
|
if (
|
||||||
|
expiresAt
|
||||||
|
.subtract(PRE_REFRESH_TIME_IN_SECONDS, "seconds")
|
||||||
|
.isSameOrBefore(dt())
|
||||||
|
) {
|
||||||
|
refreshTokens(params.token)
|
||||||
|
}
|
||||||
|
|
||||||
return params.token
|
return params.token
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
@@ -112,7 +198,7 @@ const config: NextAuthConfig = {
|
|||||||
// Assume absolute URL
|
// Assume absolute URL
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url)
|
const parsedUrl = new URL(url)
|
||||||
if (/\.scandichotels\.com$/.test(parsedUrl.hostname)) {
|
if (parsedUrl.hostname.endsWith(".scandichotels.com")) {
|
||||||
authLogger.debug(`[auth] subdomain URL accepted, returning: ${url}`)
|
authLogger.debug(`[auth] subdomain URL accepted, returning: ${url}`)
|
||||||
// Allows any subdomains on all top level domains above
|
// Allows any subdomains on all top level domains above
|
||||||
return url
|
return url
|
||||||
|
|||||||
6
apps/partner-sas/env/server.ts
vendored
6
apps/partner-sas/env/server.ts
vendored
@@ -18,8 +18,9 @@ export const env = createEnv({
|
|||||||
.transform((s) => s === "true")
|
.transform((s) => s === "true")
|
||||||
.default("false"),
|
.default("false"),
|
||||||
PUBLIC_URL: z.string().default(""),
|
PUBLIC_URL: z.string().default(""),
|
||||||
SAS_AUTH_ENDPOINT: z.string().default(""),
|
SAS_AUTH_ENDPOINT: z.string(),
|
||||||
SAS_AUTH_CLIENTID: z.string().default(""),
|
SAS_AUTH_CLIENTID: z.string(),
|
||||||
|
SAS_AUTH_CLIENT_SECRET: z.string(),
|
||||||
NEXTAUTH_DEBUG: z
|
NEXTAUTH_DEBUG: z
|
||||||
.string()
|
.string()
|
||||||
.refine((s) => s === "true" || s === "false")
|
.refine((s) => s === "true" || s === "false")
|
||||||
@@ -46,6 +47,7 @@ export const env = createEnv({
|
|||||||
NEXTAUTH_DEBUG: process.env.NEXTAUTH_DEBUG,
|
NEXTAUTH_DEBUG: process.env.NEXTAUTH_DEBUG,
|
||||||
PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL,
|
PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL,
|
||||||
SAS_AUTH_CLIENTID: process.env.SAS_AUTH_CLIENTID,
|
SAS_AUTH_CLIENTID: process.env.SAS_AUTH_CLIENTID,
|
||||||
|
SAS_AUTH_CLIENT_SECRET: process.env.SAS_AUTH_CLIENT_SECRET,
|
||||||
SAS_AUTH_ENDPOINT: process.env.SAS_AUTH_ENDPOINT,
|
SAS_AUTH_ENDPOINT: process.env.SAS_AUTH_ENDPOINT,
|
||||||
SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
|
SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
|
||||||
SENTRY_SERVER_SAMPLERATE: process.env.SENTRY_SERVER_SAMPLERATE,
|
SENTRY_SERVER_SAMPLERATE: process.env.SENTRY_SERVER_SAMPLERATE,
|
||||||
|
|||||||
Reference in New Issue
Block a user