oauth4webapi 2.7.0 → 2.8.1
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 +2 -1
- package/build/index.d.ts +58 -2
- package/build/index.js +170 -10
- package/package.json +7 -5
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.
|
|
47
|
+
import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.8.1/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
|
|
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?:
|
|
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.
|
|
4
|
+
const VERSION = 'v2.8.1';
|
|
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 (
|
|
348
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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
|
-
|
|
1033
|
+
jti: 'jwt id',
|
|
1028
1034
|
nonce: 'nonce',
|
|
1029
1035
|
s_hash: 'state hash',
|
|
1030
|
-
|
|
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}" (${
|
|
1045
|
+
throw new OPE(`JWT "${claim}" (${jwtClaimNames[claim]}) claim missing`);
|
|
1036
1046
|
}
|
|
1037
1047
|
}
|
|
1038
1048
|
return result;
|
|
@@ -1759,3 +1769,153 @@ 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 clockSkew = getClockSkew(options);
|
|
1789
|
+
const proof = await validateJwt(request.headers.get('dpop'), checkSigningAlgorithm.bind(undefined, undefined, as?.dpop_signing_alg_values_supported || SUPPORTED_JWS_ALGS), async ({ jwk, alg }) => {
|
|
1790
|
+
if (!jwk) {
|
|
1791
|
+
throw new OPE('DPoP Proof is missing the jwk header parameter');
|
|
1792
|
+
}
|
|
1793
|
+
const key = await importJwk(alg, jwk);
|
|
1794
|
+
if (key.type !== 'public') {
|
|
1795
|
+
throw new OPE('DPoP Proof jwk header parameter must contain a public key');
|
|
1796
|
+
}
|
|
1797
|
+
return key;
|
|
1798
|
+
}, clockSkew, getClockTolerance(options))
|
|
1799
|
+
.then(checkJwtType.bind(undefined, 'dpop+jwt'))
|
|
1800
|
+
.then(validatePresence.bind(undefined, ['iat', 'jti', 'ath', 'htm', 'htu']));
|
|
1801
|
+
const now = epochTime() + clockSkew;
|
|
1802
|
+
const diff = Math.abs(now - proof.claims.iat);
|
|
1803
|
+
if (diff > 300) {
|
|
1804
|
+
throw new OPE('DPoP Proof iat is not recent enough');
|
|
1805
|
+
}
|
|
1806
|
+
if (proof.claims.htm !== request.method) {
|
|
1807
|
+
throw new OPE('DPoP Proof htm mismatch');
|
|
1808
|
+
}
|
|
1809
|
+
if (typeof proof.claims.htu !== 'string' ||
|
|
1810
|
+
normalizeHtu(proof.claims.htu) !== normalizeHtu(request.url)) {
|
|
1811
|
+
throw new OPE('DPoP Proof htu mismatch');
|
|
1812
|
+
}
|
|
1813
|
+
{
|
|
1814
|
+
const accessToken = request.headers.get('authorization').split(' ')[1];
|
|
1815
|
+
const expected = b64u(await crypto.subtle.digest('SHA-256', encoder.encode(accessToken)));
|
|
1816
|
+
if (proof.claims.ath !== expected) {
|
|
1817
|
+
throw new OPE('DPoP Proof ath mismatch');
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
{
|
|
1821
|
+
let components;
|
|
1822
|
+
switch (proof.header.jwk.kty) {
|
|
1823
|
+
case 'EC':
|
|
1824
|
+
components = {
|
|
1825
|
+
crv: proof.header.jwk.crv,
|
|
1826
|
+
kty: proof.header.jwk.kty,
|
|
1827
|
+
x: proof.header.jwk.x,
|
|
1828
|
+
y: proof.header.jwk.y,
|
|
1829
|
+
};
|
|
1830
|
+
break;
|
|
1831
|
+
case 'OKP':
|
|
1832
|
+
components = {
|
|
1833
|
+
crv: proof.header.jwk.crv,
|
|
1834
|
+
kty: proof.header.jwk.kty,
|
|
1835
|
+
x: proof.header.jwk.x,
|
|
1836
|
+
};
|
|
1837
|
+
break;
|
|
1838
|
+
case 'RSA':
|
|
1839
|
+
components = {
|
|
1840
|
+
e: proof.header.jwk.e,
|
|
1841
|
+
kty: proof.header.jwk.kty,
|
|
1842
|
+
n: proof.header.jwk.n,
|
|
1843
|
+
};
|
|
1844
|
+
break;
|
|
1845
|
+
default:
|
|
1846
|
+
throw new UnsupportedOperationError();
|
|
1847
|
+
}
|
|
1848
|
+
const expected = b64u(await crypto.subtle.digest('SHA-256', encoder.encode(JSON.stringify(components))));
|
|
1849
|
+
if (accessTokenClaims.cnf.jkt !== expected) {
|
|
1850
|
+
throw new OPE('JWT Access Token confirmation mismatch');
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
export async function experimental_validateJwtAccessToken(as, request, expectedAudience, options) {
|
|
1855
|
+
assertAs(as);
|
|
1856
|
+
if (!looseInstanceOf(request, Request)) {
|
|
1857
|
+
throw new TypeError('"request" must be an instance of Request');
|
|
1858
|
+
}
|
|
1859
|
+
if (!validateString(expectedAudience)) {
|
|
1860
|
+
throw new OPE('"expectedAudience" must be a non-empty string');
|
|
1861
|
+
}
|
|
1862
|
+
const authorization = request.headers.get('authorization');
|
|
1863
|
+
if (!authorization) {
|
|
1864
|
+
throw new OPE('"request" is missing an Authorization HTTP Header');
|
|
1865
|
+
}
|
|
1866
|
+
let { 0: scheme, 1: accessToken, length } = authorization.split(' ');
|
|
1867
|
+
scheme = scheme.toLowerCase();
|
|
1868
|
+
switch (scheme) {
|
|
1869
|
+
case 'dpop':
|
|
1870
|
+
case 'bearer':
|
|
1871
|
+
break;
|
|
1872
|
+
default:
|
|
1873
|
+
throw new UnsupportedOperationError('unsupported Authorization HTTP Header scheme');
|
|
1874
|
+
}
|
|
1875
|
+
if (length !== 2) {
|
|
1876
|
+
throw new OPE('invalid Authorization HTTP Header format');
|
|
1877
|
+
}
|
|
1878
|
+
const requiredClaims = [
|
|
1879
|
+
'iss',
|
|
1880
|
+
'exp',
|
|
1881
|
+
'aud',
|
|
1882
|
+
'sub',
|
|
1883
|
+
'iat',
|
|
1884
|
+
'jti',
|
|
1885
|
+
'client_id',
|
|
1886
|
+
];
|
|
1887
|
+
if (options?.requireDPoP || scheme === 'dpop' || request.headers.has('dpop')) {
|
|
1888
|
+
requiredClaims.push('cnf');
|
|
1889
|
+
}
|
|
1890
|
+
const { claims } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined, undefined, SUPPORTED_JWS_ALGS), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(options), getClockTolerance(options))
|
|
1891
|
+
.then(checkJwtType.bind(undefined, 'at+jwt'))
|
|
1892
|
+
.then(validatePresence.bind(undefined, requiredClaims))
|
|
1893
|
+
.then(validateIssuer.bind(undefined, as.issuer))
|
|
1894
|
+
.then(validateAudience.bind(undefined, expectedAudience));
|
|
1895
|
+
for (const claim of ['client_id', 'jti', 'sub']) {
|
|
1896
|
+
if (typeof claims[claim] !== 'string') {
|
|
1897
|
+
throw new OPE(`unexpected JWT "${claim}" claim type`);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if ('cnf' in claims) {
|
|
1901
|
+
if (!isJsonObject(claims.cnf)) {
|
|
1902
|
+
throw new OPE('unexpected JWT "cnf" (confirmation) claim value');
|
|
1903
|
+
}
|
|
1904
|
+
const { 0: cnf, length } = Object.keys(claims.cnf);
|
|
1905
|
+
if (length) {
|
|
1906
|
+
if (length !== 1) {
|
|
1907
|
+
throw new UnsupportedOperationError('multiple confirmation claims are not supported');
|
|
1908
|
+
}
|
|
1909
|
+
if (cnf !== 'jkt') {
|
|
1910
|
+
throw new UnsupportedOperationError('unsupported JWT Confirmation method');
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (options?.requireDPoP ||
|
|
1915
|
+
scheme === 'dpop' ||
|
|
1916
|
+
claims.cnf?.jkt !== undefined ||
|
|
1917
|
+
request.headers.has('dpop')) {
|
|
1918
|
+
await validateDPoP(as, request, claims, options);
|
|
1919
|
+
}
|
|
1920
|
+
return claims;
|
|
1921
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oauth4webapi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
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,14 +67,14 @@
|
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@koa/cors": "^5.0.0",
|
|
67
69
|
"@types/koa__cors": "^5.0.0",
|
|
68
|
-
"@types/node": "^20.11.
|
|
70
|
+
"@types/node": "^20.11.6",
|
|
69
71
|
"@types/oidc-provider": "^8.4.3",
|
|
70
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.
|
|
75
|
-
"esbuild": "^0.19.
|
|
76
|
+
"edge-runtime": "^2.5.8",
|
|
77
|
+
"esbuild": "^0.19.12",
|
|
76
78
|
"jose": "^5.2.0",
|
|
77
79
|
"oidc-provider": "^8.4.5",
|
|
78
80
|
"patch-package": "^8.0.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.
|
|
91
|
+
"typedoc-plugin-mdn-links": "^3.1.13",
|
|
90
92
|
"typescript": "^5.3.3",
|
|
91
93
|
"undici": "^5.28.2"
|
|
92
94
|
}
|