oauth4webapi 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,12 +15,13 @@ The following features are currently in scope and implemented in this software:
15
15
  - UserInfo and Protected Resource Requests
16
16
  - Authorization Server Issuer Identification
17
17
  - JWT Secured Introspection, Response Mode (JARM), Authorization Request (JAR), and UserInfo
18
+ - Validating incoming JWT Access Tokens
18
19
 
19
20
  ## [Certification](https://openid.net/certification/faq/)
20
21
 
21
22
  [<img width="96" height="50" align="right" src="https://user-images.githubusercontent.com/241506/166977513-7cd710a9-7f60-4944-aebe-a658e9f36375.png" alt="OpenID Certification">](#certification)
22
23
 
23
- [Filip Skokan](https://github.com/panva) has certified that [this software](https://github.com/panva/oauth4webapi) conforms to the Basic RP Conformance Profile of the OpenID Connect™ protocol.
24
+ [Filip Skokan](https://github.com/panva) has certified that [this software](https://github.com/panva/oauth4webapi) conforms to the Basic, FAPI 1.0 Advanced, FAPI 2.0 Security Profile, and FAPI 2.0 Message Signing Relying Party Conformance Profiles of the OpenID Connect™ protocol.
24
25
 
25
26
  ## [💗 Help the project](https://github.com/sponsors/panva)
26
27
 
@@ -43,7 +44,7 @@ import * as oauth2 from 'oauth4webapi'
43
44
  **`example`** Deno import
44
45
 
45
46
  ```js
46
- import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.6.0/mod.ts'
47
+ import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.8.0/mod.ts'
47
48
  ```
48
49
 
49
50
  - Authorization Code Flow - OpenID Connect [source](examples/code.ts), or plain OAuth 2 [source](examples/oauth.ts)
package/build/index.d.ts CHANGED
@@ -1109,6 +1109,7 @@ interface JWTPayload {
1109
1109
  readonly nbf?: number;
1110
1110
  readonly exp?: number;
1111
1111
  readonly iat?: number;
1112
+ readonly cnf?: ConfirmationClaims;
1112
1113
  readonly [claim: string]: JsonValue | undefined;
1113
1114
  }
1114
1115
  export interface IDToken extends JWTPayload {
@@ -1120,6 +1121,7 @@ export interface IDToken extends JWTPayload {
1120
1121
  readonly nonce?: string;
1121
1122
  readonly auth_time?: number;
1122
1123
  readonly azp?: string;
1124
+ readonly [claim: string]: JsonValue | undefined;
1123
1125
  }
1124
1126
  export interface TokenEndpointResponse {
1125
1127
  readonly access_token: string;
@@ -1304,11 +1306,13 @@ export interface IntrospectionRequestOptions extends HttpRequestOptions, Authent
1304
1306
  * @see [draft-ietf-oauth-jwt-introspection-response-12 - JWT Response for OAuth Token Introspection](https://www.ietf.org/archive/id/draft-ietf-oauth-jwt-introspection-response-12.html#section-4)
1305
1307
  */
1306
1308
  export declare function introspectionRequest(as: AuthorizationServer, client: Client, token: string, options?: IntrospectionRequestOptions): Promise<Response>;
1307
- export interface IntrospectionConfirmationClaims {
1309
+ export interface ConfirmationClaims {
1308
1310
  readonly 'x5t#S256'?: string;
1309
1311
  readonly jkt?: string;
1310
1312
  readonly [claim: string]: JsonValue | undefined;
1311
1313
  }
1314
+ /** @ignore */
1315
+ export type IntrospectionConfirmationClaims = ConfirmationClaims;
1312
1316
  export interface IntrospectionResponse {
1313
1317
  readonly active: boolean;
1314
1318
  readonly client_id?: string;
@@ -1323,7 +1327,7 @@ export interface IntrospectionResponse {
1323
1327
  readonly sub?: string;
1324
1328
  readonly nbf?: number;
1325
1329
  readonly token_type?: string;
1326
- readonly cnf?: IntrospectionConfirmationClaims;
1330
+ readonly cnf?: ConfirmationClaims;
1327
1331
  readonly [claim: string]: JsonValue | undefined;
1328
1332
  }
1329
1333
  /**
@@ -1370,7 +1374,8 @@ export declare function validateJwtAuthResponse(as: AuthorizationServer, client:
1370
1374
  *
1371
1375
  * @param as Authorization Server Metadata.
1372
1376
  * @param client Client Metadata.
1373
- * @param parameters Authorization Response.
1377
+ * @param parameters Authorization Response parameters as URLSearchParams or an instance of URL with
1378
+ * parameters in a fragment/hash.
1374
1379
  * @param expectedNonce Expected ID Token `nonce` claim value.
1375
1380
  * @param expectedState Expected `state` parameter value. Default is {@link expectNoState}.
1376
1381
  * @param maxAge ID Token {@link IDToken.auth_time `auth_time`} claim value will be checked to be
@@ -1386,7 +1391,7 @@ export declare function validateJwtAuthResponse(as: AuthorizationServer, client:
1386
1391
  *
1387
1392
  * @see [Financial-grade API Security Profile 1.0 - Part 2: Advanced](https://openid.net/specs/openid-financial-api-part-2-1_0.html#id-token-as-detached-signature)
1388
1393
  */
1389
- export declare function experimental_validateDetachedSignatureResponse(as: AuthorizationServer, client: Client, parameters: URLSearchParams, expectedNonce: string, expectedState?: string | typeof expectNoState, maxAge?: number | typeof skipAuthTimeCheck, options?: HttpRequestOptions): Promise<URLSearchParams | OAuth2Error>;
1394
+ export declare function experimental_validateDetachedSignatureResponse(as: AuthorizationServer, client: Client, parameters: URLSearchParams | URL, expectedNonce: string, expectedState?: string | typeof expectNoState, maxAge?: number | typeof skipAuthTimeCheck, options?: HttpRequestOptions): Promise<URLSearchParams | OAuth2Error>;
1390
1395
  /**
1391
1396
  * DANGER ZONE
1392
1397
  *
@@ -1511,4 +1516,56 @@ export interface GenerateKeyPairOptions {
1511
1516
  * @group Utilities
1512
1517
  */
1513
1518
  export declare function generateKeyPair(alg: JWSAlgorithm, options?: GenerateKeyPairOptions): Promise<CryptoKeyPair>;
1519
+ export interface JWTAccessTokenClaims extends JWTPayload {
1520
+ readonly iss: string;
1521
+ readonly exp: number;
1522
+ readonly aud: string | string[];
1523
+ readonly sub: string;
1524
+ readonly iat: number;
1525
+ readonly jti: string;
1526
+ readonly client_id: string;
1527
+ readonly [claim: string]: JsonValue | undefined;
1528
+ }
1529
+ export interface ValidateJWTAccessTokenOptions extends HttpRequestOptions {
1530
+ /** Indicates whether DPoP use is required. */
1531
+ requireDPoP?: boolean;
1532
+ /** Same functionality as in {@link Client} */
1533
+ [clockSkew]?: number;
1534
+ /** Same functionality as in {@link Client} */
1535
+ [clockTolerance]?: number;
1536
+ }
1537
+ /**
1538
+ * This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
1539
+ * compatible changes or removal may occur in any future release.
1540
+ *
1541
+ * Validates use of JSON Web Token (JWT) OAuth 2.0 Access Tokens for a given {@link Request} as per
1542
+ * RFC 9068 and optionally also RFC 9449.
1543
+ *
1544
+ * This does validate the presence and type of all required claims as well as the values of the
1545
+ * {@link JWTAccessTokenClaims.iss `iss`}, {@link JWTAccessTokenClaims.exp `exp`},
1546
+ * {@link JWTAccessTokenClaims.aud `aud`} claims.
1547
+ *
1548
+ * This does NOT validate the {@link JWTAccessTokenClaims.sub `sub`},
1549
+ * {@link JWTAccessTokenClaims.jti `jti`}, and {@link JWTAccessTokenClaims.client_id `client_id`}
1550
+ * claims beyond just checking that they're present and that their type is a string. If you need to
1551
+ * validate these values further you would do so after this function's execution.
1552
+ *
1553
+ * This does NOT validate the DPoP Proof JWT nonce. If your server indicates RS-provided nonces to
1554
+ * clients you would check these after this function's execution.
1555
+ *
1556
+ * This does NOT validate authorization claims such as `scope` either, you would do so after this
1557
+ * function's execution.
1558
+ *
1559
+ * @param as Authorization Server to accept JWT Access Tokens from.
1560
+ * @param request
1561
+ * @param expectedAudience Audience identifier the resource server expects for itself.
1562
+ * @param options
1563
+ *
1564
+ * @group JWT Access Tokens
1565
+ * @group Experimental
1566
+ *
1567
+ * @see [RFC 9068 - JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://www.rfc-editor.org/rfc/rfc9068.html)
1568
+ * @see [RFC 9449 - OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)](https://www.rfc-editor.org/rfc/rfc9449.html)
1569
+ */
1570
+ export declare function experimental_validateJwtAccessToken(as: AuthorizationServer, request: Request, expectedAudience: string, options?: ValidateJWTAccessTokenOptions): Promise<JWTAccessTokenClaims>;
1514
1571
  export {};
package/build/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  let USER_AGENT;
2
2
  if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
3
3
  const NAME = 'oauth4webapi';
4
- const VERSION = 'v2.6.0';
4
+ const VERSION = 'v2.8.0';
5
5
  USER_AGENT = `${NAME}/${VERSION}`;
6
6
  }
7
7
  function looseInstanceOf(input, expected) {
@@ -344,15 +344,19 @@ function keyToJws(key) {
344
344
  }
345
345
  }
346
346
  function getClockSkew(client) {
347
- if (Number.isFinite(client[clockSkew])) {
348
- return client[clockSkew];
347
+ if (client && clockSkew in client) {
348
+ if (Number.isFinite(client[clockSkew])) {
349
+ return client[clockSkew];
350
+ }
349
351
  }
350
352
  return 0;
351
353
  }
352
354
  function getClockTolerance(client) {
353
- const tolerance = client[clockTolerance];
354
- if (Number.isFinite(tolerance) && Math.sign(tolerance) !== -1) {
355
- return tolerance;
355
+ if (client && clockTolerance in client) {
356
+ const tolerance = client[clockTolerance];
357
+ if (Number.isFinite(tolerance) && Math.sign(tolerance) !== -1) {
358
+ return tolerance;
359
+ }
356
360
  }
357
361
  return 30;
358
362
  }
@@ -1019,20 +1023,26 @@ export async function authorizationCodeGrantRequest(as, client, callbackParamete
1019
1023
  parameters.set('code', code);
1020
1024
  return tokenEndpointRequest(as, client, 'authorization_code', parameters, options);
1021
1025
  }
1022
- const idTokenClaimNames = {
1026
+ const jwtClaimNames = {
1023
1027
  aud: 'audience',
1028
+ c_hash: 'code hash',
1029
+ client_id: 'client id',
1024
1030
  exp: 'expiration time',
1025
1031
  iat: 'issued at',
1026
1032
  iss: 'issuer',
1027
- sub: 'subject',
1033
+ jti: 'jwt id',
1028
1034
  nonce: 'nonce',
1029
1035
  s_hash: 'state hash',
1030
- c_hash: 'code hash',
1036
+ sub: 'subject',
1037
+ ath: 'access token hash',
1038
+ htm: 'http method',
1039
+ htu: 'http uri',
1040
+ cnf: 'confirmation',
1031
1041
  };
1032
1042
  function validatePresence(required, result) {
1033
1043
  for (const claim of required) {
1034
1044
  if (result.claims[claim] === undefined) {
1035
- throw new OPE(`JWT "${claim}" (${idTokenClaimNames[claim]}) claim missing`);
1045
+ throw new OPE(`JWT "${claim}" (${jwtClaimNames[claim]}) claim missing`);
1036
1046
  }
1037
1047
  }
1038
1048
  return result;
@@ -1465,6 +1475,12 @@ async function idTokenHashMatches(data, actual, alg, key) {
1465
1475
  export async function experimental_validateDetachedSignatureResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options) {
1466
1476
  assertAs(as);
1467
1477
  assertClient(client);
1478
+ if (parameters instanceof URL) {
1479
+ if (!parameters.hash.length) {
1480
+ throw new TypeError('"parameters" as an instance of URL must contain a hash (fragment) with the Authorization Response parameters');
1481
+ }
1482
+ parameters = new URLSearchParams(parameters.hash.slice(1));
1483
+ }
1468
1484
  if (!(parameters instanceof URLSearchParams)) {
1469
1485
  throw new TypeError('"parameters" must be an instance of URLSearchParams');
1470
1486
  }
@@ -1753,3 +1769,147 @@ export async function generateKeyPair(alg, options) {
1753
1769
  }
1754
1770
  return (crypto.subtle.generateKey(algorithm, options?.extractable ?? false, ['sign', 'verify']));
1755
1771
  }
1772
+ function normalizeHtu(htu) {
1773
+ const url = new URL(htu);
1774
+ url.search = '';
1775
+ url.hash = '';
1776
+ return url.href;
1777
+ }
1778
+ async function validateDPoP(as, request, accessTokenClaims, options) {
1779
+ if (!request.headers.has('dpop')) {
1780
+ throw new OPE('operation indicated DPoP use but the request has no DPoP HTTP Header');
1781
+ }
1782
+ if (request.headers.get('authorization')?.toLowerCase().startsWith('dpop ') === false) {
1783
+ throw new OPE(`operation indicated DPoP use but the request's Authorization HTTP Header scheme is not DPoP`);
1784
+ }
1785
+ if (typeof accessTokenClaims.cnf?.jkt !== 'string') {
1786
+ throw new OPE('operation indicated DPoP use but the JWT Access Token has no jkt confirmation claim');
1787
+ }
1788
+ const proof = await validateJwt(request.headers.get('dpop'), checkSigningAlgorithm.bind(undefined, undefined, as?.dpop_signing_alg_values_supported || SUPPORTED_JWS_ALGS), async ({ jwk, alg }) => {
1789
+ if (!jwk) {
1790
+ throw new OPE('DPoP Proof is missing the jwk header parameter');
1791
+ }
1792
+ const key = await importJwk(alg, jwk);
1793
+ if (key.type !== 'public') {
1794
+ throw new OPE('DPoP Proof jwk header parameter must contain a public key');
1795
+ }
1796
+ return key;
1797
+ }, getClockSkew(options), getClockTolerance(options))
1798
+ .then(checkJwtType.bind(undefined, 'dpop+jwt'))
1799
+ .then(validatePresence.bind(undefined, ['iat', 'jti', 'ath', 'htm', 'htu']));
1800
+ if (proof.claims.htm !== request.method) {
1801
+ throw new OPE('DPoP Proof htm mismatch');
1802
+ }
1803
+ if (typeof proof.claims.htu !== 'string' ||
1804
+ normalizeHtu(proof.claims.htu) !== normalizeHtu(request.url)) {
1805
+ throw new OPE('DPoP Proof htu mismatch');
1806
+ }
1807
+ {
1808
+ const accessToken = request.headers.get('authorization').split(' ')[1];
1809
+ const expected = b64u(await crypto.subtle.digest('SHA-256', encoder.encode(accessToken)));
1810
+ if (proof.claims.ath !== expected) {
1811
+ throw new OPE('DPoP Proof ath mismatch');
1812
+ }
1813
+ }
1814
+ {
1815
+ let components;
1816
+ switch (proof.header.jwk.kty) {
1817
+ case 'EC':
1818
+ components = {
1819
+ crv: proof.header.jwk.crv,
1820
+ kty: proof.header.jwk.kty,
1821
+ x: proof.header.jwk.x,
1822
+ y: proof.header.jwk.y,
1823
+ };
1824
+ break;
1825
+ case 'OKP':
1826
+ components = {
1827
+ crv: proof.header.jwk.crv,
1828
+ kty: proof.header.jwk.kty,
1829
+ x: proof.header.jwk.x,
1830
+ };
1831
+ break;
1832
+ case 'RSA':
1833
+ components = {
1834
+ e: proof.header.jwk.e,
1835
+ kty: proof.header.jwk.kty,
1836
+ n: proof.header.jwk.n,
1837
+ };
1838
+ break;
1839
+ default:
1840
+ throw new UnsupportedOperationError();
1841
+ }
1842
+ const expected = b64u(await crypto.subtle.digest('SHA-256', encoder.encode(JSON.stringify(components))));
1843
+ if (accessTokenClaims.cnf.jkt !== expected) {
1844
+ throw new OPE('JWT Access Token confirmation mismatch');
1845
+ }
1846
+ }
1847
+ }
1848
+ export async function experimental_validateJwtAccessToken(as, request, expectedAudience, options) {
1849
+ assertAs(as);
1850
+ if (!looseInstanceOf(request, Request)) {
1851
+ throw new TypeError('"request" must be an instance of Request');
1852
+ }
1853
+ if (!validateString(expectedAudience)) {
1854
+ throw new OPE('"expectedAudience" must be a non-empty string');
1855
+ }
1856
+ const authorization = request.headers.get('authorization');
1857
+ if (!authorization) {
1858
+ throw new OPE('"request" is missing an Authorization HTTP Header');
1859
+ }
1860
+ let { 0: scheme, 1: accessToken, length } = authorization.split(' ');
1861
+ scheme = scheme.toLowerCase();
1862
+ switch (scheme) {
1863
+ case 'dpop':
1864
+ case 'bearer':
1865
+ break;
1866
+ default:
1867
+ throw new UnsupportedOperationError('unsupported Authorization HTTP Header scheme');
1868
+ }
1869
+ if (length !== 2) {
1870
+ throw new OPE('invalid Authorization HTTP Header format');
1871
+ }
1872
+ const requiredClaims = [
1873
+ 'iss',
1874
+ 'exp',
1875
+ 'aud',
1876
+ 'sub',
1877
+ 'iat',
1878
+ 'jti',
1879
+ 'client_id',
1880
+ ];
1881
+ if (options?.requireDPoP || scheme === 'dpop' || request.headers.has('dpop')) {
1882
+ requiredClaims.push('cnf');
1883
+ }
1884
+ const { claims } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined, undefined, SUPPORTED_JWS_ALGS), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(options), getClockTolerance(options))
1885
+ .then(checkJwtType.bind(undefined, 'at+jwt'))
1886
+ .then(validatePresence.bind(undefined, requiredClaims))
1887
+ .then(validateIssuer.bind(undefined, as.issuer))
1888
+ .then(validateAudience.bind(undefined, expectedAudience));
1889
+ for (const claim of ['client_id', 'jti', 'sub']) {
1890
+ if (typeof claims[claim] !== 'string') {
1891
+ throw new OPE(`unexpected JWT "${claim}" claim type`);
1892
+ }
1893
+ }
1894
+ if ('cnf' in claims) {
1895
+ if (!isJsonObject(claims.cnf)) {
1896
+ throw new OPE('unexpected JWT "cnf" (confirmation) claim value');
1897
+ }
1898
+ const { 0: cnf, length } = Object.keys(claims.cnf);
1899
+ if (length) {
1900
+ if (length !== 1) {
1901
+ throw new UnsupportedOperationError('multiple confirmation claims are not supported');
1902
+ }
1903
+ if (cnf !== 'jkt') {
1904
+ throw new UnsupportedOperationError('unsupported JWT Confirmation method');
1905
+ }
1906
+ }
1907
+ }
1908
+ if (options?.requireDPoP ||
1909
+ scheme === 'dpop' ||
1910
+ claims.cnf?.jkt !== undefined ||
1911
+ request.headers.has('dpop')) {
1912
+ await validateDPoP(as, request, claims, options);
1913
+ }
1914
+ return claims;
1915
+ }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "oauth4webapi",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "OAuth 2 / OpenID Connect for JavaScript Runtimes",
5
5
  "keywords": [
6
+ "access token",
6
7
  "auth",
7
8
  "authentication",
8
9
  "authorization",
@@ -17,6 +18,7 @@
17
18
  "electron",
18
19
  "fapi",
19
20
  "javascript",
21
+ "jwt",
20
22
  "netlify",
21
23
  "next",
22
24
  "nextjs",
@@ -65,18 +67,18 @@
65
67
  "devDependencies": {
66
68
  "@koa/cors": "^5.0.0",
67
69
  "@types/koa__cors": "^5.0.0",
68
- "@types/node": "^20.10.8",
70
+ "@types/node": "^20.11.5",
69
71
  "@types/oidc-provider": "^8.4.3",
70
- "@types/qunit": "^2.19.9",
72
+ "@types/qunit": "^2.19.10",
71
73
  "archiver": "^6.0.1",
72
74
  "ava": "^5.3.1",
73
75
  "chrome-launcher": "^1.1.0",
74
- "edge-runtime": "^2.5.7",
76
+ "edge-runtime": "^2.5.8",
75
77
  "esbuild": "^0.19.11",
76
78
  "jose": "^5.2.0",
77
- "oidc-provider": "^8.4.4",
79
+ "oidc-provider": "^8.4.5",
78
80
  "patch-package": "^8.0.0",
79
- "prettier": "^3.1.1",
81
+ "prettier": "^3.2.4",
80
82
  "prettier-plugin-jsdoc": "^1.3.0",
81
83
  "puppeteer-core": "^21.7.0",
82
84
  "qunit": "^2.20.0",
@@ -86,7 +88,7 @@
86
88
  "tsx": "^4.7.0",
87
89
  "typedoc": "^0.25.7",
88
90
  "typedoc-plugin-markdown": "^3.17.1",
89
- "typedoc-plugin-mdn-links": "^3.1.11",
91
+ "typedoc-plugin-mdn-links": "^3.1.13",
90
92
  "typescript": "^5.3.3",
91
93
  "undici": "^5.28.2"
92
94
  }