From afd3fbe5dca2f6eaf9843510a31be3a7236166ce Mon Sep 17 00:00:00 2001 From: Marsel Shaikhin Date: Thu, 4 Apr 2024 18:32:32 +0200 Subject: [PATCH] feat(#635): allow changing refresh request body via json pointer --- docs/content/2.configuration/2.nuxt-config.md | 18 +++- playground-refresh/nuxt.config.ts | 3 +- src/module.ts | 1 + src/runtime/composables/refresh/useAuth.ts | 7 +- src/runtime/helpers.ts | 91 ++++++++++++++++--- src/runtime/plugins/refresh-token.server.ts | 7 +- src/runtime/types.ts | 18 +++- 7 files changed, 118 insertions(+), 27 deletions(-) diff --git a/docs/content/2.configuration/2.nuxt-config.md b/docs/content/2.configuration/2.nuxt-config.md index c0b6768c..738460f2 100644 --- a/docs/content/2.configuration/2.nuxt-config.md +++ b/docs/content/2.configuration/2.nuxt-config.md @@ -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. * diff --git a/playground-refresh/nuxt.config.ts b/playground-refresh/nuxt.config.ts index 13721118..9ce3e9a7 100644 --- a/playground-refresh/nuxt.config.ts +++ b/playground-refresh/nuxt.config.ts @@ -20,7 +20,8 @@ export default defineNuxtConfig({ sameSiteAttribute: 'lax' }, refreshToken: { - signInResponseRefreshTokenPointer: '/token/refreshToken' + signInResponseRefreshTokenPointer: '/token/refreshToken', + refreshRequestTokenPointer: '/refreshToken' } }, globalAppMiddleware: { diff --git a/src/module.ts b/src/module.ts index f10a35ee..c02f4384 100644 --- a/src/module.ts +++ b/src/module.ts @@ -83,6 +83,7 @@ const defaultsByBackend: { }, refreshToken: { signInResponseRefreshTokenPointer: '/refreshToken', + refreshRequestTokenPointer: '/refreshToken', cookieName: 'auth.refresh-token', maxAgeInSeconds: 60 * 60 * 24 * 7 // 7 days }, diff --git a/src/runtime/composables/refresh/useAuth.ts b/src/runtime/composables/refresh/useAuth.ts index e6d45114..6e2c8ea9 100644 --- a/src/runtime/composables/refresh/useAuth.ts +++ b/src/runtime/composables/refresh/useAuth.ts @@ -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' @@ -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 } = @@ -91,9 +92,7 @@ const refresh = async () => { const response = await _fetch>(nuxt, path, { method, headers, - body: { - refreshToken: refreshToken.value - } + body: objectFromJsonPointer(refreshRequestTokenPointer, refreshToken.value) }) const extractedToken = jsonPointerGet( diff --git a/src/runtime/helpers.ts b/src/runtime/helpers.ts index a563b02a..125c39bd 100644 --- a/src/runtime/helpers.ts +++ b/src/runtime/helpers.ts @@ -49,22 +49,11 @@ export const useTypedBackendConfig = ( * @param obj * @param pointer */ -export const jsonPointerGet = ( +export function jsonPointerGet ( obj: Record, pointer: string -): string | Record => { - 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 { + const refTokens = Array.isArray(pointer) ? pointer : jsonPointerParse(pointer) for (let i = 0; i < refTokens.length; ++i) { const tok = refTokens[i] @@ -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, + 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} 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 { + 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, '~')) +} diff --git a/src/runtime/plugins/refresh-token.server.ts b/src/runtime/plugins/refresh-token.server.ts index 41cb7987..4e04f139 100644 --- a/src/runtime/plugins/refresh-token.server.ts +++ b/src/runtime/plugins/refresh-token.server.ts @@ -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' @@ -18,6 +18,7 @@ export default defineNuxtPlugin({ const provider = config.provider as DeepRequired 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({ @@ -27,9 +28,7 @@ export default defineNuxtPlugin({ try { const response = await _fetch>(nuxtApp, path, { method, - body: { - refreshToken: refreshToken.value - }, + body: objectFromJsonPointer(refreshRequestTokenPointer, refreshToken.value), headers }) diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 18c20dca..ce088b04 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -212,12 +212,26 @@ export type ProviderLocalRefresh = Omit & { * 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. *