@udibo/oauth2 0.9.1 → 0.9.2
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/esm/server/authorization-server.d.ts +9 -1
- package/esm/server/authorization-server.js +3 -2
- package/esm/server/grants/authorization-code.d.ts +7 -4
- package/esm/server/services/client.d.ts +7 -3
- package/esm/testing/contract/client.js +11 -0
- package/esm/testing/services.d.ts +4 -2
- package/esm/testing/services.js +10 -8
- package/package.json +1 -1
|
@@ -126,6 +126,11 @@ export interface AuthorizationServerContext<Client extends ClientInterface, User
|
|
|
126
126
|
services: AuthorizationServerServices<Client, User, S>;
|
|
127
127
|
/** The issuer identifier URL for this request. */
|
|
128
128
|
issuer?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Scopes advertised for this request in discovery metadata. Overrides the
|
|
131
|
+
* server-wide default, including when set to an empty array.
|
|
132
|
+
*/
|
|
133
|
+
scopesSupported?: string[];
|
|
129
134
|
/** URL of the authorization endpoint. Defaults to `${issuer}/authorize`. */
|
|
130
135
|
authorizationEndpoint?: string;
|
|
131
136
|
/** URL of the token endpoint. Defaults to `${issuer}/token`. */
|
|
@@ -164,7 +169,10 @@ export interface AuthorizationServerOptions<Client extends ClientInterface, User
|
|
|
164
169
|
resolve: (request: Request) => AuthorizationServerContext<Client, User, S> | Promise<AuthorizationServerContext<Client, User, S>>;
|
|
165
170
|
/** Grants the server supports, keyed by `grant_type` (each key must equal `grant.grantType`). */
|
|
166
171
|
grants: AuthorizationServerGrants<Client, User, S>;
|
|
167
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Default scopes advertised in RFC 8414 metadata when the request context
|
|
174
|
+
* does not supply scopesSupported.
|
|
175
|
+
*/
|
|
168
176
|
scopesSupported?: string[];
|
|
169
177
|
/**
|
|
170
178
|
* OIDC issuance keys. Configuring this turns the OIDC provider surface on:
|
|
@@ -1099,8 +1099,9 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
1099
1099
|
metadata.code_challenge_methods_supported = challengeMethods;
|
|
1100
1100
|
}
|
|
1101
1101
|
metadata.response_types_supported = hasAuthCodeGrant ? ["code"] : [];
|
|
1102
|
-
|
|
1103
|
-
|
|
1102
|
+
const scopesSupported = context.scopesSupported ?? this.scopesSupported;
|
|
1103
|
+
if (scopesSupported) {
|
|
1104
|
+
metadata.scopes_supported = scopesSupported;
|
|
1104
1105
|
}
|
|
1105
1106
|
if (this.signingKeys) {
|
|
1106
1107
|
this.#advertiseEndpoints(metadata, endpoints, true);
|
|
@@ -50,10 +50,13 @@ export interface AuthorizationCodeGrantOptions<Client extends ClientInterface, U
|
|
|
50
50
|
* authentication, never a replacement for it, so a confidential client's
|
|
51
51
|
* secret is still required on this grant.
|
|
52
52
|
*
|
|
53
|
-
* Defaults to `true`: confidential clients authenticate alongside PKCE
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* Defaults to `true`: confidential clients authenticate alongside PKCE, and
|
|
54
|
+
* a public client authenticates by `client_id` alone — one that attaches a
|
|
55
|
+
* `client_secret` it was never issued is refused, as on every other grant.
|
|
56
|
+
* Setting `false` explicitly enables legacy behavior where PKCE replaces
|
|
57
|
+
* client credentials: a request carrying a `code_verifier` is looked up by
|
|
58
|
+
* id, so a presented secret is not checked at all. Use that opt-out only for
|
|
59
|
+
* an integration that requires it.
|
|
57
60
|
*
|
|
58
61
|
* @default true
|
|
59
62
|
* @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-4.1.3
|
|
@@ -17,13 +17,17 @@ export interface ClientServiceInterface<Client extends ClientInterface, User> {
|
|
|
17
17
|
* always advertises `none` in `token_endpoint_auth_methods_supported`, so
|
|
18
18
|
* an implementation that demands a secret from every client makes that
|
|
19
19
|
* advertisement false.
|
|
20
|
+
* - A **public** client must resolve `undefined` when a non-empty `secret` is
|
|
21
|
+
* presented. It was issued no secret, so a request carrying one is
|
|
22
|
+
* misconfigured or confused about the client's type, and the server answers
|
|
23
|
+
* `invalid_client` rather than silently treating it as public. An empty
|
|
24
|
+
* `secret` carries no credential — a Basic header of `client_id:` decodes
|
|
25
|
+
* to one — and counts as presenting none, so the client resolves.
|
|
20
26
|
* - A **confidential** client resolves only with its correct `secret`. With
|
|
21
27
|
* the secret missing or wrong, resolve `undefined`.
|
|
22
28
|
*
|
|
23
29
|
* `runClientServiceContractTests` from `@udibo/oauth2/testing/contract` pins
|
|
24
|
-
*
|
|
25
|
-
* public client that presents a `secret` resolves, so do not rely on either
|
|
26
|
-
* answer.
|
|
30
|
+
* all three rules; run it against your implementation.
|
|
27
31
|
*
|
|
28
32
|
* @param id The presented `client_id` — unauthenticated input.
|
|
29
33
|
* @param secret The presented `client_secret`, omitted when the request
|
|
@@ -82,6 +82,17 @@ export function runClientServiceContractTests(options) {
|
|
|
82
82
|
const auth = await clientService.getAuthenticated(client.id);
|
|
83
83
|
assertEquals(auth?.id, client.id);
|
|
84
84
|
});
|
|
85
|
+
it("returns undefined for a public client that presents a secret", async () => {
|
|
86
|
+
const client = makeClient(9);
|
|
87
|
+
await options.addClient(clientService, client);
|
|
88
|
+
assertStrictEquals(await clientService.getAuthenticated(client.id, "unissued"), undefined);
|
|
89
|
+
});
|
|
90
|
+
it("returns the public client when the presented secret is empty", async () => {
|
|
91
|
+
const client = makeClient(10);
|
|
92
|
+
await options.addClient(clientService, client);
|
|
93
|
+
const auth = await clientService.getAuthenticated(client.id, "");
|
|
94
|
+
assertEquals(auth?.id, client.id);
|
|
95
|
+
});
|
|
85
96
|
it("returns undefined for a confidential client when no secret is supplied", async () => {
|
|
86
97
|
const client = makeClient(3);
|
|
87
98
|
await options.addClient(clientService, client, "secret");
|
|
@@ -99,7 +99,7 @@ export declare class MemoryUserService<U extends MemoryUserShape> implements Use
|
|
|
99
99
|
* A client is treated as confidential when {@link add} is called with a
|
|
100
100
|
* secret, and public when it is not — there is no separate flag to
|
|
101
101
|
* remember. `getAuthenticated` requires the secret for confidential
|
|
102
|
-
* clients and
|
|
102
|
+
* clients, and requires public clients to present none.
|
|
103
103
|
*/
|
|
104
104
|
export declare class MemoryClientService<C extends ClientInterface, U> implements ClientServiceInterface<C, U> {
|
|
105
105
|
#private;
|
|
@@ -122,7 +122,9 @@ export declare class MemoryClientService<C extends ClientInterface, U> implement
|
|
|
122
122
|
/**
|
|
123
123
|
* Resolves the client when authentication succeeds, otherwise `undefined`.
|
|
124
124
|
* Confidential clients (registered with a secret) require a matching
|
|
125
|
-
* `secret`; public clients (registered without one)
|
|
125
|
+
* `secret`; public clients (registered without one) must present none, so a
|
|
126
|
+
* secret they were never issued fails authentication rather than being
|
|
127
|
+
* ignored. An empty `secret` counts as none.
|
|
126
128
|
*/
|
|
127
129
|
getAuthenticated(id: string, secret?: string): Promise<C | undefined>;
|
|
128
130
|
/**
|
package/esm/testing/services.js
CHANGED
|
@@ -109,7 +109,7 @@ export class MemoryUserService {
|
|
|
109
109
|
* A client is treated as confidential when {@link add} is called with a
|
|
110
110
|
* secret, and public when it is not — there is no separate flag to
|
|
111
111
|
* remember. `getAuthenticated` requires the secret for confidential
|
|
112
|
-
* clients and
|
|
112
|
+
* clients, and requires public clients to present none.
|
|
113
113
|
*/
|
|
114
114
|
export class MemoryClientService {
|
|
115
115
|
#clientsById = new Map();
|
|
@@ -150,19 +150,21 @@ export class MemoryClientService {
|
|
|
150
150
|
/**
|
|
151
151
|
* Resolves the client when authentication succeeds, otherwise `undefined`.
|
|
152
152
|
* Confidential clients (registered with a secret) require a matching
|
|
153
|
-
* `secret`; public clients (registered without one)
|
|
153
|
+
* `secret`; public clients (registered without one) must present none, so a
|
|
154
|
+
* secret they were never issued fails authentication rather than being
|
|
155
|
+
* ignored. An empty `secret` counts as none.
|
|
154
156
|
*/
|
|
155
157
|
async getAuthenticated(id, secret) {
|
|
156
158
|
const client = this.#clientsById.get(id);
|
|
157
159
|
if (!client)
|
|
158
160
|
return undefined;
|
|
159
161
|
const storedHash = this.#secretHashByClientId.get(id);
|
|
160
|
-
if (storedHash)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
if (!storedHash)
|
|
163
|
+
return secret ? undefined : client;
|
|
164
|
+
if (!secret)
|
|
165
|
+
return undefined;
|
|
166
|
+
if (await sha256Hash(secret) !== storedHash)
|
|
167
|
+
return undefined;
|
|
166
168
|
return client;
|
|
167
169
|
}
|
|
168
170
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udibo/oauth2",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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",
|