@udibo/oauth2 0.12.1 → 0.13.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
@@ -5,15 +5,15 @@ Udibo's identity service, or host an authorization server for your own app. The
5
5
  package includes clients, authorization and resource servers, Hono middleware,
6
6
  React bindings, and optional login flows over your own database.
7
7
 
8
- **Udibo's managed identity service is in private beta.** See
9
- [udibo.com](https://www.udibo.com/) for access. The self-hosted examples run
10
- locally without a hosted account.
8
+ Using Udibo's managed identity service requires a Udibo account; see
9
+ [udibo.com](https://www.udibo.com/) for how to get one. The self-hosted examples
10
+ run locally without a hosted account.
11
11
 
12
12
  ## Choose your path
13
13
 
14
14
  | Your application needs | Start here |
15
15
  | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
16
- | Udibo to handle sign-in | [Use Udibo's identity service](docs/guides/use-udibo.md) — private-beta access required |
16
+ | Udibo to handle sign-in | [Use Udibo's identity service](docs/guides/use-udibo.md) — requires a Udibo account |
17
17
  | Its own login and authorization server | [Run the quickstart](docs/quickstart.md), then [host authorization for your app](docs/guides/become-an-oauth-provider.md) |
18
18
  | An API that accepts access tokens | [Protect an API](docs/guides/protect-an-api.md) |
19
19
  | A React application with server-held tokens | [Juniper starter](templates/juniper/README.md) or [React Router starter](templates/react-router/README.md) |
@@ -21,6 +21,16 @@ export type ClientCredentialsGrantOptions<Client extends ClientInterface, User,
21
21
  * Used for machine-to-machine authentication where the client itself
22
22
  * is the resource owner. Does not support refresh tokens.
23
23
  *
24
+ * **Only confidential clients may use it.** RFC 6749 §4.4 restricts this grant
25
+ * to confidential clients, so a request that presents no client secret — a
26
+ * public client, or a confidential client leaving its secret out — is refused
27
+ * with 401 `invalid_client`. Register a machine client with a secret and send
28
+ * that secret with HTTP Basic or as `client_secret` in the body. The grant
29
+ * relies on `ClientServiceInterface.getAuthenticated` refusing a public client
30
+ * that presents a secret, which `runClientServiceContractTests` pins. A client
31
+ * service written before 0.9.2 must pass `runClientServiceContractTests`, or
32
+ * `client_credentials` is not limited to confidential clients.
33
+ *
24
34
  * **The token need not carry a user.** RFC 6749 §4.4 has no resource owner, so
25
35
  * a `ClientServiceInterface.getUser` that resolves nothing is the conformant
26
36
  * case, not an error: the grant issues a token whose {@linkcode Token.user} is
@@ -37,6 +47,20 @@ export declare class ClientCredentialsGrant<Client extends ClientInterface, User
37
47
  readonly issuesIdToken = false;
38
48
  /** Creates the grant from its {@linkcode ClientCredentialsGrantOptions}. */
39
49
  constructor(options: ClientCredentialsGrantOptions<Client, User, S>);
50
+ /**
51
+ * Authenticates the client from a request that must carry its client
52
+ * secret, by HTTP Basic or in the body. Credentials are read through
53
+ * {@linkcode getClientCredentials} and checked by the grant's
54
+ * `clientService.getAuthenticated`.
55
+ *
56
+ * A subclass that authenticates clients by other means, such as a signed
57
+ * client assertion, overrides this method and must itself admit only
58
+ * confidential clients.
59
+ *
60
+ * @throws {InvalidClientError} If the request presents no non-empty client
61
+ * secret, or authentication fails.
62
+ */
63
+ getAuthenticatedClient(request: Request, body: FormData): Promise<Client>;
40
64
  /**
41
65
  * Handles a token request for the client credentials grant. Issues an access
42
66
  * token (never a refresh token) to the client acting as its own resource
@@ -3,6 +3,8 @@
3
3
  * RFC 6749 Section 4.4.
4
4
  * @module
5
5
  */
6
+ import { InvalidClientError } from "../../errors.js";
7
+ import { authenticateClientCredentials } from "../client-authentication.js";
6
8
  import { AbstractGrant, } from "./grant.js";
7
9
  /**
8
10
  * The client credentials grant type.
@@ -11,6 +13,16 @@ import { AbstractGrant, } from "./grant.js";
11
13
  * Used for machine-to-machine authentication where the client itself
12
14
  * is the resource owner. Does not support refresh tokens.
13
15
  *
16
+ * **Only confidential clients may use it.** RFC 6749 §4.4 restricts this grant
17
+ * to confidential clients, so a request that presents no client secret — a
18
+ * public client, or a confidential client leaving its secret out — is refused
19
+ * with 401 `invalid_client`. Register a machine client with a secret and send
20
+ * that secret with HTTP Basic or as `client_secret` in the body. The grant
21
+ * relies on `ClientServiceInterface.getAuthenticated` refusing a public client
22
+ * that presents a secret, which `runClientServiceContractTests` pins. A client
23
+ * service written before 0.9.2 must pass `runClientServiceContractTests`, or
24
+ * `client_credentials` is not limited to confidential clients.
25
+ *
14
26
  * **The token need not carry a user.** RFC 6749 §4.4 has no resource owner, so
15
27
  * a `ClientServiceInterface.getUser` that resolves nothing is the conformant
16
28
  * case, not an error: the grant issues a token whose {@linkcode Token.user} is
@@ -29,6 +41,27 @@ export class ClientCredentialsGrant extends AbstractGrant {
29
41
  constructor(options) {
30
42
  super({ ...options, allowRefreshToken: false });
31
43
  }
44
+ /**
45
+ * Authenticates the client from a request that must carry its client
46
+ * secret, by HTTP Basic or in the body. Credentials are read through
47
+ * {@linkcode getClientCredentials} and checked by the grant's
48
+ * `clientService.getAuthenticated`.
49
+ *
50
+ * A subclass that authenticates clients by other means, such as a signed
51
+ * client assertion, overrides this method and must itself admit only
52
+ * confidential clients.
53
+ *
54
+ * @throws {InvalidClientError} If the request presents no non-empty client
55
+ * secret, or authentication fails.
56
+ */
57
+ async getAuthenticatedClient(request, body) {
58
+ const credentials = this.getClientCredentials(request, body);
59
+ if (!credentials.clientSecret) {
60
+ throw new InvalidClientError("client authentication failed");
61
+ }
62
+ const { clientService } = await this.resolveServices(request);
63
+ return await authenticateClientCredentials(credentials, clientService);
64
+ }
32
65
  /**
33
66
  * Handles a token request for the client credentials grant. Issues an access
34
67
  * token (never a refresh token) to the client acting as its own resource
@@ -13,7 +13,9 @@ import "../_dnt.polyfills.js";
13
13
  * that some browsers resolve as a host, and any value containing a C0 control
14
14
  * character or `DEL` — browsers strip TAB/CR/LF before parsing a URL, so
15
15
  * `/<TAB>/evil.com` would otherwise navigate to `evil.com`. Anything else
16
- * collapses to `fallback` (default `/`).
16
+ * collapses to `fallback` (default `/`), which is returned verbatim. The
17
+ * fallback is trusted caller configuration; do not pass user-controlled input
18
+ * there.
17
19
  *
18
20
  * Percent-encoded control characters (`/%09/x`) are safe and pass through: a
19
21
  * browser does not decode them before parsing, so they stay on the origin.
@@ -34,17 +36,17 @@ import "../_dnt.polyfills.js";
34
36
  * ```
35
37
  */
36
38
  export declare function safeReturnTo(value: string | null | undefined, fallback?: string): string;
37
- /** Options for {@link loginContinuation}. */
39
+ /** Trusted application configuration for {@link loginContinuation}. */
38
40
  export interface LoginContinuationOptions {
39
41
  /**
40
- * The authorization endpoint (absolute URL or path). When `returnTo` points at
41
- * it, the flow is **resumed** (the in-flight authorize request already carries
42
+ * Trusted authorization endpoint (absolute URL or path). When `returnTo` points
43
+ * at it, the flow is **resumed** (the in-flight authorize request already carries
42
44
  * its `state`). Omit it and every `returnTo` starts a fresh login.
43
45
  */
44
46
  authorizeEndpoint?: string;
45
- /** The login path to start a fresh login. Defaults to `"/auth/login"`. */
47
+ /** Trusted login path for a fresh login. Defaults to `"/auth/login"`. */
46
48
  loginPath?: string;
47
- /** Fallback when `returnTo` is missing/unsafe. Defaults to `"/"`. */
49
+ /** Trusted fallback for missing/unsafe `returnTo`. Defaults to `"/"`. */
48
50
  defaultReturnTo?: string;
49
51
  }
50
52
  /**
@@ -61,6 +63,7 @@ export interface LoginContinuationOptions {
61
63
  *
62
64
  * Pure and client-safe — the same logic behind `HonoBff.loginContinuation`
63
65
  * (server) and `BffClient.loginContinuation` (browser). `returnTo` is
64
- * open-redirect-guarded via {@link safeReturnTo}.
66
+ * open-redirect-guarded via {@link safeReturnTo}. The fallback, authorization
67
+ * endpoint and login path are trusted application configuration.
65
68
  */
66
69
  export declare function loginContinuation(returnTo: string | null | undefined, options?: LoginContinuationOptions): string;
package/esm/utils/url.js CHANGED
@@ -22,7 +22,9 @@ function hasControlCharacter(value) {
22
22
  * that some browsers resolve as a host, and any value containing a C0 control
23
23
  * character or `DEL` — browsers strip TAB/CR/LF before parsing a URL, so
24
24
  * `/<TAB>/evil.com` would otherwise navigate to `evil.com`. Anything else
25
- * collapses to `fallback` (default `/`).
25
+ * collapses to `fallback` (default `/`), which is returned verbatim. The
26
+ * fallback is trusted caller configuration; do not pass user-controlled input
27
+ * there.
26
28
  *
27
29
  * Percent-encoded control characters (`/%09/x`) are safe and pass through: a
28
30
  * browser does not decode them before parsing, so they stay on the origin.
@@ -74,7 +76,8 @@ export function safeReturnTo(value, fallback = "/") {
74
76
  *
75
77
  * Pure and client-safe — the same logic behind `HonoBff.loginContinuation`
76
78
  * (server) and `BffClient.loginContinuation` (browser). `returnTo` is
77
- * open-redirect-guarded via {@link safeReturnTo}.
79
+ * open-redirect-guarded via {@link safeReturnTo}. The fallback, authorization
80
+ * endpoint and login path are trusted application configuration.
78
81
  */
79
82
  export function loginContinuation(returnTo, options = {}) {
80
83
  const safe = safeReturnTo(returnTo, options.defaultReturnTo ?? "/");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udibo/oauth2",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "A flexible OAuth2 toolkit: framework-agnostic authorization & resource servers, a typed client, Hono adapters (including a backend-for-frontend), and React bindings.",
5
5
  "repository": {
6
6
  "type": "git",