use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Digest, Sha256};
use super::ConciergeError;
const VERIFIER_BYTES: usize = 32;
const STATE_BYTES: usize = 32;
#[derive(Clone)]
pub struct PkceVerifier(String);
impl PkceVerifier {
pub fn generate() -> Result<Self, ConciergeError> {
Ok(Self(random_b64("a PKCE verifier", VERIFIER_BYTES)?))
}
pub fn from_stored(verifier: impl Into<String>) -> Self {
Self(verifier.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn challenge(&self) -> String {
URL_SAFE_NO_PAD.encode(Sha256::digest(self.0.as_bytes()))
}
}
impl std::fmt::Debug for PkceVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("PkceVerifier(<redacted>)")
}
}
pub fn random_state() -> Result<String, ConciergeError> {
random_b64("an authorize state", STATE_BYTES)
}
fn random_b64(what: &'static str, len: usize) -> Result<String, ConciergeError> {
let mut bytes = vec![0u8; len];
getrandom::fill(&mut bytes).map_err(|source| ConciergeError::Randomness { what, source })?;
Ok(URL_SAFE_NO_PAD.encode(bytes))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Prompt {
SelectAccount,
}
#[derive(Clone, Debug)]
pub struct Authorize<'a> {
pub concierge_origin: &'a str,
pub client_id: &'a str,
pub redirect_uri: &'a str,
pub state: &'a str,
pub verifier: &'a PkceVerifier,
pub prompt: Option<Prompt>,
}
impl Authorize<'_> {
pub fn url(&self) -> String {
let mut url = format!(
"{}/api/auth/authorize?response_type=code&client_id={}&redirect_uri={}&state={}&code_challenge={}&code_challenge_method=S256",
self.concierge_origin.trim_end_matches('/'),
encode(self.client_id),
encode(self.redirect_uri),
encode(self.state),
self.verifier.challenge(),
);
match self.prompt {
Some(Prompt::SelectAccount) => url.push_str("&prompt=select_account"),
None => {}
}
url
}
}
fn encode(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => out.push(char::from(byte)),
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}