1use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use std::collections::HashMap;
19use thiserror::Error;
20use url::{self, Url};
21
22#[derive(Debug, Error, Deserialize)]
25#[error("{description}")]
26pub struct ApiError {
27 pub error: String,
28
29 #[serde(rename = "error_description")]
30 pub description: String,
31
32 #[serde(rename = "error_debug")]
33 pub debug: String,
34
35 pub request_id: String,
36}
37
38#[derive(Debug, Deserialize)]
39pub struct CompletedRequest {
40 pub redirect_to: String,
41}
42
43#[derive(Debug, Deserialize)]
44pub struct OAuth2Client {
45 pub metadata: HashMap<String, String>,
46}
47
48#[derive(Debug, Deserialize)]
51pub struct LoginRequest {
52 pub client: OAuth2Client,
53
54 #[serde(default)]
55 pub context: HashMap<String, Value>,
56
57 pub skip: bool,
58
59 pub subject: String,
60}
61
62#[derive(Debug, Serialize)]
63struct AcceptLoginRequest {
64 acr: Option<String>,
65 context: Option<HashMap<String, Value>>,
66 force_subject_identifier: Option<String>,
67 remember: Option<bool>,
68 remember_for: Option<u64>,
69 subject: String,
70}
71
72#[derive(Debug, Deserialize)]
75pub struct ConsentRequest {
76 #[serde(default)]
77 pub context: HashMap<String, Value>,
78
79 pub requested_access_token_audience: Vec<String>,
80
81 pub requested_scope: Vec<String>,
82
83 pub skip: bool,
84
85 pub subject: String,
86}
87
88#[derive(Debug, Serialize)]
89struct AcceptConsentRequest {
90 grant_access_token_audience: Vec<String>,
91 grant_scope: Vec<String>,
92 handled_at: DateTime<Utc>,
93 remember: Option<bool>,
94 remember_for: Option<u64>,
95 session: Option<ConsentRequestSession>,
96}
97
98#[derive(Debug, Serialize)]
99struct ConsentRequestSession {
100 id_token: Option<HashMap<String, Value>>,
101}
102
103#[derive(Debug, Serialize)]
106pub struct AcceptLogoutRequest;
107
108#[derive(Debug, Error)]
109pub enum Error {
110 #[error(transparent)]
111 RequestError(#[from] reqwest::Error),
112
113 #[error(transparent)]
114 URLParseError(#[from] url::ParseError),
115
116 #[error("API error: {}", .0.description)]
117 ApiError(#[from] ApiError),
118
119 #[error("Unknown error: {0}")]
120 UnknownError(String),
121}
122
123#[derive(Debug, Clone)]
124pub struct Hydra {
125 url: Url,
126 client: reqwest::blocking::Client,
127}
128
129impl Hydra {
130 pub fn new(url: Url) -> Hydra {
131 Hydra {
132 url,
133 client: reqwest::blocking::Client::new(),
134 }
135 }
136
137 pub fn get_login_request(&self, login_challenge: String) -> Result<LoginRequest, Error> {
140 self.get(
141 self.endpoint("/oauth2/auth/requests/login")?,
142 Some(format!("login_challenge={}", login_challenge).as_str()),
143 )
144 }
145
146 #[allow(clippy::too_many_arguments)]
147 pub fn accept_login_request(
148 &self,
149 login_challenge: String,
150 subject: String,
151 acr: Option<String>,
152 context: Option<HashMap<String, Value>>,
153 force_subject_identifier: Option<String>,
154 remember: Option<bool>,
155 remember_for: Option<u64>,
156 ) -> Result<CompletedRequest, Error> {
157 let body = AcceptLoginRequest {
158 acr,
159 context,
160 force_subject_identifier,
161 remember,
162 remember_for,
163 subject,
164 };
165
166 self.put(
167 self.endpoint("/oauth2/auth/requests/login/accept")?,
168 Some(format!("login_challenge={}", login_challenge).as_str()),
169 Some(body),
170 )
171 }
172
173 pub fn get_consent_request(&self, consent_challenge: String) -> Result<ConsentRequest, Error> {
176 self.get(
177 self.endpoint("/oauth2/auth/requests/consent")?,
178 Some(format!("consent_challenge={}", consent_challenge).as_str()),
179 )
180 }
181
182 pub fn accept_consent_request(
183 &self,
184 consent_challenge: String,
185 grant_access_token_audience: Vec<String>,
186 grant_scope: Vec<String>,
187 remember: Option<bool>,
188 remember_for: Option<u64>,
189 claims: Option<HashMap<String, Value>>,
190 ) -> Result<CompletedRequest, Error> {
191 let session = match claims.is_some() {
192 true => Some(ConsentRequestSession { id_token: claims }),
193 false => None,
194 };
195
196 let body = AcceptConsentRequest {
197 grant_access_token_audience,
198 grant_scope,
199 handled_at: Utc::now(),
200 remember,
201 remember_for,
202 session,
203 };
204
205 self.put(
206 self.endpoint("/oauth2/auth/requests/consent/accept")?,
207 Some(format!("consent_challenge={}", consent_challenge).as_str()),
208 Some(body),
209 )
210 }
211
212 pub fn accept_logout_request(
215 &self,
216 logout_challenge: String,
217 ) -> Result<CompletedRequest, Error> {
218 self.put(
219 self.endpoint("/oauth2/auth/requests/logout/accept")?,
220 Some(format!("logout_challenge={}", logout_challenge).as_str()),
221 AcceptLogoutRequest,
222 )
223 }
224
225 fn endpoint(&self, endpoint: &str) -> Result<Url, Error> {
228 self.url
229 .clone()
230 .join(endpoint)
231 .map_err(Error::URLParseError)
232 }
233
234 fn deserialize<R: for<'de> Deserialize<'de>>(
235 r: reqwest::blocking::Response,
236 ) -> Result<R, Error> {
237 let status = r.status();
238
239 if status.is_success() {
240 r.json().map_err(Error::RequestError)
241 } else {
242 match r.json::<ApiError>() {
243 Ok(api_error) => Err(Error::ApiError(api_error)),
244 Err(_) => Err(Error::UnknownError(format!(
245 "unable to parse reply from Hydra API (status: {})",
246 status.clone()
247 ))),
248 }
249 }
250 }
251
252 fn get<T: for<'de> Deserialize<'de>>(&self, url: Url, query: Option<&str>) -> Result<T, Error> {
253 let mut url = url;
254 url.set_query(query);
255
256 let r = self.client.get(url).send()?;
257
258 Hydra::deserialize(r)
259 }
260
261 fn put<T: Serialize, R: for<'de> Deserialize<'de>>(
262 &self,
263 url: Url,
264 query: Option<&str>,
265 body: T,
266 ) -> Result<R, Error> {
267 let mut url = url;
268 url.set_query(query);
269
270 let r = self.client.put(url).json(&body).send()?;
271
272 Hydra::deserialize(r)
273 }
274}