http_auth/basic.rs
1// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! `Basic` authentication scheme as in
5//! [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617).
6
7use std::convert::TryFrom;
8
9use crate::ChallengeRef;
10
11/// Encodes the given credentials.
12///
13/// This can be used to preemptively send `Basic` authentication, without
14/// sending an unauthenticated request and waiting for a `401 Unauthorized`
15/// response.
16///
17/// The caller should use the returned string as an `Authorization` or
18/// `Proxy-Authorization` header value.
19///
20/// The caller is responsible for `username` and `password` being in the
21/// correct format. Servers may expect arguments to be in Unicode
22/// Normalization Form C as noted in [RFC 7617 section
23/// 2.1](https://datatracker.ietf.org/doc/html/rfc7617#section-2.1).
24///
25/// ```rust
26/// assert_eq!(
27/// http_auth::basic::encode_credentials("Aladdin", "open sesame"),
28/// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
29/// );
30pub fn encode_credentials(username: &str, password: &str) -> String {
31 use base64::Engine as _;
32 let user_pass = format!("{}:{}", username, password);
33 const PREFIX: &str = "Basic ";
34 let mut value = String::with_capacity(PREFIX.len() + base64_encoded_len(user_pass.len()));
35 value.push_str(PREFIX);
36 base64::engine::general_purpose::STANDARD.encode_string(&user_pass[..], &mut value);
37 value
38}
39
40/// Returns the base64-encoded length for the given input length, including padding.
41fn base64_encoded_len(input_len: usize) -> usize {
42 (input_len + 2) / 3 * 4
43}
44
45/// Client for a `Basic` challenge, as in
46/// [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617).
47///
48/// This implementation always uses `UTF-8`. Thus it doesn't use or store the
49/// `charset` parameter, which the RFC only allows to be set to `UTF-8` anyway.
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct BasicClient {
52 realm: Box<str>,
53}
54
55impl BasicClient {
56 pub fn realm(&self) -> &str {
57 &self.realm
58 }
59
60 /// Responds to the challenge with the supplied parameters.
61 ///
62 /// This is functionally identical to [`encode_credentials`]; no parameters
63 /// of the `BasicClient` are needed to produce the credentials.
64 #[inline]
65 pub fn respond(&self, username: &str, password: &str) -> String {
66 encode_credentials(username, password)
67 }
68}
69
70impl TryFrom<&ChallengeRef<'_>> for BasicClient {
71 type Error = String;
72
73 fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> {
74 if !value.scheme.eq_ignore_ascii_case("Basic") {
75 return Err(format!(
76 "BasicClient doesn't support challenge scheme {:?}",
77 value.scheme
78 ));
79 }
80 let mut realm = None;
81 for (k, v) in &value.params {
82 if k.eq_ignore_ascii_case("realm") {
83 realm = Some(v.to_unescaped());
84 }
85 }
86 let realm = realm.ok_or("missing required parameter realm")?;
87 Ok(BasicClient {
88 realm: realm.into_boxed_str(),
89 })
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn basic() {
99 // Example from https://datatracker.ietf.org/doc/html/rfc7617#section-2
100 let ctx = BasicClient {
101 realm: "WallyWorld".into(),
102 };
103 assert_eq!(
104 ctx.respond("Aladdin", "open sesame"),
105 "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
106 );
107
108 // Example from https://datatracker.ietf.org/doc/html/rfc7617#section-2.1
109 // Note that this crate *always* uses UTF-8, not just when the server requests it.
110 let ctx = BasicClient {
111 realm: "foo".into(),
112 };
113 assert_eq!(ctx.respond("test", "123\u{A3}"), "Basic dGVzdDoxMjPCow==");
114 }
115}