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