diff --git a/.changeset/lazy-ducks-fix.md b/.changeset/lazy-ducks-fix.md new file mode 100644 index 0000000..708c9e9 --- /dev/null +++ b/.changeset/lazy-ducks-fix.md @@ -0,0 +1,5 @@ +--- +"@animo-id/oauth2": patch +--- + +feat: add refresh_token grant type diff --git a/packages/oauth2/src/Oauth2Client.ts b/packages/oauth2/src/Oauth2Client.ts index 9752900..614aeba 100644 --- a/packages/oauth2/src/Oauth2Client.ts +++ b/packages/oauth2/src/Oauth2Client.ts @@ -2,8 +2,10 @@ import { objectToQueryParams } from '@animo-id/oauth2-utils' import { type RetrieveAuthorizationCodeAccessTokenOptions, type RetrievePreAuthorizedCodeAccessTokenOptions, + type RetrieveRefreshTokenAccessTokenOptions, retrieveAuthorizationCodeAccessToken, retrievePreAuthorizedCodeAccessToken, + retrieveRefreshTokenAccessToken, } from './access-token/retrieve-access-token' import { type SendAuthorizationChallengeRequestOptions, @@ -186,6 +188,25 @@ export class Oauth2Client { return result } + public async retrieveRefreshTokenAccessToken({ + authorizationServerMetadata, + additionalRequestPayload, + refreshToken, + resource, + dpop, + }: Omit) { + const result = await retrieveRefreshTokenAccessToken({ + authorizationServerMetadata, + refreshToken, + additionalRequestPayload, + resource, + callbacks: this.options.callbacks, + dpop, + }) + + return result + } + public async resourceRequest(options: ResourceRequestOptions) { return resourceRequest(options) } diff --git a/packages/oauth2/src/access-token/retrieve-access-token.ts b/packages/oauth2/src/access-token/retrieve-access-token.ts index 1f0ac49..59cc32f 100644 --- a/packages/oauth2/src/access-token/retrieve-access-token.ts +++ b/packages/oauth2/src/access-token/retrieve-access-token.ts @@ -7,7 +7,11 @@ import { type RequestDpopOptions, createDpopJwt, extractDpopNonceFromHeaders } f import { shouldRetryTokenRequestWithDPoPNonce } from '../dpop/dpop-retry' import { Oauth2ClientErrorResponseError } from '../error/Oauth2ClientErrorResponseError' import type { AuthorizationServerMetadata } from '../metadata/authorization-server/v-authorization-server-metadata' -import { authorizationCodeGrantIdentifier, preAuthorizedCodeGrantIdentifier } from '../v-grant-type' +import { + authorizationCodeGrantIdentifier, + preAuthorizedCodeGrantIdentifier, + refreshTokenGrantIdentifier, +} from '../v-grant-type' import { type AccessTokenRequest, type AccessTokenResponse, @@ -126,6 +130,39 @@ export async function retrieveAuthorizationCodeAccessToken( return accessTokenResponse } +export interface RetrieveRefreshTokenAccessTokenOptions extends RetrieveAccessTokenBaseOptions { + /** + * The refresh token + */ + refreshToken: string + + /** + * Additional payload to include in the access token request. Items will be encoded and sent + * using x-www-form-urlencoded format. Nested items (JSON) will be stringified and url encoded. + */ + additionalRequestPayload?: Record +} + +export async function retrieveRefreshTokenAccessToken( + options: RetrieveRefreshTokenAccessTokenOptions +): Promise { + const request = { + grant_type: refreshTokenGrantIdentifier, + refresh_token: options.refreshToken, + resource: options.resource, + ...options.additionalRequestPayload, + } satisfies AccessTokenRequest + + const accessTokenResponse = await retrieveAccessTokenWithDpopRetry({ + authorizationServerMetadata: options.authorizationServerMetadata, + request, + dpop: options.dpop, + callbacks: options.callbacks, + }) + + return accessTokenResponse +} + interface RetrieveAccessTokenOptions extends RetrieveAccessTokenBaseOptions { /** * The access token request body diff --git a/packages/oauth2/src/access-token/v-access-token.ts b/packages/oauth2/src/access-token/v-access-token.ts index bafbd25..3a8dbb9 100644 --- a/packages/oauth2/src/access-token/v-access-token.ts +++ b/packages/oauth2/src/access-token/v-access-token.ts @@ -2,7 +2,11 @@ import * as v from 'valibot' import { vHttpsUrl } from '@animo-id/oauth2-utils' import { vOauth2ErrorResponse } from '../common/v-oauth2-error' -import { vAuthorizationCodeGrantIdentifier, vPreAuthorizedCodeGrantIdentifier } from '../v-grant-type' +import { + vAuthorizationCodeGrantIdentifier, + vPreAuthorizedCodeGrantIdentifier, + vRefreshTokenGrantIdentifier, +} from '../v-grant-type' export const vAccessTokenRequest = v.intersect([ v.looseObject({ @@ -13,12 +17,16 @@ export const vAccessTokenRequest = v.intersect([ code: v.optional(v.string()), redirect_uri: v.optional(v.pipe(v.string(), v.url())), + // Refresh token grant + refresh_token: v.optional(v.string()), + resource: v.optional(vHttpsUrl), code_verifier: v.optional(v.string()), grant_type: v.union([ vPreAuthorizedCodeGrantIdentifier, vAuthorizationCodeGrantIdentifier, + vRefreshTokenGrantIdentifier, // string makes the previous ones unessary, but it does help with error messages v.string(), ]), @@ -53,6 +61,8 @@ export const vAccessTokenResponse = v.looseObject({ scope: v.optional(v.string()), state: v.optional(v.string()), + refresh_token: v.optional(v.string()), + // Oid4vci specific parameters c_nonce: v.optional(v.string()), c_nonce_expires_in: v.optional(v.pipe(v.number(), v.integer())), diff --git a/packages/oauth2/src/index.ts b/packages/oauth2/src/index.ts index cb9de1a..9eb454e 100644 --- a/packages/oauth2/src/index.ts +++ b/packages/oauth2/src/index.ts @@ -120,4 +120,7 @@ export { type PreAuthorizedCodeGrantIdentifier, vPreAuthorizedCodeGrantIdentifier, preAuthorizedCodeGrantIdentifier, + type RefreshTokenGrantIdentifier, + vRefreshTokenGrantIdentifier, + refreshTokenGrantIdentifier, } from './v-grant-type' diff --git a/packages/oauth2/src/v-grant-type.ts b/packages/oauth2/src/v-grant-type.ts index 312de16..b48514b 100644 --- a/packages/oauth2/src/v-grant-type.ts +++ b/packages/oauth2/src/v-grant-type.ts @@ -7,3 +7,7 @@ export type PreAuthorizedCodeGrantIdentifier = v.InferOutput + +export const vRefreshTokenGrantIdentifier = v.literal('refresh_token') +export const refreshTokenGrantIdentifier = vRefreshTokenGrantIdentifier.literal +export type RefreshTokenGrantIdentifier = v.InferOutput