use std::borrow::Cow;
use std::sync::Arc;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine as _;
use bytes::Buf as _;
use http::{header, HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
use http_body::Body as _;
use serde::Serialize;
use sha2::{Digest as _, Sha256};
use crate::authorization::{AuthorizationError, AuthorizationRequest};
use crate::client::ClientId;
use crate::device::{normalize_user_code, DeviceGrant, DeviceGrantState};
use crate::error::{ErrorCode, ErrorResponse};
use crate::events::{Attempt, AttemptOutcome, RateLimitDecision};
use crate::grant::GrantType;
use crate::metadata::well_known_path;
use crate::scope::ScopeSet;
use crate::server::{AuthorizationServer, Clock, DeviceApprovalError, TokenRequest, UserApproval};
use crate::store::Storage;
use crate::token::TokenTypeHint;
pub use bytes::Bytes;
#[derive(Debug, Default, Clone)]
pub struct Body(Option<Bytes>);
impl Body {
pub fn empty() -> Self {
Body(None)
}
pub fn into_bytes(self) -> Bytes {
self.0.unwrap_or_default()
}
}
impl From<Bytes> for Body {
fn from(bytes: Bytes) -> Self {
match bytes.is_empty() {
true => Body(None),
false => Body(Some(bytes)),
}
}
}
impl From<Vec<u8>> for Body {
fn from(value: Vec<u8>) -> Self {
Body::from(Bytes::from(value))
}
}
impl From<String> for Body {
fn from(value: String) -> Self {
Body::from(Bytes::from(value))
}
}
impl From<&'static str> for Body {
fn from(value: &'static str) -> Self {
Body::from(Bytes::from_static(value.as_bytes()))
}
}
impl http_body::Body for Body {
type Data = Bytes;
type Error = std::convert::Infallible;
fn poll_frame(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
std::task::Poll::Ready(self.0.take().map(|b| Ok(http_body::Frame::data(b))))
}
fn is_end_stream(&self) -> bool {
self.0.is_none()
}
fn size_hint(&self) -> http_body::SizeHint {
http_body::SizeHint::with_exact(self.0.as_ref().map_or(0, |b| b.len() as u64))
}
}
pub type Response = http::Response<Body>;
fn respond(status: StatusCode, body: impl Into<Body>) -> Response {
let mut resp = Response::new(body.into());
*resp.status_mut() = status;
resp
}
pub type SubjectResolver = Arc<dyn Fn(&HeaderMap) -> Option<String> + Send + Sync>;
pub type CsrfTokenHook = Arc<dyn Fn(&HeaderMap) -> Option<String> + Send + Sync>;
#[non_exhaustive]
pub enum ApprovalDecision {
Approve,
Deny,
#[cfg(feature = "consent")]
ApproveAndRemember,
Respond(Box<Response>),
}
#[non_exhaustive]
pub struct ApprovalRequest<'a> {
pub headers: &'a HeaderMap,
pub subject: &'a str,
pub client_id: &'a ClientId,
pub scope: &'a ScopeSet,
pub redirect_uri: &'a str,
pub state: Option<&'a str>,
pub resource: &'a [String],
#[cfg(feature = "rar")]
pub authorization_details: &'a crate::rar::AuthorizationDetails,
pub uri: &'a Uri,
#[cfg(feature = "consent")]
pub remembered: Option<&'a crate::consent::ConsentRecord>,
}
pub type ApprovalResolver = Arc<dyn Fn(&ApprovalRequest<'_>) -> ApprovalDecision + Send + Sync>;
#[cfg(feature = "consent")]
pub type AuthenticationReporter =
Arc<dyn Fn(&HeaderMap) -> Option<crate::consent::Authentication> + Send + Sync>;
enum VerificationProtection {
Unwired,
Tokens {
issue: CsrfTokenHook,
consume: CsrfTokenHook,
},
Disabled,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ServiceError {
EndpointOutsideIssuer {
endpoint: &'static str,
url: String,
},
DuplicatePath {
path: String,
},
MetadataNotSerializable {
detail: String,
},
#[cfg(feature = "jwt")]
JwksNotSerializable {
detail: String,
},
#[cfg(not(feature = "jwt"))]
JwksNotServable {
url: String,
},
}
impl std::fmt::Display for ServiceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ServiceError::EndpointOutsideIssuer { endpoint, url } => write!(
f,
"advertised {endpoint} ({url}) is not under the issuer, so this router cannot \
serve it"
),
ServiceError::DuplicatePath { path } => {
write!(f, "two endpoints resolve to the same path {path}")
}
ServiceError::MetadataNotSerializable { detail } => {
write!(f, "RFC 8414 metadata could not be serialized: {detail}")
}
#[cfg(feature = "jwt")]
ServiceError::JwksNotSerializable { detail } => {
write!(f, "RFC 7517 key set could not be serialized: {detail}")
}
#[cfg(not(feature = "jwt"))]
ServiceError::JwksNotServable { url } => write!(
f,
"advertised jwks_uri ({url}) is under the issuer, but this build has no jwt \
feature and so has no key set to serve there"
),
}
}
}
impl std::error::Error for ServiceError {}
struct Inner<S: Storage, C: Clock> {
server: Arc<AuthorizationServer<S, C>>,
metadata: Bytes,
#[cfg(feature = "jwt")]
jwks: Option<Bytes>,
challenge: HeaderValue,
origin: String,
subject: Option<SubjectResolver>,
approval: Option<ApprovalResolver>,
#[cfg(feature = "consent")]
authentication: Option<AuthenticationReporter>,
verification: VerificationProtection,
routes: Routes,
}
impl<S: Storage, C: Clock> Inner<S, C> {
fn subject(&self, headers: &HeaderMap) -> Option<String> {
self.subject.as_ref().and_then(|f| f(headers))
}
}
pub struct ServiceBuilder<S: Storage, C: Clock> {
server: Arc<AuthorizationServer<S, C>>,
subject: Option<SubjectResolver>,
approval: Option<ApprovalResolver>,
#[cfg(feature = "consent")]
authentication: Option<AuthenticationReporter>,
verification: VerificationProtection,
}
impl<S: Storage + 'static, C: Clock + 'static> ServiceBuilder<S, C> {
pub fn new(server: Arc<AuthorizationServer<S, C>>) -> Self {
ServiceBuilder {
server,
subject: None,
approval: None,
#[cfg(feature = "consent")]
authentication: None,
verification: VerificationProtection::Unwired,
}
}
pub fn with_subject_resolver<F>(mut self, resolver: F) -> Self
where
F: Fn(&HeaderMap) -> Option<String> + Send + Sync + 'static,
{
self.subject = Some(Arc::new(resolver));
self
}
pub fn with_approval_resolver<F>(mut self, resolver: F) -> Self
where
F: Fn(&ApprovalRequest<'_>) -> ApprovalDecision + Send + Sync + 'static,
{
self.approval = Some(Arc::new(resolver));
self
}
#[cfg(feature = "consent")]
pub fn with_authentication_reporter<F>(mut self, reporter: F) -> Self
where
F: Fn(&HeaderMap) -> Option<crate::consent::Authentication> + Send + Sync + 'static,
{
self.authentication = Some(Arc::new(reporter));
self
}
pub fn with_csrf_tokens<I, V>(mut self, issue: I, consume: V) -> Self
where
I: Fn(&HeaderMap) -> Option<String> + Send + Sync + 'static,
V: Fn(&HeaderMap) -> Option<String> + Send + Sync + 'static,
{
self.verification = VerificationProtection::Tokens {
issue: Arc::new(issue),
consume: Arc::new(consume),
};
self
}
pub fn dangerously_disable_verification_protections(mut self) -> Self {
self.verification = VerificationProtection::Disabled;
self
}
pub fn build(self) -> Result<AuthorizationService<S, C>, ServiceError> {
let config = self.server.config();
#[allow(unused_mut)]
let mut meta = self.server.metadata();
let issuer = meta.issuer.clone();
#[cfg(feature = "mtls")]
{
meta.token_endpoint_auth_methods_supported.retain(|m| {
m != crate::mtls::TLS_CLIENT_AUTH && m != crate::mtls::SELF_SIGNED_TLS_CLIENT_AUTH
});
meta.tls_client_certificate_bound_access_tokens = false;
}
let default_introspection_endpoint = format!("{issuer}/introspect");
let authorize = endpoint_path(
&issuer,
"authorization_endpoint",
&meta.authorization_endpoint,
)?;
let token = endpoint_path(&issuer, "token_endpoint", &meta.token_endpoint)?;
let device = endpoint_path(
&issuer,
"device_authorization_endpoint",
&meta.device_authorization_endpoint,
)?;
let introspect = Some(endpoint_path(
&issuer,
"introspection_endpoint",
match &config.introspection_endpoint {
Some(u) => u,
None => &default_introspection_endpoint,
},
)?);
let revoke = match &meta.revocation_endpoint {
Some(u) => Some(endpoint_path(&issuer, "revocation_endpoint", u)?),
None => None,
};
let register = match &meta.registration_endpoint {
Some(u) => Some(endpoint_path(&issuer, "registration_endpoint", u)?),
None => None,
};
let manage_prefix = register
.as_ref()
.filter(|_| {
config
.registration
.as_ref()
.is_some_and(|r| r.management_enabled)
})
.map(|p| format!("{p}/"));
let manage = manage_prefix.as_ref().map(|p| format!("{p}{{client_id}}"));
let verification =
endpoint_path(&issuer, "verification_uri", &config.verification_uri).ok();
#[cfg(feature = "par")]
let par = match &meta.pushed_authorization_request_endpoint {
Some(u) => Some(endpoint_path(
&issuer,
"pushed_authorization_request_endpoint",
u,
)?),
None => None,
};
#[cfg(feature = "jwt")]
let jwks_path = match &meta.jwks_uri {
Some(url) => Some(endpoint_path(&issuer, "jwks_uri", url)?),
None => None,
};
#[cfg(not(feature = "jwt"))]
if let Some(url) = &meta.jwks_uri {
if endpoint_path(&issuer, "jwks_uri", url).is_ok() {
return Err(ServiceError::JwksNotServable {
url: url.to_string(),
});
}
}
#[cfg(feature = "jwt")]
let jwks = match (&jwks_path, self.server.jwks()) {
(Some(_), Some(keys)) => Some(Bytes::from(serde_json::to_vec(&keys).map_err(|e| {
ServiceError::JwksNotSerializable {
detail: e.to_string(),
}
})?)),
_ => None,
};
let well_known = encode_route_path(&well_known_path(&issuer));
let metadata = serde_json::to_vec(&meta)
.map_err(|e| ServiceError::MetadataNotSerializable {
detail: e.to_string(),
})?
.into();
let mut paths: Vec<&str> = vec![&well_known, &authorize, &token, &device];
paths.extend(introspect.as_deref());
paths.extend(revoke.as_deref());
paths.extend(verification.as_deref());
paths.extend(register.as_deref());
paths.extend(manage.as_deref());
#[cfg(feature = "par")]
paths.extend(par.as_deref());
#[cfg(feature = "jwt")]
paths.extend(jwks_path.as_deref());
for i in 0..paths.len() {
if paths[i + 1..].contains(&paths[i]) {
return Err(ServiceError::DuplicatePath {
path: paths[i].to_string(),
});
}
}
let routes = Routes {
well_known,
authorize,
token,
device,
introspect,
revoke,
verification,
register,
manage: manage_prefix,
#[cfg(feature = "par")]
par,
#[cfg(feature = "jwt")]
jwks: jwks_path,
};
Ok(AuthorizationService {
inner: Arc::new(Inner {
server: self.server,
metadata,
#[cfg(feature = "jwt")]
jwks,
challenge: HeaderValue::from_str(&format!(
"Basic realm=\"{}\"",
escape_quoted_string(&issuer)
))
.unwrap_or_else(|_| HeaderValue::from_static("Basic realm=\"oauth\"")),
origin: issuer_origin(&issuer).to_string(),
subject: self.subject,
approval: self.approval,
#[cfg(feature = "consent")]
authentication: self.authentication,
verification: self.verification,
routes,
}),
})
}
}
pub const MAX_BODY_BYTES: usize = 64 * 1024;
pub const MAX_FORM_PARAMETERS: usize = 64;
fn endpoint_path(issuer: &str, endpoint: &'static str, url: &str) -> Result<String, ServiceError> {
match url.strip_prefix(issuer) {
Some(rest) if rest.starts_with('/') => {
let prefix = crate::metadata::issuer_path(issuer);
let mut path = String::with_capacity(prefix.len() + rest.len());
path.push_str(prefix);
path.push_str(rest);
Ok(encode_route_path(&path))
}
_ => Err(ServiceError::EndpointOutsideIssuer {
endpoint,
url: url.to_string(),
}),
}
}
fn issuer_origin(issuer: &str) -> &str {
let authority_at = match issuer.find("://") {
Some(i) => i + 3,
None => 0,
};
match issuer[authority_at..].find('/') {
Some(i) => &issuer[..authority_at + i],
None => issuer,
}
}
#[derive(Debug)]
struct Routes {
well_known: String,
authorize: String,
token: String,
device: String,
introspect: Option<String>,
revoke: Option<String>,
verification: Option<String>,
register: Option<String>,
manage: Option<String>,
#[cfg(feature = "par")]
par: Option<String>,
#[cfg(feature = "jwt")]
jwks: Option<String>,
}
enum Route<'a> {
Metadata,
Authorize,
Token,
Device,
Introspect,
Revoke,
Verification,
Register,
Manage(&'a str),
#[cfg(feature = "par")]
Par,
#[cfg(feature = "jwt")]
Jwks,
}
impl Routes {
fn resolve<'a>(&self, path: &'a str) -> Option<Route<'a>> {
if path == self.well_known {
return Some(Route::Metadata);
}
if path == self.authorize {
return Some(Route::Authorize);
}
if path == self.token {
return Some(Route::Token);
}
if path == self.device {
return Some(Route::Device);
}
if self.introspect.as_deref() == Some(path) {
return Some(Route::Introspect);
}
if self.revoke.as_deref() == Some(path) {
return Some(Route::Revoke);
}
if self.verification.as_deref() == Some(path) {
return Some(Route::Verification);
}
if self.register.as_deref() == Some(path) {
return Some(Route::Register);
}
#[cfg(feature = "par")]
if self.par.as_deref() == Some(path) {
return Some(Route::Par);
}
#[cfg(feature = "jwt")]
if self.jwks.as_deref() == Some(path) {
return Some(Route::Jwks);
}
if let Some(prefix) = &self.manage {
if let Some(rest) = path.strip_prefix(prefix.as_str()) {
if !rest.is_empty() && !rest.contains('/') {
return Some(Route::Manage(rest));
}
}
}
None
}
}
fn allowed(route: &Route<'_>) -> &'static str {
match route {
Route::Metadata | Route::Authorize => "GET, HEAD",
#[cfg(feature = "jwt")]
Route::Jwks => "GET, HEAD",
Route::Token | Route::Device | Route::Introspect | Route::Revoke | Route::Register => {
"POST"
}
#[cfg(feature = "par")]
Route::Par => "POST",
Route::Verification => "GET, HEAD, POST",
Route::Manage(_) => "GET, HEAD, PUT, DELETE",
}
}
pub struct AuthorizationService<S: Storage, C: Clock> {
inner: Arc<Inner<S, C>>,
}
impl<S: Storage, C: Clock> std::fmt::Debug for AuthorizationService<S, C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthorizationService")
.field("routes", &self.inner.routes)
.finish_non_exhaustive()
}
}
impl<S: Storage, C: Clock> Clone for AuthorizationService<S, C> {
fn clone(&self) -> Self {
AuthorizationService {
inner: Arc::clone(&self.inner),
}
}
}
impl<S: Storage, C: Clock> AuthorizationService<S, C> {
pub async fn handle<B>(&self, request: Request<B>) -> Response
where
B: http_body::Body,
{
let state = &*self.inner;
let (parts, body) = request.into_parts();
let method = parts.method;
let headers = parts.headers;
let uri = parts.uri;
let path = uppercase_escapes(uri.path());
let route = match state.routes.resolve(&path) {
Some(route) => route,
None => return respond(StatusCode::NOT_FOUND, Body::empty()),
};
let head = method == Method::HEAD;
let method = match head {
true => Method::GET,
false => method,
};
let mut response = self.dispatch(route, &method, headers, &uri, body).await;
if head {
let length = response.body().size_hint().exact().unwrap_or(0);
*response.body_mut() = Body::empty();
if let Ok(value) = HeaderValue::from_str(&length.to_string()) {
response.headers_mut().insert(header::CONTENT_LENGTH, value);
}
}
response
}
async fn dispatch<B>(
&self,
route: Route<'_>,
method: &Method,
headers: HeaderMap,
uri: &Uri,
body: B,
) -> Response
where
B: http_body::Body,
{
let state = &*self.inner;
macro_rules! form_body {
() => {
match collect_body(body, MAX_BODY_BYTES).await {
Ok(bytes) => bytes,
Err(e) => return body_error(e),
}
};
}
match (route, method.as_str()) {
(Route::Metadata, "GET") => metadata_handler(state),
#[cfg(feature = "jwt")]
(Route::Jwks, "GET") => jwks_handler(state),
(Route::Authorize, "GET") => authorize_handler(state, &headers, uri).await,
(Route::Token, "POST") => token_handler(state, &headers, &form_body!()).await,
(Route::Device, "POST") => {
device_authorization_handler(state, &headers, &form_body!()).await
}
(Route::Introspect, "POST") => introspect_handler(state, &headers, &form_body!()).await,
(Route::Revoke, "POST") => revoke_handler(state, &headers, &form_body!()).await,
#[cfg(feature = "par")]
(Route::Par, "POST") => {
pushed_authorization_handler(state, &headers, &form_body!()).await
}
(Route::Verification, "GET") => verification_page_handler(state, &headers, uri).await,
(Route::Verification, "POST") => {
verification_submit_handler(state, &headers, &form_body!()).await
}
(Route::Register, "POST") => register_handler(state, &headers, &form_body!()).await,
(Route::Manage(client_id), "GET") => {
read_registration_handler(state, &headers, &decode_path_segment(client_id)).await
}
(Route::Manage(client_id), "PUT") => {
let id = decode_path_segment(client_id).into_owned();
update_registration_handler(state, &headers, &id, &form_body!()).await
}
(Route::Manage(client_id), "DELETE") => {
delete_registration_handler(state, &headers, &decode_path_segment(client_id)).await
}
(route, _) => {
let mut resp = respond(StatusCode::METHOD_NOT_ALLOWED, Body::empty());
resp.headers_mut()
.insert(header::ALLOW, HeaderValue::from_static(allowed(&route)));
resp
}
}
}
}
enum BodyError {
TooLarge,
Incomplete,
}
fn body_error(e: BodyError) -> Response {
match e {
BodyError::TooLarge => text_response(
StatusCode::PAYLOAD_TOO_LARGE,
"request body exceeds this server's limit",
),
BodyError::Incomplete => {
text_response(StatusCode::BAD_REQUEST, "request body was not received")
}
}
}
async fn collect_body<B>(body: B, limit: usize) -> Result<Bytes, BodyError>
where
B: http_body::Body,
{
let hint = body.size_hint();
if hint.lower() > limit as u64 {
return Err(BodyError::TooLarge);
}
let expected = hint.upper().unwrap_or(hint.lower()).min(limit as u64) as usize;
let mut collected: Vec<u8> = Vec::with_capacity(expected);
let mut body = std::pin::pin!(body);
loop {
match std::future::poll_fn(|cx| body.as_mut().poll_frame(cx)).await {
None => break,
Some(Err(_)) => return Err(BodyError::Incomplete),
Some(Ok(frame)) => {
if let Ok(mut data) = frame.into_data() {
if collected.len().saturating_add(data.remaining()) > limit {
return Err(BodyError::TooLarge);
}
while data.has_remaining() {
let chunk = data.chunk();
collected.extend_from_slice(chunk);
let n = chunk.len();
data.advance(n);
}
}
}
}
}
Ok(Bytes::from(collected))
}
#[cfg(feature = "axum")]
impl<S, C> From<AuthorizationService<S, C>> for axum::Router
where
S: Storage + Send + Sync + 'static,
C: Clock + Send + Sync + 'static,
{
fn from(service: AuthorizationService<S, C>) -> axum::Router {
axum::Router::new().fallback(move |request: axum::extract::Request| {
let service = service.clone();
async move {
match tokio::spawn(async move { service.handle(request).await }).await {
Ok(response) => response.map(|body| axum::body::Body::from(body.into_bytes())),
Err(_) => {
let mut response = axum::http::Response::new(axum::body::Body::empty());
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
response
}
}
}
})
}
}
fn json_content_type() -> HeaderValue {
HeaderValue::from_static("application/json;charset=UTF-8")
}
#[cfg(feature = "jwt")]
fn jwks_content_type() -> HeaderValue {
HeaderValue::from_static("application/jwk-set+json")
}
fn html_content_type() -> HeaderValue {
HeaderValue::from_static("text/html;charset=UTF-8")
}
fn json_body<T: Serialize>(value: &T) -> Vec<u8> {
serde_json::to_vec(value).unwrap_or_else(|_| br#"{"error":"server_error"}"#.to_vec())
}
fn no_store(headers: &mut HeaderMap) {
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_static("no-store, no-cache, max-age=0"),
);
headers.insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
}
fn ok_json<T: Serialize>(value: &T) -> Response {
let mut resp = respond(StatusCode::OK, json_body(value));
let headers = resp.headers_mut();
headers.insert(header::CONTENT_TYPE, json_content_type());
no_store(headers);
resp
}
fn error_response(err: &ErrorResponse, via_header: bool, challenge: &HeaderValue) -> Response {
let mut status =
StatusCode::from_u16(err.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
if status == StatusCode::UNAUTHORIZED && !via_header {
status = StatusCode::BAD_REQUEST;
}
let mut resp = respond(status, json_body(err));
let headers = resp.headers_mut();
headers.insert(header::CONTENT_TYPE, json_content_type());
no_store(headers);
if status == StatusCode::UNAUTHORIZED {
headers.insert(header::WWW_AUTHENTICATE, challenge.clone());
}
resp
}
fn html_response(status: StatusCode, body: String) -> Response {
let mut resp = respond(status, body);
let headers = resp.headers_mut();
headers.insert(header::CONTENT_TYPE, html_content_type());
no_store(headers);
headers.insert(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_static(
"default-src 'none'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
),
);
headers.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
headers.insert(
header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
);
headers.insert(
header::REFERRER_POLICY,
HeaderValue::from_static("no-referrer"),
);
resp
}
fn hex_value(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
fn decode_component(raw: &str) -> Cow<'_, str> {
percent_decode(raw, true)
}
fn decode_path_segment(raw: &str) -> Cow<'_, str> {
percent_decode(raw, false)
}
fn encode_route_path(path: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let bytes = path.as_bytes();
let mut out = String::with_capacity(path.len());
let mut skip = 0usize;
for (i, &b) in bytes.iter().enumerate() {
if skip > 0 {
skip -= 1;
continue;
}
if b == b'%' {
if let (Some(&h), Some(&l)) = (bytes.get(i + 1), bytes.get(i + 2)) {
if hex_value(h).is_some() && hex_value(l).is_some() {
out.push('%');
out.push(h.to_ascii_uppercase() as char);
out.push(l.to_ascii_uppercase() as char);
skip = 2;
continue;
}
}
}
let verbatim = b.is_ascii_alphanumeric()
|| matches!(
b,
b'-' | b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b':'
| b'@'
| b'/'
| b'%'
);
if verbatim {
out.push(b as char);
} else {
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
}
out
}
fn uppercase_escapes(path: &str) -> Cow<'_, str> {
let bytes = path.as_bytes();
let needs = bytes.iter().enumerate().any(|(i, &b)| {
b == b'%'
&& matches!(
(bytes.get(i + 1), bytes.get(i + 2)),
(Some(&h), Some(&l))
if hex_value(h).is_some()
&& hex_value(l).is_some()
&& (h.is_ascii_lowercase() || l.is_ascii_lowercase())
)
});
if !needs {
return Cow::Borrowed(path);
}
let mut out = String::with_capacity(path.len());
let mut i = 0;
while i < bytes.len() {
match (bytes[i], bytes.get(i + 1), bytes.get(i + 2)) {
(b'%', Some(&h), Some(&l)) if hex_value(h).is_some() && hex_value(l).is_some() => {
out.push('%');
out.push(h.to_ascii_uppercase() as char);
out.push(l.to_ascii_uppercase() as char);
i += 3;
}
_ => {
let start = i;
i += 1;
while i < bytes.len() && bytes[i] != b'%' {
i += 1;
}
out.push_str(&path[start..i]);
}
}
}
Cow::Owned(out)
}
fn percent_decode(raw: &str, plus_is_space: bool) -> Cow<'_, str> {
if !raw
.bytes()
.any(|b| b == b'%' || (plus_is_space && b == b'+'))
{
return Cow::Borrowed(raw);
}
let bytes = raw.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'+' if plus_is_space => {
out.push(b' ');
i += 1;
}
b'%' if i + 2 < bytes.len() => {
match hex_pair(hex_value(bytes[i + 1]), hex_value(bytes[i + 2])) {
Some((h, l)) => {
out.push((h << 4) | l);
i += 3;
}
None => {
out.push(b'%');
i += 1;
}
}
}
b => {
out.push(b);
i += 1;
}
}
}
Cow::Owned(match String::from_utf8(out) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
})
}
fn hex_pair(a: Option<u8>, b: Option<u8>) -> Option<(u8, u8)> {
match (a, b) {
(Some(h), Some(l)) => Some((h, l)),
_ => None,
}
}
type Pair<'a> = (Cow<'a, str>, Cow<'a, str>);
struct TooManyParameters;
fn too_many_parameters() -> Response {
text_response(
StatusCode::PAYLOAD_TOO_LARGE,
"request carries too many parameters",
)
}
fn text_response(status: StatusCode, body: &'static str) -> Response {
let mut resp = respond(status, body);
resp.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain;charset=UTF-8"),
);
resp
}
fn parse_pairs(input: &str) -> Result<Vec<Pair<'_>>, TooManyParameters> {
let mut separators = 0usize;
for b in input.bytes() {
if b == b'&' {
separators += 1;
if separators >= MAX_FORM_PARAMETERS {
return Err(TooManyParameters);
}
}
}
let bound = separators + 1;
let mut pairs = Vec::with_capacity(bound);
pairs.extend(
input
.split('&')
.filter(|part| !part.is_empty())
.map(|part| match part.split_once('=') {
Some((k, v)) => (decode_component(k), decode_component(v)),
None => (decode_component(part), Cow::Borrowed("")),
}),
);
Ok(pairs)
}
fn param<'a>(pairs: &'a [Pair<'a>], name: &str) -> Option<&'a str> {
pairs
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.as_ref())
}
fn required<'a>(pairs: &'a [Pair<'a>], name: &'static str) -> Result<&'a str, ErrorResponse> {
param(pairs, name).ok_or_else(|| {
let description: Cow<'static, str> = match name {
"code" => Cow::Borrowed("missing required parameter code"),
"device_code" => Cow::Borrowed("missing required parameter device_code"),
"refresh_token" => Cow::Borrowed("missing required parameter refresh_token"),
"token" => Cow::Borrowed("missing required parameter token"),
"subject_token" => Cow::Borrowed("missing required parameter subject_token"),
"subject_token_type" => Cow::Borrowed("missing required parameter subject_token_type"),
other => Cow::Owned(format!("missing required parameter {other}")),
};
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(description)
})
}
fn resource_indicators(pairs: &[Pair<'_>]) -> Vec<String> {
pairs
.iter()
.filter(|(k, _)| k == "resource")
.map(|(_, v)| v.as_ref().to_string())
.collect()
}
fn refuse_authorization_details(pairs: &[Pair<'_>]) -> Option<ErrorResponse> {
param(pairs, "authorization_details").map(|_| {
ErrorResponse::new(ErrorCode::InvalidAuthorizationDetails)
.with_description("this server does not accept authorization_details on this grant")
})
}
fn optional_scope(pairs: &[Pair<'_>]) -> Result<Option<ScopeSet>, ErrorResponse> {
match param(pairs, "scope") {
None => Ok(None),
Some(s) => ScopeSet::parse(s).map(Some).map_err(|_| {
ErrorResponse::new(ErrorCode::InvalidScope)
.with_description("scope is not a space-delimited RFC 6749 s3.3 token list")
}),
}
}
struct Credentials {
client_id: String,
client_secret: Option<String>,
#[cfg(feature = "client-assertion")]
client_assertion_type: Option<String>,
#[cfg(feature = "client-assertion")]
client_assertion: Option<String>,
}
impl Credentials {
fn credential(&self) -> crate::server::ClientCredential<'_> {
crate::server::ClientCredential {
client_secret: self.client_secret.as_deref(),
#[cfg(feature = "client-assertion")]
client_assertion_type: self.client_assertion_type.as_deref(),
#[cfg(feature = "client-assertion")]
client_assertion: self.client_assertion.as_deref(),
#[cfg(feature = "mtls")]
certificate: None,
}
}
}
fn basic_attempted(headers: &HeaderMap) -> bool {
headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.is_some_and(|v| v.len() >= 6 && v[..6].eq_ignore_ascii_case("basic "))
}
fn decode_basic(headers: &HeaderMap) -> Result<(String, String), ErrorResponse> {
let malformed = || {
ErrorResponse::new(ErrorCode::InvalidClient)
.with_description("malformed HTTP Basic credentials (RFC 6749 s2.3.1)")
};
let raw = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.ok_or_else(malformed)?;
let encoded = raw.get(6..).ok_or_else(malformed)?.trim();
let decoded = BASE64_STANDARD.decode(encoded).map_err(|_| malformed())?;
let text = String::from_utf8(decoded).map_err(|_| malformed())?;
let (id, secret) = text.split_once(':').ok_or_else(malformed)?;
Ok((
decode_component(id).into_owned(),
decode_component(secret).into_owned(),
))
}
fn credentials(headers: &HeaderMap, form: &[Pair<'_>]) -> Result<Credentials, ErrorResponse> {
credentials_where(headers, form, false)
}
#[cfg(feature = "par")]
fn pushed_request_credentials(
headers: &HeaderMap,
form: &[Pair<'_>],
) -> Result<Credentials, ErrorResponse> {
credentials_where(headers, form, true)
}
fn credentials_where(
headers: &HeaderMap,
form: &[Pair<'_>],
client_id_is_a_request_parameter: bool,
) -> Result<Credentials, ErrorResponse> {
let basic = basic_attempted(headers);
let body_id = param(form, "client_id");
let body_secret = param(form, "client_secret");
#[cfg(feature = "client-assertion")]
if let Some(assertion) = param(form, "client_assertion") {
if basic || body_secret.is_some() {
return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("more than one client authentication method (RFC 6749 s2.3)"));
}
let client_id = match body_id {
Some(id) => id.to_string(),
None => crate::client_assertion::unverified_subject(assertion)
.ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidClient)
.with_description("the client assertion names no client")
})?
.to_string(),
};
return Ok(Credentials {
client_id,
client_secret: None,
client_assertion_type: param(form, "client_assertion_type").map(str::to_string),
client_assertion: Some(assertion.to_string()),
});
}
match (basic, body_id, body_secret) {
(true, None, None) | (true, Some(_), None) if client_id_is_a_request_parameter => {
let (client_id, client_secret) = decode_basic(headers)?;
Ok(Credentials {
client_id,
client_secret: Some(client_secret),
#[cfg(feature = "client-assertion")]
client_assertion_type: None,
#[cfg(feature = "client-assertion")]
client_assertion: None,
})
}
(true, None, None) => {
let (client_id, client_secret) = decode_basic(headers)?;
Ok(Credentials {
client_id,
client_secret: Some(client_secret),
#[cfg(feature = "client-assertion")]
client_assertion_type: None,
#[cfg(feature = "client-assertion")]
client_assertion: None,
})
}
(true, _, _) => Err(ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("more than one client authentication method (RFC 6749 s2.3)")),
(false, Some(id), secret) => Ok(Credentials {
client_id: id.to_string(),
client_secret: secret.map(str::to_string),
#[cfg(feature = "client-assertion")]
client_assertion_type: None,
#[cfg(feature = "client-assertion")]
client_assertion: None,
}),
(false, None, _) => Err(ErrorResponse::new(ErrorCode::InvalidClient)
.with_description("no client authentication or client_id")),
}
}
fn metadata_handler<S: Storage, C: Clock>(state: &Inner<S, C>) -> Response {
let mut resp = respond(StatusCode::OK, state.metadata.clone());
resp.headers_mut()
.insert(header::CONTENT_TYPE, json_content_type());
resp
}
#[cfg(feature = "jwt")]
fn jwks_handler<S: Storage, C: Clock>(state: &Inner<S, C>) -> Response {
match &state.jwks {
Some(bytes) => {
let mut resp = respond(StatusCode::OK, bytes.clone());
resp.headers_mut()
.insert(header::CONTENT_TYPE, jwks_content_type());
resp
}
None => respond(StatusCode::NOT_FOUND, Body::empty()),
}
}
async fn token_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
body: &Bytes,
) -> Response {
let via_header = basic_attempted(headers);
let text = String::from_utf8_lossy(body);
let form = match parse_pairs(&text) {
Ok(form) => form,
Err(TooManyParameters) => return too_many_parameters(),
};
let grant = match param(&form, "grant_type") {
None => {
return error_response(
&ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("missing required parameter grant_type"),
via_header,
&state.challenge,
)
}
Some(value) => match GrantType::parse(value) {
Some(g) => g,
None => {
return error_response(
&ErrorResponse::new(ErrorCode::UnsupportedGrantType)
.with_description("this server does not implement the requested grant"),
via_header,
&state.challenge,
)
}
},
};
let mut creds = match credentials(headers, &form) {
Ok(c) => c,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let client_id = ClientId::new(std::mem::take(&mut creds.client_id));
let client_secret: Option<String> = None;
#[cfg(feature = "dpop")]
let dpop_proof = {
let mut values = headers.get_all(crate::dpop::DPOP_HEADER).iter();
let first = values.next();
if values.next().is_some() {
state
.server
.hooks()
.emit(|| crate::events::Event::DpopProofRefused {
failure: crate::dpop::DpopFailure::Malformed,
});
return error_response(
&ErrorResponse::new(ErrorCode::InvalidDpopProof)
.with_description("more than one DPoP header (RFC 9449 s4.3)"),
via_header,
&state.challenge,
);
}
match first.map(|v| v.to_str()) {
None => None,
Some(Ok(value)) => Some(value),
Some(Err(_)) => {
state
.server
.hooks()
.emit(|| crate::events::Event::DpopProofRefused {
failure: crate::dpop::DpopFailure::Malformed,
});
return error_response(
&ErrorResponse::new(ErrorCode::InvalidDpopProof)
.with_description("the DPoP header is not a compact JWS"),
via_header,
&state.challenge,
);
}
}
};
let request = match grant {
GrantType::AuthorizationCode => {
let code = match required(&form, "code") {
Ok(v) => v.to_string(),
Err(e) => return error_response(&e, via_header, &state.challenge),
};
TokenRequest::AuthorizationCode {
client_id,
client_secret,
code,
redirect_uri: param(&form, "redirect_uri").map(str::to_string),
code_verifier: param(&form, "code_verifier").map(str::to_string),
}
}
GrantType::ClientCredentials => {
let scope = match optional_scope(&form) {
Ok(s) => s,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
TokenRequest::ClientCredentials {
client_id,
client_secret,
scope,
}
}
GrantType::DeviceCode => {
let device_code = match required(&form, "device_code") {
Ok(v) => v.to_string(),
Err(e) => return error_response(&e, via_header, &state.challenge),
};
TokenRequest::DeviceCode {
client_id,
client_secret,
device_code,
}
}
GrantType::RefreshToken => {
let refresh_token = match required(&form, "refresh_token") {
Ok(v) => v.to_string(),
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let scope = match optional_scope(&form) {
Ok(s) => s,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
TokenRequest::RefreshToken {
client_id,
client_secret,
refresh_token,
scope,
}
}
#[cfg(feature = "token-exchange")]
GrantType::TokenExchange => {
#[cfg(feature = "dpop")]
if dpop_proof.is_some() {
state
.server
.hooks()
.emit(|| crate::events::Event::DpopProofRefused {
failure: crate::dpop::DpopFailure::NotAcceptedHere,
});
return error_response(
&ErrorResponse::new(ErrorCode::InvalidDpopProof).with_description(
"this server does not issue sender-constrained tokens through RFC 8693 \
token exchange",
),
via_header,
&state.challenge,
);
}
return token_exchange_response(state, &form, client_id, &creds, via_header).await;
}
};
let resources = resource_indicators(&form);
let context = crate::server::TokenRequestContext {
credential: creds.credential(),
resources: &resources,
authorization_details: param(&form, "authorization_details"),
#[cfg(feature = "dpop")]
dpop_proof,
};
match state.server.token_with_context(request, context).await {
Ok(response) => ok_json(&response),
Err(e) => error_response(&e, via_header, &state.challenge),
}
}
#[cfg(feature = "token-exchange")]
async fn token_exchange_response<S: Storage, C: Clock>(
state: &Inner<S, C>,
form: &[Pair<'_>],
client_id: ClientId,
creds: &Credentials,
via_header: bool,
) -> Response {
const SUBJECT_NOT_A_TOKEN_TYPE: &str =
"subject_token_type is not a token type RFC 8693 s3 registers";
const ACTOR_NOT_A_TOKEN_TYPE: &str =
"actor_token_type is not a token type RFC 8693 s3 registers";
const REQUESTED_NOT_A_TOKEN_TYPE: &str =
"requested_token_type is not a token type RFC 8693 s3 registers";
fn token_type(
refusal: &'static str,
value: &str,
) -> Result<crate::token_exchange::TokenTypeIdentifier, ErrorResponse> {
crate::token_exchange::TokenTypeIdentifier::parse(value)
.ok_or_else(|| ErrorResponse::new(ErrorCode::InvalidRequest).with_description(refusal))
}
if let Some(refusal) = refuse_authorization_details(form) {
return error_response(&refusal, via_header, &state.challenge);
}
let subject_token = match required(form, "subject_token") {
Ok(v) => v,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let subject_token_type = match required(form, "subject_token_type")
.and_then(|v| token_type(SUBJECT_NOT_A_TOKEN_TYPE, v))
{
Ok(v) => v,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let actor_token = param(form, "actor_token");
let actor_token_type = match param(form, "actor_token_type")
.map(|v| token_type(ACTOR_NOT_A_TOKEN_TYPE, v))
.transpose()
{
Ok(v) => v,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let requested_token_type = match param(form, "requested_token_type")
.map(|v| token_type(REQUESTED_NOT_A_TOKEN_TYPE, v))
.transpose()
{
Ok(v) => v,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let scope = match optional_scope(form) {
Ok(s) => s,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let resource = resource_indicators(form);
let audience: Vec<String> = form
.iter()
.filter(|(k, _)| k == "audience")
.map(|(_, v)| v.as_ref().to_string())
.collect();
let request = crate::token_exchange::TokenExchangeRequest {
client_id: &client_id,
client_secret: creds.client_secret.as_deref(),
#[cfg(feature = "client-assertion")]
client_assertion_type: creds.client_assertion_type.as_deref(),
#[cfg(feature = "client-assertion")]
client_assertion: creds.client_assertion.as_deref(),
subject_token,
subject_token_type,
actor_token,
actor_token_type,
resource: &resource,
audience: &audience,
scope: scope.as_ref(),
requested_token_type,
};
match crate::token_exchange::TokenExchange::exchange_token(&*state.server, &request).await {
Ok(exchanged) => ok_json(&exchanged.response),
Err(e) => error_response(&e, via_header, &state.challenge),
}
}
async fn device_authorization_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
body: &Bytes,
) -> Response {
let via_header = basic_attempted(headers);
let text = String::from_utf8_lossy(body);
let form = match parse_pairs(&text) {
Ok(form) => form,
Err(TooManyParameters) => return too_many_parameters(),
};
if let Some(refusal) = refuse_authorization_details(&form) {
return error_response(&refusal, via_header, &state.challenge);
}
let mut creds = match credentials(headers, &form) {
Ok(c) => c,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let client_id = ClientId::new(std::mem::take(&mut creds.client_id));
let scope = match optional_scope(&form) {
Ok(s) => s,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
match state
.server
.device_authorization_with_credential(&client_id, &creds.credential(), scope.as_ref())
.await
{
Ok(response) => ok_json(&response),
Err(e) => error_response(&e, via_header, &state.challenge),
}
}
#[cfg(feature = "par")]
async fn pushed_authorization_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
body: &Bytes,
) -> Response {
let via_header = basic_attempted(headers);
let text = String::from_utf8_lossy(body);
let form = match parse_pairs(&text) {
Ok(form) => form,
Err(TooManyParameters) => return too_many_parameters(),
};
let mut creds = match pushed_request_credentials(headers, &form) {
Ok(c) => c,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let client_id = ClientId::new(std::mem::take(&mut creds.client_id));
let parameters: Vec<(&str, &str)> =
form.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect();
match state
.server
.pushed_authorization_request_with_credential(&client_id, &creds.credential(), ¶meters)
.await
{
Ok(response) => {
let status =
StatusCode::from_u16(response.http_status()).unwrap_or(StatusCode::CREATED);
let mut resp = respond(status, json_body(&response));
let h = resp.headers_mut();
h.insert(header::CONTENT_TYPE, json_content_type());
no_store(h);
resp
}
Err(e) => error_response(&e, via_header, &state.challenge),
}
}
async fn introspect_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
body: &Bytes,
) -> Response {
let via_header = basic_attempted(headers);
let text = String::from_utf8_lossy(body);
let form = match parse_pairs(&text) {
Ok(form) => form,
Err(TooManyParameters) => return too_many_parameters(),
};
let mut creds = match credentials(headers, &form) {
Ok(c) => c,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let client_id = ClientId::new(std::mem::take(&mut creds.client_id));
let token = match required(&form, "token") {
Ok(v) => v,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
match state
.server
.introspection_response_with_credential(&client_id, &creds.credential(), token)
.await
{
Ok(response) => ok_json(&response),
Err(e) => error_response(&e, via_header, &state.challenge),
}
}
async fn revoke_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
body: &Bytes,
) -> Response {
let via_header = basic_attempted(headers);
let text = String::from_utf8_lossy(body);
let form = match parse_pairs(&text) {
Ok(form) => form,
Err(TooManyParameters) => return too_many_parameters(),
};
let mut creds = match credentials(headers, &form) {
Ok(c) => c,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let client_id = ClientId::new(std::mem::take(&mut creds.client_id));
let token = match required(&form, "token") {
Ok(v) => v,
Err(e) => return error_response(&e, via_header, &state.challenge),
};
let hint = param(&form, "token_type_hint").and_then(|h| h.parse::<TokenTypeHint>().ok());
match state
.server
.revoke_with_credential(&client_id, &creds.credential(), token, hint)
.await
{
Ok(()) => {
let mut resp = respond(StatusCode::OK, Body::empty());
no_store(resp.headers_mut());
resp
}
Err(e) => error_response(&e, via_header, &state.challenge),
}
}
fn bearer_token(headers: &HeaderMap) -> Option<&str> {
let raw = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())?;
if raw.len() < 7 || !raw[..7].eq_ignore_ascii_case("bearer ") {
return None;
}
let token = raw[7..].trim();
(!token.is_empty()).then_some(token)
}
fn registration_error(failure: &crate::registration::RegistrationFailure) -> Response {
let status =
StatusCode::from_u16(failure.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let mut resp = match failure {
crate::registration::RegistrationFailure::Invalid(body) => {
let mut resp = respond(status, json_body(body));
resp.headers_mut()
.insert(header::CONTENT_TYPE, json_content_type());
resp
}
_ => respond(status, Body::empty()),
};
let headers = resp.headers_mut();
no_store(headers);
if status == StatusCode::UNAUTHORIZED {
headers.insert(header::WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"));
}
resp
}
fn client_metadata(body: &Bytes) -> Result<crate::registration::ClientMetadata, Box<Response>> {
serde_json::from_slice(body).map_err(|_| {
Box::new(registration_error(
&crate::registration::RegistrationFailure::Invalid(
crate::registration::RegistrationErrorResponse::new(
crate::registration::RegistrationErrorCode::InvalidClientMetadata,
"the request body is not an RFC 7591 s2 client metadata JSON object",
),
),
))
})
}
async fn register_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
body: &Bytes,
) -> Response {
if let Err(e) = state.server.admit_registration() {
return registration_error(&e);
}
let metadata = match client_metadata(body) {
Ok(m) => m,
Err(response) => return *response,
};
match state
.server
.register_admitted_client(&metadata, bearer_token(headers))
.await
{
Ok(info) => {
let mut resp = respond(StatusCode::CREATED, json_body(&info));
let h = resp.headers_mut();
h.insert(header::CONTENT_TYPE, json_content_type());
no_store(h);
resp
}
Err(e) => registration_error(&e),
}
}
async fn read_registration_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
client_id: &str,
) -> Response {
let token = bearer_token(headers).unwrap_or_default();
match state
.server
.read_registration(&ClientId::new(client_id), token)
.await
{
Ok(info) => ok_json(&info),
Err(e) => registration_error(&e),
}
}
async fn update_registration_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
client_id: &str,
body: &Bytes,
) -> Response {
let token = bearer_token(headers).unwrap_or_default();
if let Err(e) = state
.server
.authenticate_registration(&ClientId::new(client_id), token)
.await
{
return registration_error(&e);
}
let metadata = match client_metadata(body) {
Ok(m) => m,
Err(response) => return *response,
};
match state
.server
.update_registration(&ClientId::new(client_id), token, &metadata)
.await
{
Ok(info) => ok_json(&info),
Err(e) => registration_error(&e),
}
}
async fn delete_registration_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
client_id: &str,
) -> Response {
let token = bearer_token(headers).unwrap_or_default();
match state
.server
.delete_registration(&ClientId::new(client_id), token)
.await
{
Ok(()) => {
let mut resp = respond(StatusCode::NO_CONTENT, Body::empty());
no_store(resp.headers_mut());
resp
}
Err(e) => registration_error(&e),
}
}
async fn resolve_authorization_request<S: Storage, C: Clock>(
state: &Inner<S, C>,
pairs: &[Pair<'_>],
) -> Result<crate::authorization::ValidatedAuthorizationRequest, AuthorizationError> {
#[cfg(any(feature = "par", feature = "jar"))]
{
#[cfg(feature = "par")]
let by_reference = param(pairs, "request_uri");
#[cfg(not(feature = "par"))]
let by_reference: Option<&str> = None;
#[cfg(feature = "jar")]
let by_value = param(pairs, "request");
#[cfg(not(feature = "jar"))]
let by_value: Option<&str> = None;
if by_reference.is_some() && by_value.is_some() {
return Err(AuthorizationError::Direct(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"request and request_uri must not both be sent (RFC 9101 s5)",
),
));
}
if by_reference.is_some() || by_value.is_some() {
let client_id = match param(pairs, "client_id") {
Some(id) => id,
None => {
return Err(AuthorizationError::Direct(
ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("client_id is required (RFC 9126 s4, RFC 9101 s5)"),
))
}
};
#[cfg(feature = "par")]
if let Some(request_uri) = by_reference {
return state
.server
.validate_pushed_authorization_request(client_id, request_uri)
.await;
}
#[cfg(feature = "jar")]
if let Some(request_object) = by_value {
return state
.server
.validate_signed_authorization_request(client_id, request_object)
.await;
}
}
}
let request =
AuthorizationRequest::from_pairs(pairs.iter().map(|(k, v)| (k.as_ref(), v.clone())));
state.server.validate_authorization_request(&request).await
}
async fn authorize_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
uri: &Uri,
) -> Response {
let received_at = state.server.now();
let pairs = match parse_pairs(uri.query().unwrap_or_default()) {
Ok(pairs) => pairs,
Err(TooManyParameters) => return too_many_parameters(),
};
let validated = match resolve_authorization_request(state, &pairs).await {
Ok(v) => v,
Err(AuthorizationError::Direct(e)) => {
return error_response(&e, false, &state.challenge);
}
Err(AuthorizationError::Redirect(r)) => return redirect(r.location()),
};
let subject = match state.subject(headers) {
Some(s) => s,
None => {
return match state.subject.is_some() {
true => {
unwired("no authenticated resource owner: nobody is signed in for this request")
}
false => unwired(
"no authenticated resource owner; the host must supply a subject resolver",
),
}
}
};
#[cfg(feature = "consent")]
let remembered = state
.server
.remembered_consent(&validated.client_id, &subject)
.await
.unwrap_or(None);
let approval = match &state.approval {
Some(resolver) => resolver(&ApprovalRequest {
headers,
subject: &subject,
client_id: &validated.client_id,
scope: &validated.scope,
redirect_uri: &validated.redirect_uri,
state: validated.state.as_deref(),
resource: &validated.resource,
#[cfg(feature = "rar")]
authorization_details: &validated.authorization_details,
uri,
#[cfg(feature = "consent")]
remembered: remembered.as_deref(),
}),
None => {
return unwired(
"no approval step is configured; the host must supply an approval resolver \
(RFC 6749 s10.12)",
)
}
};
#[cfg(feature = "consent")]
let mut remember = false;
match approval {
ApprovalDecision::Approve => {}
#[cfg(feature = "consent")]
ApprovalDecision::ApproveAndRemember => remember = true,
ApprovalDecision::Deny => return redirect(validated.denied().location()),
ApprovalDecision::Respond(response) => return *response,
}
#[cfg(feature = "consent")]
let authentication = state.authentication.as_ref().and_then(|f| f(headers));
#[cfg(feature = "consent")]
let issued = state
.server
.issue_authorization_code_with_authentication(
UserApproval::granted_at(&validated, subject.clone(), received_at),
&validated.authentication_requirement,
authentication.as_ref(),
)
.await;
#[cfg(not(feature = "consent"))]
let issued = state
.server
.issue_authorization_code(UserApproval::granted_at(&validated, subject, received_at))
.await;
#[cfg(feature = "consent")]
if remember && issued.is_ok() {
let _ = state
.server
.record_consent(
&validated.client_id,
&subject,
&validated.scope,
&validated.resource,
authentication,
)
.await;
}
match issued {
Ok(response) => redirect(response.location(&validated.redirect_uri)),
Err(AuthorizationError::Direct(e)) => error_response(&e, false, &state.challenge),
Err(AuthorizationError::Redirect(r)) => redirect(r.location()),
}
}
fn unwired(why: &'static str) -> Response {
let err = ErrorResponse::new(ErrorCode::AccessDenied).with_description(why);
let mut resp = respond(StatusCode::FORBIDDEN, json_body(&err));
let headers = resp.headers_mut();
headers.insert(header::CONTENT_TYPE, json_content_type());
no_store(headers);
resp
}
fn redirect(location: String) -> Response {
match HeaderValue::from_str(&location) {
Ok(value) => {
let mut resp = respond(StatusCode::FOUND, Body::empty());
let headers = resp.headers_mut();
headers.insert(header::LOCATION, value);
no_store(headers);
resp
}
Err(_) => error_response(
&ErrorResponse::new(ErrorCode::ServerError),
false,
&HeaderValue::from_static("Basic realm=\"oauth\""),
),
}
}
fn constant_time_eq(a: &str, b: &str) -> bool {
let da = Sha256::digest(a.as_bytes());
let db = Sha256::digest(b.as_bytes());
let mut acc: u8 = 0;
for i in 0..32 {
acc |= da[i] ^ db[i];
}
acc == 0
}
fn is_form_urlencoded(headers: &HeaderMap) -> bool {
headers
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|v| v.split(';').next().unwrap_or_default().trim())
.is_some_and(|mime| mime.eq_ignore_ascii_case("application/x-www-form-urlencoded"))
}
fn same_origin(headers: &HeaderMap, origin: &str) -> bool {
if let Some(site) = headers.get("sec-fetch-site").and_then(|v| v.to_str().ok()) {
return site.eq_ignore_ascii_case("same-origin");
}
headers
.get(header::ORIGIN)
.and_then(|v| v.to_str().ok())
.is_some_and(|value| value.eq_ignore_ascii_case(origin))
}
#[derive(Clone, Copy)]
enum CodeEntry {
Uncharged,
AlreadyCharged,
Refused,
}
struct Throttled;
async fn pending_grant<S: Storage, C: Clock>(
state: &Inner<S, C>,
entered_user_code: &str,
entry: CodeEntry,
) -> Result<Option<(DeviceGrant, Option<String>)>, Throttled> {
let hooks = state.server.hooks();
match entry {
CodeEntry::Refused => return Err(Throttled),
CodeEntry::Uncharged => {
if hooks.check(Attempt::DeviceUserCodeEntry) == RateLimitDecision::Deny {
return Err(Throttled);
}
}
CodeEntry::AlreadyCharged => {}
}
let normalized = normalize_user_code(entered_user_code);
let grant = state
.server
.store()
.find_device_grant_by_user_code(&normalized)
.await
.ok()
.flatten()
.filter(|g| g.state == DeviceGrantState::Pending);
if matches!(entry, CodeEntry::Uncharged) {
hooks.record(
Attempt::DeviceUserCodeEntry,
if grant.is_some() {
AttemptOutcome::Succeeded
} else {
AttemptOutcome::Failed
},
);
}
let Some(grant) = grant else {
return Ok(None);
};
let name = state
.server
.store()
.get_client(&grant.client_id)
.await
.ok()
.flatten()
.and_then(|c| c.name.clone());
Ok(Some((grant, name)))
}
const THROTTLED_MESSAGE: &str = "Too many attempts. Wait and try again.";
async fn render_verification<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
entered: &str,
status: StatusCode,
message: Option<&str>,
entry: CodeEntry,
) -> Response {
let csrf = match &state.verification {
VerificationProtection::Unwired => {
return html_response(
StatusCode::INTERNAL_SERVER_ERROR,
verification_message(
"This server is not configured to accept device approvals. The host must \
supply CSRF tokens (RFC 6749 s10.12).",
),
)
}
VerificationProtection::Tokens { issue, .. } => match issue(headers) {
Some(token) => Some(token),
None => {
return html_response(
StatusCode::FORBIDDEN,
verification_message("You are not signed in."),
)
}
},
VerificationProtection::Disabled => None,
};
let looked_up = match entered.is_empty() {
true => Ok(None),
false => pending_grant(state, entered, entry).await,
};
let (status, message) = match (message, &looked_up) {
(Some(m), _) => (status, Some(m)),
(None, Err(Throttled)) => (StatusCode::TOO_MANY_REQUESTS, Some(THROTTLED_MESSAGE)),
(None, Ok(None)) if !entered.is_empty() => (status, Some("That code was not recognised.")),
(None, _) => (status, None),
};
let grant = looked_up.unwrap_or(None);
html_response(
status,
verification_page(entered, message, grant.as_ref(), csrf.as_deref()),
)
}
async fn verification_page_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
uri: &Uri,
) -> Response {
let pairs = match parse_pairs(uri.query().unwrap_or_default()) {
Ok(pairs) => pairs,
Err(TooManyParameters) => return too_many_parameters(),
};
let prefill = param(&pairs, "user_code").unwrap_or_default();
render_verification(
state,
headers,
prefill,
StatusCode::OK,
None,
CodeEntry::Uncharged,
)
.await
}
async fn verification_submit_handler<S: Storage, C: Clock>(
state: &Inner<S, C>,
headers: &HeaderMap,
body: &Bytes,
) -> Response {
if !is_form_urlencoded(headers) {
return html_response(
StatusCode::UNSUPPORTED_MEDIA_TYPE,
verification_message("Expected an application/x-www-form-urlencoded submission."),
);
}
let text = String::from_utf8_lossy(body);
let form = match parse_pairs(&text) {
Ok(form) => form,
Err(TooManyParameters) => return too_many_parameters(),
};
let user_code = param(&form, "user_code").unwrap_or_default();
let protected = !matches!(state.verification, VerificationProtection::Disabled);
if protected {
if !same_origin(headers, &state.origin) {
return html_response(
StatusCode::FORBIDDEN,
verification_message("That request did not come from this site."),
);
}
let expected =
match &state.verification {
VerificationProtection::Tokens { consume, .. } => consume(headers),
VerificationProtection::Unwired => return html_response(
StatusCode::INTERNAL_SERVER_ERROR,
verification_message(
"This server is not configured to accept device approvals. The host must \
supply CSRF tokens (RFC 6749 s10.12).",
),
),
VerificationProtection::Disabled => None,
};
let presented = param(&form, "csrf_token").unwrap_or_default();
let ok = expected.is_some_and(|e| constant_time_eq(&e, presented));
if !ok {
return html_response(
StatusCode::FORBIDDEN,
verification_message("That form has expired. Start again."),
);
}
}
if user_code.is_empty() {
return render_verification(
state,
headers,
"",
StatusCode::BAD_REQUEST,
Some("Enter the code shown on your device."),
CodeEntry::Uncharged,
)
.await;
}
let action = param(&form, "action").unwrap_or_default();
let denied = action == "deny";
let approved = action == "approve" || !protected;
if !denied && !approved {
return render_verification(
state,
headers,
user_code,
StatusCode::OK,
None,
CodeEntry::Uncharged,
)
.await;
}
let subject = match state.subject(headers) {
Some(s) => s,
None => {
return html_response(
StatusCode::FORBIDDEN,
verification_message("You are not signed in."),
)
}
};
let outcome = if denied {
state.server.deny_device(user_code).await
} else {
state.server.approve_device(user_code, subject).await
};
match outcome {
Ok(()) if denied => html_response(StatusCode::OK, verification_message("Request denied.")),
Ok(()) => html_response(
StatusCode::OK,
verification_message("Approved. You can return to your device."),
),
Err(e) => {
let message = match e {
DeviceApprovalError::UnknownUserCode => "That code was not recognised.",
DeviceApprovalError::Expired => "That code has expired. Start again on the device.",
DeviceApprovalError::NotPending => "That code has already been used.",
DeviceApprovalError::Storage(_) => "Something went wrong. Try again.",
DeviceApprovalError::RateLimited => THROTTLED_MESSAGE,
};
let status = match e {
DeviceApprovalError::Storage(_) => StatusCode::INTERNAL_SERVER_ERROR,
DeviceApprovalError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
_ => StatusCode::BAD_REQUEST,
};
let entry = match e {
DeviceApprovalError::RateLimited => CodeEntry::Refused,
_ => CodeEntry::AlreadyCharged,
};
render_verification(state, headers, user_code, status, Some(message), entry).await
}
}
}
fn escape_quoted_string(value: &str) -> Cow<'_, str> {
if !value.contains(['"', '\\']) {
return Cow::Borrowed(value);
}
let mut out = String::with_capacity(value.len() + 8);
for c in value.chars() {
if c == '"' || c == '\\' {
out.push('\\');
}
out.push(c);
}
Cow::Owned(out)
}
fn escape_html(value: &str, out: &mut String) {
for c in value.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
}
const PAGE_HEAD: &str = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">\
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\
<title>Device authorization</title></head><body><h1>Device authorization</h1>";
fn verification_page(
prefill: &str,
message: Option<&str>,
grant: Option<&(DeviceGrant, Option<String>)>,
csrf: Option<&str>,
) -> String {
let mut html = String::with_capacity(768);
html.push_str(PAGE_HEAD);
if let Some(message) = message {
html.push_str("<p>");
escape_html(message, &mut html);
html.push_str("</p>");
}
html.push_str("<form method=\"post\">");
if let Some(token) = csrf {
html.push_str("<input type=\"hidden\" name=\"csrf_token\" value=\"");
escape_html(token, &mut html);
html.push_str("\">");
}
match grant {
None => {
html.push_str(
"<label for=\"user_code\">Code shown on your device</label>\
<input id=\"user_code\" name=\"user_code\" autocomplete=\"off\" \
spellcheck=\"false\" value=\"",
);
escape_html(prefill, &mut html);
html.push_str("\"><button type=\"submit\">Continue</button>");
}
Some((grant, name)) => {
html.push_str("<p>The application <strong>");
escape_html(
name.as_deref().unwrap_or_else(|| grant.client_id.as_str()),
&mut html,
);
html.push_str("</strong> (<code>");
escape_html(grant.client_id.as_str(), &mut html);
html.push_str("</code>) is asking to access your account.</p><p>It will be allowed: ");
let scope = grant.scope.to_string();
if scope.is_empty() {
html.push_str("no scopes");
} else {
escape_html(&scope, &mut html);
}
html.push_str("</p><p>Code on your device: <code>");
escape_html(&grant.user_code, &mut html);
html.push_str("</code></p><input type=\"hidden\" name=\"user_code\" value=\"");
escape_html(&grant.user_code, &mut html);
html.push_str(
"\"><button type=\"submit\" name=\"action\" value=\"approve\">Approve</button>\
<button type=\"submit\" name=\"action\" value=\"deny\">Deny</button>",
);
}
}
html.push_str("</form></body></html>");
html
}
fn verification_message(message: &str) -> String {
let mut html = String::with_capacity(256);
html.push_str(
"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">\
<title>Device authorization</title></head><body><p>",
);
escape_html(message, &mut html);
html.push_str("</p></body></html>");
html
}
#[cfg(test)]
#[path = "tests/http.rs"]
mod tests;