@cloudflare/workers-oauth-provider 0.9.0 → 0.10.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 +21 -5
- package/dist/oauth-provider.d.ts +24 -1
- package/dist/oauth-provider.js +51 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,12 @@ See [Client registration](#client-registration) for the matching provider option
|
|
|
36
36
|
The provider accepts either plain `ExportedHandler` objects or classes extending `WorkerEntrypoint`. This example uses both.
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
AuthorizationError,
|
|
41
|
+
OAuthProvider,
|
|
42
|
+
type AuthRequest,
|
|
43
|
+
type OAuthHelpers,
|
|
44
|
+
} from '@cloudflare/workers-oauth-provider';
|
|
40
45
|
import { WorkerEntrypoint } from 'cloudflare:workers';
|
|
41
46
|
|
|
42
47
|
interface AuthProps {
|
|
@@ -72,9 +77,18 @@ const defaultHandler: ExportedHandler<Env> = {
|
|
|
72
77
|
let oauthRequest: AuthRequest;
|
|
73
78
|
try {
|
|
74
79
|
oauthRequest = await env.OAUTH_PROVIDER.parseAuthRequest(request);
|
|
75
|
-
} catch {
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!(error instanceof AuthorizationError)) throw error;
|
|
82
|
+
if (!error.redirectUri) {
|
|
83
|
+
// Unknown clients and invalid redirects must be rendered locally.
|
|
84
|
+
return new Response(error.description, { status: 400 });
|
|
85
|
+
}
|
|
86
|
+
const redirect = new URL(error.redirectUri);
|
|
87
|
+
redirect.searchParams.set('error', error.code);
|
|
88
|
+
redirect.searchParams.set('error_description', error.description);
|
|
89
|
+
if (error.state) redirect.searchParams.set('state', error.state);
|
|
90
|
+
if (error.issuer) redirect.searchParams.set('iss', error.issuer);
|
|
91
|
+
return Response.redirect(redirect, 302);
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
const client = await env.OAUTH_PROVIDER.lookupClient(oauthRequest.clientId);
|
|
@@ -228,7 +242,9 @@ A typical flow has three steps:
|
|
|
228
242
|
2. Authenticate the user, show consent, and decide which scopes to grant.
|
|
229
243
|
3. Call `completeAuthorization()` and redirect to its returned `redirectTo` URL.
|
|
230
244
|
|
|
231
|
-
`
|
|
245
|
+
`parseAuthRequest()` throws an exported `AuthorizationError` for expected request validation failures. Its optional `redirectUri` is present only after the client and exact registered redirect URI have been validated. Without it, render the error locally and never redirect. With it, the application can safely construct an OAuth error redirect using the error's `code`, `description`, original `state`, and RFC 9207 `issuer`, as shown in the quick start.
|
|
246
|
+
|
|
247
|
+
`completeAuthorization()` repeats response-type validation before writing a grant or revoking existing grants. Validation errors from reconstructed requests are also typed as `AuthorizationError`, but applications should not construct redirects from untrusted reconstructed values; the redirect context is attached only by `parseAuthRequest()`.
|
|
232
248
|
|
|
233
249
|
`completeAuthorization()` stores a new grant and, by default, revokes existing grants for the same user and client after the new grant is safely stored. Set `revokeExistingGrants: false` only when the application intentionally allows concurrent grants for the same user and client.
|
|
234
250
|
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -299,6 +299,29 @@ interface EmaOptions<Env = Cloudflare.Env> {
|
|
|
299
299
|
}
|
|
300
300
|
//#endregion
|
|
301
301
|
//#region src/oauth-capabilities.d.ts
|
|
302
|
+
type AuthorizationErrorCode = 'invalid_request' | 'unauthorized_client' | 'access_denied' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable';
|
|
303
|
+
interface AuthorizationErrorOptions {
|
|
304
|
+
/** Wire-safe OAuth authorization error description. */
|
|
305
|
+
description: string;
|
|
306
|
+
/** Exact registered redirect URI. Present only after client and redirect validation. */
|
|
307
|
+
redirectUri?: string;
|
|
308
|
+
/** Original client state, when supplied. */
|
|
309
|
+
state?: string;
|
|
310
|
+
/** Authorization server issuer for RFC 9207 error responses. */
|
|
311
|
+
issuer?: string;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Expected authorization-request validation failure. Absence of `redirectUri`
|
|
315
|
+
* means a caller MUST render locally and MUST NOT redirect.
|
|
316
|
+
*/
|
|
317
|
+
declare class AuthorizationError extends Error {
|
|
318
|
+
readonly code: AuthorizationErrorCode;
|
|
319
|
+
readonly description: string;
|
|
320
|
+
readonly redirectUri?: string;
|
|
321
|
+
readonly state?: string;
|
|
322
|
+
readonly issuer?: string;
|
|
323
|
+
constructor(code: AuthorizationErrorCode, options: AuthorizationErrorOptions);
|
|
324
|
+
}
|
|
302
325
|
declare function isValidOAuthScopeToken(scopeToken: string): boolean;
|
|
303
326
|
//#endregion
|
|
304
327
|
//#region src/oauth-provider.d.ts
|
|
@@ -1511,4 +1534,4 @@ declare function getJwtCryptoAlgorithms(alg: string): {
|
|
|
1511
1534
|
verifyAlgorithm: Parameters<SubtleCrypto['verify']>[0];
|
|
1512
1535
|
};
|
|
1513
1536
|
//#endregion
|
|
1514
|
-
export { AuthRequest, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, ExternalTokenError, ExternalTokenErrorOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthError, OAuthErrorOptions, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|
|
1537
|
+
export { AuthRequest, AuthorizationError, type AuthorizationErrorCode, type AuthorizationErrorOptions, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, ExternalTokenError, ExternalTokenErrorOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthError, OAuthErrorOptions, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|
package/dist/oauth-provider.js
CHANGED
|
@@ -2,6 +2,30 @@ import { WorkerEntrypoint } from "cloudflare:workers";
|
|
|
2
2
|
|
|
3
3
|
//#region src/oauth-capabilities.ts
|
|
4
4
|
const OAUTH_SCOPE_TOKEN_PATTERN = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
5
|
+
/**
|
|
6
|
+
* Expected authorization-request validation failure. Absence of `redirectUri`
|
|
7
|
+
* means a caller MUST render locally and MUST NOT redirect.
|
|
8
|
+
*/
|
|
9
|
+
var AuthorizationError = class extends Error {
|
|
10
|
+
constructor(code, options) {
|
|
11
|
+
super(options.description);
|
|
12
|
+
this.name = "AuthorizationError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.description = options.description;
|
|
15
|
+
this.redirectUri = options.redirectUri;
|
|
16
|
+
this.state = options.state;
|
|
17
|
+
this.issuer = options.issuer;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/** @internal Attach context after exact client redirect validation succeeds. */
|
|
21
|
+
function withAuthorizationRedirect(error, redirectUri, state, issuer) {
|
|
22
|
+
return new AuthorizationError(error.code, {
|
|
23
|
+
description: error.description,
|
|
24
|
+
redirectUri,
|
|
25
|
+
state,
|
|
26
|
+
issuer
|
|
27
|
+
});
|
|
28
|
+
}
|
|
5
29
|
function buildOAuthServerCapabilities(options) {
|
|
6
30
|
return {
|
|
7
31
|
grantTypes: [
|
|
@@ -30,20 +54,20 @@ function validateClientCapabilities(server, client) {
|
|
|
30
54
|
if (client.grantTypes.includes("implicit") !== client.responseTypes.includes("token")) throw new Error("grant_types implicit and response_types token must be registered together");
|
|
31
55
|
}
|
|
32
56
|
function validateAuthorizationResponseType(server, responseType, clientResponseTypes) {
|
|
33
|
-
if (!responseType) throw new
|
|
34
|
-
if (!server.responseTypes.includes(responseType)) throw new
|
|
35
|
-
if (!(clientResponseTypes ?? ["code"]).includes(responseType)) throw new
|
|
57
|
+
if (!responseType) throw new AuthorizationError("invalid_request", { description: "response_type is required" });
|
|
58
|
+
if (!server.responseTypes.includes(responseType)) throw new AuthorizationError("unsupported_response_type", { description: `The authorization server does not support response_type ${responseType}` });
|
|
59
|
+
if (!(clientResponseTypes ?? ["code"]).includes(responseType)) throw new AuthorizationError("unauthorized_client", { description: `The client is not registered for response_type ${responseType}` });
|
|
36
60
|
}
|
|
37
61
|
/** Parse a PKCE method, applying RFC 7636's default of `plain`. */
|
|
38
62
|
function normalizePkceCodeChallengeMethod(method) {
|
|
39
63
|
const effectiveMethod = method ?? "plain";
|
|
40
|
-
if (effectiveMethod !== "plain" && effectiveMethod !== "S256") throw new
|
|
64
|
+
if (effectiveMethod !== "plain" && effectiveMethod !== "S256") throw new AuthorizationError("invalid_request", { description: `Unsupported PKCE code_challenge_method: ${effectiveMethod}` });
|
|
41
65
|
return effectiveMethod;
|
|
42
66
|
}
|
|
43
67
|
/** Require a syntactically valid PKCE method that the server advertises. */
|
|
44
68
|
function validatePkceCodeChallengeMethod(server, method) {
|
|
45
69
|
const effectiveMethod = normalizePkceCodeChallengeMethod(method);
|
|
46
|
-
if (!server.codeChallengeMethods.includes(effectiveMethod)) throw new
|
|
70
|
+
if (!server.codeChallengeMethods.includes(effectiveMethod)) throw new AuthorizationError("invalid_request", { description: "The plain PKCE method is not allowed. Use S256 instead." });
|
|
47
71
|
return effectiveMethod;
|
|
48
72
|
}
|
|
49
73
|
/** Validate authorization-request PKCE against server and client capabilities. */
|
|
@@ -52,8 +76,8 @@ function validateAuthorizationPkce(server, request, client) {
|
|
|
52
76
|
validatePkceCodeChallengeMethod(server, request.codeChallengeMethod);
|
|
53
77
|
return;
|
|
54
78
|
}
|
|
55
|
-
if (request.codeChallengeMethod) throw new
|
|
56
|
-
if (request.responseType === "code" && client.tokenEndpointAuthMethod === "none") throw new
|
|
79
|
+
if (request.codeChallengeMethod) throw new AuthorizationError("invalid_request", { description: "PKCE code_challenge is required when code_challenge_method is provided." });
|
|
80
|
+
if (request.responseType === "code" && client.tokenEndpointAuthMethod === "none") throw new AuthorizationError("invalid_request", { description: "Public clients must use PKCE with the authorization code flow." });
|
|
57
81
|
}
|
|
58
82
|
function validateAuthorizationServerScopes(scopes) {
|
|
59
83
|
if (!scopes) return;
|
|
@@ -2137,7 +2161,6 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2137
2161
|
grant_types: clientInfo.grantTypes,
|
|
2138
2162
|
response_types: clientInfo.responseTypes,
|
|
2139
2163
|
token_endpoint_auth_method: clientInfo.tokenEndpointAuthMethod,
|
|
2140
|
-
registration_client_uri: `${this.options.clientRegistrationEndpoint}/${clientId}`,
|
|
2141
2164
|
client_id_issued_at: clientInfo.registrationDate
|
|
2142
2165
|
};
|
|
2143
2166
|
if (clientInfo.i18n) {
|
|
@@ -3208,7 +3231,7 @@ var OAuthHelpersImpl = class {
|
|
|
3208
3231
|
* Parses an OAuth authorization request from the HTTP request
|
|
3209
3232
|
* @param request - The HTTP request containing OAuth parameters
|
|
3210
3233
|
* @returns The parsed authorization request parameters
|
|
3211
|
-
* @throws
|
|
3234
|
+
* @throws AuthorizationError for expected authorization-request validation failures
|
|
3212
3235
|
* @throws CimdFetchError when the client ID is a CIMD URL whose document cannot be resolved
|
|
3213
3236
|
*/
|
|
3214
3237
|
async parseAuthRequest(request) {
|
|
@@ -3223,22 +3246,33 @@ var OAuthHelpersImpl = class {
|
|
|
3223
3246
|
const issuer = this.provider.getAuthorizationServerIssuer(url);
|
|
3224
3247
|
const resourceParams = url.searchParams.getAll("resource");
|
|
3225
3248
|
const resourceParam = resourceParams.length > 0 ? resourceParams.length === 1 ? resourceParams[0] : resourceParams : void 0;
|
|
3226
|
-
|
|
3249
|
+
if (!clientId) throw new AuthorizationError("invalid_request", { description: "client_id is required" });
|
|
3250
|
+
const clientInfo = await this.lookupClient(clientId);
|
|
3251
|
+
if (!clientInfo) throw new AuthorizationError("invalid_request", { description: "Invalid client_id" });
|
|
3252
|
+
try {
|
|
3253
|
+
validateRedirectUriScheme(redirectUri);
|
|
3254
|
+
} catch {
|
|
3255
|
+
throw new AuthorizationError("invalid_request", { description: "Invalid redirect URI" });
|
|
3256
|
+
}
|
|
3257
|
+
if (!redirectUri || !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) throw new AuthorizationError("invalid_request", { description: "Invalid redirect URI" });
|
|
3258
|
+
const withRedirect = (error) => {
|
|
3259
|
+
throw withAuthorizationRedirect(error, redirectUri, state || void 0, issuer);
|
|
3260
|
+
};
|
|
3227
3261
|
let resource = parseResourceParameter(resourceParam);
|
|
3228
|
-
if (resourceParam && !resource)
|
|
3262
|
+
if (resourceParam && !resource) withRedirect(new AuthorizationError("invalid_request", { description: "The resource parameter must be a valid absolute URI without a fragment" }));
|
|
3229
3263
|
const configuredResource = this.provider.options.resourceMetadata?.resource;
|
|
3230
|
-
if (configuredResource && !isExactResource(resource, configuredResource))
|
|
3264
|
+
if (configuredResource && !isExactResource(resource, configuredResource)) withRedirect(new AuthorizationError("invalid_request", { description: `The resource parameter must exactly match ${configuredResource}` }));
|
|
3231
3265
|
resource ??= url.origin;
|
|
3232
|
-
|
|
3233
|
-
const clientInfo = await this.lookupClient(clientId);
|
|
3234
|
-
if (!clientInfo) throw new Error(`Invalid client. The clientId provided does not match to this client.`);
|
|
3235
|
-
if (!redirectUri || !isValidRedirectUri(redirectUri, clientInfo.redirectUris)) throw new Error(`Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.`);
|
|
3266
|
+
try {
|
|
3236
3267
|
validateAuthorizationResponseType(this.provider.serverCapabilities, responseType, clientInfo.responseTypes);
|
|
3237
3268
|
validateAuthorizationPkce(this.provider.serverCapabilities, {
|
|
3238
3269
|
responseType,
|
|
3239
3270
|
codeChallenge,
|
|
3240
3271
|
codeChallengeMethod
|
|
3241
3272
|
}, clientInfo);
|
|
3273
|
+
} catch (error) {
|
|
3274
|
+
if (error instanceof AuthorizationError) withRedirect(error);
|
|
3275
|
+
throw error;
|
|
3242
3276
|
}
|
|
3243
3277
|
return {
|
|
3244
3278
|
responseType,
|
|
@@ -3671,4 +3705,4 @@ var OAuthHelpersImpl = class {
|
|
|
3671
3705
|
var oauth_provider_default = OAuthProvider;
|
|
3672
3706
|
|
|
3673
3707
|
//#endregion
|
|
3674
|
-
export { CimdFetchError, ExternalTokenError, GrantType, OAuthError, OAuthProvider, base64UrlToBytes, oauth_provider_default as default, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|
|
3708
|
+
export { AuthorizationError, CimdFetchError, ExternalTokenError, GrantType, OAuthError, OAuthProvider, base64UrlToBytes, oauth_provider_default as default, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|