use serde::Serialize;
use std::sync::Arc;
use crate::{
App,
headers::{CacheControl, Header},
};
use super::{
AuthorizationServerMetadata, ProtectedResourceMetadata, authorization_server_metadata_url,
openid_configuration_url, protected_resource_metadata_url,
};
const RESOURCE_METADATA_NOT_CONFIGURED: &str = "OAuth protected resource metadata is not configured. \
Use `App::with_oauth_resource_metadata` or `App::set_oauth_resource_metadata` to configure it.";
const SERVER_METADATA_NOT_CONFIGURED: &str = "OAuth authorization server metadata is not configured. \
Use `App::with_oauth_server_metadata` or `App::set_oauth_server_metadata` to configure it.";
impl App {
pub fn with_oauth_resource_metadata<F>(mut self, config: F) -> Self
where
F: FnOnce(ProtectedResourceMetadata) -> ProtectedResourceMetadata,
{
let metadata = self.oauth_resource_metadata.take().unwrap_or_default();
self.oauth_resource_metadata = Some(config(metadata));
self
}
pub fn set_oauth_resource_metadata<M>(mut self, metadata: M) -> Self
where
M: Into<ProtectedResourceMetadata>,
{
self.oauth_resource_metadata = Some(metadata.into());
self
}
pub fn with_oauth_server_metadata<F>(mut self, config: F) -> Self
where
F: FnOnce(AuthorizationServerMetadata) -> AuthorizationServerMetadata,
{
let metadata = self
.oauth_server_metadata
.take()
.unwrap_or_else(|| AuthorizationServerMetadata::new(""));
self.oauth_server_metadata = Some(config(metadata));
self
}
pub fn set_oauth_server_metadata<M>(mut self, metadata: M) -> Self
where
M: Into<AuthorizationServerMetadata>,
{
self.oauth_server_metadata = Some(metadata.into());
self
}
pub fn use_oauth_resource_metadata(&mut self) -> &mut Self {
let metadata = self
.oauth_resource_metadata
.clone()
.expect(RESOURCE_METADATA_NOT_CONFIGURED);
let metadata_url = protected_resource_metadata_url(&metadata.resource)
.unwrap_or_else(|err| panic!("OAuth protected resource metadata: {err}"));
serve_metadata(self, well_known_route(&metadata_url), metadata);
#[cfg(feature = "jwt-auth")]
{
self.oauth_resource_metadata_url = Some(metadata_url);
}
self
}
pub fn use_oauth_server_metadata(&mut self) -> &mut Self {
let metadata = self
.oauth_server_metadata
.clone()
.expect(SERVER_METADATA_NOT_CONFIGURED);
let metadata_url = authorization_server_metadata_url(&metadata.issuer)
.unwrap_or_else(|err| panic!("OAuth authorization server metadata: {err}"));
serve_metadata(self, well_known_route(&metadata_url), metadata);
self
}
pub fn use_oidc_metadata(&mut self) -> &mut Self {
let metadata = self
.oauth_server_metadata
.clone()
.expect(SERVER_METADATA_NOT_CONFIGURED);
let metadata_url = openid_configuration_url(&metadata.issuer)
.unwrap_or_else(|err| panic!("OpenID Connect discovery metadata: {err}"));
serve_metadata(self, well_known_route(&metadata_url), metadata);
self
}
}
fn well_known_route(metadata_url: &str) -> &str {
let after_scheme = metadata_url.find("://").expect("derived URL is absolute") + 3;
let path_start = metadata_url[after_scheme..]
.find('/')
.expect("derived URL contains the well-known path");
&metadata_url[after_scheme + path_start..]
}
fn serve_metadata<T>(app: &mut App, path: &str, metadata: T)
where
T: Serialize + Send + Sync + 'static,
{
let cache_control = metadata_cache_control();
let metadata = Arc::new(metadata);
app.map_get(path, move || {
let metadata = Arc::clone(&metadata);
let cache_control = cache_control.clone();
async move { crate::ok!(metadata.as_ref(); [cache_control]) }
});
}
fn metadata_cache_control() -> Header<CacheControl> {
Header::try_from(CacheControl::default().with_public().with_max_age(3600))
.expect("valid cache control header")
}
#[cfg(test)]
mod tests {
use crate::App;
#[test]
fn it_composes_oauth_metadata_builder_calls() {
let app = App::new()
.set_oauth_resource_metadata("https://api.example.com")
.with_oauth_resource_metadata(|metadata| metadata.with_scopes(["read"]))
.set_oauth_server_metadata("https://auth.example.com")
.with_oauth_server_metadata(|metadata| {
metadata.with_token_endpoint("https://auth.example.com/token")
});
let resource = app.oauth_resource_metadata.as_ref().unwrap();
assert_eq!(resource.resource, "https://api.example.com");
assert_eq!(resource.scopes_supported, ["read"]);
let server = app.oauth_server_metadata.as_ref().unwrap();
assert_eq!(server.issuer, "https://auth.example.com");
assert_eq!(
server.token_endpoint.as_deref(),
Some("https://auth.example.com/token")
);
assert_eq!(server.response_types_supported, ["code"]);
assert_eq!(server.grant_types_supported, ["authorization_code"]);
}
#[test]
fn it_seeds_server_metadata_closure_with_oauth21_prefills() {
let app = App::new().with_oauth_server_metadata(|metadata| {
metadata.with_issuer("https://auth.example.com")
});
let server = app.oauth_server_metadata.as_ref().unwrap();
assert_eq!(server.response_types_supported, ["code"]);
assert_eq!(server.grant_types_supported, ["authorization_code"]);
}
#[test]
#[should_panic(expected = "OAuth protected resource metadata is not configured")]
fn it_panics_when_resource_metadata_is_not_configured() {
App::new().use_oauth_resource_metadata();
}
#[test]
#[should_panic(expected = "OAuth authorization server metadata is not configured")]
fn it_panics_when_server_metadata_is_not_configured() {
App::new().use_oauth_server_metadata();
}
#[test]
#[should_panic(expected = "OAuth authorization server metadata is not configured")]
fn it_panics_when_oidc_metadata_is_not_configured() {
App::new().use_oidc_metadata();
}
}