From 925f68839eafbe9cc35daddb55a6716257362d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Est=C3=A9ban?= Date: Mon, 2 Sep 2024 11:56:17 +0200 Subject: [PATCH] refactor: request token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: request token * chore: fix import * up * up * Merge branch 'main' into refactor/request-token * [autofix.ci] apply automated fixes * chore: fix types issue * chore: lint --------- Co-authored-by: Sébastien Chopin Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/runtime/server/lib/oauth/auth0.ts | 41 +++++------- src/runtime/server/lib/oauth/battledotnet.ts | 47 ++++++-------- src/runtime/server/lib/oauth/cognito.ts | 31 +++++---- src/runtime/server/lib/oauth/discord.ts | 40 +++++------- src/runtime/server/lib/oauth/facebook.ts | 24 +++---- src/runtime/server/lib/oauth/github.ts | 34 +++++----- src/runtime/server/lib/oauth/google.ts | 43 +++++-------- src/runtime/server/lib/oauth/keycloak.ts | 42 ++++--------- src/runtime/server/lib/oauth/linkedin.ts | 53 ++++++---------- src/runtime/server/lib/oauth/microsoft.ts | 55 ++++++---------- src/runtime/server/lib/oauth/paypal.ts | 41 +++++------- src/runtime/server/lib/oauth/spotify.ts | 42 ++++++------- src/runtime/server/lib/oauth/steam.ts | 2 +- src/runtime/server/lib/oauth/twitch.ts | 42 +++++-------- src/runtime/server/lib/oauth/x.ts | 43 +++++-------- src/runtime/server/lib/oauth/xsuaa.ts | 40 +++++------- src/runtime/server/lib/oauth/yandex.ts | 38 ++++------- src/runtime/server/lib/utils.ts | 66 +++++++++++++++++++- 18 files changed, 318 insertions(+), 406 deletions(-) diff --git a/src/runtime/server/lib/oauth/auth0.ts b/src/runtime/server/lib/oauth/auth0.ts index 88090ec5..fcb68d92 100644 --- a/src/runtime/server/lib/oauth/auth0.ts +++ b/src/runtime/server/lib/oauth/auth0.ts @@ -1,8 +1,8 @@ import type { H3Event } from 'h3' -import { eventHandler, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleMissingConfiguration, handleAccessTokenErrorResponse } from '../utils' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' import type { OAuthConfig } from '#auth-utils' @@ -70,7 +70,6 @@ export function oauthAuth0EventHandler({ config, onSuccess, onError }: OAuthConf config = defu(config, useRuntimeConfig(event).oauth?.auth0, { authorizationParams: {}, }) as OAuthAuth0Config - const { code } = getQuery(event) if (!config.clientId || !config.clientSecret || !config.domain) { return handleMissingConfiguration(event, 'auth0', ['clientId', 'clientSecret', 'domain'], onError) @@ -78,8 +77,10 @@ export function oauthAuth0EventHandler({ config, onSuccess, onError }: OAuthConf const authorizationURL = `https://${config.domain}/authorize` const tokenURL = `https://${config.domain}/oauth/token` - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const query = getQuery<{ code?: string }>(event) + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || ['openid', 'offline_access'] if (config.emailRequired && !config.scope.includes('email')) { config.scope.push('email') @@ -100,25 +101,17 @@ export function oauthAuth0EventHandler({ config, onSuccess, onError }: OAuthConf ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - tokenURL as string, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: { - grant_type: 'authorization_code', - client_id: config.clientId, - client_secret: config.clientSecret, - redirect_uri: parsePath(redirectURL).pathname, - code, - }, + const tokens = await requestAccessToken(tokenURL as string, { + headers: { + 'Content-Type': 'application/json', + }, + body: { + grant_type: 'authorization_code', + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, + code: query.code, }, - ).catch((error) => { - return { error } }) if (tokens.error) { diff --git a/src/runtime/server/lib/oauth/battledotnet.ts b/src/runtime/server/lib/oauth/battledotnet.ts index 0f5d2c15..d8a193eb 100644 --- a/src/runtime/server/lib/oauth/battledotnet.ts +++ b/src/runtime/server/lib/oauth/battledotnet.ts @@ -1,10 +1,10 @@ -import { randomUUID } from 'node:crypto' import type { H3Event } from 'h3' -import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' -import { useRuntimeConfig } from '#imports' +import { randomUUID } from 'uncrypto' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig, createError } from '#imports' import type { OAuthConfig } from '#auth-utils' export interface OAuthBattledotnetConfig { @@ -62,8 +62,7 @@ export function oauthBattledotnetEventHandler({ config, onSuccess, onError }: OA authorizationParams: {}, }) as OAuthBattledotnetConfig - const query = getQuery(event) - const { code } = query + const query = getQuery<{ code?: string, error?: string }>(event) if (query.error) { const error = createError({ @@ -80,8 +79,9 @@ export function oauthBattledotnetEventHandler({ config, onSuccess, onError }: OA ) } - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || ['openid'] config.region = config.region || 'EU' @@ -109,27 +109,16 @@ export function oauthBattledotnetEventHandler({ config, onSuccess, onError }: OA config.scope.push('openid') } - const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64') - - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - config.tokenURL as string, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${authCode}`, - }, - params: { - code, - grant_type: 'authorization_code', - scope: config.scope.join(' '), - redirect_uri: parsePath(redirectURL).pathname, - }, + const tokens = await requestAccessToken(config.tokenURL as string, { + headers: { + Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`, + }, + params: { + grant_type: 'authorization_code', + scope: config.scope.join(' '), + redirect_uri: redirectURL, + code: query.code, }, - ).catch((error) => { - return { error } }) if (tokens.error) { diff --git a/src/runtime/server/lib/oauth/cognito.ts b/src/runtime/server/lib/oauth/cognito.ts index 71e1b5da..65fcf339 100644 --- a/src/runtime/server/lib/oauth/cognito.ts +++ b/src/runtime/server/lib/oauth/cognito.ts @@ -1,8 +1,8 @@ import type { H3Event } from 'h3' -import { eventHandler, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' import type { OAuthConfig } from '#auth-utils' @@ -54,7 +54,6 @@ export function oauthCognitoEventHandler({ config, onSuccess, onError }: OAuthCo config = defu(config, useRuntimeConfig(event).oauth?.cognito, { authorizationParams: {}, }) as OAuthCognitoConfig - const { code } = getQuery(event) if (!config.clientId || !config.clientSecret || !config.userPoolId || !config.region) { return handleMissingConfiguration(event, 'cognito', ['clientId', 'clientSecret', 'userPoolId', 'region'], onError) @@ -65,8 +64,10 @@ export function oauthCognitoEventHandler({ config, onSuccess, onError }: OAuthCo const authorizationURL = `https://${urlBase}/oauth2/authorize` const tokenURL = `https://${urlBase}/oauth2/token` - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const query = getQuery<{ code?: string }>(event) + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || ['openid', 'profile'] // Redirect to Cognito login page return sendRedirect( @@ -81,20 +82,18 @@ export function oauthCognitoEventHandler({ config, onSuccess, onError }: OAuthCo ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( + const tokens = await requestAccessToken( tokenURL as string, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + body: { + grant_type: 'authorization_code', + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, + code: query.code, }, - body: `grant_type=authorization_code&client_id=${config.clientId}&client_secret=${config.clientSecret}&redirect_uri=${parsePath(redirectURL).pathname}&code=${code}`, }, - ).catch((error) => { - return { error } - }) + ) if (tokens.error) { return handleAccessTokenErrorResponse(event, 'cognito', tokens, onError) diff --git a/src/runtime/server/lib/oauth/discord.ts b/src/runtime/server/lib/oauth/discord.ts index 86c4e03a..aa7e29d8 100644 --- a/src/runtime/server/lib/oauth/discord.ts +++ b/src/runtime/server/lib/oauth/discord.ts @@ -1,8 +1,8 @@ import type { H3Event } from 'h3' -import { eventHandler, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parseURL, stringifyParsedURL } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' import type { OAuthConfig } from '#auth-utils' @@ -67,14 +67,15 @@ export function oauthDiscordEventHandler({ config, onSuccess, onError }: OAuthCo profileRequired: true, authorizationParams: {}, }) as OAuthDiscordConfig - const { code } = getQuery(event) + const query = getQuery<{ code?: string }>(event) if (!config.clientId || !config.clientSecret) { return handleMissingConfiguration(event, 'discord', ['clientId', 'clientSecret'], onError) } - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || [] if (config.emailRequired && !config.scope.includes('email')) { config.scope.push('email') @@ -96,27 +97,14 @@ export function oauthDiscordEventHandler({ config, onSuccess, onError }: OAuthCo ) } - const parsedRedirectUrl = parseURL(redirectURL) - parsedRedirectUrl.search = '' - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - config.tokenURL as string, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: 'authorization_code', - redirect_uri: stringifyParsedURL(parsedRedirectUrl), - code: code as string, - }).toString(), + const tokens = await requestAccessToken(config.tokenURL as string, { + body: { + client_id: config.clientId, + client_secret: config.clientSecret, + grant_type: 'authorization_code', + redirect_uri: redirectURL, + code: query.code, }, - ).catch((error) => { - return { error } }) if (tokens.error) { diff --git a/src/runtime/server/lib/oauth/facebook.ts b/src/runtime/server/lib/oauth/facebook.ts index 9ebe6d50..373bdc43 100644 --- a/src/runtime/server/lib/oauth/facebook.ts +++ b/src/runtime/server/lib/oauth/facebook.ts @@ -1,15 +1,9 @@ import type { H3Event } from 'h3' -import { - eventHandler, - createError, - getQuery, - getRequestURL, - sendRedirect, -} from 'h3' +import { eventHandler, getQuery, sendRedirect } from 'h3' import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' -import { useRuntimeConfig } from '#imports' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig, createError } from '#imports' import type { OAuthConfig } from '#auth-utils' export interface OAuthFacebookConfig { @@ -74,7 +68,8 @@ export function oauthFacebookEventHandler({ tokenURL: 'https://graph.facebook.com/v19.0/oauth/access_token', authorizationParams: {}, }) as OAuthFacebookConfig - const query = getQuery(event) + + const query = getQuery<{ code?: string, error?: string }>(event) if (query.error) { const error = createError({ @@ -90,7 +85,8 @@ export function oauthFacebookEventHandler({ return handleMissingConfiguration(event, 'facebook', ['clientId'], onError) } - const redirectURL = config.redirectURL || getRequestURL(event).href + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + if (!query.code) { config.scope = config.scope || [] // Redirect to Facebook Oauth page @@ -104,13 +100,11 @@ export function oauthFacebookEventHandler({ ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch(config.tokenURL as string, { - method: 'POST', + const tokens = await requestAccessToken(config.tokenURL as string, { body: { client_id: config.clientId, client_secret: config.clientSecret, + grant_type: 'authorization_code', redirect_uri: redirectURL, code: query.code, }, diff --git a/src/runtime/server/lib/oauth/github.ts b/src/runtime/server/lib/oauth/github.ts index 41926c22..1fa167a7 100644 --- a/src/runtime/server/lib/oauth/github.ts +++ b/src/runtime/server/lib/oauth/github.ts @@ -1,9 +1,9 @@ import type { H3Event } from 'h3' -import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' +import { eventHandler, getQuery, sendRedirect } from 'h3' import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' -import { useRuntimeConfig } from '#imports' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig, createError } from '#imports' import type { OAuthConfig } from '#auth-utils' export interface OAuthGitHubConfig { @@ -64,7 +64,8 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon tokenURL: 'https://github.com/login/oauth/access_token', authorizationParams: {}, }) as OAuthGitHubConfig - const query = getQuery(event) + + const query = getQuery<{ code?: string, error?: string }>(event) if (query.error) { const error = createError({ @@ -80,13 +81,14 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon return handleMissingConfiguration(event, 'github', ['clientId', 'clientSecret'], onError) } + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + if (!query.code) { config.scope = config.scope || [] if (config.emailRequired && !config.scope.includes('user:email')) { config.scope.push('user:email') } - // Redirect to GitHub Oauth page - const redirectURL = config.redirectURL || getRequestURL(event).href + return sendRedirect( event, withQuery(config.authorizationURL as string, { @@ -98,19 +100,15 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - config.tokenURL as string, - { - method: 'POST', - body: { - client_id: config.clientId, - client_secret: config.clientSecret, - code: query.code, - }, + const tokens = await requestAccessToken(config.tokenURL as string, { + body: { + grant_type: 'authorization_code', + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, + code: query.code, }, - ) + }) if (tokens.error) { return handleAccessTokenErrorResponse(event, 'github', tokens, onError) diff --git a/src/runtime/server/lib/oauth/google.ts b/src/runtime/server/lib/oauth/google.ts index e63d671f..2f159b2a 100644 --- a/src/runtime/server/lib/oauth/google.ts +++ b/src/runtime/server/lib/oauth/google.ts @@ -1,13 +1,8 @@ import type { H3Event } from 'h3' -import { - eventHandler, - getQuery, - getRequestURL, - sendRedirect, -} from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' import type { OAuthConfig } from '#auth-utils' @@ -76,14 +71,15 @@ export function oauthGoogleEventHandler({ userURL: 'https://www.googleapis.com/oauth2/v3/userinfo', authorizationParams: {}, }) as OAuthGoogleConfig - const { code } = getQuery(event) + + const query = getQuery<{ code?: string }>(event) if (!config.clientId) { return handleMissingConfiguration(event, 'google', ['clientId'], onError) } - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + if (!query.code) { config.scope = config.scope || ['email', 'profile'] // Redirect to Google Oauth page return sendRedirect( @@ -98,23 +94,16 @@ export function oauthGoogleEventHandler({ ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const body: any = { - grant_type: 'authorization_code', - redirect_uri: parsePath(redirectURL).pathname, - client_id: config.clientId, - client_secret: config.clientSecret, - code, - } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch(config.tokenURL as string, { - method: 'POST', - body, - }).catch((error) => { - return { error } + const tokens = await requestAccessToken(config.tokenURL as string, { + body: { + grant_type: 'authorization_code', + code: query.code as string, + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, + }, }) + if (tokens.error) { return handleAccessTokenErrorResponse(event, 'google', tokens, onError) } diff --git a/src/runtime/server/lib/oauth/keycloak.ts b/src/runtime/server/lib/oauth/keycloak.ts index f5a4e2d7..d9124d53 100644 --- a/src/runtime/server/lib/oauth/keycloak.ts +++ b/src/runtime/server/lib/oauth/keycloak.ts @@ -1,15 +1,9 @@ import type { H3Event } from 'h3' -import { - eventHandler, - createError, - getQuery, - getRequestURL, - sendRedirect, -} from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' -import { useRuntimeConfig } from '#imports' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig, createError } from '#imports' import type { OAuthConfig } from '#auth-utils' export interface OAuthKeycloakConfig { @@ -62,8 +56,7 @@ export function oauthKeycloakEventHandler({ authorizationParams: {}, }) as OAuthKeycloakConfig - const query = getQuery(event) - const { code } = query + const query = getQuery<{ code?: string, error?: string }>(event) if (query.error) { const error = createError({ @@ -88,9 +81,9 @@ export function oauthKeycloakEventHandler({ const authorizationURL = `${realmURL}/protocol/openid-connect/auth` const tokenURL = `${realmURL}/protocol/openid-connect/token` - const redirectURL = config.redirectURL || getRequestURL(event).href + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) - if (!code) { + if (!query.code) { config.scope = config.scope || ['openid'] // Redirect to Keycloak Oauth page @@ -111,23 +104,14 @@ export function oauthKeycloakEventHandler({ config.scope.push('openid') } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch(tokenURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ + const tokens = await requestAccessToken(tokenURL, { + body: { + grant_type: 'authorization_code', client_id: config.clientId, client_secret: config.clientSecret, - grant_type: 'authorization_code', - redirect_uri: parsePath(redirectURL).pathname, - code: code as string, - }).toString(), - }).catch((error) => { - return { error } - }) + redirect_uri: redirectURL, + code: query.code, + } }) if (tokens.error) { return handleAccessTokenErrorResponse(event, 'keycloak', tokens, onError) diff --git a/src/runtime/server/lib/oauth/linkedin.ts b/src/runtime/server/lib/oauth/linkedin.ts index bc084d42..f802baa0 100644 --- a/src/runtime/server/lib/oauth/linkedin.ts +++ b/src/runtime/server/lib/oauth/linkedin.ts @@ -1,9 +1,10 @@ -import type { H3Event, H3Error } from 'h3' -import { eventHandler, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parseURL, stringifyParsedURL } from 'ufo' +import type { H3Event } from 'h3' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' +import type { OAuthConfig } from '#auth-utils' export interface OAuthLinkedInConfig { /** @@ -50,28 +51,22 @@ export interface OAuthLinkedInConfig { redirectURL?: string } -interface OAuthConfig { - config?: OAuthLinkedInConfig - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise | void - onError?: (event: H3Event, error: H3Error) => Promise | void -} - -export function oauthLinkedInEventHandler({ config, onSuccess, onError }: OAuthConfig) { +export function oauthLinkedInEventHandler({ config, onSuccess, onError }: OAuthConfig) { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.linkedin, { authorizationURL: 'https://www.linkedin.com/oauth/v2/authorization', tokenURL: 'https://www.linkedin.com/oauth/v2/accessToken', authorizationParams: {}, }) as OAuthLinkedInConfig - const { code } = getQuery(event) + const query = getQuery<{ code?: string }>(event) if (!config.clientId || !config.clientSecret) { return handleMissingConfiguration(event, 'linkedin', ['clientId', 'clientSecret'], onError) } - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || [] if (!config.scope.length) { config.scope.push('profile', 'openid', 'email') @@ -92,28 +87,16 @@ export function oauthLinkedInEventHandler({ config, onSuccess, onError }: OAuthC ) } - const parsedRedirectUrl = parseURL(redirectURL) - parsedRedirectUrl.search = '' - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - config.tokenURL as string, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - code: code as string, - client_id: config.clientId, - client_secret: config.clientSecret, - redirect_uri: stringifyParsedURL(parsedRedirectUrl), - }).toString(), + const tokens = await requestAccessToken(config.tokenURL as string, { + body: { + grant_type: 'authorization_code', + code: query.code as string, + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, }, - ).catch((error) => { - return { error } }) + if (tokens.error) { return handleAccessTokenErrorResponse(event, 'linkedin', tokens, onError) } diff --git a/src/runtime/server/lib/oauth/microsoft.ts b/src/runtime/server/lib/oauth/microsoft.ts index 3fda05de..7a4a9ce0 100644 --- a/src/runtime/server/lib/oauth/microsoft.ts +++ b/src/runtime/server/lib/oauth/microsoft.ts @@ -1,9 +1,10 @@ -import type { H3Event, H3Error } from 'h3' -import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parsePath } from 'ufo' +import type { H3Event } from 'h3' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' -import { useRuntimeConfig } from '#imports' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig, createError } from '#imports' +import type { OAuthConfig } from '#auth-utils' export interface OAuthMicrosoftConfig { /** @@ -58,19 +59,13 @@ export interface OAuthMicrosoftConfig { redirectURL?: string } -interface OAuthConfig { - config?: OAuthMicrosoftConfig - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise | void - onError?: (event: H3Event, error: H3Error) => Promise | void -} - -export function oauthMicrosoftEventHandler({ config, onSuccess, onError }: OAuthConfig) { +export function oauthMicrosoftEventHandler({ config, onSuccess, onError }: OAuthConfig) { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.microsoft, { authorizationParams: {}, }) as OAuthMicrosoftConfig - const { code } = getQuery(event) + + const query = getQuery<{ code?: string }>(event) if (!config.clientId || !config.clientSecret || !config.tenant) { return handleMissingConfiguration(event, 'microsoft', ['clientId', 'clientSecret', 'tenant'], onError) @@ -79,8 +74,9 @@ export function oauthMicrosoftEventHandler({ config, onSuccess, onError }: OAuth const authorizationURL = config.authorizationURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/authorize` const tokenURL = config.tokenURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/token` - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { const scope = config.scope && config.scope.length > 0 ? config.scope : ['User.Read'] // Redirect to Microsoft Oauth page return sendRedirect( @@ -95,27 +91,16 @@ export function oauthMicrosoftEventHandler({ config, onSuccess, onError }: OAuth ) } - const data = new URLSearchParams() - data.append('grant_type', 'authorization_code') - data.append('client_id', config.clientId) - data.append('client_secret', config.clientSecret) - data.append('redirect_uri', parsePath(redirectURL).pathname) - data.append('code', String(code)) - - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - tokenURL as string, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: data, + const tokens = await requestAccessToken(tokenURL, { + body: { + grant_type: 'authorization_code', + code: query.code as string, + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, }, - ).catch((error) => { - return { error } }) + if (tokens.error) { return handleAccessTokenErrorResponse(event, 'microsoft', tokens, onError) } diff --git a/src/runtime/server/lib/oauth/paypal.ts b/src/runtime/server/lib/oauth/paypal.ts index 092ef972..ca1670cf 100644 --- a/src/runtime/server/lib/oauth/paypal.ts +++ b/src/runtime/server/lib/oauth/paypal.ts @@ -1,9 +1,9 @@ import type { H3Event } from 'h3' -import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' -import { useRuntimeConfig } from '#imports' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig, createError } from '#imports' import type { OAuthConfig } from '#auth-utils' export interface OAuthPaypalConfig { @@ -72,7 +72,8 @@ export function oauthPaypalEventHandler({ config, onSuccess, onError }: OAuthCon tokenURL: 'https://api-m.paypal.com/v1/oauth2/token', authorizationParams: {}, }) as OAuthPaypalConfig - const { code } = getQuery(event) + + const query = getQuery<{ code?: string }>(event) if (!config.clientId) { return handleMissingConfiguration(event, 'paypal', ['clientId'], onError) @@ -86,8 +87,8 @@ export function oauthPaypalEventHandler({ config, onSuccess, onError }: OAuthCon config.tokenURL = `https://${paypalAPI}/v1/oauth2/token` } - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + if (!query.code) { config.scope = config.scope || [] if (!config.scope.includes('openid')) { config.scope.push('openid') @@ -110,25 +111,15 @@ export function oauthPaypalEventHandler({ config, onSuccess, onError }: OAuthCon ) } - const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64') - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - config.tokenURL as string, - { - method: 'POST', - headers: { - 'Authorization': `Basic ${authCode}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - params: { - grant_type: 'authorization_code', - redirect_uri: encodeURIComponent(parsePath(redirectURL).pathname), - code, - }, + const tokens = await requestAccessToken(config.tokenURL as string, { + headers: { + Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`, + }, + params: { + grant_type: 'authorization_code', + redirect_uri: redirectURL, + code: query.code, }, - ).catch((error) => { - return { error } }) if (tokens.error) { diff --git a/src/runtime/server/lib/oauth/spotify.ts b/src/runtime/server/lib/oauth/spotify.ts index d0f638b8..5f2a25cb 100644 --- a/src/runtime/server/lib/oauth/spotify.ts +++ b/src/runtime/server/lib/oauth/spotify.ts @@ -1,8 +1,8 @@ import type { H3Event } from 'h3' -import { eventHandler, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' import type { OAuthConfig } from '#auth-utils' @@ -62,14 +62,15 @@ export function oauthSpotifyEventHandler({ config, onSuccess, onError }: OAuthCo tokenURL: 'https://accounts.spotify.com/api/token', authorizationParams: {}, }) as OAuthSpotifyConfig - const { code } = getQuery(event) + const query = getQuery<{ code?: string }>(event) if (!config.clientId || !config.clientSecret) { return handleMissingConfiguration(event, 'spotify', ['clientId', 'clientSecret'], onError) } - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || [] if (config.emailRequired && !config.scope.includes('user-read-email')) { config.scope.push('user-read-email') @@ -87,31 +88,24 @@ export function oauthSpotifyEventHandler({ config, onSuccess, onError }: OAuthCo ) } - const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64') - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - config.tokenURL as string, - { - method: 'POST', - headers: { - 'Authorization': `Basic ${authCode}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - params: { - grant_type: 'authorization_code', - redirect_uri: parsePath(redirectURL).pathname, - code, - }, + const tokens = await requestAccessToken(config.tokenURL as string, { + headers: { + Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`, + }, + body: { + client_id: config.clientId, + grant_type: 'authorization_code', + redirect_uri: redirectURL, + code: query.code, }, - ).catch((error) => { - return { error } }) + if (tokens.error) { return handleAccessTokenErrorResponse(event, 'spotify', tokens, onError) } const accessToken = tokens.access_token + // TODO: improve typing // eslint-disable-next-line @typescript-eslint/no-explicit-any const user: any = await $fetch('https://api.spotify.com/v1/me', { diff --git a/src/runtime/server/lib/oauth/steam.ts b/src/runtime/server/lib/oauth/steam.ts index 70f2194b..96e4361a 100644 --- a/src/runtime/server/lib/oauth/steam.ts +++ b/src/runtime/server/lib/oauth/steam.ts @@ -32,7 +32,7 @@ export function oauthSteamEventHandler({ config, onSuccess, onError }: OAuthConf config = defu(config, useRuntimeConfig(event).oauth?.steam, { authorizationURL: 'https://steamcommunity.com/openid/login', }) as OAuthSteamConfig - const query: Record = getQuery(event) + const query = getQuery>(event) if (!config.apiKey) { return handleMissingConfiguration(event, 'steam', ['apiKey'], onError) diff --git a/src/runtime/server/lib/oauth/twitch.ts b/src/runtime/server/lib/oauth/twitch.ts index 7ca88948..ae14983e 100644 --- a/src/runtime/server/lib/oauth/twitch.ts +++ b/src/runtime/server/lib/oauth/twitch.ts @@ -1,9 +1,9 @@ import type { H3Event } from 'h3' -import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' -import { useRuntimeConfig } from '#imports' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig, createError } from '#imports' import type { OAuthConfig } from '#auth-utils' export interface OAuthTwitchConfig { @@ -65,14 +65,16 @@ export function oauthTwitchEventHandler({ config, onSuccess, onError }: OAuthCon tokenURL: 'https://id.twitch.tv/oauth2/token', authorizationParams: {}, }) as OAuthTwitchConfig - const { code } = getQuery(event) + + const query = getQuery<{ code?: string }>(event) if (!config.clientId) { return handleMissingConfiguration(event, 'twitch', ['clientId'], onError) } - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || [] if (config.emailRequired && !config.scope.includes('user:read:email')) { config.scope.push('user:read:email') @@ -90,26 +92,16 @@ export function oauthTwitchEventHandler({ config, onSuccess, onError }: OAuthCon ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - config.tokenURL as string, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - params: { - grant_type: 'authorization_code', - redirect_uri: parsePath(redirectURL).pathname, - client_id: config.clientId, - client_secret: config.clientSecret, - code, - }, + const tokens = await requestAccessToken(config.tokenURL as string, { + body: { + grant_type: 'authorization_code', + redirect_uri: redirectURL, + client_id: config.clientId, + client_secret: config.clientSecret, + code: query.code, }, - ).catch((error) => { - return { error } }) + if (tokens.error) { return handleAccessTokenErrorResponse(event, 'twitch', tokens, onError) } diff --git a/src/runtime/server/lib/oauth/x.ts b/src/runtime/server/lib/oauth/x.ts index 6a07bd32..36c16541 100644 --- a/src/runtime/server/lib/oauth/x.ts +++ b/src/runtime/server/lib/oauth/x.ts @@ -1,14 +1,13 @@ -import { randomUUID } from 'node:crypto' import type { H3Event } from 'h3' import { eventHandler, getQuery, - getRequestURL, sendRedirect, } from 'h3' -import { withQuery, parsePath } from 'ufo' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' +import { randomUUID } from 'uncrypto' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' import type { OAuthConfig } from '#auth-utils' @@ -77,14 +76,16 @@ export function oauthXEventHandler({ code_challenge: randomUUID(), }, }) as OAuthXConfig - const { code } = getQuery(event) + + const query = getQuery<{ code?: string }>(event) if (!config.clientId) { return handleMissingConfiguration(event, 'x', ['clientId'], onError) } - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || ['tweet.read', 'users.read', 'offline.access'] // Redirect to X Oauth page return sendRedirect( @@ -100,28 +101,18 @@ export function oauthXEventHandler({ ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const params: any = { - grant_type: 'authorization_code', - code_verifier: config.authorizationParams?.code_challenge, - redirect_uri: parsePath(redirectURL).pathname, - code, - } - - const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64') - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch(config.tokenURL as string, { - method: 'POST', + const tokens = await requestAccessToken(config.tokenURL as string, { headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${authCode}`, + Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`, + }, + params: { + grant_type: 'authorization_code', + code_verifier: config.authorizationParams?.code_challenge, + redirect_uri: redirectURL, + code: query.code, }, - params, - }).catch((error) => { - return { error } }) + if (tokens.error) { return handleAccessTokenErrorResponse(event, 'x', tokens, onError) } diff --git a/src/runtime/server/lib/oauth/xsuaa.ts b/src/runtime/server/lib/oauth/xsuaa.ts index b1e67a5c..165509f7 100644 --- a/src/runtime/server/lib/oauth/xsuaa.ts +++ b/src/runtime/server/lib/oauth/xsuaa.ts @@ -1,8 +1,8 @@ import type { H3Event } from 'h3' -import { eventHandler, getQuery, getRequestURL, sendRedirect } from 'h3' -import { withQuery, parsePath } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' import type { OAuthConfig } from '#auth-utils' @@ -39,7 +39,8 @@ export interface OAuthXSUAAConfig { export function oauthXSUAAEventHandler({ config, onSuccess, onError }: OAuthConfig) { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.xsuaa) as OAuthXSUAAConfig - const { code } = getQuery(event) + + const query = getQuery<{ code?: string }>(event) if (!config.clientId || !config.clientSecret || !config.domain) { return handleMissingConfiguration(event, 'xsuaa', ['clientId', 'clientSecret', 'domain'], onError) @@ -47,8 +48,9 @@ export function oauthXSUAAEventHandler({ config, onSuccess, onError }: OAuthConf const authorizationURL = `https://${config.domain}/oauth/authorize` const tokenURL = `https://${config.domain}/oauth/token` - const redirectURL = config.redirectURL || getRequestURL(event).href - if (!code) { + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { config.scope = config.scope || [] // Redirect to XSUAA Oauth page return sendRedirect( @@ -62,26 +64,16 @@ export function oauthXSUAAEventHandler({ config, onSuccess, onError }: OAuthConf ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( - tokenURL as string, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - client_id: config.clientId, - client_secret: config.clientSecret, - redirect_uri: parsePath(redirectURL).pathname, - code: `${code}`, - }), + const tokens = await requestAccessToken(tokenURL as string, { + body: { + grant_type: 'authorization_code', + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, + code: query.code, }, - ).catch((error) => { - return { error } }) + if (tokens.error) { return handleAccessTokenErrorResponse(event, 'auth0', tokens, onError) } diff --git a/src/runtime/server/lib/oauth/yandex.ts b/src/runtime/server/lib/oauth/yandex.ts index f7420d6a..09e2ddbc 100644 --- a/src/runtime/server/lib/oauth/yandex.ts +++ b/src/runtime/server/lib/oauth/yandex.ts @@ -1,13 +1,8 @@ import type { H3Event } from 'h3' -import { - eventHandler, - getQuery, - getRequestURL, - sendRedirect, -} from 'h3' -import { withQuery, parseURL, stringifyParsedURL } from 'ufo' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' import { defu } from 'defu' -import { handleAccessTokenErrorResponse, handleMissingConfiguration } from '../utils' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' import { useRuntimeConfig } from '#imports' import type { OAuthConfig } from '#auth-utils' @@ -75,15 +70,15 @@ export function oauthYandexEventHandler({ userURL: 'https://login.yandex.ru/info', }) as OAuthYandexConfig - const { code } = getQuery(event) + const query = getQuery<{ code?: string }>(event) if (!config.clientId || !config.clientSecret) { return handleMissingConfiguration(event, 'yandex', ['clientId', 'clientSecret'], onError) } - const redirectURL = config.redirectURL || getRequestURL(event).href + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) - if (!code) { + if (!query.code) { config.scope = config.scope || [] if (config.emailRequired && !config.scope.includes('login:email')) { config.scope.push('login:email') @@ -100,23 +95,16 @@ export function oauthYandexEventHandler({ ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch(config.tokenURL as string, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ + const tokens = await requestAccessToken(config.tokenURL as string, { + body: { + grant_type: 'authorization_code', + code: query.code as string, client_id: config.clientId, client_secret: config.clientSecret, - grant_type: 'authorization_code', - redirect_uri: stringifyParsedURL(parseURL(redirectURL)), - code: code as string, - }).toString(), - }).catch((error) => { - return { error } + redirect_uri: redirectURL, + }, }) + if (tokens.error) { return handleAccessTokenErrorResponse(event, 'yandex', tokens, onError) } diff --git a/src/runtime/server/lib/utils.ts b/src/runtime/server/lib/utils.ts index 39c70343..efd41ba7 100644 --- a/src/runtime/server/lib/utils.ts +++ b/src/runtime/server/lib/utils.ts @@ -1,9 +1,71 @@ import type { H3Event } from 'h3' - +import { getRequestURL } from 'h3' +import { FetchError } from 'ofetch' import { snakeCase, upperFirst } from 'scule' -import type { OnError, OAuthProvider } from '#auth-utils' +import type { OAuthProvider, OnError } from '#auth-utils' import { createError } from '#imports' +export function getOAuthRedirectURL(event: H3Event): string { + const requestURL = getRequestURL(event) + + return `${requestURL.protocol}//${requestURL.host}${requestURL.pathname}` +} + +/** + * Request an access token body. + * + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 + */ +interface RequestAccessTokenBody { + grant_type: 'authorization_code' + code: string + redirect_uri: string + client_id: string + client_secret?: string +} + +interface RequestAccessTokenOptions { + body?: RequestAccessTokenBody + params?: Record + headers?: Record +} + +/** + * Request an access token from the OAuth provider. + * + * When an error occurs, only the error data is returned. + * + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 + */ +// TODO: waiting for https://github.com/atinux/nuxt-auth-utils/pull/140 +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function requestAccessToken(url: string, options: RequestAccessTokenOptions): Promise { + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + ...options.headers, + } + + // Encode the body as a URLSearchParams if the content type is 'application/x-www-form-urlencoded'. + const body = headers['Content-Type'] === 'application/x-www-form-urlencoded' + ? new URLSearchParams(options.body as unknown as Record || options.params || {}, + ).toString() + : options.body + + return $fetch(url, { + method: 'POST', + headers, + body, + }).catch((error) => { + /** + * For a better error handling, only unauthorized errors are intercepted, and other errors are re-thrown. + */ + if (error instanceof FetchError && error.status === 401) { + return error.data + } + throw error + }) +} + /** * Handle OAuth access token error response *