Skip to content

Commit

Permalink
feat(#635): allow changing refresh request body via json pointer
Browse files Browse the repository at this point in the history
  • Loading branch information
phoenix-ru committed Apr 4, 2024
1 parent a8000e0 commit afd3fbe
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 27 deletions.
18 changes: 16 additions & 2 deletions docs/content/2.configuration/2.nuxt-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,26 @@ type ProviderRefresh = {
* E.g., setting this to `/token/refreshToken` and returning an object like `{ token: { refreshToken: 'THE_REFRESH__TOKEN' }, timestamp: '2023' }` from the `signIn` endpoint will
* result in `nuxt-auth` extracting and storing `THE_REFRESH__TOKEN`.
*
* This follows the JSON Pointer standard, see it's RFC6901 here: https://www.rfc-editor.org/rfc/rfc6901
* This follows the JSON Pointer standard, see its RFC6901 here: https://www.rfc-editor.org/rfc/rfc6901
*
* @default /refreshToken Access the `refreshToken` property of the sign-in response object
* @default '/refreshToken' Access the `refreshToken` property of the sign-in response object
* @example / Access the root of the sign-in response object, useful when your endpoint returns a plain, non-object string as the refreshToken
*/
signInResponseRefreshTokenPointer?: string
/**
* How to do a fetch for the refresh token.
*
* This is especially useful when you have an external backend signing tokens. Refer to this issue to get more information: https://github.com/sidebase/nuxt-auth/issues/635.
*
* ### Example
* Setting this to `/refresh/token` would make Nuxt Auth send the `POST /api/auth/refresh` with the following BODY: `{ "refresh": { "token": "..." } }
*
* ### Notes
* This follows the JSON Pointer standard, see its RFC6901 here: https://www.rfc-editor.org/rfc/rfc6901
*
* @default '/refreshToken'
*/
refreshRequestTokenPointer?: string;
/**
* It refers to the name of the property when it is stored in a cookie.
*
Expand Down
3 changes: 2 additions & 1 deletion playground-refresh/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export default defineNuxtConfig({
sameSiteAttribute: 'lax'
},
refreshToken: {
signInResponseRefreshTokenPointer: '/token/refreshToken'
signInResponseRefreshTokenPointer: '/token/refreshToken',
refreshRequestTokenPointer: '/refreshToken'
}
},
globalAppMiddleware: {
Expand Down
1 change: 1 addition & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const defaultsByBackend: {
},
refreshToken: {
signInResponseRefreshTokenPointer: '/refreshToken',
refreshRequestTokenPointer: '/refreshToken',
cookieName: 'auth.refresh-token',
maxAgeInSeconds: 60 * 60 * 24 * 7 // 7 days
},
Expand Down
7 changes: 3 additions & 4 deletions src/runtime/composables/refresh/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Ref } from 'vue'
import { callWithNuxt } from '#app'
import { jsonPointerGet, useTypedBackendConfig } from '../../helpers'
import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../../helpers'
import { useAuth as useLocalAuth } from '../local/useAuth'
import { _fetch } from '../../utils/fetch'
import { getRequestURLWN } from '../../utils/callWithNuxt'
Expand Down Expand Up @@ -79,6 +79,7 @@ const refresh = async () => {
const nuxt = useNuxtApp()
const config = useTypedBackendConfig(useRuntimeConfig(), 'refresh')
const { path, method } = config.endpoints.refresh
const refreshRequestTokenPointer = config.refreshToken.refreshRequestTokenPointer

const { getSession } = useLocalAuth()
const { refreshToken, token, rawToken, rawRefreshToken, lastRefreshedAt } =
Expand All @@ -91,9 +92,7 @@ const refresh = async () => {
const response = await _fetch<Record<string, any>>(nuxt, path, {
method,
headers,
body: {
refreshToken: refreshToken.value
}
body: objectFromJsonPointer(refreshRequestTokenPointer, refreshToken.value)
})

const extractedToken = jsonPointerGet(
Expand Down
91 changes: 77 additions & 14 deletions src/runtime/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,11 @@ export const useTypedBackendConfig = <T extends SupportedAuthProviders>(
* @param obj
* @param pointer
*/
export const jsonPointerGet = (
export function jsonPointerGet (
obj: Record<string, any>,
pointer: string
): string | Record<string, any> => {
const unescape = (str: string) => str.replace(/~1/g, '/').replace(/~0/g, '~')
const parse = (pointer: string) => {
if (pointer === '') {
return []
}
if (pointer.charAt(0) !== '/') {
throw new Error('Invalid JSON pointer: ' + pointer)
}
return pointer.substring(1).split(/\//).map(unescape)
}

const refTokens = Array.isArray(pointer) ? pointer : parse(pointer)
): string | Record<string, any> {
const refTokens = Array.isArray(pointer) ? pointer : jsonPointerParse(pointer)

for (let i = 0; i < refTokens.length; ++i) {
const tok = refTokens[i]
Expand All @@ -75,3 +64,77 @@ export const jsonPointerGet = (
}
return obj
}

/**
* Sets a value on an object
*
* RFC / Standard: https://www.rfc-editor.org/rfc/rfc6901
*
* Adapted from https://github.com/manuelstofer/json-pointer/blob/931b0f9c7178ca09778087b4b0ac7e4f505620c2/index.js#L68-L103
*/
export function jsonPointerSet (
obj: Record<string, any>,
pointer: string | string[],
value: any
) {
const refTokens = Array.isArray(pointer) ? pointer : jsonPointerParse(pointer)
let nextTok: string | number = refTokens[0]

if (refTokens.length === 0) {
throw new Error('Can not set the root object')
}

for (let i = 0; i < refTokens.length - 1; ++i) {
let tok: string | number = refTokens[i]
if (typeof tok !== 'string' && typeof tok !== 'number') {
tok = String(tok)
}
if (tok === '__proto__' || tok === 'constructor' || tok === 'prototype') {
continue
}
if (tok === '-' && Array.isArray(obj)) {
tok = obj.length
}
nextTok = refTokens[i + 1]

if (!(tok in obj)) {
if (nextTok.match(/^(\d+|-)$/)) {
obj[tok] = []
} else {
obj[tok] = {}
}
}
obj = obj[tok]
}
if (nextTok === '-' && Array.isArray(obj)) {
nextTok = obj.length
}
obj[nextTok] = value
}

/**
* Creates an object from a value and a pointer.
* This is equivalent to calling `jsonPointerSet` on an empty object.
* @returns {Record<string, any>} An object with a value set at an arbitrary pointer.
* @example objectFromJsonPointer('/refresh', 'someToken') // { refresh: 'someToken' }
*/
export function objectFromJsonPointer (pointer: string | string[], value: any): Record<string, any> {
const result = {}
jsonPointerSet(result, pointer, value)
return result
}

/**
* Converts a json pointer into a array of reference tokens
*
* Adapted from https://github.com/manuelstofer/json-pointer/blob/931b0f9c7178ca09778087b4b0ac7e4f505620c2/index.js#L217-L221
*/
function jsonPointerParse (pointer: string): string[] {
if (pointer === '') {
return []
}
if (pointer.charAt(0) !== '/') {
throw new Error('Invalid JSON pointer: ' + pointer)
}
return pointer.substring(1).split(/\//).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'))
}
7 changes: 3 additions & 4 deletions src/runtime/plugins/refresh-token.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DeepRequired } from 'ts-essentials'
import { _fetch } from '../utils/fetch'
import { jsonPointerGet, useTypedBackendConfig } from '../helpers'
import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../helpers'
import type { ProviderLocalRefresh } from '../types'
import { defineNuxtPlugin, useAuthState, useRuntimeConfig } from '#imports'

Expand All @@ -18,6 +18,7 @@ export default defineNuxtPlugin({
const provider = config.provider as DeepRequired<ProviderLocalRefresh>

const { path, method } = provider.endpoints.refresh
const refreshRequestTokenPointer = provider.refreshToken.refreshRequestTokenPointer

// include header in case of auth is required to avoid 403 rejection
const headers = new Headers({
Expand All @@ -27,9 +28,7 @@ export default defineNuxtPlugin({
try {
const response = await _fetch<Record<string, any>>(nuxtApp, path, {
method,
body: {
refreshToken: refreshToken.value
},
body: objectFromJsonPointer(refreshRequestTokenPointer, refreshToken.value),
headers
})

Expand Down
18 changes: 16 additions & 2 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,26 @@ export type ProviderLocalRefresh = Omit<ProviderLocal, 'type'> & {
* E.g., setting this to `/refreshToken/bearer` and returning an object like `{ refreshToken: { bearer: 'THE_AUTH_TOKEN' }, timestamp: '2023' }` from the `signIn` endpoint will
* result in `nuxt-auth` extracting and storing `THE_AUTH_TOKEN`.
*
* This follows the JSON Pointer standard, see it's RFC6901 here: https://www.rfc-editor.org/rfc/rfc6901
* This follows the JSON Pointer standard, see its RFC6901 here: https://www.rfc-editor.org/rfc/rfc6901
*
* @default /refreshToken Access the `refreshToken` property of the sign-in response object
* @default '/refreshToken' Access the `refreshToken` property of the sign-in response object
* @example / Access the root of the sign-in response object, useful when your endpoint returns a plain, non-object string as the token
*/
signInResponseRefreshTokenPointer?: string;
/**
* How to do a fetch for the refresh token.
*
* This is especially useful when you have an external backend signing tokens. Refer to this issue to get more information: https://github.com/sidebase/nuxt-auth/issues/635.
*
* ### Example
* Setting this to `/refresh/token` would make Nuxt Auth send the `POST /api/auth/refresh` with the following BODY: `{ "refresh": { "token": "..." } }
*
* ### Notes
* This follows the JSON Pointer standard, see its RFC6901 here: https://www.rfc-editor.org/rfc/rfc6901
*
* @default '/refreshToken'
*/
refreshRequestTokenPointer?: string;
/**
* It refers to the name of the property when it is stored in a cookie.
*
Expand Down

0 comments on commit afd3fbe

Please sign in to comment.