use core::fmt;
use crate::encoding::base64url_decode;
use super::client::RegisteredClient;
const MAX_SCOPE_LEN: usize = 500;
#[derive(Debug, Clone, Copy)]
pub struct AuthorizeRequest<'a> {
pub response_type: &'a str,
pub client_id: &'a str,
pub redirect_uri: Option<&'a str>,
pub scope: Option<&'a str>,
pub state: Option<&'a str>,
pub code_challenge: Option<&'a str>,
pub code_challenge_method: Option<&'a str>,
}
#[derive(Debug, Clone)]
pub struct ValidatedAuthorizeRequest {
redirect_uri: String,
scope: String,
state: Option<String>,
code_challenge: Option<String>,
}
impl ValidatedAuthorizeRequest {
#[must_use]
#[inline]
pub fn redirect_uri(&self) -> &str {
&self.redirect_uri
}
#[must_use]
#[inline]
pub fn scope(&self) -> &str {
&self.scope
}
#[must_use]
#[inline]
pub fn state(&self) -> Option<&str> {
self.state.as_deref()
}
#[must_use]
#[inline]
pub fn code_challenge(&self) -> Option<&str> {
self.code_challenge.as_deref()
}
}
#[doc(alias = "error_code")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AuthorizeErrorCode {
UnsupportedResponseType,
InvalidScope,
InvalidRequest,
}
impl AuthorizeErrorCode {
#[must_use]
#[inline]
pub fn as_str(self) -> &'static str {
match self {
Self::UnsupportedResponseType => "unsupported_response_type",
Self::InvalidScope => "invalid_scope",
Self::InvalidRequest => "invalid_request",
}
}
}
impl fmt::Display for AuthorizeErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DisplayErrorReason {
UnknownClient,
MissingRedirectUri,
InvalidRedirectUri,
}
impl DisplayErrorReason {
#[must_use]
#[inline]
pub fn as_str(self) -> &'static str {
match self {
Self::UnknownClient => "unknown or inactive client",
Self::MissingRedirectUri => "missing redirect_uri",
Self::InvalidRedirectUri => "redirect_uri does not match a registered URI",
}
}
}
#[doc(alias = "authorize_error")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AuthorizeError {
Display(DisplayErrorReason),
Redirect(AuthorizeRedirectError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthorizeRedirectError {
redirect_uri: String,
code: AuthorizeErrorCode,
state: Option<String>,
}
impl AuthorizeRedirectError {
#[must_use]
#[inline]
pub fn redirect_uri(&self) -> &str {
&self.redirect_uri
}
#[must_use]
#[inline]
pub fn code(&self) -> AuthorizeErrorCode {
self.code
}
#[must_use]
#[inline]
pub fn state(&self) -> Option<&str> {
self.state.as_deref()
}
}
impl fmt::Display for AuthorizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Display(reason) => {
write!(f, "authorize: display error: {}", reason.as_str())
}
Self::Redirect(err) => {
write!(f, "authorize: redirect error: {}", err.code.as_str())
}
}
}
}
impl std::error::Error for AuthorizeError {}
const S256_CHALLENGE_LEN: usize = 43;
pub fn validate_authorize_request(
request: &AuthorizeRequest<'_>,
client: Option<&RegisteredClient>,
) -> Result<ValidatedAuthorizeRequest, AuthorizeError> {
let client = match client {
Some(c) if c.active() && c.client_id() == request.client_id => c,
_ => return Err(AuthorizeError::Display(DisplayErrorReason::UnknownClient)),
};
let redirect_uri = request.redirect_uri.ok_or(AuthorizeError::Display(
DisplayErrorReason::MissingRedirectUri,
))?;
if !client.is_registered_redirect_uri(redirect_uri) {
return Err(AuthorizeError::Display(
DisplayErrorReason::InvalidRedirectUri,
));
}
let redirect_err = |code: AuthorizeErrorCode| {
AuthorizeError::Redirect(AuthorizeRedirectError {
redirect_uri: redirect_uri.to_owned(),
code,
state: request.state.map(ToOwned::to_owned),
})
};
if request.response_type != "code" {
return Err(redirect_err(AuthorizeErrorCode::UnsupportedResponseType));
}
let scope = request.scope.unwrap_or("");
if !client.allows_scopes(scope) {
return Err(redirect_err(AuthorizeErrorCode::InvalidScope));
}
let mut canonical: Vec<&str> = Vec::new();
for s in scope.split(' ').filter(|s| !s.is_empty()) {
if !canonical.contains(&s) {
canonical.push(s);
}
}
let scope: String = canonical.join(" ");
if scope.len() > MAX_SCOPE_LEN {
return Err(redirect_err(AuthorizeErrorCode::InvalidScope));
}
let code_challenge = if let Some(challenge) = request.code_challenge {
let method = request.code_challenge_method.unwrap_or("plain");
if method != "S256" || !is_well_formed_s256_challenge(challenge) {
return Err(redirect_err(AuthorizeErrorCode::InvalidRequest));
}
Some(challenge.to_owned())
} else {
if client.require_pkce() {
return Err(redirect_err(AuthorizeErrorCode::InvalidRequest));
}
None
};
Ok(ValidatedAuthorizeRequest {
redirect_uri: redirect_uri.to_owned(),
scope,
state: request.state.map(ToOwned::to_owned),
code_challenge,
})
}
fn is_well_formed_s256_challenge(challenge: &str) -> bool {
if challenge.len() != S256_CHALLENGE_LEN {
return false;
}
match base64url_decode(challenge) {
Ok(bytes) => bytes.len() == 32,
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oauth::PkceChallenge;
use crate::oauth::server::ClientType;
fn client() -> RegisteredClient {
RegisteredClient::builder("c1", ClientType::Confidential)
.redirect_uri("https://app.example.com/cb")
.allowed_scope("openid")
.allowed_scope("profile")
.require_pkce(true)
.build()
.expect("valid test client")
}
fn valid_challenge() -> String {
PkceChallenge::generate().unwrap().challenge().to_owned()
}
fn base_request(challenge: &str) -> AuthorizeRequest<'_> {
AuthorizeRequest {
response_type: "code",
client_id: "c1",
redirect_uri: Some("https://app.example.com/cb"),
scope: Some("openid profile"),
state: Some("xyz"),
code_challenge: Some(challenge),
code_challenge_method: Some("S256"),
}
}
#[test]
fn granted_scope_is_normalized() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.scope = Some(" openid profile ");
let v = validate_authorize_request(&req, Some(&c)).unwrap();
assert_eq!(v.scope(), "openid profile");
}
#[test]
fn accepts_valid_request() {
let c = client();
let chal = valid_challenge();
let req = base_request(&chal);
let v = validate_authorize_request(&req, Some(&c)).unwrap();
assert_eq!(v.redirect_uri(), "https://app.example.com/cb");
assert_eq!(v.scope(), "openid profile");
assert_eq!(v.state(), Some("xyz"));
assert_eq!(v.code_challenge(), Some(chal.as_str()));
}
#[test]
fn unknown_client_is_display_error() {
let chal = valid_challenge();
let req = base_request(&chal);
let err = validate_authorize_request(&req, None).unwrap_err();
assert_eq!(
err,
AuthorizeError::Display(DisplayErrorReason::UnknownClient)
);
}
#[test]
fn inactive_client_is_display_error() {
let c = RegisteredClient::builder("c1", ClientType::Confidential)
.redirect_uri("https://app.example.com/cb")
.active(false)
.build()
.expect("valid test client");
let chal = valid_challenge();
let req = base_request(&chal);
let err = validate_authorize_request(&req, Some(&c)).unwrap_err();
assert_eq!(
err,
AuthorizeError::Display(DisplayErrorReason::UnknownClient)
);
}
#[test]
fn mismatched_client_id_is_display_error() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.client_id = "other";
let err = validate_authorize_request(&req, Some(&c)).unwrap_err();
assert_eq!(
err,
AuthorizeError::Display(DisplayErrorReason::UnknownClient)
);
}
#[test]
fn missing_redirect_uri_is_display_error() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.redirect_uri = None;
let err = validate_authorize_request(&req, Some(&c)).unwrap_err();
assert_eq!(
err,
AuthorizeError::Display(DisplayErrorReason::MissingRedirectUri)
);
}
#[test]
fn unregistered_redirect_uri_is_display_error() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.redirect_uri = Some("https://evil.example.com/cb");
let err = validate_authorize_request(&req, Some(&c)).unwrap_err();
assert_eq!(
err,
AuthorizeError::Display(DisplayErrorReason::InvalidRedirectUri)
);
}
#[test]
fn non_exact_redirect_uri_is_display_error() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.redirect_uri = Some("https://app.example.com/cb/");
let err = validate_authorize_request(&req, Some(&c)).unwrap_err();
assert_eq!(
err,
AuthorizeError::Display(DisplayErrorReason::InvalidRedirectUri)
);
}
fn expect_redirect(err: AuthorizeError) -> AuthorizeRedirectError {
match err {
AuthorizeError::Redirect(e) => e,
AuthorizeError::Display(r) => panic!("expected redirect, got display: {r:?}"),
}
}
#[test]
fn bad_response_type_redirects() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.response_type = "token";
let e = expect_redirect(validate_authorize_request(&req, Some(&c)).unwrap_err());
assert_eq!(e.code(), AuthorizeErrorCode::UnsupportedResponseType);
assert_eq!(e.redirect_uri(), "https://app.example.com/cb");
assert_eq!(e.state(), Some("xyz"));
}
#[test]
fn disallowed_scope_redirects() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.scope = Some("openid admin");
let e = expect_redirect(validate_authorize_request(&req, Some(&c)).unwrap_err());
assert_eq!(e.code(), AuthorizeErrorCode::InvalidScope);
assert_eq!(e.state(), Some("xyz"));
}
#[test]
fn missing_pkce_when_required_redirects() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.code_challenge = None;
req.code_challenge_method = None;
let e = expect_redirect(validate_authorize_request(&req, Some(&c)).unwrap_err());
assert_eq!(e.code(), AuthorizeErrorCode::InvalidRequest);
}
#[test]
fn plain_pkce_method_is_rejected() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.code_challenge_method = Some("plain");
let e = expect_redirect(validate_authorize_request(&req, Some(&c)).unwrap_err());
assert_eq!(e.code(), AuthorizeErrorCode::InvalidRequest);
}
#[test]
fn omitted_pkce_method_defaults_to_plain_and_is_rejected() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.code_challenge_method = None;
let e = expect_redirect(validate_authorize_request(&req, Some(&c)).unwrap_err());
assert_eq!(e.code(), AuthorizeErrorCode::InvalidRequest);
}
#[test]
fn malformed_challenge_redirects() {
let c = client();
let bad = "!".repeat(S256_CHALLENGE_LEN);
let req = base_request(&bad);
let e = expect_redirect(validate_authorize_request(&req, Some(&c)).unwrap_err());
assert_eq!(e.code(), AuthorizeErrorCode::InvalidRequest);
}
#[test]
fn wrong_length_challenge_redirects() {
let c = client();
let short = "abc";
let req = base_request(short);
let e = expect_redirect(validate_authorize_request(&req, Some(&c)).unwrap_err());
assert_eq!(e.code(), AuthorizeErrorCode::InvalidRequest);
}
#[test]
fn non_s256_named_method_is_rejected() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.code_challenge_method = Some("S384");
let e = expect_redirect(validate_authorize_request(&req, Some(&c)).unwrap_err());
assert_eq!(e.code(), AuthorizeErrorCode::InvalidRequest);
}
#[test]
fn pkce_optional_when_not_required() {
let c = RegisteredClient::builder("pub", ClientType::Public)
.redirect_uri("https://spa.example.com/cb")
.allowed_scope("openid")
.require_pkce(false)
.build()
.expect("valid test client");
let req = AuthorizeRequest {
response_type: "code",
client_id: "pub",
redirect_uri: Some("https://spa.example.com/cb"),
scope: Some("openid"),
state: None,
code_challenge: None,
code_challenge_method: None,
};
let v = validate_authorize_request(&req, Some(&c)).unwrap();
assert_eq!(v.code_challenge(), None);
assert_eq!(v.state(), None);
}
#[test]
fn empty_scope_is_allowed() {
let c = client();
let chal = valid_challenge();
let mut req = base_request(&chal);
req.scope = None;
let v = validate_authorize_request(&req, Some(&c)).unwrap();
assert_eq!(v.scope(), "");
}
#[test]
fn error_code_strings() {
assert_eq!(
AuthorizeErrorCode::UnsupportedResponseType.as_str(),
"unsupported_response_type"
);
assert_eq!(AuthorizeErrorCode::InvalidScope.as_str(), "invalid_scope");
assert_eq!(
AuthorizeErrorCode::InvalidRequest.as_str(),
"invalid_request"
);
}
#[test]
fn error_implements_std_error() {
let err: Box<dyn std::error::Error> =
Box::new(AuthorizeError::Display(DisplayErrorReason::UnknownClient));
assert!(err.to_string().contains("display error"));
}
}