1pub mod binding;
4pub mod conformance;
5pub mod deliberate;
6pub mod drafts;
7pub mod membership;
8pub mod membership_admission;
9pub mod membership_conformance;
10pub mod membership_pages;
11pub mod refusal;
12pub mod rights;
13#[cfg(feature = "http")]
14pub mod signin;
15pub mod verify;
16
17pub use membership::{GrantLog, MembershipDecision, MembershipRequest, Subject, Verdict};
18pub use refusal::Refusal;
19pub use rights::{Claims, Decision, Holder, Mode, Right, Target};
20pub use verify::{KeySet, VerifiedPass};
21
22#[cfg(feature = "http")]
23use reqwest::Client as Http;
24use serde::{Deserialize, Serialize};
25#[cfg(feature = "http")]
26use url::Url;
27
28#[derive(Debug, thiserror::Error)]
30pub enum Error {
31 #[error("wrong_audience")]
33 WrongAudience,
34 #[error("wrong_issuer")]
36 WrongIssuer,
37 #[error("pass_expired")]
39 Expired,
40 #[error("pass_not_yet_valid")]
42 NotYetValid,
43 #[error("unpublished_key: {0}")]
45 UnpublishedKey(String),
46 #[error("signature_refused")]
48 Signature(#[source] ed25519_dalek::SignatureError),
49 #[error("pass_encoding_refused")]
51 Encoding(#[from] base64::DecodeError),
52 #[error("pass_contract_refused")]
54 Json(#[from] serde_json::Error),
55 #[error("rights_outside_audience")]
57 RightsOutsideAudience,
58 #[error("rights_truncated: {0}")]
60 Truncated(String),
61 #[error("contract_refused: {0}")]
63 Invalid(&'static str),
64 #[error("Lys could not be asked")]
68 Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
69 #[error("Lys could not be asked: {0}")]
71 CannotAsk(&'static str),
72 #[error("endpoint_refused")]
74 Url(#[from] url::ParseError),
75 #[error("{name}: {reason}")]
78 Binding {
79 name: &'static str,
81 reason: &'static str,
83 },
84 #[error("token_refused: {name}: {reason}")]
86 TokenRefused {
87 name: String,
89 reason: String,
91 },
92}
93
94impl Error {
95 #[must_use]
97 pub fn name(&self) -> &str {
98 match self {
99 Self::WrongAudience => "wrong_audience",
100 Self::WrongIssuer => "wrong_issuer",
101 Self::Expired => "pass_expired",
102 Self::NotYetValid => "pass_not_yet_valid",
103 Self::UnpublishedKey(_) => "unpublished_key",
104 Self::Signature(_) => "signature_refused",
105 Self::Encoding(_) => "pass_encoding_refused",
106 Self::Json(_) | Self::Invalid(_) => "contract_refused",
107 Self::RightsOutsideAudience => "rights_outside_audience",
108 Self::Truncated(_) => "rights_truncated",
109 Self::Transport(_) | Self::CannotAsk(_) => "lys_could_not_be_asked",
110 Self::Url(_) => "endpoint_refused",
111 Self::Binding { name, .. } => name,
112 Self::TokenRefused { name, .. } => name,
113 }
114 }
115}
116
117#[cfg(feature = "http")]
119#[derive(Clone, Debug)]
120pub struct Client {
121 http: Http,
122 issuer: Url,
123}
124
125#[cfg(feature = "http")]
126impl Client {
127 pub fn new(http: Http, mut issuer: Url) -> Result<Self, Error> {
129 if !matches!(issuer.scheme(), "http" | "https")
130 || issuer.host_str().is_none()
131 || !issuer.username().is_empty()
132 || issuer.password().is_some()
133 || issuer.query().is_some()
134 || issuer.fragment().is_some()
135 {
136 return Err(Error::Invalid("issuer is not an HTTP origin"));
137 }
138 if !issuer.path().ends_with('/') {
139 issuer.set_path(&format!("{}/", issuer.path()));
140 }
141 Ok(Self { http, issuer })
142 }
143
144 fn endpoint(&self, relative: &str) -> Result<Url, Error> {
145 Ok(self.issuer.join(relative)?)
146 }
147
148 pub async fn fetch_keys(&self) -> Result<KeySet, Error> {
150 let response = self
151 .http
152 .get(self.endpoint("oauth/jwks")?)
153 .send()
154 .await
155 .map_err(|error| Error::Transport(Box::new(error)))?;
156 let text = response
157 .error_for_status()
158 .map_err(|error| Error::Transport(Box::new(error)))?
159 .text()
160 .await
161 .map_err(|error| Error::Transport(Box::new(error)))?;
162 KeySet::from_json(&text)
163 }
164
165 pub async fn fetch_token(&self, request: &TokenRequest<'_>) -> Result<TokenResponse, Error> {
167 self.token(&code_form(request)?).await
168 }
169
170 pub async fn fetch_bound_token(
174 &self,
175 request: &TokenRequest<'_>,
176 ) -> Result<TokenResponse, Error> {
177 let mut form = code_form(request)?.to_vec();
178 form.push(("grant_binding", binding::BINDING_REQUEST));
179 bound(self.token(&form).await?)
180 }
181
182 pub async fn refresh_bound_token(
184 &self,
185 client_id: &str,
186 client_secret: &str,
187 refresh_token: &str,
188 ) -> Result<TokenResponse, Error> {
189 if client_id.is_empty() || client_secret.is_empty() || refresh_token.is_empty() {
190 return Err(Error::Invalid("refresh request is incomplete"));
191 }
192 bound(
193 self.token(&[
194 ("grant_type", "refresh_token"),
195 ("client_id", client_id),
196 ("client_secret", client_secret),
197 ("refresh_token", refresh_token),
198 ("grant_binding", binding::BINDING_REQUEST),
199 ])
200 .await?,
201 )
202 }
203
204 pub async fn refresh_token(
206 &self,
207 client_id: &str,
208 client_secret: &str,
209 refresh_token: &str,
210 ) -> Result<TokenResponse, Error> {
211 if client_id.is_empty() || client_secret.is_empty() || refresh_token.is_empty() {
212 return Err(Error::Invalid("refresh request is incomplete"));
213 }
214 self.token(&[
215 ("grant_type", "refresh_token"),
216 ("client_id", client_id),
217 ("client_secret", client_secret),
218 ("refresh_token", refresh_token),
219 ])
220 .await
221 }
222
223 async fn token(&self, form: &[(&str, &str)]) -> Result<TokenResponse, Error> {
224 let response = self
225 .http
226 .post(self.endpoint("oauth/token")?)
227 .form(form)
228 .send()
229 .await
230 .map_err(|error| Error::Transport(Box::new(error)))?;
231 read_token(response).await
232 }
233}
234
235#[cfg(feature = "http")]
237fn code_form<'a>(request: &TokenRequest<'a>) -> Result<[(&'static str, &'a str); 6], Error> {
238 if request.client_id.is_empty()
239 || request.client_secret.is_empty()
240 || request.code.is_empty()
241 || request.redirect_uri.is_empty()
242 || request.code_verifier.is_empty()
243 {
244 return Err(Error::Invalid("token request is incomplete"));
245 }
246 Ok([
247 ("grant_type", "authorization_code"),
248 ("client_id", request.client_id),
249 ("client_secret", request.client_secret),
250 ("code", request.code),
251 ("redirect_uri", request.redirect_uri),
252 ("code_verifier", request.code_verifier),
253 ])
254}
255
256#[cfg(feature = "http")]
258fn bound(answer: TokenResponse) -> Result<TokenResponse, Error> {
259 binding::required(answer.grant_binding.as_deref())?;
260 Ok(answer)
261}
262
263#[cfg(feature = "http")]
267pub(crate) async fn read_token(response: reqwest::Response) -> Result<TokenResponse, Error> {
268 if !response.status().is_success() {
269 let refused: TokenRefusal = response
270 .json()
271 .await
272 .map_err(|error| Error::Transport(Box::new(error)))?;
273 if refused.error.is_empty()
274 || refused.refusal.is_empty()
275 || refused.reason != refused.error_description
276 {
277 return Err(Error::CannotAsk("token refusal has no name"));
278 }
279 return Err(Error::TokenRefused {
280 name: refused.refusal,
281 reason: refused.error_description,
282 });
283 }
284 let answer: TokenResponse = response
285 .json()
286 .await
287 .map_err(|error| Error::Transport(Box::new(error)))?;
288 if answer.token_type != "Bearer" || answer.access_token.is_empty() || answer.expires_in == 0 {
289 return Err(Error::CannotAsk("invalid token response"));
290 }
291 Ok(answer)
292}
293
294pub struct TokenRequest<'a> {
296 pub client_id: &'a str,
298 pub client_secret: &'a str,
300 pub code: &'a str,
302 pub redirect_uri: &'a str,
304 pub code_verifier: &'a str,
306}
307
308#[derive(Deserialize, Serialize)]
310#[serde(deny_unknown_fields)]
311pub struct TokenResponse {
312 pub access_token: String,
314 pub token_type: String,
316 pub expires_in: u64,
318 pub id_token: Option<String>,
320 pub refresh_token: Option<String>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub grant_binding: Option<String>,
326}
327
328#[cfg(feature = "http")]
329#[derive(Deserialize)]
330#[serde(deny_unknown_fields)]
331struct TokenRefusal {
332 error: String,
333 error_description: String,
334 refusal: String,
335 reason: String,
336}