Skip to main content

TestAuthority

Struct TestAuthority 

Source
pub struct TestAuthority { /* private fields */ }
Available on crate feature testing only.
Expand description

A self-consistent fake authorization server for a consumer’s tests: a loopback HTTP server that serves OpenID Connect discovery, RFC 8414 discovery and a JWKS, with an issuer and jwks_uri that agree with each other, plus everything needed to build a matching config and mint matching tokens.

It publishes the throwaway RSA, P-256 and Ed25519 keys, so a token signed with any algorithm TokenBuilder::alg accepts validates. Every published key carries its own alg (one JWK per RSA algorithm, kids such as test-key-a for RS256 and test-key-a-ps256), so a validator never logs the alg-less-key warning for this JWKS.

The server runs until the TestAuthority is dropped, which stops its accept loop.

§Security

Test-only: the keys are public. See the module docs.

§Examples

use oauth_resource_server::OAuthValidator;
use oauth_resource_server::testing::TestAuthority;

let authority = TestAuthority::start().await;
// Adjust anything before the config is resolved.
let config = authority.config(|c| c.required_scopes = vec!["notes:write".into()]);
let validator = OAuthValidator::new(&config).unwrap();

let ok = authority.token().scopes(["notes:write"]).sign();
assert!(validator.validate(&ok).await.is_ok());
assert_eq!(authority.jwks_fetches(), 1); // the JWKS was fetched once

Implementations§

Source§

impl TestAuthority

Source

pub const AUDIENCE: &'static str = "https://api.example.test/audience"

The audience config accepts and token stamps by default: https://api.example.test/audience. Deliberately different from RESOURCE, the way an API identifier (RFC 8707 style) differs from the resource’s URL in real deployments, so a test catches code that swaps the two.

Source

pub const RESOURCE: &'static str = "https://api.example.test/"

The protected resource config uses: https://api.example.test/.

Source

pub const SCOPE: &'static str = "api:read"

The scope config requires and token carries by default: api:read.

Source

pub async fn start() -> Self

Start the fake authority on a loopback port. Its issuer is its own base URL (http://127.0.0.1:<port>); discovery is served at both /.well-known/openid-configuration and /.well-known/oauth-authorization-server, and the JWKS at /jwks.

§Panics

If no loopback port can be bound. Must be called inside a tokio runtime.

Source

pub fn issuer(&self) -> &str

The issuer this authority stamps into discovery and, by default, into tokens: http://127.0.0.1:<port>.

Source

pub fn jwks_uri(&self) -> &str

The URL of the JWKS: http://127.0.0.1:<port>/jwks.

Source

pub fn jwks_fetches(&self) -> usize

How many times the JWKS has been fetched.

Source

pub fn discovery_fetches(&self) -> usize

How many times a discovery document (OpenID Connect or RFC 8414) has been fetched. Zero when the config sets jwks_uri.

Source

pub fn set_response_delay(&self, delay: Duration)

Stall every response by delay, to test a slow authority. Applies to connections accepted after the call; Duration::ZERO turns it off.

Source

pub fn rotate_key(&self)

Rotate the RSA signing key: from now on token signs RSA tokens with the other throwaway key (KEY_B_PEM after the first rotation, KEY_A_PEM again after the second), and the JWKS is republished with both RSA keys, as a real authority does while tokens signed with the old key are still in flight. The P-256 and Ed25519 keys are unaffected. Follow with withdraw_old_key to model an authority that retires the old key.

A token built before the rotation keeps the old key (the builder captures the key when token is called). A running validator learns the new key on an unknown-kid refetch (at most one per 60 seconds) or OAuthValidator::refresh_now.

Source

pub fn withdraw_old_key(&self)

Withdraw the previous RSA key that rotate_key left published: the JWKS then holds only the active RSA key. A validator that has the old key cached keeps trusting it until its next successful refresh (refresh_now or the hourly background pass); after that, tokens signed with the old key fail. A no-op when nothing is retained.

Source

pub fn config( &self, adjust: impl FnOnce(&mut OAuthConfig), ) -> ResolvedOAuthConfig

A ResolvedOAuthConfig for this authority, with neutral defaults:

  • issuer and jwks_uri are this authority’s own;
  • audience is AUDIENCE (https://api.example.test/audience) and resource RESOURCE (https://api.example.test/);
  • api:read (SCOPE) is the one required scope;
  • settings are named oauth.* in errors (KeyNaming::Dotted);
  • everything else is the crate default, so require_at_jwt is off.

The authority is plain http on loopback, which OAuthConfig::resolve accepts without allow_insecure_http. adjust edits the OAuthConfig before it is resolved: set require_at_jwt, add audiences, clear jwks_uri to exercise discovery, and so on.

§Panics

If the adjusted config does not resolve (the message is the ConfigError text, every problem at once), or adjust sets enabled to false. Acceptable in a test helper; use OAuthConfig::resolve directly to assert on a refusal.

§Examples
use oauth_resource_server::testing::TestAuthority;

let authority = TestAuthority::start().await;
let config = authority.config(|c| {
    c.require_at_jwt = true;
    c.jwks_uri = None; // find the keys through discovery instead
});
assert_eq!(config.issuer, authority.issuer());
assert!(config.jwks_uri.is_none());
Source

pub fn token(&self) -> TokenBuilder

Start a TokenBuilder whose defaults produce a token the config validator accepts: this authority’s issuer, AUDIENCE, subject test-user, scope SCOPE, valid for an hour, typ: at+jwt, signed RS256 with the currently active RSA key.

Trait Implementations§

Source§

impl Debug for TestAuthority

Source§

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

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

impl Drop for TestAuthority

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

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> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
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> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

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

Source§

type Error = !

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

fn try_from(value: U) -> Result<T, !>

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.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more