Skip to content

Commit c2c3499

Browse files
committed
feat(oauth2): support for refresh tokens
Signed-off-by: Henrique Dias <mail@hacdias.com>
1 parent 8ca38b5 commit c2c3499

11 files changed

Lines changed: 404 additions & 19 deletions

.changeset/rare-oranges-eat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@openid4vc/oauth2": patch
3+
---
4+
5+
Add authorization server support for refresh tokens, as well as verifying requests with refresh token grants.

packages/oauth2/src/Oauth2AuthorizationServer.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseWithErrorHandling } from '@openid4vc/utils'
1+
import { encodeToBase64Url, parseWithErrorHandling } from '@openid4vc/utils'
22
import { type CreateAccessTokenOptions, createAccessTokenJwt } from './access-token/create-access-token'
33
import {
44
type CreateAccessTokenResponseOptions,
@@ -8,8 +8,10 @@ import { type ParseAccessTokenRequestOptions, parseAccessTokenRequest } from './
88
import {
99
type VerifyAuthorizationCodeAccessTokenRequestOptions,
1010
type VerifyPreAuthorizedCodeAccessTokenRequestOptions,
11+
type VerifyRefreshTokenAccessTokenRequestOptions,
1112
verifyAuthorizationCodeAccessTokenRequest,
1213
verifyPreAuthorizedCodeAccessTokenRequest,
14+
verifyRefreshTokenAccessTokenRequest,
1315
} from './access-token/verify-access-token-request'
1416
import {
1517
type CreateAuthorizationChallengeErrorResponseOptions,
@@ -95,12 +97,22 @@ export class Oauth2AuthorizationServer {
9597
})
9698
}
9799

100+
public verifyRefreshTokenAccessTokenRequest(options: Omit<VerifyRefreshTokenAccessTokenRequestOptions, 'callbacks'>) {
101+
return verifyRefreshTokenAccessTokenRequest({
102+
...options,
103+
callbacks: this.options.callbacks,
104+
})
105+
}
106+
98107
/**
99108
* Create an access token response.
100109
*
101110
* The `sub` claim can be used to identify the resource owner is subsequent requests.
102111
* For pre-auth flow this can be the pre-authorized_code but there are no requirements
103112
* on the value.
113+
*
114+
* To generate a refresh token, set the `refreshToken` option to `true`. You can
115+
* also provide a custom refresh token string.
104116
*/
105117
public async createAccessTokenResponse(
106118
options: Pick<
@@ -118,6 +130,7 @@ export class Oauth2AuthorizationServer {
118130
Pick<CreateAccessTokenResponseOptions, 'cNonce' | 'cNonceExpiresIn'> & {
119131
additionalAccessTokenPayload?: CreateAccessTokenOptions['additionalPayload']
120132
additionalAccessTokenResponsePayload?: CreateAccessTokenResponseOptions['additionalPayload']
133+
refreshToken?: boolean | string
121134
}
122135
) {
123136
const { jwt: accessToken } = await createAccessTokenJwt({
@@ -136,6 +149,12 @@ export class Oauth2AuthorizationServer {
136149

137150
return createAccessTokenResponse({
138151
accessToken,
152+
refreshToken:
153+
typeof options.refreshToken === 'string'
154+
? options.refreshToken
155+
: options.refreshToken
156+
? encodeToBase64Url(await this.options.callbacks.generateRandom(32))
157+
: undefined,
139158
callbacks: this.options.callbacks,
140159
expiresInSeconds: options.expiresInSeconds,
141160
tokenType: options.dpop ? 'DPoP' : 'Bearer',

packages/oauth2/src/access-token/__tests__/parse-access-token-request.test.mts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, expect, test } from 'vitest'
22
import { Oauth2ServerErrorResponseError } from '../../error/Oauth2ServerErrorResponseError.js'
3-
import { authorizationCodeGrantIdentifier, preAuthorizedCodeGrantIdentifier } from '../../z-grant-type.js'
3+
import {
4+
authorizationCodeGrantIdentifier,
5+
preAuthorizedCodeGrantIdentifier,
6+
refreshTokenGrantIdentifier,
7+
} from '../../z-grant-type.js'
48
import { parseAccessTokenRequest } from '../parse-access-token-request.js'
59

610
describe('Parse Access Token Request', () => {
@@ -47,7 +51,7 @@ describe('Parse Access Token Request', () => {
4751
).toThrow(`The grant type 'something' is not supported`)
4852
})
4953

50-
test('handles mising pre-authorized_code for pre-auth grant_type', () => {
54+
test('handles missing pre-authorized_code for pre-auth grant_type', () => {
5155
expect(() =>
5256
parseAccessTokenRequest({
5357
accessTokenRequest: {
@@ -62,7 +66,7 @@ describe('Parse Access Token Request', () => {
6266
).toThrow(`Missing required 'pre-authorized_code' for grant type '${preAuthorizedCodeGrantIdentifier}'`)
6367
})
6468

65-
test('handles mising code for authorization_code grant_type', () => {
69+
test('handles missing code for authorization_code grant_type', () => {
6670
expect(() =>
6771
parseAccessTokenRequest({
6872
accessTokenRequest: {
@@ -168,7 +172,34 @@ describe('Parse Access Token Request', () => {
168172
})
169173
})
170174

171-
test('handles invalid dpop jwt', () => {
175+
test('handles refresh token grant', () => {
176+
expect(
177+
parseAccessTokenRequest({
178+
accessTokenRequest: {
179+
grant_type: refreshTokenGrantIdentifier,
180+
refresh_token: 'mamma-mia',
181+
},
182+
request: {
183+
headers: new Headers({}),
184+
method: 'POST',
185+
url: 'https://request.com/token',
186+
},
187+
})
188+
).toEqual({
189+
accessTokenRequest: {
190+
grant_type: refreshTokenGrantIdentifier,
191+
refresh_token: 'mamma-mia',
192+
},
193+
grant: {
194+
grantType: refreshTokenGrantIdentifier,
195+
refreshToken: 'mamma-mia',
196+
},
197+
dpopJwt: undefined,
198+
pkceCodeVerifier: undefined,
199+
})
200+
})
201+
202+
test('handles invalid dpop jwt', () =>
172203
expect(() =>
173204
parseAccessTokenRequest({
174205
accessTokenRequest: {
@@ -193,10 +224,9 @@ describe('Parse Access Token Request', () => {
193224
error: 'invalid_dpop_proof',
194225
error_description: `Request contains a 'DPoP' header, but the value is not a valid DPoP jwt`,
195226
})
196-
)
197-
})
227+
))
198228

199-
test('handles invalid client attestation jwt', () => {
229+
test('handles invalid client attestation jwt', () =>
200230
expect(() =>
201231
parseAccessTokenRequest({
202232
accessTokenRequest: {
@@ -220,6 +250,5 @@ describe('Parse Access Token Request', () => {
220250
error_description:
221251
'Request contains client attestation header, but the values are not valid client attestation and client attestation PoP header.',
222252
})
223-
)
224-
})
253+
))
225254
})

0 commit comments

Comments
 (0)