Skip to main content

ServerConfig

Struct ServerConfig 

Source
#[non_exhaustive]
pub struct ServerConfig {
Show 32 fields pub issuer: String, pub verification_uri: String, pub authorization_endpoint: Option<String>, pub token_endpoint: Option<String>, pub device_authorization_endpoint: Option<String>, pub introspection_endpoint: Option<String>, pub revocation_endpoint: Option<String>, pub jwks_uri: Option<String>, pub registration: Option<Box<RegistrationConfig>>, pub par: Option<Box<ParConfig>>, pub jar: Option<Box<JarConfig>>, pub cimd: Option<Box<CimdPolicy>>, pub scopes_supported: Option<Vec<String>>, pub allowed_resources: Option<Box<[Box<str>]>>, pub resource_servers: Option<Box<[ResourceServerRegistration]>>, pub service_documentation: Option<String>, pub authorization_details_types_supported: Option<Vec<String>>, pub protected_resources: Option<Vec<String>>, pub access_token_format: AccessTokenFormat, pub authorization_code_ttl: Duration, pub include_verification_uri_complete: bool, pub device_code_ttl: Duration, pub poll_interval: Duration, pub slow_down_increment: Duration, pub access_token_ttl: Duration, pub issue_refresh_tokens: bool, pub allow_sender_constrained_exchange: bool, pub allow_authorization_details_exchange: bool, pub refresh_token_ttl: Option<Duration>, pub refresh_reuse_window: Duration, pub require_dpop: bool, pub user_code_length: usize,
}
Expand description

Server configuration. ServerConfig::new fills RFC-shaped defaults; every field is public so hosts override what they need. #[non_exhaustive]: this struct’s field set VARIES WITH CARGO FEATURES, so a host that writes a full struct literal has a build that breaks the day anything in their dependency graph enables a feature they did not ask for. Construct with new() and assign the fields you want. This is the one attribute on this type that cannot be added after publication, because by then somebody’s struct literal is in production.

Fields (Non-exhaustive)§

This struct is marked as non-exhaustive
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§issuer: String

The issuer identifier (RFC 8414 issuer): the canonical https URL of this AS.

RFC 8414 section 2 requires the https scheme in production. This crate does NOT enforce it, because the same code has to be runnable over plain HTTP on loopback for conformance runs and local development; enforcing transport security is the host’s job, and the host is the only party that knows whether it is behind a TLS terminator.

§verification_uri: String

Where a user goes to enter a device user code (RFC 8628 verification_uri).

§authorization_endpoint: Option<String>

RFC 8414 authorization_endpoint. None derives {issuer}/authorize.

§token_endpoint: Option<String>

RFC 8414 token_endpoint. None derives {issuer}/token.

§device_authorization_endpoint: Option<String>

RFC 8628 device_authorization_endpoint. None derives {issuer}/device_authorization.

§introspection_endpoint: Option<String>

RFC 7662 introspection_endpoint. None derives {issuer}/introspect.

§revocation_endpoint: Option<String>

RFC 7009 revocation_endpoint. None derives {issuer}/revoke.

§jwks_uri: Option<String>

RFC 8414 jwks_uri. None (the default) means this server publishes no keys, which is the truth for opaque access tokens.

§registration: Option<Box<RegistrationConfig>>

RFC 7591 dynamic client registration. None is the DEFAULT and means registration is OFF: no registration_endpoint is advertised, no route is served, and AuthorizationServer::register_dynamic_client answers crate::registration::RegistrationFailure::Disabled.

Turning it on is meant to be a sentence somebody wrote and a reviewer can find: config.registration = Some(Box::new(RegistrationConfig::new())). RFC 7591 section 5 is why (an open registration endpoint lets anyone mint a client), and enabling it is still not sufficient: a crate::registration::RegistrationPolicy must also be installed or every registration is refused. See the crate::registration module docs.

BOXED so that the overwhelmingly common None costs one null pointer on every ServerConfig rather than the whole struct, and allocates nothing.

§par: Option<Box<ParConfig>>
Available on crate feature par only.

RFC 9126 pushed authorization requests. None is the DEFAULT and means PAR is OFF: no pushed_authorization_request_endpoint is advertised and AuthorizationServer::pushed_authorization_request refuses.

BOXED for the same reason as ServerConfig::registration: the overwhelmingly common None costs one null pointer on every ServerConfig rather than the whole struct, and allocates nothing.

§jar: Option<Box<JarConfig>>
Available on crate feature jar only.

RFC 9101 signed request objects. None is the DEFAULT and means JAR is OFF: a request parameter is answered with request_not_supported rather than parsed.

§cimd: Option<Box<CimdPolicy>>
Available on crate feature cimd only.

draft-ietf-oauth-client-id-metadata-document-01 client identifier metadata documents. None is the DEFAULT and means the mechanism is OFF: the RFC 8414 document says client_id_metadata_document_supported: false.

SETTING IT IS A CLAIM ABOUT THE HOST, not about this crate, and that is why it is a config field rather than a constant derived from the cargo feature. This crate performs no fetch (see crate::cimd), so compiling the feature in proves only that the VALIDATOR is available; whether this deployment actually dereferences a client identifier URL is something only the host knows. Deriving the advertised member from the feature would publish a capability the build might always refuse, which is a defect shape this crate has already shipped twice.

BOXED for the same reason as ServerConfig::registration.

§scopes_supported: Option<Vec<String>>

RFC 8414 scopes_supported. None omits the member rather than claiming an empty catalogue.

§allowed_resources: Option<Box<[Box<str>]>>

The RFC 8707 resource indicators this server is WILLING to issue tokens for.

None (the default) means no restriction, which is the pre-existing behaviour and is why it is the default: turning refusal on by default would break every deployment already using resource indicators. It is also why None is a real risk rather than a neutral one, and the risk is worth stating here rather than in a changelog.

Option<Box<[Box<str>]>> rather than Vec<String> so a host that never sets it pays ONE pointer on every ServerConfig, not a 24 byte vector header. The list is written once at construction and only ever iterated.

RFC 8707 section 2 requires invalid_target when the server “is unwilling or unable to issue an access token” for a requested resource. With this empty, the server has no notion of unwilling: any syntactically valid absolute URI is accepted. Under the jwt feature the requested resource then REPLACES the configured audience in the RFC 9068 aud claim, so any client can obtain a token this server signed, carrying another resource server’s identifier in aud. That server verifies the signature against our JWKS, sees its own identifier, and authorises.

A deployment serving more than one resource server should set this.

§resource_servers: Option<Box<[ResourceServerRegistration]>>

Which registered clients are RESOURCE SERVERS, and which RFC 8707 resource identifiers each one answers for. This is what opens the channel RFC 7662 section 1 describes, in which the specification “allows authorized protected resources to query the authorization server”; empty (the default) means this server introspects for the token’s own client and nobody else, which is what it did through 0.9.1.

A resource server is NOT a new kind of principal. It registers as an ordinary confidential crate::Client and authenticates to the introspection endpoint with whatever this build accepts from any client – client_secret_basic, client_secret_post, RFC 7523 private_key_jwt or client_secret_jwt, RFC 8705 mutual TLS – through the same authenticate_client every other endpoint uses. Section 2.1 requires the endpoint be protected; reusing the client credential machinery is how it is protected, and inventing a second credential type would have meant a second thing to get constant-time comparison, rotation and revocation right on.

What this adds on top of authentication is AUTHORIZATION, and that is the part that is not optional. Authenticating as a resource server must not mean reading every token in the store: an introspection endpoint that answers any authenticated resource server about any token is a token-scanning oracle, which is section 4’s warning with a credential stapled to it. So a resource server is answered about a token ONLY when the token’s own crate::IssuedToken::resource set names one of the identifiers registered here.

AN EMPTY resource ON THE TOKEN NAMES NOBODY, AND IS REFUSED TO EVERY RESOURCE SERVER. That is the same fail-open reading crate::jwt::Audience::names_a_resource_server exists to refuse, arriving through the other door: a grant that requested no resource indicator is restricted to nothing in particular, and reading “restricted to nothing in particular” as “so anyone may ask about it” would hand every resource server in the deployment every token that did not happen to use RFC 8707. The token’s own client can still introspect it, which is the pre-0.9.2 behaviour and is unchanged.

§WHAT THIS COSTS YOUR RATE LIMITER, and what to set

SETTING THIS CHANGES THE TRAFFIC SHAPE AT THE CLIENT-AUTHENTICATION BUDGET, and it is the one consequence of registering a resource server that is not visible from anything else on this page. An introspection is a client authentication like any other – that is the whole point of the paragraph above – so it is charged crate::rate_limit::ATTEMPT_COST against crate::events::Attempt::ClientAuthentication keyed on the RESOURCE SERVER’s client_id. Through 0.9.1 that budget only ever carried a client asking about tokens it had itself been issued, so its volume tracked issuance. A resource server introspects ONCE PER CALL AT THE PROTECTED RESOURCE, at a rate set by that API’s own clients.

crate::rate_limit::DEFAULT_CLIENT_AUTHENTICATION_CAPACITY is 6000 a minute, which is 100 a second, and it was derived from a client’s token traffic. Left alone it becomes the protected resource’s request ceiling, per node.

AND IT DOES NOT READ AS A THROTTLE WHEN IT BITES. RFC 7662 introspection over the ceiling is refused with a bare invalid_client, the same answer a wrong secret gets, because a distinct code would tell an attacker they had found a live client id. A resource server that fails closed then refuses EVERY request it is handling, and its operator is looking at what appears to be a credential problem. The crate::events::EventSink channel is where the two are distinguishable: crate::events::Event::ClientAuthenticationFailed carries crate::events::ClientAuthFailure::RateLimited for a throttle and crate::events::ClientAuthFailure::SecretMismatch for a credential that did not verify. A deployment that registers resource servers should install one.

So, two things:

  • Size the budget for the API, not for a client: crate::rate_limit::RateLimitConfig::with_client_authentication_capacity_for raises ONE registration and leaves every other client_id where it was. Raising client_authentication_capacity globally would also raise how many wrong secrets every other registration admits per window, each of which can cost the host an argon2id.
  • CACHE THE INTROSPECTION RESPONSE at the resource server, which RFC 7662 section 4 recommends and which is the only measure that changes the traffic shape rather than the ceiling. It costs a bounded delay before a revocation is observed.

A host that implements crate::events::RateLimiter itself makes the same decision in its own terms; there is no introspection-specific crate::events::Attempt variant to key on, deliberately, and the module docs on crate::rate_limit say why.

Option<Box<[_]>> rather than Vec<_> for the reason ServerConfig::allowed_resources gives next door and with the same measurement behind it: the list is written once at construction and only ever iterated, so the growable shape buys nothing, and a boxed slice is 16 bytes against a vector header’s 24 on every ServerConfig in every deployment. MEASURED: ServerConfig 464 before this field, 488 as a Vec, 480 as this.

§service_documentation: Option<String>

RFC 8414 service_documentation.

§authorization_details_types_supported: Option<Vec<String>>
Available on crate feature rar only.

RFC 9396 section 10 authorization_details_types_supported: the authorization details types this deployment actually implements.

None is the DEFAULT and means NO type is supported, so every authorization_details request is refused with invalid_authorization_details. That is not conservatism for its own sake, it is section 5: “The AS MUST refuse to process any unknown authorization details type”, and a server that has been told nothing about a type cannot be said to know it. Compiling the rar feature in is therefore not the same as turning it on; a host turns it on by naming its types here.

§protected_resources: Option<Vec<String>>
Available on crate feature resource-metadata only.

RFC 9728 section 4 protected_resources: the resource identifiers of the protected resources this AS issues tokens for. None (the default) omits the member; see crate::metadata::AuthorizationServerMetadata::protected_resources, and note that this is the AS half only. The DOCUMENT each of those resources publishes is crate::resource_metadata::ProtectedResourceMetadata, and publishing it is the resource’s own job, not this server’s.

§access_token_format: AccessTokenFormat
Available on crate feature jwt only.

What the client receives as its access_token. Defaults to AccessTokenFormat::Opaque, which is the behaviour of this crate without the jwt feature; setting AccessTokenFormat::Jwt makes the wire token an RFC 9068 at+jwt while the AS-side record is still persisted, so introspection and revocation are unchanged.

§authorization_code_ttl: Duration

Authorization code lifetime. RFC 6749 section 4.1.2 recommends a maximum of 10 minutes; the default is 60 seconds, which is ample for a redirect round trip.

§include_verification_uri_complete: bool

Whether device authorization responses include verification_uri_complete ({verification_uri}?user_code={code}). false by default.

RFC 8628 section 5.4 (Remote Phishing) is why this is a decision and not a convenience setting. The attack is that an attacker starts a device grant for their OWN client and mails the victim the link (“click here to finish setting up your TV”); the victim, already signed in, lands on a page that needs one click, and the attacker collects the tokens. Section 5.4 names TYPING THE CODE as the friction that makes this hard, and this member is precisely the removal of that friction: the code arrives pre-filled from a URL the user did not compose.

OFF by default as of 0.9.1, having been on. Section 3.3.1 makes the member OPTIONAL, so omitting it is conformant and costs a deployment only the QR-code convenience, while including it by default made every host that never read this paragraph pay for a capability it did not ask for. That is the same posture the rest of this config takes: ServerConfig::registration and the PAR and JAR blocks are all off until a host says otherwise. A host that turns this ON should pair it with a verification page that names the client and the scope and requires an affirmative action, which is what section 3.3 asks for and what the http feature’s page does.

§device_code_ttl: Duration

Device code and user code lifetime. Default 600 seconds.

§poll_interval: Duration

Initial minimum poll spacing (RFC 8628 interval). Default 5 seconds.

§slow_down_increment: Duration

How much a slow_down raises the required spacing. RFC 8628 section 3.5 mandates the client add 5 seconds, which is the default.

§access_token_ttl: Duration

Access token lifetime. Default 3600 seconds.

§issue_refresh_tokens: bool

Whether user-approved grants also issue a refresh token. Default true.

§allow_sender_constrained_exchange: bool

RFC 8693: whether a SENDER-CONSTRAINED subject token may be exchanged.

false by default, which means the exchange is REFUSED with invalid_request when the subject token carries an RFC 9449 DPoP or RFC 8705 mutual-TLS binding. See the “A SENDER-CONSTRAINED subject token is REFUSED” section of the crate::token_exchange module docs for the full argument; the short form is that the token this server would hand back belongs to the EXCHANGING client, which does not hold the original client’s key, so it can only be a plain bearer token. Anyone able to authenticate as any client registered for this grant could then post a stolen bound token and receive a spendable one, and the property the deployment turned DPoP on to buy would be gone.

§What turning it on gives up

Exactly that property, and it is worth being blunt about it: with true, a sender- constrained token becomes exchangeable for an UNBOUND bearer token, so a leaked bound token is once again worth something to whoever finds it, via one request to this endpoint. The binding is not propagated (a cnf naming a key the new holder cannot prove would be a broken grant dressed as a secure one), it is DROPPED, and nothing downstream is told.

It exists because 0.9.0 and earlier did exactly this silently, so a deployment that has already built on the downgrade needs a way to keep running while it migrates. It is not a tuning knob: a host that sets it has decided that its delegation topology is trusted enough to hold the binding for it, and that decision belongs in a sentence somebody wrote and a reviewer can find.

§allow_authorization_details_exchange: bool

Allow an RFC 8693 exchange of a subject token that carries RFC 9396 authorization_details, propagating those details onto a token issued to a DIFFERENT client. false by default, and the default is the safe one.

§Why this is off

The exchange applies TWO ceilings to scope: the subject token’s granted scope (RFC 6749 section 6 narrowing) and then the exchanging client’s own crate::client::Client::allowed_scopes, because the issued token belongs to a different principal. It applied NONE to authorization_details, which are strictly more specific: RFC 9396 exists precisely because a scope token cannot say “transfer 50 euros to IBAN X”.

So the weaker rule was applied to the more dangerous grant. A downstream service registered for read, which receives a payments client’s token because forwarding the caller’s token is exactly what this grant is for, could exchange it asking for read, pass both scope ceilings, and walk away with a token issued to ITSELF carrying the full payment authorization, signed into the RFC 9068 claim, visible over RFC 7662 introspection, and with a fresh lifetime that outlives the token it came from.

§Why an opt-in rather than a per-client ceiling

The correct ceiling is a per-client registration of the detail types a client may hold, the analogue of allowed_scopes. crate::client::Client has no such field, and adding one is a breaking change to a type hosts construct. Until it exists there is nothing to narrow against, so the honest choice is to refuse and let a host that has reasoned about its own delegation topology say so. Setting this to true accepts that any client permitted this grant may inherit any detail any subject token carries.

§refresh_token_ttl: Option<Duration>

Absolute refresh chain lifetime. Rotation preserves the chain’s original expiry rather than sliding it, so this is a ceiling on the whole chain and not on one token.

None (the default) means NO TIME EXPIRY AT ALL: a refresh chain established once lives until something revokes it. None is the default because it is the pre-existing behaviour and turning expiry on by default would sign every deployment’s users out on an interval nobody chose, so, exactly as with ServerConfig::allowed_resources, None is a real risk rather than a neutral one and the risk belongs here rather than in a changelog.

What it costs: a refresh token exfiltrated once is a credential for the user’s account FOREVER, and rotation does not fix that. RFC 9700 section 4.14.2 reuse detection catches the thief only if the legitimate client comes back and presents the token the thief already spent; a thief who steals a chain the user has abandoned, or who simply rotates it faster than the real client does, is never detected by anything and holds access indefinitely. Setting this bounds that to a window, and OAuth 2.1 draft section 6.1 asks for either a finite lifetime or rotation on the same grounds.

A deployment whose users are humans, and whose refresh tokens sit on devices those humans lose, should set this.

§refresh_reuse_window: Duration

How long a ROTATED (spent) refresh token is retained purely so that its reuse can be detected, when its chain has no absolute expiry of its own. Default 30 days.

Reuse detection (OAuth 2.1 draft section 6.1, RFC 9700 section 4.14.2) only works while the superseded token is still recognisable, so this is the window in which a stolen-and-rotated token still triggers revocation of its family. Past it the record is sweepable and a presentation reads as an unknown token. When the chain HAS an absolute expiry, that expiry is used instead: there is nothing left to protect once the chain itself is dead.

§require_dpop: bool
Available on crate feature dpop only.

RFC 9449: whether EVERY token request must carry a DPoP proof.

false by default, which means “DPoP is available, and a client that wants a sender-constrained token asks for one by presenting a proof”. true is the FAPI 2.0 posture: it refuses every token request without a proof, which is a breaking change for every existing client of the deployment and therefore a sentence somebody has to write on purpose rather than a default anybody inherits.

§user_code_length: usize

User code length in symbols, excluding the display hyphen. Default MIN_USER_CODE_LENGTH (about 34 bits over the 20-symbol alphabet, the RFC 8628 section 6.1 example shape).

Values below MIN_USER_CODE_LENGTH are CLAMPED UP at generation, not honoured. This is not tuning: 4 symbols is about 160,000 possibilities, which is seconds of guessing against an endpoint this library cannot rate limit, and 0 produces an empty code that every grant collides on. Clamping rather than rejecting keeps a misconfiguration from becoming a runtime failure at the one moment a user is standing in front of a device.

Implementations§

Source§

impl ServerConfig

Source

pub fn new( issuer: impl Into<String>, verification_uri: impl Into<String>, ) -> Self

A config with RFC-shaped defaults; issuer and verification_uri have no sane default and are required.

Trait Implementations§

Source§

impl Clone for ServerConfig

Source§

fn clone(&self) -> ServerConfig

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ServerConfig

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Eq for ServerConfig

Source§

impl PartialEq for ServerConfig

Source§

fn eq(&self, other: &ServerConfig) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for ServerConfig

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.