oauth4webapi 2.7.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,6 +15,7 @@ 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
 
@@ -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.7.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
  /**
@@ -1512,4 +1516,56 @@ export interface GenerateKeyPairOptions {
1512
1516
  * @group Utilities
1513
1517
  */
1514
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>;
1515
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.7.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;
@@ -1759,3 +1769,147 @@ export async function generateKeyPair(alg, options) {
1759
1769
  }
1760
1770
  return (crypto.subtle.generateKey(algorithm, options?.extractable ?? false, ['sign', 'verify']));
1761
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.7.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",
@@ -71,7 +73,7 @@
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
79
  "oidc-provider": "^8.4.5",
@@ -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.12",
91
+ "typedoc-plugin-mdn-links": "^3.1.13",
90
92
  "typescript": "^5.3.3",
91
93
  "undici": "^5.28.2"
92
94
  }