oauth_as/registration.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! RFC 7591 dynamic client registration and RFC 7592 registration management.
5//!
6//! # Read this before you turn it on (RFC 7591 section 5)
7//!
8//! Dynamic registration lets a caller CREATE A CLIENT. Section 5 of RFC 7591 says so plainly: an
9//! open registration endpoint is available to anyone on the internet, and the authorization server
10//! "MUST" treat what it is handed as untrusted, because a registration is an attacker-chosen
11//! `client_name` on a consent screen, an attacker-chosen redirect URI, and an identity that every
12//! later threat model quietly assumes is scarce. This crate's own security review made the same
13//! point from the other side: several residual risks here are bounded by "the attacker must
14//! control a registered client", and dynamic registration turns that from an assumption into a
15//! form submission.
16//!
17//! So the shape of this module is decided by that, not by convenience:
18//!
19//! - It is OFF unless [`crate::server::ServerConfig::registration`] is `Some`. There is no
20//! "enabled by default", no environment variable, and no way to reach it by accident. A host
21//! that does not set that field cannot be dynamically registered against, and pays 8 bytes and
22//! no allocation for the privilege.
23//! - Enabling it is not enough. A [`RegistrationPolicy`] must also be installed (see
24//! [`crate::server::AuthorizationServer::with_registration_policy`]), and with none installed
25//! every registration is REFUSED. That is the opposite default to the rate limiter, and the
26//! reasoning is in [`crate::events::Hooks::registration_policy`].
27//! - What a registrant may ask for is bounded by [`RegistrationConfig`]: which grants, which
28//! scopes. The defaults are the narrow ones (no `client_credentials`, no device grant, no
29//! scopes), because the widening should be a sentence the host wrote.
30//! - Every registration, update and deletion is an audit event
31//! ([`crate::events::Event::ClientRegistered`] and friends), carrying no credential.
32//!
33//! # The registration access token (RFC 7592 section 2)
34//!
35//! Management is authenticated by a bearer token minted at registration. It reads, REWRITES and
36//! DELETES a registration, so it is at least as powerful as the client secret it sits next to, and
37//! it is stored exactly the same way: as a one-way [`crate::client::SecretHash`], compared in
38//! constant time, never in plaintext and never with `==`.
39//!
40//! That has one visible consequence, and it is a deliberate deviation worth stating rather than
41//! burying. RFC 7592 section 3 lists `registration_access_token` (and, for a confidential client,
42//! `client_secret`) as members of the client information response, which the read and update
43//! responses of sections 2.1 and 2.2 also use. This server cannot ECHO either one, because it does
44//! not have them: it kept a verifier, not the credential. A credential appears in a response
45//! exactly once, in the response that MINTED it, and after that the client holds the only copy. The
46//! alternative is storing two live bearer credentials in plaintext for the lifetime of every
47//! registration, which is the thing [`crate::client::SecretHash`] exists to stop.
48//!
49//! So a read (section 2.1) returns neither, and an update (section 2.2) returns no
50//! `registration_access_token` — this server never rotates that one, so there is never a new one to
51//! hand back. An update DOES return a `client_secret` in one case, and only that case: when the
52//! updated metadata moves the client from `token_endpoint_auth_method: none` to a method that needs
53//! a secret, [`AuthorizationServer::update_registration`] mints one, because a client that has just
54//! become confidential and was told nothing would be a client that can no longer authenticate at
55//! all. That is a mint, not an echo, and it obeys the same once-only rule as the rest.
56//!
57//! # What is NOT implemented, and why
58//!
59//! - `software_statement` and `software_id` (RFC 7591 sections 2.3 and 3.1.1). A software
60//! statement is a JWT that has to be verified against a trust anchor the HOST owns, and there is
61//! no honest default for "which issuers do you trust to vouch for a client". A request carrying
62//! one is REFUSED with `invalid_software_statement` rather than ignored: RFC 7591 section 2
63//! tells a server to ignore metadata it does not understand, but a software statement is an
64//! assertion the client believes is being HONOURED, and silently dropping it would register a
65//! client on terms nobody agreed to.
66//! - The optional human-readable metadata of RFC 7591 section 2 (`client_uri`, `logo_uri`,
67//! `contacts`, `tos_uri`, `policy_uri`). They are ignored, as section 2 permits, because this
68//! server renders no branded consent screen, so storing them would be storing
69//! attacker-supplied strings for no purpose.
70//! - `jwks` and `jwks_uri` (RFC 7591 section 2), and this one is a GAP rather than a decision.
71//! The crate does RFC 7523 client assertions under the `client-assertion` feature, so a key
72//! registered here would be a key the token endpoint could use; modelling these two members is
73//! most of what it would take to make `private_key_jwt` registrable, and it is not done. Until
74//! it is, a `private_key_jwt` client is one the HOST provisions out of band, and asking to
75//! register one is refused rather than accepted-and-ignored (see the note on the
76//! `AUTH_METHOD_*` constants below).
77
78use serde::{Deserialize, Serialize};
79
80use crate::client::{Client, ClientAuth, ClientId, DynamicRegistration, SecretHash};
81use crate::events::ClientAuthFailure;
82use crate::grant::GrantType;
83use crate::scope::ScopeSet;
84use crate::server::{AuthorizationServer, Clock, ServerConfig};
85use crate::store::{Storage, StorageError};
86
87/// RFC 7591 section 2 client metadata: the registration request body, and the echoed half of the
88/// section 3.2.1 client information response.
89///
90/// Only the members this server actually acts on are modelled. RFC 7591 section 2 requires a
91/// server to IGNORE metadata it does not understand, which is what `serde`'s default handling of
92/// unknown fields does here, so a client that sends `logo_uri` is not refused for it.
93#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
94pub struct ClientMetadata {
95 /// Redirection URIs (RFC 6749 section 3.1.2). REQUIRED for a client registering the
96 /// authorization code grant, since that grant has nowhere to deliver a code without one.
97 #[serde(default, skip_serializing_if = "Vec::is_empty")]
98 pub redirect_uris: Vec<String>,
99 /// How the client will authenticate at the token endpoint. Omitted means
100 /// `client_secret_basic`, which is the default RFC 7591 section 2 states.
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub token_endpoint_auth_method: Option<String>,
103 /// The grants this client will use. OMITTED (`None`) means `["authorization_code"]`
104 /// (section 2), which is why this is an `Option` rather than a `Vec` that is empty when
105 /// absent: section 2 gives omission a meaning, and an explicitly empty list means the opposite
106 /// of that meaning. Collapsing the two would make `{"grant_types": []}` silently register the
107 /// authorization code grant.
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub grant_types: Option<Vec<String>>,
110 /// The authorization response types this client will use. Omitted (`None`) means `["code"]`
111 /// (section 2); see [`ClientMetadata::grant_types`] for why this is an `Option`.
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub response_types: Option<Vec<String>>,
114 /// Human-readable name, shown to a resource owner. ATTACKER-CHOSEN when registration is open:
115 /// it is echoed into this crate's device verification page, which escapes it, and any host
116 /// consent screen must do the same.
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub client_name: Option<String>,
119 /// The space-delimited scope list (RFC 6749 section 3.3) this client may request.
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub scope: Option<String>,
122 /// RFC 7591 section 2.3. NOT evaluated by this server, and its presence is a refusal rather
123 /// than an omission: see the module docs.
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub software_statement: Option<String>,
126}
127
128/// The RFC 7591 section 3.2.1 client information response, which RFC 7592 section 3 reuses for
129/// read and update.
130///
131/// `Debug` is hand-written (below) because two of these fields are live bearer credentials at the
132/// moment this value exists, and this is the value a host is most likely to log: it is what it is
133/// about to serialize.
134#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct ClientInformation {
136 /// REQUIRED (section 3.2.1). The identifier this server minted.
137 pub client_id: String,
138 /// Present only for a confidential registration, and only on the response that MINTED it: see
139 /// the module docs on why a read cannot return it.
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub client_secret: Option<String>,
142 /// Seconds since the Unix epoch (section 3.2.1).
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub client_id_issued_at: Option<u64>,
145 /// REQUIRED when a `client_secret` is issued (section 3.2.1). `0` means it never expires.
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub client_secret_expires_at: Option<u64>,
148 /// RFC 7592 section 3. Present only on the response that minted it.
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub registration_access_token: Option<String>,
151 /// RFC 7592 section 3: where this registration is read, updated and deleted. Absent when the
152 /// host did not enable management.
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub registration_client_uri: Option<String>,
155 /// The registered metadata, echoed as section 3.2.1 requires, INCLUDING any value this server
156 /// substituted for what was asked (section 3.2.1: the response reflects what was registered,
157 /// not what was requested).
158 #[serde(flatten)]
159 pub metadata: ClientMetadata,
160}
161
162/// Hand-written so neither credential reaches a debug format, on the same reasoning as
163/// [`crate::client::ClientAuth`]'s and [`crate::server::TokenRequest`]'s. The Some/None
164/// distinction is kept: "a secret was issued" is registration shape, not a secret.
165impl std::fmt::Debug for ClientInformation {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 fn redact_opt<T>(value: &Option<T>) -> Option<&'static str> {
168 value.as_ref().map(|_| "[redacted]")
169 }
170 f.debug_struct("ClientInformation")
171 .field("client_id", &self.client_id)
172 .field("client_secret", &redact_opt(&self.client_secret))
173 .field("client_id_issued_at", &self.client_id_issued_at)
174 .field("client_secret_expires_at", &self.client_secret_expires_at)
175 .field(
176 "registration_access_token",
177 &redact_opt(&self.registration_access_token),
178 )
179 .field("registration_client_uri", &self.registration_client_uri)
180 .field("metadata", &self.metadata)
181 .finish()
182 }
183}
184
185/// The RFC 7591 section 3.2.2 error codes.
186///
187/// A SEPARATE registry from RFC 6749 section 5.2, and modelled separately for that reason: the two
188/// share no value, they are returned by different endpoints, and collapsing them into
189/// [`crate::error::ErrorCode`] would let a token-endpoint code be emitted here (or the reverse)
190/// with nothing to catch it.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
192#[serde(rename_all = "snake_case")]
193#[non_exhaustive]
194pub enum RegistrationErrorCode {
195 /// One or more `redirect_uris` is invalid: not an absolute URI, or carrying a fragment, or
196 /// absent for a grant that needs one.
197 InvalidRedirectUri,
198 /// Some other submitted metadata value is invalid, or names something this server will not
199 /// register.
200 InvalidClientMetadata,
201 /// The software statement presented is invalid. This server evaluates none, so any is: see
202 /// the module docs.
203 InvalidSoftwareStatement,
204}
205
206impl RegistrationErrorCode {
207 /// The registered wire spelling.
208 pub fn as_str(self) -> &'static str {
209 match self {
210 RegistrationErrorCode::InvalidRedirectUri => "invalid_redirect_uri",
211 RegistrationErrorCode::InvalidClientMetadata => "invalid_client_metadata",
212 RegistrationErrorCode::InvalidSoftwareStatement => "invalid_software_statement",
213 }
214 }
215}
216
217impl std::fmt::Display for RegistrationErrorCode {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.write_str(self.as_str())
220 }
221}
222
223/// The RFC 7591 section 3.2.2 error response body: a 400 with `error` and an optional
224/// `error_description`.
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub struct RegistrationErrorResponse {
227 /// The registered code.
228 pub error: RegistrationErrorCode,
229 /// Human-readable detail for the developer.
230 ///
231 /// `Cow<'static, str>` for the same reason [`crate::error::ErrorResponse`] uses one: every
232 /// refusal `validate` can produce describes a RULE rather than a value, so the description is
233 /// always a string constant, and RFC 7591 section 1.2 makes the initial access token optional,
234 /// which means a host may expose this endpoint to unauthenticated callers who then choose its
235 /// refusal rate. Size neutral: `Option<Cow<'static, str>>` is 24 bytes, exactly what
236 /// `Option<String>` was.
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub error_description: Option<std::borrow::Cow<'static, str>>,
239}
240
241impl RegistrationErrorResponse {
242 /// An error with a description attached.
243 pub fn new(
244 error: RegistrationErrorCode,
245 description: impl Into<std::borrow::Cow<'static, str>>,
246 ) -> Self {
247 RegistrationErrorResponse {
248 error,
249 error_description: Some(description.into()),
250 }
251 }
252}
253
254impl std::fmt::Display for RegistrationErrorResponse {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 match &self.error_description {
257 Some(d) => write!(f, "{}: {d}", self.error),
258 None => f.write_str(self.error.as_str()),
259 }
260 }
261}
262
263/// For the reason [`crate::error::ErrorResponse`] is one: this is the value a host is handed when
264/// a registration is refused, and a host that propagates it with `?` into a `Box<dyn Error>`
265/// should not have to care which of the two sibling refusal bodies it is holding. There is no
266/// `source`: the refusal describes a rule this server applied, not a failure underneath it.
267impl std::error::Error for RegistrationErrorResponse {}
268
269/// Why a registration or management request was refused.
270///
271/// One enum for RFC 7591 and RFC 7592 because the two endpoints share every failure mode they have
272/// in common, and splitting them would make a host match twice on the same four cases.
273#[derive(Debug)]
274#[non_exhaustive]
275pub enum RegistrationFailure {
276 /// This server does not offer the endpoint: the host set no
277 /// [`crate::server::ServerConfig::registration`], or set one with management turned off. A 404
278 /// on the wire, because the honest answer to "is there a registration endpoint here" is no.
279 Disabled,
280 /// The RFC 7591 section 1.2 initial access token, or the RFC 7592 section 2 registration
281 /// access token, was missing, wrong, or refused by the host's [`RegistrationPolicy`]. 401,
282 /// with an RFC 6750 section 3 `Bearer` challenge.
283 ///
284 /// Deliberately ONE answer for all of those, including "no such registration": distinguishing
285 /// them would turn this endpoint into an oracle for which client ids exist, exactly as
286 /// `invalid_client` collapses the same two cases at the token endpoint.
287 Unauthorized,
288 /// The metadata is not acceptable (RFC 7591 section 3.2.2). 400.
289 Invalid(RegistrationErrorResponse),
290 /// The storage seam failed. 500, and the wire learns nothing else.
291 Storage(StorageError),
292}
293
294impl RegistrationFailure {
295 /// The HTTP status this refusal takes.
296 pub fn http_status(&self) -> u16 {
297 match self {
298 RegistrationFailure::Disabled => 404,
299 RegistrationFailure::Unauthorized => 401,
300 RegistrationFailure::Invalid(_) => 400,
301 RegistrationFailure::Storage(_) => 500,
302 }
303 }
304}
305
306impl std::fmt::Display for RegistrationFailure {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 match self {
309 RegistrationFailure::Disabled => f.write_str("dynamic client registration is disabled"),
310 RegistrationFailure::Unauthorized => f.write_str("not authorized to register"),
311 RegistrationFailure::Invalid(e) => write!(f, "{e}"),
312 RegistrationFailure::Storage(e) => write!(f, "{e}"),
313 }
314 }
315}
316
317impl std::error::Error for RegistrationFailure {}
318
319/// The refusal a randomness failure becomes on these two endpoints.
320///
321/// RFC 7591 registration and RFC 7592 management are ORDINARY ROUTES (`POST /register`,
322/// `PUT /register/{id}`), so a `getrandom` that will not answer — an exhausted descriptor table, a
323/// seccomp policy, a container without the syscall — has to become a refusal here exactly as it
324/// does at the token and authorization endpoints. Through 0.9.0 these four draws used the
325/// panicking `random_hex`, on the strength of a comment asserting they were "outside the request
326/// path"; they never were.
327///
328/// [`RegistrationFailure::Storage`] is the crate's 500, and it is the honest answer for the same
329/// reason `crate::server::randomness_error` collapses onto RFC 6749 section 5.2 `server_error`: the
330/// caller must learn only that this server could not fulfil the request and that retrying later is
331/// the right response. The message says which internal condition it was, for the host's own logs.
332fn randomness_failure() -> RegistrationFailure {
333 RegistrationFailure::Storage(StorageError::new(
334 "the OS would not provide randomness for a registration artifact",
335 ))
336}
337
338/// What the host's [`RegistrationPolicy`] is told about one attempt.
339///
340/// Everything borrows: the policy is called inside the request the host is already driving.
341/// `#[non_exhaustive]` because later releases will have more to say about the caller.
342#[non_exhaustive]
343pub struct RegistrationAttempt<'a> {
344 /// The RFC 7591 section 1.2 initial access token the request presented, if any. This crate
345 /// does not interpret it: the host decides what an acceptable one is, because it is the host
346 /// that issued it (or that recognises an allowlisted API key, or a signed invite, or nothing
347 /// at all).
348 pub initial_access_token: Option<&'a str>,
349 /// The metadata being registered, parsed but NOT yet validated, so a policy can refuse on
350 /// content (a `client_name` impersonating the deployment, a redirect URI on a domain the host
351 /// will not serve) before this server ever writes it down.
352 pub metadata: &'a ClientMetadata,
353}
354
355/// Hand-written: the initial access token is a bearer credential, and this struct exists only
356/// inside a request path, which is precisely where a host is most likely to debug-print it.
357impl std::fmt::Debug for RegistrationAttempt<'_> {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 f.debug_struct("RegistrationAttempt")
360 .field(
361 "initial_access_token",
362 &self.initial_access_token.map(|_| "[redacted]"),
363 )
364 .field("metadata", self.metadata)
365 .finish()
366 }
367}
368
369/// What a [`RegistrationPolicy`] decided.
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
371pub enum RegistrationDecision {
372 /// Register the client, subject to the metadata being valid.
373 Allow,
374 /// Refuse. The wire gets 401 and nothing else; the host knows why and this crate does not
375 /// need to.
376 Deny,
377}
378
379/// Who may create a client here. THIS LIBRARY HAS NO OPINION, and cannot have one: it never sees
380/// a request, so it has no caller, no IP, no tenant and no invite list.
381///
382/// With NO policy installed, every registration is denied. That is not a safe default chosen for
383/// tidiness, it is the only reading of RFC 7591 section 5 that does not leave an anonymous
384/// client-minting endpoint on the internet because somebody set a config field and moved on. A
385/// host that genuinely wants an open endpoint writes a two-line policy that returns
386/// [`RegistrationDecision::Allow`], and that line is then something a reviewer can find.
387pub trait RegistrationPolicy: Send + Sync {
388 /// Decide whether this attempt may create a client. Called BEFORE the metadata is validated
389 /// and before anything is written, so a refusal costs one call and touches no storage.
390 fn authorize(&self, attempt: &RegistrationAttempt<'_>) -> RegistrationDecision;
391}
392
393/// What dynamic registration is allowed to produce here. Held behind
394/// [`crate::server::ServerConfig::registration`], which is `None` (registration off) by default.
395///
396/// Every bound below is a CEILING on what an anonymous, or merely policy-approved, registrant can
397/// obtain. The defaults are the narrow ones on purpose; see [`RegistrationConfig::new`].
398#[derive(Debug, Clone, PartialEq, Eq)]
399/// `#[non_exhaustive]`: this struct's field set VARIES WITH CARGO FEATURES, so a host that writes a
400/// full struct literal has a build that breaks the day anything in their dependency graph enables a
401/// feature they did not ask for. Construct with `new()` and assign the fields you want. This is the
402/// one attribute on this type that cannot be added after publication, because by then somebody's
403/// struct literal is in production.
404#[non_exhaustive]
405pub struct RegistrationConfig {
406 /// RFC 8414 section 2 `registration_endpoint`. `None` derives `{issuer}/register`.
407 pub registration_endpoint: Option<String>,
408 /// The grants a registration may ask for. Anything outside this list is
409 /// `invalid_client_metadata`.
410 pub allowed_grant_types: Vec<GrantType>,
411 /// The ceiling on a registration's `scope`. A request outside it is `invalid_client_metadata`;
412 /// the default is EMPTY, so a host must say what a registrant may reach.
413 pub allowed_scopes: ScopeSet,
414 /// How long an issued client secret lives. `None` (the default) means it never expires, which
415 /// is what `client_secret_expires_at: 0` says on the wire (RFC 7591 section 3.2.1).
416 pub client_secret_ttl: Option<std::time::Duration>,
417 /// Whether RFC 7592 read, update and delete are offered at all. `true` by default: a client
418 /// that can be created and never corrected or deleted leaves the host doing registration
419 /// lifecycle by hand.
420 pub management_enabled: bool,
421}
422
423impl Default for RegistrationConfig {
424 fn default() -> Self {
425 RegistrationConfig::new()
426 }
427}
428
429impl RegistrationConfig {
430 /// The narrow defaults: the authorization code grant with refresh, no scopes, management on.
431 ///
432 /// `client_credentials` and the device grant are deliberately absent. A `client_credentials`
433 /// registration mints tokens with NO resource owner anywhere in the picture, so an open
434 /// registration endpoint that grants it is an open token endpoint; the device grant makes the
435 /// registrant able to allocate user codes, which RFC 8628 section 5.1 says are only adequate
436 /// in combination with rate limiting. Both are one line for a host to add, with its eyes open.
437 pub fn new() -> Self {
438 RegistrationConfig {
439 registration_endpoint: None,
440 allowed_grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
441 allowed_scopes: ScopeSet::default(),
442 client_secret_ttl: None,
443 management_enabled: true,
444 }
445 }
446
447 /// The advertised registration endpoint for `issuer`.
448 pub fn endpoint(&self, issuer: &str) -> String {
449 match &self.registration_endpoint {
450 Some(url) => url.clone(),
451 None => format!("{}/register", issuer.trim_end_matches('/')),
452 }
453 }
454}
455
456/// The largest number of `redirect_uris` one dynamic registration may declare.
457///
458/// # Why there is a cap at all
459///
460/// RFC 7591 section 2 sets none, and the list is not read here: it is read on EVERY authorization
461/// request for the registered client, as a linear scan with exact string comparison, because
462/// OAuth 2.1 section 4.1.3 forbids anything cheaper than exact matching. So an unbounded list is a
463/// cost bought once, at an endpoint whose [`RegistrationPolicy`] a host may well have opened to
464/// anonymous callers, and then paid per request for as long as the registration exists. That
465/// durability is what makes this worth a constant rather than a shrug.
466///
467/// # Why 16
468///
469/// It is counted from what a real client needs: one redirect URI per deployment environment
470/// (production, staging, a review app or two) times one per platform that needs its own
471/// (web, a native custom scheme, a loopback range for a desktop app). That is a handful, and
472/// sixteen is several times a handful. A registrant that genuinely needs more has one client
473/// standing in for several, which is a modelling problem this cap makes visible rather than a
474/// limit it imposes; separate clients also give the deployment separate secrets and separate
475/// revocation, which is the better shape anyway.
476///
477/// # Why the scan stays linear
478///
479/// At sixteen it is faster than hashing, and it is the same argument as
480/// [`crate::server::MAX_RESOURCE_INDICATORS`]. The defect was the missing bound, not the loop.
481pub const MAX_REGISTERED_REDIRECT_URIS: usize = 16;
482
483// The `token_endpoint_auth_method` values one may REGISTER here, which are a strict SUBSET of the
484// ones RFC 8414 `token_endpoint_auth_methods_supported` advertises, and deliberately so. That
485// document describes the TOKEN ENDPOINT (RFC 8414 s2), which really does accept
486// `client_secret_jwt`, `private_key_jwt`, `tls_client_auth` and `self_signed_tls_client_auth` for
487// a client the host provisioned out of band; narrowing it to this list would lie to every
488// statically configured client that uses one.
489//
490// The other four are out of reach of REGISTRATION, not of the server:
491//
492// - `private_key_jwt` and `client_secret_jwt` need a key. [`ClientMetadata`] models neither `jwks`
493// nor `jwks_uri` (RFC 7591 s2), so there is nowhere for a registrant to put one, and
494// `client_secret_jwt` additionally needs the shared secret IN THE CLEAR at verification time
495// while a registration keeps only a one-way [`SecretHash`].
496// - `tls_client_auth` and `self_signed_tls_client_auth` need the RFC 8705 s2.1.1 subject
497// parameters (`tls_client_auth_subject_dn` and the four SAN forms), which are likewise not
498// modelled.
499//
500// Accepting any of the four would mint a registration the token endpoint could never honour,
501// which is worse than refusing it: RFC 7591 s3.2.2 gives `invalid_client_metadata` for exactly
502// this, a value the server will not register. Closing the gap means modelling `jwks`/`jwks_uri`
503// and the RFC 8705 subject parameters, and that is the change to make, not a wider list here.
504const AUTH_METHOD_NONE: &str = "none";
505const AUTH_METHOD_BASIC: &str = "client_secret_basic";
506const AUTH_METHOD_POST: &str = "client_secret_post";
507
508/// RFC 7591 section 2: the `response_type` that corresponds to the authorization code grant. It is
509/// also the only one OAuth 2.1 keeps, the implicit grant's `token` having been removed.
510const RESPONSE_TYPE_CODE: &str = "code";
511
512/// Whether a redirect URI is one this server can ever match.
513///
514/// The strictness here is set by what the AUTHORIZATION endpoint does with it, not by what looks
515/// tidy. `AuthorizationServer::validate_authorization_request` compares the requested
516/// `redirect_uri` against the registered one by EXACT STRING MATCH (OAuth 2.1 section 4.1.3), so a
517/// registration this server accepts but can never match is not a lenient registration, it is a
518/// client that will be told `invalid_request` forever with no way to find out why. RFC 6749
519/// section 3.1.2 settles both halves: the URI MUST be absolute, and it MUST NOT carry a fragment.
520///
521/// Delegated to the RFC 8707 resource-indicator check rather than restated, because that function
522/// already implements exactly this rule (absolute URI, no fragment, nothing outside printable
523/// ASCII, which RFC 3986 requires of a URI anyway) and two copies of one rule drift.
524fn redirect_uri_is_registerable(value: &str) -> bool {
525 crate::authorization::is_valid_resource_indicator(value)
526}
527
528/// The metadata as this server will actually record it.
529///
530/// `pub(crate)` rather than private because [`crate::cimd`] validates a
531/// draft-ietf-oauth-client-id-metadata-document-01 client with THIS function rather than a second
532/// copy of it. The members of a client identifier metadata document come from the same OAuth
533/// Dynamic Client Registration Metadata registry (-01 section 4.1), so the rules are the same
534/// rules, and this crate has already paid once for a rule that existed twice and drifted.
535///
536/// `Clone`, `PartialEq` and `Eq` exist for [`crate::cimd::ValidatedClientIdDocument`], which is the
537/// only thing that holds one of these beyond the call that produced it. They are NOT feature gated,
538/// and that was checked rather than assumed: `scripts/size-report.sh`'s `default` row is byte for
539/// byte identical with and without them, because LTO deletes three impls a default build never
540/// reaches. Gating them would have been noise for a measured zero.
541#[derive(Debug, Clone, PartialEq, Eq)]
542pub(crate) struct Registered {
543 pub(crate) redirect_uris: Vec<String>,
544 pub(crate) grant_types: Vec<GrantType>,
545 pub(crate) response_types: Vec<String>,
546 pub(crate) token_endpoint_auth_method: String,
547 pub(crate) scope: ScopeSet,
548 pub(crate) client_name: Option<String>,
549}
550
551/// Validate one RFC 7591 section 2 metadata document against what this deployment will register.
552///
553/// Every refusal here is a registration this server would otherwise have written down and then
554/// been unable to honour. That is the standard the rules are set to: not "is this plausible" but
555/// "will the endpoints that later read this record be able to act on it".
556pub(crate) fn validate(
557 metadata: &ClientMetadata,
558 config: &RegistrationConfig,
559) -> Result<Registered, RegistrationFailure> {
560 // RFC 7591 s2.3 / s3.2.2. First, because a client that sent one is asking to be registered on
561 // terms this server has not read, and nothing after this point would be the registration it
562 // asked for. See the module docs for why this is a refusal and not an ignored member.
563 if metadata.software_statement.is_some() {
564 return Err(RegistrationFailure::Invalid(
565 RegistrationErrorResponse::new(
566 RegistrationErrorCode::InvalidSoftwareStatement,
567 "this server does not evaluate software statements (RFC 7591 s2.3)",
568 ),
569 ));
570 }
571
572 // RFC 7591 s2: absent `grant_types` defaults to `["authorization_code"]`.
573 let grant_types: Vec<GrantType> =
574 match metadata.grant_types.as_deref() {
575 None => vec![GrantType::AuthorizationCode],
576 Some(values) => {
577 let mut out = Vec::with_capacity(values.len());
578 for value in values {
579 // An unknown grant type is refused rather than dropped: see the test in
580 // `src/tests/registration.rs`. `implicit` and `password` land here too, which is
581 // right, because OAuth 2.1 removes both.
582 // `GrantType::parse` and not `value.parse()`: the refusal below discards the
583 // value, and `FromStr`'s error would first copy the caller's string onto the
584 // heap to carry it there. The registration document is caller-supplied text of
585 // the caller's chosen length, same as `grant_type` at the token endpoint.
586 let grant: GrantType = GrantType::parse(value).ok_or_else(|| {
587 RegistrationFailure::Invalid(RegistrationErrorResponse::new(
588 RegistrationErrorCode::InvalidClientMetadata,
589 "grant_types names a grant this server does not implement",
590 ))
591 })?;
592 if !config.allowed_grant_types.contains(&grant) {
593 return Err(RegistrationFailure::Invalid(RegistrationErrorResponse::new(
594 RegistrationErrorCode::InvalidClientMetadata,
595 "grant_types names a grant this deployment does not offer registrants",
596 )));
597 }
598 if !out.contains(&grant) {
599 out.push(grant);
600 }
601 }
602 out
603 }
604 };
605 let uses_code = grant_types.contains(&GrantType::AuthorizationCode);
606
607 // RFC 7591 s2 spells out the correspondence between the two lists (`authorization_code` with
608 // `code`, `implicit` with `token`). It permits a server to reject OR to substitute; rejecting
609 // is the choice here, because substituting registers a client that asked for something else
610 // and tells it so only in the echoed response it may not re-read.
611 //
612 // OAuth 2.1 has exactly one response type left, so the whole correspondence reduces to: the
613 // list is `["code"]` if and only if the authorization code grant is registered.
614 let response_types: Vec<String> =
615 match metadata.response_types.as_deref() {
616 // s2: absent defaults to `["code"]`, which is only coherent when the code grant is there.
617 None => match uses_code {
618 true => vec![RESPONSE_TYPE_CODE.to_string()],
619 false => Vec::new(),
620 },
621 // An EXPLICIT empty list falls through here and is caught by the correspondence check
622 // below when the code grant is registered: the client said it uses no response type while
623 // asking for the one grant that has one.
624 Some(values) => {
625 for value in values {
626 if value != RESPONSE_TYPE_CODE {
627 return Err(RegistrationFailure::Invalid(RegistrationErrorResponse::new(
628 RegistrationErrorCode::InvalidClientMetadata,
629 "this server issues authorization codes only; OAuth 2.1 removes the \
630 implicit grant",
631 )));
632 }
633 }
634 let asks_for_code = !values.is_empty();
635 // The correspondence, in both directions: `code` without `authorization_code` is a
636 // response type nothing will produce, and `authorization_code` without `code` is a
637 // grant with no way to start.
638 if asks_for_code != uses_code {
639 return Err(RegistrationFailure::Invalid(RegistrationErrorResponse::new(
640 RegistrationErrorCode::InvalidClientMetadata,
641 "grant_types and response_types must correspond: authorization_code with \
642 code (RFC 7591 s2)",
643 )));
644 }
645 match asks_for_code {
646 true => vec![RESPONSE_TYPE_CODE.to_string()],
647 false => Vec::new(),
648 }
649 }
650 };
651
652 // RFC 7591 s2 makes `redirect_uris` required for a redirection-based flow, and s3.2.2 gives
653 // the missing case and the malformed case the same code, because both say the same thing: this
654 // client has no address a code can be delivered to.
655 if uses_code && metadata.redirect_uris.is_empty() {
656 return Err(RegistrationFailure::Invalid(
657 RegistrationErrorResponse::new(
658 RegistrationErrorCode::InvalidRedirectUri,
659 "the authorization_code grant requires at least one redirect_uri",
660 ),
661 ));
662 }
663 // A CAP, because a registration is durable and the cost it imposes is not paid here. Every
664 // authorization request for this client scans `redirect_uris` linearly with exact string
665 // comparison (OAuth 2.1 section 4.1.3 allows nothing cheaper), so an unbounded list bought
666 // once at an endpoint a policy may well have opened to anonymous callers is a per-request cost
667 // that lasts as long as the registration does. RFC 7591 section 2 sets no bound of its own.
668 if metadata.redirect_uris.len() > MAX_REGISTERED_REDIRECT_URIS {
669 return Err(RegistrationFailure::Invalid(
670 RegistrationErrorResponse::new(
671 RegistrationErrorCode::InvalidRedirectUri,
672 "too many redirect_uris",
673 ),
674 ));
675 }
676 for uri in &metadata.redirect_uris {
677 if !redirect_uri_is_registerable(uri) {
678 // The offending value is NOT echoed: it is attacker-supplied and this description
679 // goes into an error body and quite possibly a log line.
680 return Err(RegistrationFailure::Invalid(
681 RegistrationErrorResponse::new(
682 RegistrationErrorCode::InvalidRedirectUri,
683 "each redirect_uri must be an absolute URI with no fragment (RFC 6749 s3.1.2)",
684 ),
685 ));
686 }
687 }
688
689 // RFC 7591 s2: absent `token_endpoint_auth_method` defaults to `client_secret_basic`.
690 let token_endpoint_auth_method = metadata
691 .token_endpoint_auth_method
692 .clone()
693 .unwrap_or_else(|| AUTH_METHOD_BASIC.to_string());
694 if !matches!(
695 token_endpoint_auth_method.as_str(),
696 AUTH_METHOD_NONE | AUTH_METHOD_BASIC | AUTH_METHOD_POST
697 ) {
698 return Err(RegistrationFailure::Invalid(
699 RegistrationErrorResponse::new(
700 RegistrationErrorCode::InvalidClientMetadata,
701 "token_endpoint_auth_method is not one this server can REGISTER; RFC 8414 \
702 token_endpoint_auth_methods_supported describes the token endpoint, which \
703 accepts more",
704 ),
705 ));
706 }
707 // RFC 6749 s4.4 gives client credentials to confidential clients only, so this pair produces a
708 // registration whose only grant the token endpoint will refuse every time. Same argument as
709 // the redirect URI rule above.
710 if token_endpoint_auth_method == AUTH_METHOD_NONE
711 && grant_types.contains(&GrantType::ClientCredentials)
712 {
713 return Err(RegistrationFailure::Invalid(
714 RegistrationErrorResponse::new(
715 RegistrationErrorCode::InvalidClientMetadata,
716 "client_credentials requires a confidential client (RFC 6749 s4.4)",
717 ),
718 ));
719 }
720
721 // RFC 6749 s3.3 syntax, then the deployment's ceiling. Both are `invalid_client_metadata`:
722 // s3.2.2 has one code for a metadata value this server will not accept.
723 let scope =
724 match metadata.scope.as_deref() {
725 None => ScopeSet::empty(),
726 Some(s) => {
727 let requested = ScopeSet::parse(s).map_err(|_| {
728 RegistrationFailure::Invalid(RegistrationErrorResponse::new(
729 RegistrationErrorCode::InvalidClientMetadata,
730 "scope is not a space-delimited RFC 6749 s3.3 token list",
731 ))
732 })?;
733 if !requested.is_subset(&config.allowed_scopes) {
734 return Err(RegistrationFailure::Invalid(RegistrationErrorResponse::new(
735 RegistrationErrorCode::InvalidClientMetadata,
736 "scope exceeds what this deployment offers dynamically registered clients",
737 )));
738 }
739 requested
740 }
741 };
742
743 Ok(Registered {
744 redirect_uris: metadata.redirect_uris.clone(),
745 grant_types,
746 response_types,
747 token_endpoint_auth_method,
748 scope,
749 client_name: metadata.client_name.clone(),
750 })
751}
752
753/// Rebuild the RFC 7591 section 2 view of a stored registration, for the section 3.2.1 echo.
754fn registered_metadata(client: &Client, registration: &DynamicRegistration) -> ClientMetadata {
755 ClientMetadata {
756 redirect_uris: client.redirect_uris.clone(),
757 token_endpoint_auth_method: Some(registration.token_endpoint_auth_method.clone()),
758 grant_types: Some(client.grant_types.iter().map(|g| g.to_string()).collect()),
759 // Derived rather than stored: `validate` enforces the RFC 7591 section 2 correspondence
760 // between the two lists, so the response types are a function of the grant types and
761 // storing them separately would only create a second place for them to disagree.
762 response_types: Some(match client.allows_grant(GrantType::AuthorizationCode) {
763 true => vec![RESPONSE_TYPE_CODE.to_string()],
764 false => Vec::new(),
765 }),
766 client_name: client.name.clone(),
767 scope: (!client.allowed_scopes.is_empty()).then(|| client.allowed_scopes.to_string()),
768 // Never echoed: none was accepted, so echoing one would say it had been.
769 software_statement: None,
770 }
771}
772
773impl<S: Storage, C: Clock> AuthorizationServer<S, C> {
774 /// The registration configuration, when the host enabled it.
775 fn registration_config(&self) -> Result<&RegistrationConfig, RegistrationFailure> {
776 self.config()
777 .registration
778 .as_deref()
779 .ok_or(RegistrationFailure::Disabled)
780 }
781
782 /// RFC 7591 section 3.1: register a client.
783 ///
784 /// `initial_access_token` is whatever the request presented as an RFC 6750 bearer token, and
785 /// is passed straight to the host's [`RegistrationPolicy`]; this crate does not interpret it.
786 ///
787 /// On success the returned [`ClientInformation`] carries the ONLY copy of the client secret
788 /// (for a confidential registration) and of the RFC 7592 registration access token. Neither is
789 /// recoverable afterwards, by the client or by the host: see the module docs.
790 ///
791 /// # THIS FUTURE IS NOT CANCELLATION SAFE, and what a drop costs
792 ///
793 /// A dropped future stops at whatever `await` it was suspended in and never resumes, and this
794 /// crate cannot make it finish: there is no destructor that can run an `async` store call. The
795 /// token plane states this contract on [`AuthorizationServer::token`] and
796 /// [`AuthorizationServer::revoke`]. The management plane pays something different for a drop,
797 /// and until 0.9.2 said nothing at all about it.
798 ///
799 /// What it costs here follows from the once-only rule above: the credentials are minted
800 /// BEFORE [`crate::store::Storage::put_client`] and handed back only in the return value after
801 /// it. A drop at or after that write leaves a row this server will honour whose registration
802 /// access token — and, for a confidential registration, whose client secret — existed only in
803 /// the dropped frame. Nothing recovers either one: this server kept verifiers, not
804 /// credentials, so there is nothing to re-send; RFC 7592
805 /// management of that registration needs the access token that is gone, so section 2.3 cannot
806 /// delete it either; and a [`crate::client::Client`] has no expiry and is never reclaimed by
807 /// [`crate::store::Storage::sweep_expired`]. The caller sees a request that did not answer and
808 /// retries, which registers a SECOND client. The first is permanent litter that only a host
809 /// deleting it out of band removes.
810 ///
811 /// The order is not the defect and reversing it would be worse: returning the credentials
812 /// before the write would hand a caller a live-looking secret for a registration that then
813 /// failed to persist.
814 ///
815 /// WHAT A HOST MUST DO, exactly as on the token plane: drive this from a task the connection
816 /// cannot cancel — spawn the call and await the join handle — so a disconnecting client aborts
817 /// the response and not the work. This crate's axum adapter already does that for every route,
818 /// this one included, because it spawns inside a single `fallback`; see [`crate::http`]. A
819 /// host that mounts [`crate::http::AuthorizationService::handle`] itself, or calls this method
820 /// directly, owns it.
821 pub async fn register_dynamic_client(
822 &self,
823 metadata: &ClientMetadata,
824 initial_access_token: Option<&str>,
825 ) -> Result<ClientInformation, RegistrationFailure> {
826 self.admit_registration()?;
827 self.register_admitted_client(metadata, initial_access_token)
828 .await
829 }
830
831 /// THE HOST'S THROTTLE, before the host's policy and before anything is read or written — and,
832 /// for an HTTP caller, before the request BODY is parsed.
833 ///
834 /// This endpoint is the one place in the crate where a caller's request becomes a PERMANENT
835 /// row: a `Client` has no expiry and `Storage::sweep_expired` never reclaims one, so an
836 /// unthrottled registration endpoint is not a burst a deployment rides out, it is growth that
837 /// does not come back. RFC 7591 section 5 says the same thing in prose — an open registration
838 /// endpoint is available to anyone on the internet — and a [`RegistrationPolicy`] is a decision
839 /// about CONTENT, which is the wrong instrument for volume.
840 ///
841 /// The refusal is `Unauthorized`, the same answer the policy refusal gives, and deliberately
842 /// so: those two must not be distinguishable, or a caller learns from the wire whether it was
843 /// the content or the rate that stopped them. No event is emitted, because there is no
844 /// `client_id` to name and the audit vocabulary here is about credentials.
845 ///
846 /// `pub(crate)` for ONE caller and one reason, exactly as
847 /// [`AuthorizationServer::authenticate_registration`] is: `crate::http`'s RFC 7591 `POST`
848 /// handler runs it before it parses the request body, so an anonymous caller cannot buy a
849 /// `MAX_BODY_BYTES` JSON parse per request. RFC 7591 s3.1 registration MAY be anonymous, so
850 /// unlike the RFC 7592 management plane there is no credential to look at first — but the
851 /// throttle is not a credential, it is keyed on nothing at all, and nothing stopped it being
852 /// asked first. That it was not is the whole of the defect.
853 ///
854 /// [`AuthorizationServer::register_dynamic_client`] still runs it for itself, so a host calling
855 /// that method directly is throttled on the same terms. It is NOT re-run underneath the HTTP
856 /// handler, which is the difference from the management plane's arrangement: this budget is
857 /// GLOBAL and its shipped default is 60 per window, so a second charge for one request would
858 /// quietly halve a host's configured ceiling. The split into
859 /// `register_admitted_client` is what keeps the count at one, and
860 /// `tests/http_refusal_honesty.rs` counts it.
861 ///
862 /// The `registration_config` check comes FIRST and is repeated below, so that a deployment with
863 /// registration disabled answers `Disabled` exactly as it did before rather than spending a
864 /// throttle budget on an endpoint it does not serve.
865 pub(crate) fn admit_registration(&self) -> Result<(), RegistrationFailure> {
866 self.registration_config()?;
867 if self
868 .hooks()
869 .check(crate::events::Attempt::ClientRegistration)
870 == crate::events::RateLimitDecision::Deny
871 {
872 return Err(RegistrationFailure::Unauthorized);
873 }
874 Ok(())
875 }
876
877 /// RFC 7591 section 3.1 from the policy check onwards, for a caller
878 /// [`AuthorizationServer::admit_registration`] has already admitted.
879 ///
880 /// Split out so the throttle can be asked before an HTTP body is parsed and asked ONCE; see
881 /// that method. Every branch here is the one it was when this was the back half of
882 /// `register_dynamic_client`.
883 pub(crate) async fn register_admitted_client(
884 &self,
885 metadata: &ClientMetadata,
886 initial_access_token: Option<&str>,
887 ) -> Result<ClientInformation, RegistrationFailure> {
888 let config = self.registration_config()?;
889 let limited = crate::events::Attempt::ClientRegistration;
890
891 // The host decides, FIRST, before anything is validated or written. With no policy
892 // installed the answer is no: see [`RegistrationPolicy`] and RFC 7591 section 5. The
893 // refusal is deliberately indistinguishable from a bad initial access token, because a
894 // policy that refuses on content should not confirm what content it dislikes.
895 let attempt = RegistrationAttempt {
896 initial_access_token,
897 metadata,
898 };
899 match self.hooks().registration_policy() {
900 Some(policy) if policy.authorize(&attempt) == RegistrationDecision::Allow => {}
901 _ => {
902 // `None`: no client id has been minted yet, and none is minted for a refusal.
903 self.hooks()
904 .emit(|| crate::events::Event::ClientRegistrationRefusedByPolicy {
905 client_id: None,
906 });
907 self.hooks()
908 .record(limited, crate::events::AttemptOutcome::Failed);
909 return Err(RegistrationFailure::Unauthorized);
910 }
911 }
912
913 // Reported as a FAILURE, which is what a limiter counting abuse rather than traffic wants:
914 // a caller submitting documents this server refuses is the shape of somebody probing for
915 // what the policy and the validator will accept.
916 let registered = match validate(metadata, config) {
917 Ok(registered) => registered,
918 Err(refusal) => {
919 self.hooks()
920 .record(limited, crate::events::AttemptOutcome::Failed);
921 return Err(refusal);
922 }
923 };
924
925 let now = crate::server::unix_seconds(self.now());
926 // `?` rather than a panic, for the reason `randomness_failure` gives: this is an
927 // unauthenticated HTTP route, and a library that aborts its host's request handler because
928 // the OS would not hand over sixteen bytes is worse than one that answers 500.
929 let client_id =
930 ClientId::new(crate::server::try_random_hex(16).ok_or_else(randomness_failure)?);
931 let secret = if registered.token_endpoint_auth_method != AUTH_METHOD_NONE {
932 Some(crate::server::try_random_hex(32).ok_or_else(randomness_failure)?)
933 } else {
934 None
935 };
936 let secret_expires_at = secret.as_ref().map(|_| match config.client_secret_ttl {
937 // RFC 7591 section 3.2.1: 0 means the secret never expires.
938 None => 0,
939 // `saturating_add`: plain `+` panics in debug and, worse, WRAPS in release, which
940 // would report a freshly minted secret as already expired. A host-set TTL is not
941 // validated anywhere, so the ceiling is the honest answer.
942 Some(ttl) => now.unwrap_or_default().saturating_add(ttl.as_secs()),
943 });
944 let registration_access_token =
945 crate::server::try_random_hex(32).ok_or_else(randomness_failure)?;
946
947 let client = Client {
948 client_id: client_id.clone(),
949 auth: match &secret {
950 None => ClientAuth::Public,
951 Some(s) => ClientAuth::ConfidentialSecretHash {
952 hash: SecretHash::sha256(s),
953 },
954 },
955 grant_types: registered.grant_types.clone(),
956 redirect_uris: registered.redirect_uris.clone(),
957 allowed_scopes: registered.scope.clone(),
958 default_scopes: registered.scope.clone(),
959 name: registered.client_name.clone(),
960 registration: Some(Box::new(DynamicRegistration {
961 registration_access_token_hash: SecretHash::sha256(®istration_access_token),
962 client_id_issued_at: now,
963 client_secret_expires_at: secret_expires_at,
964 token_endpoint_auth_method: registered.token_endpoint_auth_method.clone(),
965 })),
966 };
967 self.store()
968 .put_client(client)
969 .await
970 .map_err(RegistrationFailure::Storage)?;
971
972 // Emitted AFTER the write: a registration that failed to persist did not happen. The
973 // outcome is reported on the same terms, and for the same reason: the row exists now.
974 self.hooks()
975 .emit(|| crate::events::Event::ClientRegistered {
976 client_id: client_id.as_str(),
977 });
978 self.hooks()
979 .record(limited, crate::events::AttemptOutcome::Succeeded);
980
981 Ok(ClientInformation {
982 client_id: client_id.as_str().to_string(),
983 client_secret: secret,
984 client_id_issued_at: now,
985 client_secret_expires_at: secret_expires_at,
986 registration_access_token: config
987 .management_enabled
988 .then_some(registration_access_token),
989 registration_client_uri: config
990 .management_enabled
991 .then(|| registration_client_uri(config, self.config(), client_id.as_str())),
992 metadata: ClientMetadata {
993 redirect_uris: registered.redirect_uris,
994 token_endpoint_auth_method: Some(registered.token_endpoint_auth_method),
995 grant_types: Some(
996 registered
997 .grant_types
998 .iter()
999 .map(|g| g.to_string())
1000 .collect(),
1001 ),
1002 response_types: Some(registered.response_types),
1003 client_name: registered.client_name,
1004 scope: (!registered.scope.is_empty()).then(|| registered.scope.to_string()),
1005 software_statement: None,
1006 },
1007 })
1008 }
1009
1010 /// Authenticate an RFC 7592 section 2 management request.
1011 ///
1012 /// Every failure is [`RegistrationFailure::Unauthorized`]: an attempt the host's rate limiter
1013 /// denied, an unknown client, a statically provisioned client that has no registration access
1014 /// token, and a wrong token are one answer on the wire, because telling them apart is an
1015 /// enumeration oracle over the client table — and, for the limiter's own refusal, because a
1016 /// distinct answer would tell an attacker they had found a live registration and merely hit
1017 /// the ceiling.
1018 ///
1019 /// The same timing caveat that `AuthorizationServer::authenticate_client` documents applies:
1020 /// this returns before any hashing when the client is unknown, so an unknown client and a
1021 /// known client with the wrong token are distinguishable by wall time. The comparison itself
1022 /// leaks nothing (see [`SecretHash::verify`]); equalising the two paths is the host's
1023 /// business.
1024 ///
1025 /// THE HOST'S [`crate::events::RateLimiter`] IS ASKED FIRST, before the store is touched, and
1026 /// the attempt is [`crate::events::Attempt::ClientAuthentication`] keyed on this `client_id`:
1027 /// the same budget the token endpoint spends, because this is the same client's other
1028 /// credential and the more powerful one. Until 0.9.2 this said the host "is expected to
1029 /// throttle anyway", which was advice the host could not take through this crate: no `Attempt`
1030 /// reached the seam from any of the three management verbs. See the body for why the budget is
1031 /// shared rather than new.
1032 ///
1033 /// What the WIRE will not say, the AUDIT CHANNEL does. Each of the four refusals emits
1034 /// [`crate::events::Event::ClientRegistrationAuthenticationFailed`] naming which one it was,
1035 /// for the same reason the token plane separates its refusals: the host is not the attacker,
1036 /// and it cannot notice somebody guessing registration access tokens if the only record of the
1037 /// guess is a `401` that looks like every other `401`. That matters more here than on the token
1038 /// plane, because RFC 7592 section 2.2 lets a landed guess rewrite `redirect_uris`.
1039 /// `pub(crate)` for ONE caller and one reason: `crate::http`'s RFC 7592 `PUT` handler runs it
1040 /// before it parses the request body, so an unauthenticated caller cannot buy a
1041 /// `MAX_BODY_BYTES` JSON parse per request. `update_registration` below still authenticates for
1042 /// itself, because a check performed by a caller is not a check the method may assume — so an
1043 /// HTTP `PUT` spends TWO units of the throttle's budget rather than one. That is stated rather
1044 /// than tuned away: at the shipped default of 6000 per client per minute it is not a ceiling
1045 /// any real management traffic reaches, and removing the second check to save a unit would
1046 /// trade a bound for an assumption.
1047 pub(crate) async fn authenticate_registration(
1048 &self,
1049 client_id: &ClientId,
1050 registration_access_token: &str,
1051 ) -> Result<(std::sync::Arc<Client>, DynamicRegistration), RegistrationFailure> {
1052 let config = self.registration_config()?;
1053 if !config.management_enabled {
1054 return Err(RegistrationFailure::Disabled);
1055 }
1056 // THE HOST'S THROTTLE, before the store is touched, exactly as
1057 // `AuthorizationServer::authenticate_client` asks before it looks a client up.
1058 //
1059 // This is a bearer credential being GUESSED AT, which is the thing RFC 9700 section 4.13
1060 // is about, and the credential here is the more powerful of the two a dynamic registration
1061 // holds: the client secret authenticates as the client, while this one REWRITES (RFC 7592
1062 // s2.2) and DELETES (s2.3) the registration, cascading through every token and refresh
1063 // chain it was issued. Through 0.9.1 the three management verbs took no `Attempt` at all,
1064 // so a host that installed a limiter had this plane open while believing otherwise, and
1065 // the note on this function told that host it "is expected to throttle anyway" without
1066 // giving it anywhere to do so through this crate's own seam.
1067 //
1068 // `Attempt::ClientAuthentication` rather than a variant of its own, and rather than
1069 // `Attempt::ClientRegistration` (which names RFC 7591 and the permanent row it writes).
1070 // Two reasons. It is the same question — may this caller keep presenting credentials as
1071 // this `client_id` — and one client's two credentials sharing one budget is the answer
1072 // that cannot be walked around by moving from one endpoint to the other. And it adds NO
1073 // new denial of service: the budget is already keyed on a `client_id` RFC 6749 s2.2 makes
1074 // public, so anybody who could exhaust it here could already exhaust it by spraying wrong
1075 // secrets at the token endpoint.
1076 let attempt = crate::events::Attempt::ClientAuthentication {
1077 client_id: client_id.as_str(),
1078 };
1079 if self.hooks().check(attempt) == crate::events::RateLimitDecision::Deny {
1080 // No `record`: the attempt never happened, so there is no outcome to report. The
1081 // refusal is the SAME `Unauthorized` a wrong token gets (see below), because a
1082 // distinct answer would tell an attacker they had found a live registration and
1083 // merely hit the ceiling.
1084 return Err(self.registration_auth_failed(
1085 client_id,
1086 ClientAuthFailure::RateLimited,
1087 false,
1088 ));
1089 }
1090 let found = self
1091 .store()
1092 .get_client(client_id)
1093 .await
1094 .map_err(RegistrationFailure::Storage)?;
1095 let client = match found {
1096 Some(client) => client,
1097 None => {
1098 return Err(self.registration_auth_failed(
1099 client_id,
1100 ClientAuthFailure::UnknownClient,
1101 true,
1102 ))
1103 }
1104 };
1105 let registration = match client.registration.as_deref() {
1106 Some(registration) => registration.clone(),
1107 // A client the host provisioned itself. Reported apart from an unknown id because it
1108 // says the id was real, which is what an operator needs to see the probe for.
1109 None => {
1110 return Err(self.registration_auth_failed(
1111 client_id,
1112 ClientAuthFailure::NoDynamicRegistration,
1113 true,
1114 ))
1115 }
1116 };
1117 if !registration
1118 .registration_access_token_hash
1119 .verify(registration_access_token, self.hooks().secret_verifier())
1120 {
1121 return Err(self.registration_auth_failed(
1122 client_id,
1123 ClientAuthFailure::SecretMismatch,
1124 true,
1125 ));
1126 }
1127 // The limiter counts FAILURES rather than traffic (see `crate::rate_limit`), so a
1128 // management call that authenticated has to say so or a legitimate client's own polling
1129 // would be charged at the rate a guessing attack is.
1130 self.hooks()
1131 .record(attempt, crate::events::AttemptOutcome::Succeeded);
1132 Ok((client, registration))
1133 }
1134
1135 /// Report one refused management authentication and answer with the single wire failure all
1136 /// four share.
1137 ///
1138 /// The presented token is NOT a parameter, and that is deliberate rather than incidental: it
1139 /// cannot be logged by a later edit to this function because it is not here to log. See the
1140 /// rule in the [`crate::events`] module docs.
1141 ///
1142 /// `attempted` says whether the limiter ALLOWED this attempt and it then failed, which is the
1143 /// only case there is an outcome to report: a refusal by the limiter itself never happened, so
1144 /// reporting it would charge the caller twice for one attempt and, on a failure-weighted
1145 /// budget like [`crate::rate_limit::FixedWindowRateLimiter`], charge the heavier of the two
1146 /// prices for work nobody did.
1147 fn registration_auth_failed(
1148 &self,
1149 client_id: &ClientId,
1150 failure: ClientAuthFailure,
1151 attempted: bool,
1152 ) -> RegistrationFailure {
1153 self.hooks().emit(
1154 || crate::events::Event::ClientRegistrationAuthenticationFailed {
1155 client_id: client_id.as_str(),
1156 failure,
1157 },
1158 );
1159 if attempted {
1160 self.hooks().record(
1161 crate::events::Attempt::ClientAuthentication {
1162 client_id: client_id.as_str(),
1163 },
1164 crate::events::AttemptOutcome::Failed,
1165 );
1166 }
1167 RegistrationFailure::Unauthorized
1168 }
1169
1170 /// RFC 7592 section 2.1: read a registration.
1171 ///
1172 /// The response carries no `client_secret` and no `registration_access_token`, because this
1173 /// server stores neither: see the module docs.
1174 pub async fn read_registration(
1175 &self,
1176 client_id: &ClientId,
1177 registration_access_token: &str,
1178 ) -> Result<ClientInformation, RegistrationFailure> {
1179 let (client, registration) = self
1180 .authenticate_registration(client_id, registration_access_token)
1181 .await?;
1182 let config = self.registration_config()?;
1183 Ok(ClientInformation {
1184 client_id: client.client_id.as_str().to_string(),
1185 client_secret: None,
1186 client_id_issued_at: registration.client_id_issued_at,
1187 client_secret_expires_at: registration.client_secret_expires_at,
1188 registration_access_token: None,
1189 registration_client_uri: Some(registration_client_uri(
1190 config,
1191 self.config(),
1192 client.client_id.as_str(),
1193 )),
1194 metadata: registered_metadata(&client, ®istration),
1195 })
1196 }
1197
1198 /// RFC 7592 section 2.2: replace a registration's metadata.
1199 ///
1200 /// The whole document is replaced, not merged: section 2.2 says the client sends its full
1201 /// metadata and that any omitted member is treated as absent. Merging would make a client that
1202 /// dropped a redirect URI keep it, which is precisely backwards for the one member that
1203 /// decides where a code may be delivered.
1204 ///
1205 /// `client_id` cannot be changed (section 2.2), and the grant and scope ceilings of
1206 /// [`RegistrationConfig`] apply again, so an update cannot reach anything a fresh registration
1207 /// could not.
1208 ///
1209 /// The response carries a `client_secret` in exactly one case: an update that moves the client
1210 /// from `token_endpoint_auth_method: none` to a method that needs one MINTS a secret, and this
1211 /// is the only response other than the original registration that ever carries a live
1212 /// credential. It is never an ECHO of an existing secret, which this server does not hold; see
1213 /// the module docs.
1214 ///
1215 /// # THIS FUTURE IS NOT CANCELLATION SAFE, and this is the drop a client cannot retry around
1216 ///
1217 /// See [`AuthorizationServer::register_dynamic_client`] for why a dropped future cannot be
1218 /// finished by this crate. The expensive drop point here is the one case above: the mint.
1219 ///
1220 /// When this call mints a secret it writes the [`crate::client::SecretHash`] of it through
1221 /// [`crate::store::Storage::compare_and_swap_client`], and returns the secret itself only in
1222 /// the value at the end of this function. A drop after that swap RESOLVES and before the
1223 /// response reaches the client leaves the store holding a verifier for a string that exists
1224 /// nowhere. The client's retry does not repair it and cannot: the stored registration is now
1225 /// confidential, so on the second pass `had_secret` is true and the arm that KEEPS THE
1226 /// EXISTING VERIFIER is taken rather than the mint. That arm is right for what it was written
1227 /// for — a metadata edit must not log a client out of the token endpoint — and nothing on the
1228 /// wire distinguishes that case from this one. The registration can never authenticate at the
1229 /// token endpoint again.
1230 ///
1231 /// The way out is RFC 7592 section 2.3, and it is the only one: this call never rotates the
1232 /// registration access token, so the client still holds it and can DELETE the registration and
1233 /// register afresh. A host whose [`RegistrationPolicy`] admits an initial access token only
1234 /// once has to provision the client again itself.
1235 ///
1236 /// Reversing the order would be worse rather than better: handing the secret back before the
1237 /// swap would give a client a credential for a write that a concurrent section 2.3 delete is
1238 /// entitled to refuse — which is exactly what the compare-and-swap exists to allow. So the
1239 /// order stands and the contract is stated instead.
1240 ///
1241 /// The cheaper drop points, for completeness: anywhere before the swap costs nothing, because
1242 /// nothing has been written; between the swap and the return, an update with no mint loses
1243 /// only the [`crate::events::Event::ClientRegistrationUpdated`], so an audit trail can miss an
1244 /// update that did happen.
1245 ///
1246 /// WHAT A HOST MUST DO is what [`AuthorizationServer::register_dynamic_client`] says: spawn
1247 /// this and await the join handle. The axum adapter does. A host driving this future from the
1248 /// connection is choosing the cost above, at whatever rate its clients disconnect.
1249 pub async fn update_registration(
1250 &self,
1251 client_id: &ClientId,
1252 registration_access_token: &str,
1253 metadata: &ClientMetadata,
1254 ) -> Result<ClientInformation, RegistrationFailure> {
1255 let (client, registration) = self
1256 .authenticate_registration(client_id, registration_access_token)
1257 .await?;
1258 let config = self.registration_config()?;
1259
1260 // The host decides on an UPDATE too, and this is not a formality. RFC 7592 section 2.2
1261 // has the client send a complete replacement metadata document, so every content control
1262 // a policy applied at registration (a `client_name` impersonating the deployment, a
1263 // `redirect_uris` entry on a domain the host will not serve) is exactly what this call can
1264 // rewrite. Consulting the policy only on the way in would leave every one of those
1265 // controls one PUT away from being void, and the registration access token is long lived
1266 // where an initial access token is typically single use.
1267 //
1268 // `initial_access_token` is None because there is no second one to present: RFC 7592
1269 // section 2 authenticates this request with the registration access token, which
1270 // `authenticate_registration` above has already verified.
1271 let attempt = RegistrationAttempt {
1272 initial_access_token: None,
1273 metadata,
1274 };
1275 match self.hooks().registration_policy() {
1276 Some(policy) if policy.authorize(&attempt) == RegistrationDecision::Allow => {}
1277 _ => {
1278 // The caller HAS authenticated here, with a registration access token this server
1279 // just verified, and is being refused anyway. See the event's own docs on why that
1280 // is a different thing from a failed authentication.
1281 self.hooks()
1282 .emit(|| crate::events::Event::ClientRegistrationRefusedByPolicy {
1283 client_id: Some(client_id.as_str()),
1284 });
1285 return Err(RegistrationFailure::Unauthorized);
1286 }
1287 }
1288
1289 let registered = validate(metadata, config)?;
1290
1291 // A change of authentication method that needs a secret the registration does not have
1292 // mints one; this is the only path other than registration itself that can produce one.
1293 let wants_secret = registered.token_endpoint_auth_method != AUTH_METHOD_NONE;
1294 let had_secret = client.auth.is_confidential();
1295 // `?` for the reason `randomness_failure` gives; RFC 7592 s2.2 is a routed `PUT` and this
1296 // is the only path other than registration itself that mints a secret.
1297 let new_secret = if wants_secret && !had_secret {
1298 Some(crate::server::try_random_hex(32).ok_or_else(randomness_failure)?)
1299 } else {
1300 None
1301 };
1302 let auth = match (&new_secret, wants_secret) {
1303 (Some(s), _) => ClientAuth::ConfidentialSecretHash {
1304 hash: SecretHash::sha256(s),
1305 },
1306 // Keeps the existing verifier: this server cannot re-issue a secret it does not hold,
1307 // and silently rotating one on every metadata edit would log the client out of the
1308 // token endpoint for changing its name.
1309 (None, true) => client.auth.clone(),
1310 (None, false) => ClientAuth::Public,
1311 };
1312 let client_secret_expires_at = match (&new_secret, wants_secret) {
1313 (Some(_), _) => Some(match config.client_secret_ttl {
1314 None => 0,
1315 Some(ttl) => {
1316 // Same saturating add as the mint path above, for the same reason.
1317 crate::server::unix_seconds(self.now())
1318 .unwrap_or_default()
1319 .saturating_add(ttl.as_secs())
1320 }
1321 }),
1322 (None, true) => registration.client_secret_expires_at,
1323 (None, false) => None,
1324 };
1325
1326 let updated_registration = DynamicRegistration {
1327 registration_access_token_hash: registration.registration_access_token_hash.clone(),
1328 client_id_issued_at: registration.client_id_issued_at,
1329 client_secret_expires_at,
1330 token_endpoint_auth_method: registered.token_endpoint_auth_method.clone(),
1331 };
1332 let updated = Client {
1333 client_id: client.client_id.clone(),
1334 auth,
1335 grant_types: registered.grant_types.clone(),
1336 redirect_uris: registered.redirect_uris.clone(),
1337 allowed_scopes: registered.scope.clone(),
1338 default_scopes: registered.scope.clone(),
1339 name: registered.client_name.clone(),
1340 registration: Some(Box::new(updated_registration.clone())),
1341 };
1342 // COMPARE-AND-SWAP, not a blind put. This function read the registration at its top, then
1343 // awaited a policy decision and a validation pass before arriving here, and a concurrent
1344 // RFC 7592 section 2.3 delete anywhere in that window used to be UNDONE by this write: the
1345 // client came back with its old credential and its old `registration_access_token_hash`,
1346 // which makes deleting a compromised registration defeatable by whoever holds the stolen
1347 // token. `client` is exactly what was read, so it is the expectation; a store that no
1348 // longer holds it refuses, and the deletion stands.
1349 //
1350 // The refusal is `Unauthorized` rather than a storage error, because by the time this
1351 // returns false the caller's registration access token names a registration that no
1352 // longer exists. That is the same answer `authenticate_registration` gives for an unknown
1353 // client, which is what this now is.
1354 //
1355 // NO AUDIT EVENT, and the absence is the fix rather than an omission. This used to report
1356 // `ClientAuthFailure::UnknownClient` on the management plane, which is a claim that an
1357 // AUTHENTICATION FAILED — and the authentication did not fail: reaching this line requires
1358 // `authenticate_registration` above to have SUCCEEDED, so the caller presented a
1359 // registration access token this server verified. The only ways here are a concurrent
1360 // section 2.3 delete and a second concurrent update, neither of which is a credential
1361 // guess. `Event::ClientRegistrationAuthenticationFailed` is the one signal a host has for
1362 // somebody guessing registration access tokens (see its docs on what a landed guess buys an
1363 // attacker), and a deployment whose own racing clients emit it is a deployment that has
1364 // learned to ignore it. What an operator sees instead is exactly what happened: a
1365 // `ClientRegistrationDeleted` (or another `ClientRegistrationUpdated`) from the request that
1366 // won, and no `ClientRegistrationUpdated` from this one.
1367 let applied = self
1368 .store()
1369 .compare_and_swap_client(&client, updated.clone())
1370 .await
1371 .map_err(RegistrationFailure::Storage)?;
1372 if !applied {
1373 return Err(RegistrationFailure::Unauthorized);
1374 }
1375 self.hooks()
1376 .emit(|| crate::events::Event::ClientRegistrationUpdated {
1377 client_id: client_id.as_str(),
1378 });
1379
1380 Ok(ClientInformation {
1381 client_id: client_id.as_str().to_string(),
1382 client_secret: new_secret,
1383 client_id_issued_at: updated_registration.client_id_issued_at,
1384 client_secret_expires_at: updated_registration.client_secret_expires_at,
1385 registration_access_token: None,
1386 registration_client_uri: Some(registration_client_uri(
1387 config,
1388 self.config(),
1389 client_id.as_str(),
1390 )),
1391 metadata: registered_metadata(&updated, &updated_registration),
1392 })
1393 }
1394
1395 /// RFC 7592 section 2.3: delete a registration.
1396 ///
1397 /// Deletion takes everything the registration was issued with it, through
1398 /// [`Storage::delete_client`]: a client that no longer exists must not still have live access
1399 /// tokens, refresh chains or outstanding authorization codes. Section 2.3 requires exactly
1400 /// that, and it is the half of deletion that is easy to skip and impossible to notice.
1401 ///
1402 /// # THIS FUTURE IS NOT CANCELLATION SAFE, and what a drop costs
1403 ///
1404 /// This is the cheap one of the three; see [`AuthorizationServer::register_dynamic_client`]
1405 /// for the contract and [`AuthorizationServer::update_registration`] for the expensive one. A
1406 /// drop before [`crate::store::Storage::delete_client`] leaves the registration standing, and
1407 /// the client still holds the registration access token, so the request is simply repeatable.
1408 /// A drop after it loses the [`crate::events::Event::ClientRegistrationDeleted`], so a host
1409 /// can find a registration gone with no audit record of who removed it. A retry after that
1410 /// answers `Unauthorized`, because the token now names a registration that does not exist —
1411 /// and pays what an unknown id pays: a
1412 /// [`crate::events::Event::ClientRegistrationAuthenticationFailed`] carrying
1413 /// [`crate::events::ClientAuthFailure::UnknownClient`], charged to the limiter as a failure.
1414 /// So a client retrying a deletion that in fact completed reads, to an operator, exactly like
1415 /// somebody guessing at a registration access token. That is the audit cost of a drop here,
1416 /// and it is the whole of it: nothing is left half-written.
1417 pub async fn delete_registration(
1418 &self,
1419 client_id: &ClientId,
1420 registration_access_token: &str,
1421 ) -> Result<(), RegistrationFailure> {
1422 self.authenticate_registration(client_id, registration_access_token)
1423 .await?;
1424 // The barrier deadline comes from the server's own token lifetimes: a deletion has to
1425 // refuse not just the records that exist now but anything an issuance already in flight
1426 // for this client is about to write. See `revocation_window`.
1427 self.store()
1428 .delete_client(client_id, self.revocation_window())
1429 .await
1430 .map_err(RegistrationFailure::Storage)?;
1431 self.hooks()
1432 .emit(|| crate::events::Event::ClientRegistrationDeleted {
1433 client_id: client_id.as_str(),
1434 });
1435 Ok(())
1436 }
1437}
1438
1439/// RFC 7592 section 3 `registration_client_uri`: `{registration_endpoint}/{client_id}`.
1440///
1441/// THE ID IS ESCAPED, and until the 0.9.1 audit this was a bare `format!` justified by the ids this
1442/// server MINTS being 32 hex characters. That justification only ever covered the dynamic path:
1443/// `read_registration` and `update_registration` mint this URL for whatever client the caller
1444/// authenticated as, and a host that provisioned a client through `register_client` chose its own
1445/// id. A space, a `?` or a `#` in one produced a `registration_client_uri` that is not a URL, and a
1446/// slash produced one that names a different resource; the client uses the value verbatim, so the
1447/// escaping has to happen where the URL is built.
1448///
1449/// RFC 3986 section 3.3: everything outside `unreserved` is percent-encoded, which is stricter than
1450/// `pchar` allows and is the safe direction. `crate::http`'s `decode_path_segment` decodes the
1451/// segment on the way back in, so the round trip is exact.
1452fn registration_client_uri(
1453 config: &RegistrationConfig,
1454 server: &ServerConfig,
1455 client_id: &str,
1456) -> String {
1457 let endpoint = config.endpoint(&server.issuer);
1458 let mut url = String::with_capacity(endpoint.len() + 1 + client_id.len());
1459 url.push_str(&endpoint);
1460 url.push('/');
1461 for byte in client_id.bytes() {
1462 match byte {
1463 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
1464 url.push(byte as char)
1465 }
1466 other => url.push_str(&format!("%{other:02X}")),
1467 }
1468 }
1469 url
1470}
1471
1472#[cfg(test)]
1473#[path = "tests/registration.rs"]
1474mod tests;