Skip to content

Commit

Permalink
feat: Implement automatic token refresh mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
das-Abroxas committed Apr 30, 2024
1 parent 8a9f81e commit 6cf54d9
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 132 deletions.
25 changes: 25 additions & 0 deletions app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ function clearError() {
fetchErrorMsg.value = ''
}
async function refreshTokens() {
const refresh_token = useCookie<string>('refresh_token')
const access_token = useCookie<string>('access_token')
// Check if tokens are set
if (refresh_token.value) {
const current_timestamp = Math.floor(Date.now() / 1000)
const refresh_expiry = parseJwt(refresh_token.value).exp
const refresh_expired = refresh_expiry - current_timestamp <= 0
// Is refresh token expired?
if (!refresh_expired) {
const access_expiry = access_token.value ? parseJwt(access_token.value).exp : 0
const access_expired = access_expiry - current_timestamp <= 60 // Only one minute left or less
console.log(`${refresh_expiry} - ${access_expiry} - ${current_timestamp} - ${access_expiry - current_timestamp} - ${access_expired}`)
if (access_expired) {
await $fetch('/auth/refresh')
}
}
}
}
onBeforeMount(() => setInterval(refreshTokens, 30000))
onMounted(() => updateUser())
</script>

Expand Down
79 changes: 40 additions & 39 deletions composables/api_wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import {
type apistorageservicesv2DeleteObjectResponse,
type modelsv2License,
type v2Collection,
type v2CreateAPITokenRequest,
type v2CreateAPITokenResponse,
type v2CreateCollectionRequest,
type v2CreateCollectionResponse,
type v2CreateDatasetRequest,
type v2CreateDatasetResponse,
type v2CreateObjectRequest,
type v2CreateObjectResponse,
type v2CreateProjectRequest,
type v2CreateProjectResponse,
type v2CreateS3CredentialsUserTokenResponse,
type v2Dataset,
type v2Endpoint,
type v2GetDownloadURLResponse,
type v2GetHierarchyResponse,
type v2GetResourceResponse,
type v2GetS3CredentialsUserTokenResponse,
type v2GetUploadURLResponse,
v2InternalRelationVariant,
type v2Object,
type v2Permission,
type v2Project,
v2RelationDirection,
v2ResourceVariant,
type v2ResourceWithPermission,
type v2SearchResourcesResponse,
type v2User
type apistorageservicesv2DeleteObjectResponse,
type modelsv2License,
type v2Collection,
type v2CreateAPITokenRequest,
type v2CreateAPITokenResponse,
type v2CreateCollectionRequest,
type v2CreateCollectionResponse,
type v2CreateDatasetRequest,
type v2CreateDatasetResponse,
type v2CreateObjectRequest,
type v2CreateObjectResponse,
type v2CreateProjectRequest,
type v2CreateProjectResponse,
type v2CreateS3CredentialsUserTokenResponse,
type v2Dataset,
type v2DeleteAPITokenResponse,
type v2Endpoint,
type v2GetDownloadURLResponse,
type v2GetHierarchyResponse,
type v2GetResourceResponse,
type v2GetS3CredentialsUserTokenResponse,
type v2GetUploadURLResponse,
v2InternalRelationVariant,
type v2Object,
type v2Permission,
type v2Project,
v2RelationDirection,
v2ResourceVariant,
type v2ResourceWithPermission,
type v2SearchResourcesResponse,
type v2User
} from "./aruna_api_json"
import {type ObjectInfo, toObjectInfo} from "~/composables/proto_conversions";
import type {ArunaError} from "~/composables/ArunaError";
Expand Down Expand Up @@ -66,20 +67,20 @@ export async function fetchEndpoint(endpointId: string): Promise<v2Endpoint | un

export async function fetchLicenses(): Promise<modelsv2License[] | undefined> {
// Fetch licenses
const licenses = await $fetch<modelsv2License[]>('/api/licenses')
return licenses
return await $fetch<modelsv2License[]>('/api/licenses')
}

export async function fetchUser(id: string | undefined): Promise<v2User | string> {
const user = await $fetch(id ? `/api/user?userId=${id}` : '/api/user').catch((e) => {
return e.toString()
})
return user
export async function fetchUser(userId: string | undefined): Promise<v2User | ArunaError> {
return await $fetch<v2User | ArunaError>('/api/user', {
method: 'GET',
query: userId ? {
userId: userId
} : {}
})
}

export async function fetchUsers(): Promise<v2User[] | undefined> {
const users = await $fetch('/api/users')
return users
return await $fetch<v2User[] | undefined>('/api/users')
}

export async function activateUser(userId: string): Promise<boolean> {
Expand Down
17 changes: 13 additions & 4 deletions composables/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import type { v2CustomAttribute } from "./aruna_api_json/models/v2CustomAttribute";
import type { v2Permission } from "./aruna_api_json/models/v2Permission";
import type { v2Token } from "./aruna_api_json/models/v2Token";
import type { v2User } from "./aruna_api_json/models/v2User";
import type {
v2CustomAttribute,
v2Permission,
v2Token,
v2User
} from "~/composables/aruna_api_json"

import { Buffer } from 'node:buffer'

export function parseJwt(token: any) {
return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
}


/* ---------- USER ---------- */
export function isUserAdmin(user: v2User | undefined): boolean {
Expand Down
3 changes: 3 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
experimental: {
clientNodeCompat: true
},
plugins: [
"~/plugins/preline.client.ts",
],
Expand Down
32 changes: 13 additions & 19 deletions server/api/user.get.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@

import { v2GetUserResponse } from '~/composables/aruna_api_json'
import {v2GetUserResponse} from '~/composables/aruna_api_json'
import {ArunaError} from "~/composables/ArunaError";

export default defineEventHandler(async event => {
const userId = getQuery(event)['userId']
const baseUrl = useRuntimeConfig().serverHostUrl
const fetchUrl = userId ? `${baseUrl}/v2/user?userId=${userId}` : `${baseUrl}/v2/user`
const response = await $fetch<v2GetUserResponse>(fetchUrl, {
headers: {
'Authorization': `Bearer ${event.context.access_token}`
}
}).catch((error) => {
if (error.data.message === "Not registered") {
return "not_registered" as string
}
return error.data.message as string
})
const userId = getQuery(event)['userId']
const baseUrl = useRuntimeConfig().serverHostUrl
const fetchUrl = `${baseUrl}/v2/user`

if (typeof response === "string") {
return response
}else{
return response.user
return await $fetch<v2GetUserResponse>(fetchUrl, {
headers: {
'Authorization': `Bearer ${event.context.access_token}`
}
}).then(response => {
return response.user ? response.user : new ArunaError(15, 'Returned user is undefined')
}).catch(error => {
return new ArunaError(error.data.code, error.data.message)
})
})
9 changes: 5 additions & 4 deletions server/middleware/intercept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
request.toString().includes('api/resource') ||
(request.toString().includes('api/endpoint') && event.method === 'GET'))
{
return
return // Just do nothing
}

const config = useRuntimeConfig().provider.local;
Expand All @@ -26,7 +26,8 @@ export default defineEventHandler(async (event) => {
if (!access_token) {
const refresh_token = getCookie(event, 'refresh_token')
if (!refresh_token) {
return sendRedirect(event, '/login')
//return sendRedirect(event, '/auth/login')
return // Refresh impossible, so just do nothing
} else {
const tokens: any = await $fetch(tokenURL, {
method: 'POST',
Expand All @@ -48,14 +49,14 @@ export default defineEventHandler(async (event) => {

setCookie(event, 'access_token', tokens.access_token,
{
httpOnly: true,
httpOnly: false,
secure: true,
sameSite: 'none',
maxAge: tokens.expires_in,
}
)
setCookie(event, 'refresh_token', tokens.refresh_token, {
httpOnly: true,
httpOnly: false,
secure: true,
sameSite: 'none',
maxAge: tokens.refresh_expires_in,
Expand Down
51 changes: 0 additions & 51 deletions server/routes/auth/callback.get.ts

This file was deleted.

62 changes: 62 additions & 0 deletions server/routes/auth/refresh.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

export default defineEventHandler(async (event) => {
if (process.client) {
return
}

const config = useRuntimeConfig().provider.local;
const realmURL = `${config.serverUrl}/realms/${config.realm}`
const tokenURL = `${realmURL}/protocol/openid-connect/token`
const refresh_token = getCookie(event, 'refresh_token')
const access_token = getCookie(event, 'access_token')

if (refresh_token) {
const refresh_expiry = parseJwt(refresh_token).exp
const access_expiry = access_token ? parseJwt(access_token).exp : 0
const current_timestamp = Math.floor(Date.now() / 1000)
const access_expired = access_expiry - current_timestamp <= 60 // Only one minute left or less

console.log(`${refresh_expiry} - ${access_expiry} - ${current_timestamp} - ${access_expiry - current_timestamp} - ${access_expired}`)

if (refresh_expiry > current_timestamp && access_expired) {
const tokens: any = await $fetch(tokenURL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: config.clientId,
client_secret: config.clientSecret,
grant_type: 'refresh_token',
refresh_token: refresh_token,
}).toString(),
}).catch((error) => {
console.log('error', error)
return {error}
})

// Set cookie values with refreshed tokens
if (tokens.access_token) {
setCookie(event, 'access_token', tokens.access_token,
{
httpOnly: false,
secure: true,
sameSite: 'none',
maxAge: tokens.expires_in,
})
}
if (tokens.refresh_token) {
setCookie(event, 'refresh_token', tokens.refresh_token, {
httpOnly: false,
secure: true,
sameSite: 'none',
maxAge: tokens.expires_in,
})
}
}
}
})

export function parseJwt(token: any) {
return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
}
Loading

0 comments on commit 6cf54d9

Please sign in to comment.