feat(WEB-215): add refresh_token
This commit is contained in:
committed by
Michael Zetterberg
parent
68f1e87169
commit
c4912bbb94
22
app/[lang]/(live)/(protected)/error.tsx
Normal file
22
app/[lang]/(live)/(protected)/error.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
"use client"
|
||||
import { useParams } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { login } from "@/constants/routes/handleAuth"
|
||||
import { SESSION_EXPIRED } from "@/server/errors/trpc"
|
||||
|
||||
import type { ErrorPage } from "@/types/next/error"
|
||||
import type { LangParams } from "@/types/params"
|
||||
|
||||
export default function ProtectedError({ error }: ErrorPage) {
|
||||
const params = useParams<LangParams>()
|
||||
|
||||
useEffect(() => {
|
||||
if (error.message === SESSION_EXPIRED) {
|
||||
const loginUrl = login[params.lang]
|
||||
window.location.assign(loginUrl)
|
||||
}
|
||||
}, [error.message, params.lang])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export default async function RootLayout({
|
||||
<VwoScript />
|
||||
</head>
|
||||
<body>
|
||||
<TrpcProvider>{children}</TrpcProvider>
|
||||
<TrpcProvider lang={params.lang}>{children}</TrpcProvider>
|
||||
<Script id="page-tracking">{`
|
||||
typeof _satellite !== "undefined" && _satellite.pageBottom();
|
||||
`}</Script>
|
||||
|
||||
48
auth.ts
48
auth.ts
@@ -55,7 +55,8 @@ export const config = {
|
||||
async signIn() {
|
||||
return true
|
||||
},
|
||||
async session({ session, token, user }) {
|
||||
async session({ session, token }) {
|
||||
session.error = token.error
|
||||
if (session.user) {
|
||||
return {
|
||||
...session,
|
||||
@@ -95,13 +96,54 @@ export const config = {
|
||||
async authorized({ auth, request }) {
|
||||
return true
|
||||
},
|
||||
async jwt({ session, token, trigger, account }) {
|
||||
async jwt({ account, session, token, trigger }) {
|
||||
if (account) {
|
||||
return {
|
||||
access_token: account.access_token,
|
||||
expires_at: account.expires_at
|
||||
? account.expires_at * 1000
|
||||
: undefined,
|
||||
refresh_token: account.refresh_token,
|
||||
}
|
||||
} else if (Date.now() < token.expires_at) {
|
||||
return token
|
||||
} else {
|
||||
try {
|
||||
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 tokens = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw tokens
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
access_token: tokens.access_token,
|
||||
expires_at: tokens.expires_at,
|
||||
refresh_token: tokens.refresh_token ?? token.refresh_token,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError" as const,
|
||||
}
|
||||
}
|
||||
}
|
||||
return token
|
||||
},
|
||||
},
|
||||
// events: {
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { httpBatchLink, loggerLink } from "@trpc/client"
|
||||
import {
|
||||
QueryCache,
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from "@tanstack/react-query"
|
||||
import { httpBatchLink, loggerLink,TRPCClientError } from "@trpc/client"
|
||||
import { AnyTRPCRouter } from "@trpc/server"
|
||||
import { useState } from "react"
|
||||
|
||||
import { login } from "@/constants/routes/handleAuth"
|
||||
import { env } from "@/env/client"
|
||||
import { SessionExpiredError } from "@/server/errors/trpc"
|
||||
import { transformer } from "@/server/transformer"
|
||||
|
||||
import { trpc } from "./client"
|
||||
|
||||
import { LangParams } from "@/types/params"
|
||||
|
||||
function initializeTrpcClient() {
|
||||
// Locally we set nextjs to run on port to 3000 so that we always guarantee
|
||||
// that trpc and next are running on the same port.
|
||||
@@ -32,8 +41,58 @@ function initializeTrpcClient() {
|
||||
})
|
||||
}
|
||||
|
||||
export default function TrpcProvider({ children }: React.PropsWithChildren) {
|
||||
const [queryClient] = useState(() => new QueryClient({}))
|
||||
export default function TrpcProvider({
|
||||
children,
|
||||
lang,
|
||||
}: React.PropsWithChildren<LangParams>) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
async onError(error) {
|
||||
if (error instanceof TRPCClientError) {
|
||||
const appError: TRPCClientError<AnyTRPCRouter> = error
|
||||
console.log({ appError })
|
||||
if (appError.data?.code === "UNAUTHORIZED") {
|
||||
if (appError.data?.cause instanceof SessionExpiredError) {
|
||||
const loginUrl = login[lang]
|
||||
window.location.assign(loginUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 3000,
|
||||
retry(failureCount, error) {
|
||||
if (error instanceof TRPCClientError) {
|
||||
const appError: TRPCClientError<AnyTRPCRouter> = error
|
||||
|
||||
// Do not retry query requests that got UNAUTHORIZED error.
|
||||
// It won't make a difference sending the same request again.
|
||||
|
||||
if (appError.data?.code) {
|
||||
if (
|
||||
[
|
||||
"UNAUTHORIZED",
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"FORBIDDEN",
|
||||
].includes(appError.data.code)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retry all client requests that fail (and are not handled above)
|
||||
// at most 3 times.
|
||||
return failureCount < 3
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
const [trpcClient] = useState(() => initializeTrpcClient())
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
|
||||
@@ -42,6 +42,11 @@ export const middleware = auth(async (request) => {
|
||||
const lang = findLang(nextUrl.pathname)!
|
||||
|
||||
const isLoggedIn = !!request.auth
|
||||
const hasError = request.auth?.error
|
||||
|
||||
if (hasError) {
|
||||
throw internalServerError(request.auth?.error)
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
const headers = new Headers(request.headers)
|
||||
|
||||
@@ -39,3 +39,13 @@ export function internalServerError(cause?: unknown) {
|
||||
cause,
|
||||
})
|
||||
}
|
||||
|
||||
export const SESSION_EXPIRED = "SESSION_EXPIRED"
|
||||
export class SessionExpiredError extends Error {}
|
||||
export function sessionExpiredError() {
|
||||
return new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: SESSION_EXPIRED,
|
||||
cause: new SessionExpiredError(SESSION_EXPIRED),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ export const protectedProcedure = t.procedure.use(async function (opts) {
|
||||
console.info(`path: ${opts.path} | type: ${opts.type}`)
|
||||
}
|
||||
|
||||
if (session?.error === "RefreshAccessTokenError") {
|
||||
throw unauthorizedError()
|
||||
}
|
||||
|
||||
if (!session?.user) {
|
||||
throw unauthorizedError()
|
||||
}
|
||||
|
||||
27
types/auth.d.ts
vendored
27
types/auth.d.ts
vendored
@@ -1,8 +1,23 @@
|
||||
import type { JWT } from "next-auth/jwt"
|
||||
|
||||
import type { RefreshTokenError } from "./authError"
|
||||
|
||||
// Module augmentation
|
||||
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
|
||||
declare module "next-auth" {
|
||||
/**
|
||||
* The shape of the account object returned in the OAuth providers' `account` callback,
|
||||
* Usually contains information about the provider being used, like OAuth tokens (`access_token`, etc).
|
||||
*/
|
||||
interface Account {}
|
||||
|
||||
/**
|
||||
* Returned by `useSession`, `auth`, contains information about the active session.
|
||||
*/
|
||||
interface Session extends RefreshTokenError {
|
||||
token: JWT
|
||||
}
|
||||
|
||||
/**
|
||||
* The shape of the user object returned in the OAuth providers' `profile` callback,
|
||||
* or the second parameter of the `session` callback, when using a database.
|
||||
@@ -11,16 +26,4 @@ declare module "next-auth" {
|
||||
given_name: string
|
||||
sub: string
|
||||
}
|
||||
/**
|
||||
* The shape of the account object returned in the OAuth providers' `account` callback,
|
||||
* Usually contains information about the provider being used, like OAuth tokens (`access_token`, etc).
|
||||
*/
|
||||
interface Account { }
|
||||
|
||||
/**
|
||||
* Returned by `useSession`, `auth`, contains information about the active session.
|
||||
*/
|
||||
interface Session {
|
||||
token: JWT
|
||||
}
|
||||
}
|
||||
|
||||
3
types/authError.ts
Normal file
3
types/authError.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface RefreshTokenError {
|
||||
error?: "RefreshAccessTokenError"
|
||||
}
|
||||
8
types/jwt.d.ts
vendored
8
types/jwt.d.ts
vendored
@@ -1,8 +1,14 @@
|
||||
import type { DefaultJWT } from "next-auth/jwt"
|
||||
|
||||
import type { RefreshTokenError } from "./authError"
|
||||
|
||||
// Module augmentation
|
||||
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
|
||||
declare module "next-auth/jwt" {
|
||||
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
|
||||
interface JWT {
|
||||
interface JWT extends DefaultJWT, RefreshTokenError {
|
||||
access_token: string
|
||||
expires_at: number
|
||||
refresh_token: string
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user