use core::fmt;
use super::client::RegisteredClient;
use super::code::ClientAuthResult;
#[doc(alias = "client_credentials_error")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ClientCredentialsDenied {
ClientAuthenticationFailed,
GrantNotAllowed,
ClientInactive,
ScopeNotAllowed,
}
impl ClientCredentialsDenied {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::ClientAuthenticationFailed => "client authentication failed",
Self::GrantNotAllowed => "client not permitted the client_credentials grant",
Self::ClientInactive => "client is inactive",
Self::ScopeNotAllowed => "requested scope exceeds the client's allowed scopes",
}
}
}
impl fmt::Display for ClientCredentialsDenied {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "client-credentials denied: {}", self.as_str())
}
}
impl std::error::Error for ClientCredentialsDenied {}
#[doc(alias = "client_credentials_verdict")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ClientCredentialsVerdict {
Accepted,
Denied(ClientCredentialsDenied),
}
#[derive(Debug, Clone, Copy)]
pub struct ClientCredentialsRequest<'a> {
pub client_auth: ClientAuthResult,
pub client: &'a RegisteredClient,
pub requested_scope: &'a str,
pub grant_allowed: bool,
}
#[must_use]
pub fn evaluate_client_credentials(req: &ClientCredentialsRequest<'_>) -> ClientCredentialsVerdict {
use ClientCredentialsDenied as D;
use ClientCredentialsVerdict::{Accepted, Denied};
if req.client_auth != ClientAuthResult::Authenticated {
return Denied(D::ClientAuthenticationFailed);
}
if !req.grant_allowed {
return Denied(D::GrantNotAllowed);
}
if !req.client.active() {
return Denied(D::ClientInactive);
}
if !req.requested_scope.is_empty() && !req.client.allows_scopes(req.requested_scope) {
return Denied(D::ScopeNotAllowed);
}
Accepted
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oauth::server::{ClientType, RegisteredClient};
fn client() -> RegisteredClient {
RegisteredClient::builder("svc_app", ClientType::Confidential)
.allowed_scope("read")
.allowed_scope("write")
.require_pkce(false)
.active(true)
.build()
.unwrap()
}
fn req<'a>(
c: &'a RegisteredClient,
auth: ClientAuthResult,
scope: &'a str,
grant_allowed: bool,
) -> ClientCredentialsRequest<'a> {
ClientCredentialsRequest {
client_auth: auth,
client: c,
requested_scope: scope,
grant_allowed,
}
}
#[test]
fn accepts_authenticated_active_in_scope() {
let c = client();
assert_eq!(
evaluate_client_credentials(&req(&c, ClientAuthResult::Authenticated, "read", true)),
ClientCredentialsVerdict::Accepted
);
}
#[test]
fn empty_scope_is_accepted() {
let c = client();
assert_eq!(
evaluate_client_credentials(&req(&c, ClientAuthResult::Authenticated, "", true)),
ClientCredentialsVerdict::Accepted
);
}
#[test]
fn rejects_failed_auth() {
let c = client();
assert_eq!(
evaluate_client_credentials(&req(&c, ClientAuthResult::Failed, "read", true)),
ClientCredentialsVerdict::Denied(ClientCredentialsDenied::ClientAuthenticationFailed)
);
}
#[test]
fn rejects_grant_not_allowed() {
let c = client();
assert_eq!(
evaluate_client_credentials(&req(&c, ClientAuthResult::Authenticated, "read", false)),
ClientCredentialsVerdict::Denied(ClientCredentialsDenied::GrantNotAllowed)
);
}
#[test]
fn rejects_scope_outside_allowed() {
let c = client();
assert_eq!(
evaluate_client_credentials(&req(
&c,
ClientAuthResult::Authenticated,
"read admin",
true
)),
ClientCredentialsVerdict::Denied(ClientCredentialsDenied::ScopeNotAllowed)
);
}
#[test]
fn rejects_inactive_client() {
let c = RegisteredClient::builder("svc_app", ClientType::Confidential)
.allowed_scope("read")
.require_pkce(false)
.active(false)
.build()
.unwrap();
assert_eq!(
evaluate_client_credentials(&req(&c, ClientAuthResult::Authenticated, "read", true)),
ClientCredentialsVerdict::Denied(ClientCredentialsDenied::ClientInactive)
);
}
}