#![deny(warnings, clippy::todo, clippy::unimplemented, missing_docs)]
use impulse_utils::errors::{ClientError, ErrorResponse};
use impulse_utils::results::CResult;
pub use authnz_common::SIGNUP_HINTS;
pub use authnz_common::{Email, SignKeypair, TokenBundle};
pub(crate) mod utils;
const AUTHNZ_CBA_CERT: &str = "__authnz_client_keypair";
const AUTHNZ_ACCESS_TOKEN: &str = "__authnz_access_token";
const AUTHNZ_REFRESH_TOKEN: &str = "__authnz_refresh_token";
const AUTHNZ_CLIENT_TOKEN: &str = "__authnz_client_token";
pub fn client_keypair() -> CResult<SignKeypair> {
if let Some(cert) = crate::utils::get_from_storage(AUTHNZ_CBA_CERT)
&& let Ok(keypair) = SignKeypair::unpack_keypair(cert)
{
Ok(keypair)
} else {
let keypair = generate_and_save()?;
Ok(keypair)
}
}
fn generate_and_save() -> CResult<SignKeypair> {
let keypair = SignKeypair::new_ed25519().map_err(ClientError::from)?;
crate::utils::put_in_storage(AUTHNZ_CBA_CERT, &keypair.pack_keypair());
Ok(keypair)
}
pub fn store_triple(tokens: &TokenBundle) -> CResult<()> {
crate::utils::put_in_storage(AUTHNZ_ACCESS_TOKEN, tokens.act());
crate::utils::put_in_storage(AUTHNZ_REFRESH_TOKEN, tokens.rft());
crate::utils::put_in_storage(AUTHNZ_CLIENT_TOKEN, tokens.cba().unwrap_or(""));
Ok(())
}
pub fn get_triple() -> CResult<TokenBundle> {
let access = crate::utils::get_from_storage(AUTHNZ_ACCESS_TOKEN).ok_or(ClientError::from_str("No access token inside LocalStorage!"))?;
let refresh = crate::utils::get_from_storage(AUTHNZ_REFRESH_TOKEN).ok_or(ClientError::from_str("No refresh token inside LocalStorage!"))?;
let client = crate::utils::get_from_storage(AUTHNZ_CLIENT_TOKEN).ok_or(ClientError::from_str("No client token inside LocalStorage!"))?;
Ok(if client.is_empty() {
TokenBundle::new_basic(access, refresh)
} else {
TokenBundle::new_with_cba(access, refresh, client)
})
}
#[allow(async_fn_in_trait)]
pub trait Authorize
where
Self: Sized,
{
async fn authorize(self, endpoint: impl AsRef<str>) -> CResult<Self>;
}
pub trait ClientPlatformAware {
fn include_creds(self) -> Self;
}
impl ClientPlatformAware for reqwest::RequestBuilder {
fn include_creds(self) -> Self {
if let Ok(triple) = get_triple() {
#[cfg(target_arch = "wasm32")]
{
self.fetch_credentials_include().header("Authorization", &triple.pack())
}
#[cfg(not(target_arch = "wasm32"))]
{
self.header("Authorization", &triple.pack())
}
} else {
#[cfg(target_arch = "wasm32")]
{
self.fetch_credentials_include()
}
#[cfg(not(target_arch = "wasm32"))]
{
self
}
}
}
}
pub trait MaybeTokensUpdate {
fn update_tokens(self) -> Self;
}
impl MaybeTokensUpdate for reqwest::Response {
fn update_tokens(self) -> Self {
#[cfg(target_arch = "wasm32")]
{
let mut old_triple = if let Ok(triple) = get_triple() {
triple
} else {
return self;
};
if let Some(new_act) = extract_header(&self, authnz_common::ACCESS_TOKEN) {
old_triple.set_act(new_act);
}
if let Some(new_cba) = extract_header(&self, authnz_common::CLIENT_TOKEN) {
old_triple.set_cba(Some(new_cba));
}
let _ = store_triple(&old_triple);
}
self
}
}
fn extract_and_decode_header(resp: &reqwest::Response, header_name: impl AsRef<str>) -> Option<Vec<u8>> {
resp.headers().get(header_name.as_ref()).and_then(|encoded| {
encoded
.to_str()
.ok()
.and_then(|str_encoded| authnz_common::base64_decode(str_encoded).ok())
})
}
fn extract_header(resp: &reqwest::Response, header_name: impl AsRef<str>) -> Option<&str> {
resp
.headers()
.get(header_name.as_ref())
.and_then(|header_val| header_val.to_str().ok())
}
fn auth_err_handler(builder: reqwest::RequestBuilder, bytes: &[u8]) -> CResult<reqwest::RequestBuilder> {
if let Ok(authorize_response) = serde_json::from_slice::<authnz_common::ApplicationAuthorizeResponse>(bytes)
&& authorize_response.authorized
{
Ok(builder.include_creds())
} else if let Ok(err_resp) = serde_json::from_slice::<ErrorResponse>(bytes) {
Err(ClientError::from_str(err_resp.err))
} else {
Err(ClientError::from_str(format!("Unknown error: `{:?}`", String::from_utf8_lossy(bytes))))
}
}
impl Authorize for reqwest::RequestBuilder {
async fn authorize(self, endpoint: impl AsRef<str>) -> CResult<Self> {
let resp = reqwest::Client::new()
.post(endpoint.as_ref())
.include_creds()
.send()
.await
.map_err(ClientError::from)?
.update_tokens();
if let Some(challenge) = extract_and_decode_header(&resp, authnz_common::CLIENT_CHALLENGE_TOKEN)
&& let Some(challenge_state) = extract_header(&resp, authnz_common::CLIENT_CHALLENGE_TOKEN)
{
let keypair = client_keypair()?;
let sign = keypair.sign_raw(&challenge);
let resp2 = reqwest::Client::new()
.post(endpoint.as_ref())
.include_creds()
.header(authnz_common::CLIENT_CHALLENGE_TOKEN, challenge_state)
.header(authnz_common::CLIENT_CHALLENGE_SIGN, authnz_common::base64_encode(&sign))
.send()
.await
.map_err(ClientError::from)?
.update_tokens()
.bytes()
.await
.map_err(ClientError::from)?;
return auth_err_handler(self, &resp2);
}
auth_err_handler(self, resp.bytes().await.map_err(ClientError::from)?.as_ref())
}
}