pub mod binding;
pub mod conformance;
pub mod deliberate;
pub mod drafts;
pub mod membership;
pub mod membership_admission;
pub mod membership_conformance;
pub mod membership_pages;
pub mod refusal;
pub mod rights;
#[cfg(feature = "http")]
pub mod signin;
pub mod verify;
pub use membership::{GrantLog, MembershipDecision, MembershipRequest, Subject, Verdict};
pub use refusal::Refusal;
pub use rights::{Claims, Decision, Holder, Mode, Right, Target};
pub use verify::{KeySet, VerifiedPass};
#[cfg(feature = "http")]
use reqwest::Client as Http;
use serde::{Deserialize, Serialize};
#[cfg(feature = "http")]
use url::Url;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("wrong_audience")]
WrongAudience,
#[error("wrong_issuer")]
WrongIssuer,
#[error("pass_expired")]
Expired,
#[error("pass_not_yet_valid")]
NotYetValid,
#[error("unpublished_key: {0}")]
UnpublishedKey(String),
#[error("signature_refused")]
Signature(#[source] ed25519_dalek::SignatureError),
#[error("pass_encoding_refused")]
Encoding(#[from] base64::DecodeError),
#[error("pass_contract_refused")]
Json(#[from] serde_json::Error),
#[error("rights_outside_audience")]
RightsOutsideAudience,
#[error("rights_truncated: {0}")]
Truncated(String),
#[error("contract_refused: {0}")]
Invalid(&'static str),
#[error("Lys could not be asked")]
Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("Lys could not be asked: {0}")]
CannotAsk(&'static str),
#[error("endpoint_refused")]
Url(#[from] url::ParseError),
#[error("{name}: {reason}")]
Binding {
name: &'static str,
reason: &'static str,
},
#[error("token_refused: {name}: {reason}")]
TokenRefused {
name: String,
reason: String,
},
}
impl Error {
#[must_use]
pub fn name(&self) -> &str {
match self {
Self::WrongAudience => "wrong_audience",
Self::WrongIssuer => "wrong_issuer",
Self::Expired => "pass_expired",
Self::NotYetValid => "pass_not_yet_valid",
Self::UnpublishedKey(_) => "unpublished_key",
Self::Signature(_) => "signature_refused",
Self::Encoding(_) => "pass_encoding_refused",
Self::Json(_) | Self::Invalid(_) => "contract_refused",
Self::RightsOutsideAudience => "rights_outside_audience",
Self::Truncated(_) => "rights_truncated",
Self::Transport(_) | Self::CannotAsk(_) => "lys_could_not_be_asked",
Self::Url(_) => "endpoint_refused",
Self::Binding { name, .. } => name,
Self::TokenRefused { name, .. } => name,
}
}
}
#[cfg(feature = "http")]
#[derive(Clone, Debug)]
pub struct Client {
http: Http,
issuer: Url,
}
#[cfg(feature = "http")]
impl Client {
pub fn new(http: Http, mut issuer: Url) -> Result<Self, Error> {
if !matches!(issuer.scheme(), "http" | "https")
|| issuer.host_str().is_none()
|| !issuer.username().is_empty()
|| issuer.password().is_some()
|| issuer.query().is_some()
|| issuer.fragment().is_some()
{
return Err(Error::Invalid("issuer is not an HTTP origin"));
}
if !issuer.path().ends_with('/') {
issuer.set_path(&format!("{}/", issuer.path()));
}
Ok(Self { http, issuer })
}
fn endpoint(&self, relative: &str) -> Result<Url, Error> {
Ok(self.issuer.join(relative)?)
}
pub async fn fetch_keys(&self) -> Result<KeySet, Error> {
let response = self
.http
.get(self.endpoint("oauth/jwks")?)
.send()
.await
.map_err(|error| Error::Transport(Box::new(error)))?;
let text = response
.error_for_status()
.map_err(|error| Error::Transport(Box::new(error)))?
.text()
.await
.map_err(|error| Error::Transport(Box::new(error)))?;
KeySet::from_json(&text)
}
pub async fn fetch_token(&self, request: &TokenRequest<'_>) -> Result<TokenResponse, Error> {
self.token(&code_form(request)?).await
}
pub async fn fetch_bound_token(
&self,
request: &TokenRequest<'_>,
) -> Result<TokenResponse, Error> {
let mut form = code_form(request)?.to_vec();
form.push(("grant_binding", binding::BINDING_REQUEST));
bound(self.token(&form).await?)
}
pub async fn refresh_bound_token(
&self,
client_id: &str,
client_secret: &str,
refresh_token: &str,
) -> Result<TokenResponse, Error> {
if client_id.is_empty() || client_secret.is_empty() || refresh_token.is_empty() {
return Err(Error::Invalid("refresh request is incomplete"));
}
bound(
self.token(&[
("grant_type", "refresh_token"),
("client_id", client_id),
("client_secret", client_secret),
("refresh_token", refresh_token),
("grant_binding", binding::BINDING_REQUEST),
])
.await?,
)
}
pub async fn refresh_token(
&self,
client_id: &str,
client_secret: &str,
refresh_token: &str,
) -> Result<TokenResponse, Error> {
if client_id.is_empty() || client_secret.is_empty() || refresh_token.is_empty() {
return Err(Error::Invalid("refresh request is incomplete"));
}
self.token(&[
("grant_type", "refresh_token"),
("client_id", client_id),
("client_secret", client_secret),
("refresh_token", refresh_token),
])
.await
}
async fn token(&self, form: &[(&str, &str)]) -> Result<TokenResponse, Error> {
let response = self
.http
.post(self.endpoint("oauth/token")?)
.form(form)
.send()
.await
.map_err(|error| Error::Transport(Box::new(error)))?;
read_token(response).await
}
}
#[cfg(feature = "http")]
fn code_form<'a>(request: &TokenRequest<'a>) -> Result<[(&'static str, &'a str); 6], Error> {
if request.client_id.is_empty()
|| request.client_secret.is_empty()
|| request.code.is_empty()
|| request.redirect_uri.is_empty()
|| request.code_verifier.is_empty()
{
return Err(Error::Invalid("token request is incomplete"));
}
Ok([
("grant_type", "authorization_code"),
("client_id", request.client_id),
("client_secret", request.client_secret),
("code", request.code),
("redirect_uri", request.redirect_uri),
("code_verifier", request.code_verifier),
])
}
#[cfg(feature = "http")]
fn bound(answer: TokenResponse) -> Result<TokenResponse, Error> {
binding::required(answer.grant_binding.as_deref())?;
Ok(answer)
}
#[cfg(feature = "http")]
pub(crate) async fn read_token(response: reqwest::Response) -> Result<TokenResponse, Error> {
if !response.status().is_success() {
let refused: TokenRefusal = response
.json()
.await
.map_err(|error| Error::Transport(Box::new(error)))?;
if refused.error.is_empty()
|| refused.refusal.is_empty()
|| refused.reason != refused.error_description
{
return Err(Error::CannotAsk("token refusal has no name"));
}
return Err(Error::TokenRefused {
name: refused.refusal,
reason: refused.error_description,
});
}
let answer: TokenResponse = response
.json()
.await
.map_err(|error| Error::Transport(Box::new(error)))?;
if answer.token_type != "Bearer" || answer.access_token.is_empty() || answer.expires_in == 0 {
return Err(Error::CannotAsk("invalid token response"));
}
Ok(answer)
}
pub struct TokenRequest<'a> {
pub client_id: &'a str,
pub client_secret: &'a str,
pub code: &'a str,
pub redirect_uri: &'a str,
pub code_verifier: &'a str,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TokenResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
pub id_token: Option<String>,
pub refresh_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grant_binding: Option<String>,
}
#[cfg(feature = "http")]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TokenRefusal {
error: String,
error_description: String,
refusal: String,
reason: String,
}