@@ -13,7 +13,14 @@ import {
1313 SessionStore ,
1414 StateStore ,
1515} from "./schema" ;
16- import { isAccessTokenExpired , maskSecret } from "./utils" ;
16+ import {
17+ AccessTokenValidationOptions ,
18+ decodeAccessToken ,
19+ isAccessTokenExpired ,
20+ isRefreshTokenExpired ,
21+ maskSecret ,
22+ validateAccessToken as validateDecodedAccessToken ,
23+ } from "./utils" ;
1724
1825export type OAuthClientOptions = {
1926 // Config
@@ -63,8 +70,8 @@ export class OAuthClient {
6370 ? oauth . ClientSecretPost (
6471 maskSecret (
6572 this . clientMetadata . clientSecret ,
66- this . clientMetadata . clientId
67- )
73+ this . clientMetadata . clientId ,
74+ ) ,
6875 )
6976 : oauth . None ( ) ;
7077 }
@@ -76,7 +83,7 @@ export class OAuthClient {
7683 async authorize ( ) {
7784 if ( ! this . clientMetadata . redirectUri ) {
7885 throw new Error (
79- "Client is not configured for the authorization code flow; missing `redirectUri`."
86+ "Client is not configured for the authorization code flow; missing `redirectUri`." ,
8087 ) ;
8188 }
8289
@@ -95,17 +102,17 @@ export class OAuthClient {
95102 authorizationUrl . searchParams . set ( "response_type" , "code" ) ;
96103 authorizationUrl . searchParams . set (
97104 "client_id" ,
98- this . clientMetadata . clientId
105+ this . clientMetadata . clientId ,
99106 ) ;
100107 authorizationUrl . searchParams . set (
101108 "redirect_uri" ,
102- this . clientMetadata . redirectUri
109+ this . clientMetadata . redirectUri ,
103110 ) ;
104111
105112 if ( this . clientMetadata . scopes ) {
106113 authorizationUrl . searchParams . set (
107114 "scope" ,
108- this . clientMetadata . scopes . join ( " " )
115+ this . clientMetadata . scopes . join ( " " ) ,
109116 ) ;
110117 }
111118
@@ -126,19 +133,19 @@ export class OAuthClient {
126133
127134 if ( ! username ) {
128135 throw new Error (
129- "No username provided for password limited authentication flow."
136+ "No username provided for password limited authentication flow." ,
130137 ) ;
131138 }
132139
133140 if ( ! password ) {
134141 throw new Error (
135- "No password provided for password limited authentication flow."
142+ "No password provided for password limited authentication flow." ,
136143 ) ;
137144 }
138145
139146 if ( ! clientSecret ) {
140147 throw new Error (
141- "Client secret not provided; password limited authorization is not allowed."
148+ "Client secret not provided; password limited authorization is not allowed." ,
142149 ) ;
143150 }
144151
@@ -170,7 +177,7 @@ export class OAuthClient {
170177 const result = await oauth . processAuthorizationCodeResponse (
171178 this . authorizationServer ,
172179 this . authorizationClient ,
173- response
180+ response ,
174181 ) ;
175182
176183 const token = await IRacingOAuthTokenResponseSchema . parseAsync ( result ) ;
@@ -191,7 +198,7 @@ export class OAuthClient {
191198 async callback ( params : URLSearchParams , sessionId ?: string ) {
192199 if ( ! this . clientMetadata . redirectUri ) {
193200 throw new Error (
194- "Client is not configured for the authorization code flow; missing `redirectUri`."
201+ "Client is not configured for the authorization code flow; missing `redirectUri`." ,
195202 ) ;
196203 }
197204
@@ -209,15 +216,15 @@ export class OAuthClient {
209216 } else {
210217 throw new OAuthCallbackError (
211218 params ,
212- `Unknown authorization session "${ stateParam } "`
219+ `Unknown authorization session "${ stateParam } "` ,
213220 ) ;
214221 }
215222
216223 if ( ! codeParam ) {
217224 throw new OAuthCallbackError (
218225 params ,
219226 'Missing "code" query parameter.' ,
220- stateData . appState
227+ stateData . appState ,
221228 ) ;
222229 }
223230
@@ -227,15 +234,15 @@ export class OAuthClient {
227234 this . authorizationServer ,
228235 this . authorizationClient ,
229236 params ,
230- stateParam
237+ stateParam ,
231238 ) ;
232239 } catch ( error ) {
233240 if ( error instanceof oauth . AuthorizationResponseError ) {
234241 throw new OAuthCallbackError (
235242 params ,
236243 "OAuth Provider returned an error" ,
237244 stateParam ,
238- error . cause
245+ error . cause ,
239246 ) ;
240247 }
241248
@@ -248,13 +255,13 @@ export class OAuthClient {
248255 this . clientAuthorization ,
249256 codeGrantParams ,
250257 this . clientMetadata . redirectUri ,
251- stateData . verifier !
258+ stateData . verifier ! ,
252259 ) ;
253260
254261 const result = await oauth . processAuthorizationCodeResponse (
255262 this . authorizationServer ,
256263 this . authorizationClient ,
257- response
264+ response ,
258265 ) ;
259266
260267 const token = await IRacingOAuthTokenResponseSchema . parseAsync ( result ) ;
@@ -266,7 +273,7 @@ export class OAuthClient {
266273 const profileResponse = await oauth . protectedResourceRequest (
267274 token . access_token ,
268275 "GET" ,
269- new URL ( "/iracing/profile" , this . clientMetadata . issuer )
276+ new URL ( "/iracing/profile" , this . clientMetadata . issuer ) ,
270277 ) ;
271278
272279 const profileData =
@@ -288,14 +295,14 @@ export class OAuthClient {
288295 this . authorizationServer ,
289296 this . authorizationClient ,
290297 this . clientAuthorization ,
291- token
298+ token ,
292299 ) ;
293300
294301 try {
295302 const result = await oauth . processRefreshTokenResponse (
296303 this . authorizationServer ,
297304 this . authorizationClient ,
298- response
305+ response ,
299306 ) ;
300307
301308 return await IRacingOAuthTokenResponseSchema . parseAsync ( result ) ;
@@ -326,7 +333,7 @@ export class OAuthClient {
326333 path : string ,
327334 headers ?: Headers ,
328335 body ?: oauth . ProtectedResourceRequestBody ,
329- options ?: oauth . ProtectedResourceRequestOptions
336+ options ?: oauth . ProtectedResourceRequestOptions ,
330337 ) {
331338 const session = await this . restoreSession ( sessionId ) ;
332339 if ( session ) {
@@ -336,11 +343,11 @@ export class OAuthClient {
336343 path ,
337344 headers ,
338345 body ,
339- options
346+ options ,
340347 ) ;
341348 } else {
342349 throw new Error (
343- `Could not find session matching ${ sessionId } . Did you forget to authenticate?`
350+ `Could not find session matching ${ sessionId } . Did you forget to authenticate?` ,
344351 ) ;
345352 }
346353 }
@@ -365,15 +372,15 @@ export class OAuthClient {
365372 path : string ,
366373 headers ?: Headers ,
367374 body ?: oauth . ProtectedResourceRequestBody ,
368- options ?: oauth . ProtectedResourceRequestOptions
375+ options ?: oauth . ProtectedResourceRequestOptions ,
369376 ) {
370377 return await oauth . protectedResourceRequest (
371378 session . access_token ,
372379 method ,
373380 new URL ( path , "https://members-ng.iracing.com" ) ,
374381 headers ,
375382 body ,
376- options
383+ options ,
377384 ) ;
378385 }
379386
@@ -398,7 +405,7 @@ export class OAuthClient {
398405
399406 private async storeSession (
400407 sessionId : string ,
401- session : IRacingOAuthTokenResponse
408+ session : IRacingOAuthTokenResponse ,
402409 ) {
403410 await this . sessionStore . set ( sessionId , session ) ;
404411 }
@@ -413,7 +420,7 @@ export class OAuthClient {
413420 undefined ,
414421 {
415422 sessionId,
416- }
423+ } ,
417424 ) ;
418425 }
419426
@@ -424,7 +431,15 @@ export class OAuthClient {
424431 "MISSING_REFRESH_TOKEN" ,
425432 {
426433 sessionId,
427- }
434+ } ,
435+ ) ;
436+ }
437+
438+ if ( isRefreshTokenExpired ( session . refresh_token ) ) {
439+ throw new OAuthRefreshError (
440+ "Refresh token cannot be refreshed" ,
441+ "Token expired" ,
442+ "REFRESH_TOKEN_EXPIRED" ,
428443 ) ;
429444 }
430445
@@ -437,6 +452,35 @@ export class OAuthClient {
437452
438453 return refreshed ;
439454 }
455+
456+ /**
457+ * Decodes an access token into its header and payload structure.
458+ *
459+ * This validates the token shape, but does not verify the token signature.
460+ */
461+ parseAccessToken ( accessToken : string ) {
462+ return decodeAccessToken ( accessToken ) ;
463+ }
464+
465+ /**
466+ * Verifies an access token signature and then performs structural and claims validation.
467+ *
468+ * This checks signature validity, expiration, issuer, audience, scope, and claim consistency.
469+ */
470+ async validateAccessToken (
471+ accessToken : string ,
472+ options : Omit <
473+ AccessTokenValidationOptions ,
474+ "issuer" | "clientId" | "requiredScopes"
475+ > = { } ,
476+ ) {
477+ return await validateDecodedAccessToken ( accessToken , {
478+ ...options ,
479+ issuer : this . clientMetadata . issuer ,
480+ clientId : this . clientMetadata . clientId ,
481+ requiredScopes : this . clientMetadata . scopes ,
482+ } ) ;
483+ }
440484}
441485
442486export type { IRacingOAuthTokenResponse } ;
0 commit comments