1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
//! # Http Client Interface for Custom Http Clients
use std::collections::HashMap;
use std::fmt::Debug;
use std::future;
use www_authenticate_parser::{parse_header, Challenge, CowStr, UniCase};
use crate::helpers::map_to_url_encoded;
/// The Http methods
#[derive(Debug, Default, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[allow(clippy::upper_case_acronyms)]
pub enum HttpMethod {
/// Retrieve data from a server.
#[default]
GET,
/// Submit data to a server.
POST,
/// Replace existing data on a server.
PUT,
/// Update a specific part of a resource.
PATCH,
/// Delete a resource from a server.
DELETE,
/// Retrieve only the headers of a resource.
HEAD,
/// Retrieve the capabilities of a server.
OPTIONS,
/// Echo the received request back to the client.
TRACE,
/// Establish a tunnel through a proxy server.
CONNECT,
}
/// The expectations set by methods such as discover, token grant, callback etc...
#[derive(Debug, Clone, Copy)]
pub struct HttpResponseExpectations {
/// Whether or not to expect body with the response.
pub body: bool,
/// Specifies if the response should be of type json and validates it.
pub json: bool,
/// Expect a raw string in the body
pub raw: bool,
/// Expected status code from the server.
pub status_code: u16,
/// Check for bearer token related errors.
pub bearer: bool,
}
/// The client certificate
#[derive(Debug, Clone)]
pub struct ClientCertificate {
/// Chain of PEM encoded X509 certificates, with the leaf certificate first.
pub cert: String,
/// PEM encoded PKCS #8 formatted private key for the leaf certificate.
pub key: String,
}
/// The supported body types for an HTTP request.
#[derive(Debug)]
pub enum RequestBody {
/// A map of form parameters encoded as application/x-www-form-urlencoded.
Form(HashMap<String, String>),
/// A JSON payload as a raw string with an application/json MIME type.
Json(String),
/// An arbitrary raw string payload.
Raw(String),
}
impl RequestBody {
/// Converts the request body to its string representation if possible.
pub fn body_string(&self) -> Option<String> {
match &self {
RequestBody::Form(form) => Some(map_to_url_encoded(form)),
RequestBody::Json(json) => Some(json.to_owned()),
RequestBody::Raw(raw) => Some(raw.to_owned()),
}
}
}
/// Request is an internal struct used to create various OIDC requests.
#[derive(Debug)]
pub struct HttpRequest {
/// Target URL of the request without query parameters.
pub url: url::Url,
/// The HTTP method to be used for the request.
pub method: HttpMethod,
/// Map of HTTP headers to be sent in the request.
pub headers: HashMap<String, Vec<String>>,
/// The optional payload to be included in the request.
pub body: Option<RequestBody>,
/// Indicates if the request requires Mutual TLS and a client certificate.
pub mtls: bool,
/// The certificate and key used for MTLS authentication.
pub client_certificate: Option<ClientCertificate>,
pub(crate) expectations: HttpResponseExpectations,
}
impl Default for HttpRequest {
fn default() -> Self {
Self::new()
}
}
#[allow(unused)]
impl HttpRequest {
/// Initializes a new request with default settings and a placeholder URL.
pub fn new() -> Self {
Self {
url: url::Url::parse("about:blank").unwrap(),
headers: HashMap::new(),
method: HttpMethod::GET,
body: None,
client_certificate: None,
mtls: false,
expectations: HttpResponseExpectations {
body: true,
status_code: 200,
json: true,
raw: false,
bearer: false,
},
}
}
/// Finalizes the request by calculating and setting the content-length header.
pub(crate) fn prepare(&mut self) {
if let Some(body) = self.body.as_ref().and_then(|b| b.body_string()) {
self.headers
.insert("content-length".to_string(), vec![body.len().to_string()]);
};
}
/// Sets the target URL for the request.
pub(crate) fn url(mut self, url: url::Url) -> Self {
self.url = url;
self
}
/// Sets the HTTP method for the request.
pub(crate) fn method(mut self, method: HttpMethod) -> Self {
self.method = method;
self
}
/// Appends a value to the specified header name.
pub(crate) fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
let name = name.into();
let value = value.into();
if let Some(values) = self.headers.get_mut(&name) {
values.push(value);
} else {
let values = vec![value];
self.headers.insert(name, values);
}
self
}
/// Overwrites any existing values for a specific header with a new list of values.
pub(crate) fn header_replace(mut self, name: impl Into<String>, value: Vec<String>) -> Self {
self.headers.insert(name.into(), value);
self
}
/// Replaces the entire header map with the provided collection.
pub(crate) fn headers(mut self, headers: HashMap<String, Vec<String>>) -> Self {
self.headers = headers;
self
}
/// Sets the request body to a JSON string and adds the appropriate content-type header.
pub(crate) fn json(mut self, json: String) -> Self {
self.headers.insert(
"content-type".to_string(),
vec!["application/json".to_string()],
);
self.body = Some(RequestBody::Json(json));
self
}
/// Sets the request body as form-encoded data and adds the appropriate content-type header.
pub(crate) fn form(mut self, form: HashMap<String, String>) -> Self {
self.headers.insert(
"content-type".to_string(),
vec!["application/x-www-form-urlencoded".to_string()],
);
self.body(map_to_url_encoded(&form))
}
/// Sets a raw string as the request body.
pub(crate) fn body(mut self, body: String) -> Self {
self.body = Some(RequestBody::Raw(body));
self
}
/// Configures whether the request should use Mutual TLS.
pub(crate) fn mtls(mut self, mtls: bool) -> Self {
self.mtls = mtls;
self
}
/// Sets the HTTP status code that the library expects for a successful operation.
pub(crate) fn expect_status_code(mut self, code: u16) -> Self {
self.expectations.status_code = code;
self
}
/// Sets the expectation that the response body should be present and valid JSON.
pub(crate) fn expect_json(mut self) -> Self {
self.expectations.json = true;
self.expectations.body = true;
self.expectations.raw = false;
self
}
/// Sets the expection that the response body should be a raw string.
pub(crate) fn expect_raw_body(mut self) -> Self {
self.expectations.body = true;
self.expectations.raw = true;
self.expectations.json = false;
self
}
}
/// Represents an HTTP response received from a server.
#[derive(Debug, Clone)]
pub struct HttpResponse {
/// The HTTP status code of the response.
pub status_code: u16,
/// The optional body content of the response as a string.
pub body: Option<String>,
/// The HTTP headers received in the response.
pub headers: HashMap<String, Vec<String>>,
}
/// Represents the parsed components of a WWW-Authenticate header.
pub struct OpenIdWwwAuthenticateParsed(Challenge);
impl OpenIdWwwAuthenticateParsed {
fn get_value(&self, key: &str) -> Option<&String> {
match &self.0 {
Challenge::Fields(challenge_fields) => challenge_fields.get(key),
_ => None,
}
}
/// Checks if the challenge follows the Token68 format.
pub fn is_token68(&self) -> bool {
matches!(self.0, Challenge::Token68(_))
}
/// Checks if the challenge contains a map of fields.
pub fn is_challenge(&self) -> bool {
matches!(self.0, Challenge::Fields(_))
}
/// Returns the raw Token68 string if present.
pub fn token68(&self) -> Option<&String> {
match &self.0 {
Challenge::Token68(t68) => Some(t68),
_ => None,
}
}
/// Returns the "realm" value from the challenge fields.
pub fn realm(&self) -> Option<&String> {
self.get_value("realm")
}
/// Returns the "error" code from the challenge fields.
pub fn error(&self) -> Option<&String> {
self.get_value("error")
}
/// Returns the "error_description" text from the challenge fields.
pub fn error_description(&self) -> Option<&String> {
self.get_value("error_description")
}
/// Returns the "error_uri" value from the challenge fields.
pub fn error_uri(&self) -> Option<&String> {
self.get_value("error_uri")
}
/// Returns the "algs" parameter value from the challenge fields.
pub fn algs(&self) -> Option<&String> {
self.get_value("algs")
}
/// Returns the "scope" parameter value from the challenge fields.
pub fn scope(&self) -> Option<&String> {
self.get_value("scope")
}
/// Returns the "resource_metadata" value from the challenge fields.
pub fn resource_metadata(&self) -> Option<&String> {
self.get_value("resource_metadata")
}
}
impl HttpResponse {
/// Extracts the value of the "content-type" header from the response.
pub fn content_type_header(&self) -> Option<&String> {
self.headers.get("content-type").and_then(|ct| ct.first())
}
/// Extracts the value of the "dpop-nonce" header from the response.
pub fn dpop_nonce_header(&self) -> Option<&String> {
self.headers.get("dpop-nonce").and_then(|ct| ct.first())
}
/// Parses the "www-authenticate" headers into a map of schemes and their associated challenges.
pub fn parsed_www_authenticate_errors(
&self,
) -> Option<HashMap<UniCase<CowStr>, OpenIdWwwAuthenticateParsed>> {
let www_headers = self.headers.get("www-authenticate");
if let Some(www_headers) = www_headers {
let mut map = HashMap::new();
for header_val in www_headers {
if let Ok((scheme, challenge)) = parse_header(header_val) {
map.insert(scheme, OpenIdWwwAuthenticateParsed(challenge));
}
}
if !map.is_empty() {
return Some(map);
}
}
None
}
}
/// This trait defines the interface for making HTTP requests used by the OpenID library.
pub trait OidcHttpClient {
/// Retrieves the client certificate for Mutual TLS if required by the request.
fn get_client_certificate(
&self,
_req: &HttpRequest,
) -> impl std::future::Future<Output = Option<ClientCertificate>> + Send {
future::ready(None)
}
/// Executes the provided HTTP request and returns the response or an error string.
fn request(
&self,
req: HttpRequest,
) -> impl std::future::Future<Output = Result<HttpResponse, String>> + Send;
}