Skip to content

Commit

Permalink
feat: add refresh token grant type
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Nov 20, 2024
1 parent 7de55f6 commit 12a517a
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/lazy-ducks-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@animo-id/oauth2": patch
---

feat: add refresh_token grant type
21 changes: 21 additions & 0 deletions packages/oauth2/src/Oauth2Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -186,6 +188,25 @@ export class Oauth2Client {
return result
}

public async retrieveRefreshTokenAccessToken({
authorizationServerMetadata,
additionalRequestPayload,
refreshToken,
resource,
dpop,
}: Omit<RetrieveRefreshTokenAccessTokenOptions, 'callbacks'>) {
const result = await retrieveRefreshTokenAccessToken({
authorizationServerMetadata,
refreshToken,
additionalRequestPayload,
resource,
callbacks: this.options.callbacks,
dpop,
})

return result
}

public async resourceRequest(options: ResourceRequestOptions) {
return resourceRequest(options)
}
Expand Down
39 changes: 38 additions & 1 deletion packages/oauth2/src/access-token/retrieve-access-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, unknown>
}

export async function retrieveRefreshTokenAccessToken(
options: RetrieveRefreshTokenAccessTokenOptions
): Promise<RetrieveAccessTokenReturn> {
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
Expand Down
12 changes: 11 additions & 1 deletion packages/oauth2/src/access-token/v-access-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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(),
]),
Expand Down Expand Up @@ -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())),
Expand Down
3 changes: 3 additions & 0 deletions packages/oauth2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@ export {
type PreAuthorizedCodeGrantIdentifier,
vPreAuthorizedCodeGrantIdentifier,
preAuthorizedCodeGrantIdentifier,
type RefreshTokenGrantIdentifier,
vRefreshTokenGrantIdentifier,
refreshTokenGrantIdentifier,
} from './v-grant-type'
4 changes: 4 additions & 0 deletions packages/oauth2/src/v-grant-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export type PreAuthorizedCodeGrantIdentifier = v.InferOutput<typeof vPreAuthoriz
export const vAuthorizationCodeGrantIdentifier = v.literal('authorization_code')
export const authorizationCodeGrantIdentifier = vAuthorizationCodeGrantIdentifier.literal
export type AuthorizationCodeGrantIdentifier = v.InferOutput<typeof vAuthorizationCodeGrantIdentifier>

export const vRefreshTokenGrantIdentifier = v.literal('refresh_token')
export const refreshTokenGrantIdentifier = vRefreshTokenGrantIdentifier.literal
export type RefreshTokenGrantIdentifier = v.InferOutput<typeof vRefreshTokenGrantIdentifier>

0 comments on commit 12a517a

Please sign in to comment.