use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use sha2::{Digest, Sha256};
use uuid::Uuid;
#[derive(Clone)]
pub enum Auth {
ClientCredentials {
client_id: String,
client_secret: String,
},
Bearer {
token: String,
},
Password {
user: String,
pass: String,
},
RefreshToken {
refresh_token: String,
client_id: String,
},
}
impl std::fmt::Debug for Auth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Auth::ClientCredentials { client_id, .. } => f
.debug_struct("ClientCredentials")
.field("client_id", client_id)
.field("client_secret", &"***")
.finish(),
Auth::Bearer { .. } => f.debug_struct("Bearer").field("token", &"***").finish(),
Auth::Password { user, .. } => f
.debug_struct("Password")
.field("user", user)
.field("pass", &"***")
.finish(),
Auth::RefreshToken { client_id, .. } => f
.debug_struct("RefreshToken")
.field("refresh_token", &"***")
.field("client_id", client_id)
.finish(),
}
}
}
impl Auth {
pub(crate) fn static_headers(&self) -> HeaderMap {
let mut h = HeaderMap::new();
match self {
Auth::Bearer { token } => {
h.insert(
AUTHORIZATION,
header_value(&format!("Bearer {}", token.trim())),
);
}
Auth::Password { .. } => {}
Auth::ClientCredentials { .. } => {}
Auth::RefreshToken { .. } => {}
}
h
}
}
fn header_value(s: &str) -> HeaderValue {
HeaderValue::from_str(s).unwrap_or_else(|_| HeaderValue::from_static(""))
}
#[derive(Clone)]
pub struct PkceChallenge {
pub code_verifier: String,
pub code_challenge: String,
pub code_challenge_method: String,
}
impl std::fmt::Debug for PkceChallenge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PkceChallenge")
.field("code_verifier", &"***")
.field("code_challenge", &self.code_challenge)
.field("code_challenge_method", &self.code_challenge_method)
.finish()
}
}
pub fn pkce_challenge() -> PkceChallenge {
let mut bytes = Vec::with_capacity(32);
bytes.extend_from_slice(Uuid::new_v4().as_bytes());
bytes.extend_from_slice(Uuid::new_v4().as_bytes());
let code_verifier = URL_SAFE_NO_PAD.encode(&bytes);
let code_challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(code_verifier.as_bytes()));
PkceChallenge {
code_verifier,
code_challenge,
code_challenge_method: "S256".to_string(),
}
}
pub fn build_authorize_url(
base_url: &str,
client_id: &str,
redirect_uri: &str,
scope: &str,
code_challenge: &str,
state: Option<&str>,
code_challenge_method: Option<&str>,
) -> String {
let base = base_url.trim_end_matches('/');
let mut q = url::form_urlencoded::Serializer::new(String::new());
q.append_pair("response_type", "code");
q.append_pair("client_id", client_id);
q.append_pair("redirect_uri", redirect_uri);
q.append_pair("scope", scope);
q.append_pair("code_challenge", code_challenge);
q.append_pair(
"code_challenge_method",
code_challenge_method.unwrap_or("S256"),
);
if let Some(s) = state {
q.append_pair("state", s);
}
format!("{base}/oauth/authorize?{}", q.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pkce_challenge_is_s256_of_verifier() {
let c = pkce_challenge();
assert_eq!(c.code_challenge_method, "S256");
assert!(c.code_verifier.len() >= 43 && c.code_verifier.len() <= 128);
assert!(c
.code_verifier
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'));
assert!(!c.code_verifier.contains('=') && !c.code_challenge.contains('='));
let expected = URL_SAFE_NO_PAD.encode(Sha256::digest(c.code_verifier.as_bytes()));
assert_eq!(c.code_challenge, expected);
assert_ne!(pkce_challenge().code_verifier, c.code_verifier);
}
#[test]
fn build_authorize_url_has_pkce_params() {
let u = build_authorize_url(
"https://acme.babelforce.com/",
"spa",
"https://app.example.com/cb",
"*",
"CHAL",
Some("xyz"),
None,
);
let parsed = url::Url::parse(&u).unwrap();
assert_eq!(parsed.path(), "/oauth/authorize");
let q: std::collections::HashMap<_, _> = parsed.query_pairs().into_owned().collect();
assert_eq!(q["response_type"], "code");
assert_eq!(q["client_id"], "spa");
assert_eq!(q["redirect_uri"], "https://app.example.com/cb");
assert_eq!(q["scope"], "*");
assert_eq!(q["code_challenge"], "CHAL");
assert_eq!(q["code_challenge_method"], "S256");
assert_eq!(q["state"], "xyz");
}
}