Skip to content

Commit

Permalink
Implement refresh access token logic
Browse files Browse the repository at this point in the history
Add refresh token logic

- Add function to the auth route to check the expiry of the token and attempt a refresh against the OIDC provider endpoint
- Add necessary fields to next-auth types
  • Loading branch information
dmtrek14 committed Nov 17, 2023
1 parent 55e79cb commit 0f6c62f
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 2 deletions.
3 changes: 2 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
OIDC_AUTHORITY=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_TOKEN_ENDPOINT=

API_BASE='http://phpreport-api:8555'
NEXT_PUBLIC_API_BASE='http://0.0.0.0:8555'

NEXTAUTH_URL=http://0.0.0.0:5173/web/v2/api/auth
NEXTAUTH_SECRET=""
NEXTAUTH_SECRET=""
58 changes: 57 additions & 1 deletion frontend/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,52 @@ import NextAuth, { NextAuthOptions } from 'next-auth'
import KeycloakProvider from 'next-auth/providers/keycloak'
import { fetchFactory } from '@/infra/lib/apiClient'
import { getCurrentUser } from '@/infra/user/getCurrentUser'
import { JWT } from 'next-auth/jwt'

/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
async function refreshAccessToken(token: JWT) {
try {
const url = `${process.env.OIDC_TOKEN_ENDPOINT}`

const params = {
grant_type: 'refresh_token',
client_id: process.env.OIDC_CLIENT_ID!,
client_secret: process.env.OIDC_CLIENT_SECRET!,
refresh_token: token.refreshToken!
}

const response = await fetch(url, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(params),
method: 'POST'
})

const refreshedTokens = await response.json()

if (!response.ok) {
throw refreshedTokens
}

return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken // Fall back to old refresh token
}
} catch (error) {
return {
...token,
error: 'RefreshAccessTokenError'
}
}
}


export const authOptions: NextAuthOptions = {
providers: [
Expand All @@ -21,11 +67,15 @@ export const authOptions: NextAuthOptions = {
async session({ session, token }) {
session.accessToken = token.accessToken
session.user = { ...session.user, ...token.user }
session.accessTokenExpires = token.accessTokenExpires
session.refreshToken = token.refreshToken
return session
},
async jwt({ token, account, profile }) {
if (account && profile) {
token.accessToken = account.access_token
token.accessTokenExpires = Date.now() + account.expires_at * 1000
token.refreshToken = account.refresh_token
token.id = profile.id

const apiClient = fetchFactory({ baseURL: process.env.API_BASE!, token: token.accessToken })
Expand All @@ -34,7 +84,13 @@ export const authOptions: NextAuthOptions = {

token.user = user
}
return token
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpires!) {
return token
}

// Access token has expired, try to update it
return refreshAccessToken(token)
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/types/next-auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ declare module 'next-auth' {
interface Session {
accessToken?: string
user: User & DefaultSession['user']
accessTokenExpires?: number
refreshToken?: string
}

/**
Expand All @@ -16,6 +18,7 @@ declare module 'next-auth' {
*/
interface Account {
access_token: string
expires_at: number
}

/** The OAuth profile returned from your provider */
Expand All @@ -30,5 +33,7 @@ declare module 'next-auth/jwt' {
id?: string
accessToken?: string
user?: User
accessTokenExpires?: number
refreshToken?: string
}
}

0 comments on commit 0f6c62f

Please sign in to comment.