1#![deny(warnings, clippy::todo, clippy::unimplemented, missing_docs)]
6
7use impulse_utils::errors::{ClientError, ErrorResponse};
8use impulse_utils::results::CResult;
9
10pub use authnz_common::SIGNUP_HINTS;
11pub use authnz_common::{Email, SignKeypair, TokenBundle};
12
13pub(crate) mod utils;
14
15const AUTHNZ_CBA_CERT: &str = "__authnz_client_keypair";
16const AUTHNZ_ACCESS_TOKEN: &str = "__authnz_access_token";
17const AUTHNZ_REFRESH_TOKEN: &str = "__authnz_refresh_token";
18const AUTHNZ_CLIENT_TOKEN: &str = "__authnz_client_token";
19
20pub fn client_keypair() -> CResult<SignKeypair> {
22 if let Some(cert) = crate::utils::get_from_storage(AUTHNZ_CBA_CERT)
23 && let Ok(keypair) = SignKeypair::unpack_keypair(cert)
24 {
25 Ok(keypair)
26 } else {
27 let keypair = generate_and_save()?;
28 Ok(keypair)
29 }
30}
31
32fn generate_and_save() -> CResult<SignKeypair> {
33 let keypair = SignKeypair::new_ed25519().map_err(ClientError::from)?;
34 crate::utils::put_in_storage(AUTHNZ_CBA_CERT, &keypair.pack_keypair());
35 Ok(keypair)
36}
37
38pub fn store_triple(tokens: &TokenBundle) -> CResult<()> {
40 crate::utils::put_in_storage(AUTHNZ_ACCESS_TOKEN, tokens.act());
41 crate::utils::put_in_storage(AUTHNZ_REFRESH_TOKEN, tokens.rft());
42 crate::utils::put_in_storage(AUTHNZ_CLIENT_TOKEN, tokens.cba().unwrap_or(""));
43
44 Ok(())
45}
46
47pub fn get_triple() -> CResult<TokenBundle> {
49 let access = crate::utils::get_from_storage(AUTHNZ_ACCESS_TOKEN).ok_or(ClientError::from_str("No access token inside LocalStorage!"))?;
50 let refresh = crate::utils::get_from_storage(AUTHNZ_REFRESH_TOKEN).ok_or(ClientError::from_str("No refresh token inside LocalStorage!"))?;
51 let client = crate::utils::get_from_storage(AUTHNZ_CLIENT_TOKEN).ok_or(ClientError::from_str("No client token inside LocalStorage!"))?;
52
53 Ok(if client.is_empty() {
54 TokenBundle::new_basic(access, refresh)
55 } else {
56 TokenBundle::new_with_cba(access, refresh, client)
57 })
58}
59
60#[allow(async_fn_in_trait)]
61pub trait Authorize
63where
64 Self: Sized,
65{
66 async fn authorize(self, endpoint: impl AsRef<str>) -> CResult<Self>;
68}
69
70pub trait ClientPlatformAware {
72 fn include_creds(self) -> Self;
74}
75
76impl ClientPlatformAware for reqwest::RequestBuilder {
77 fn include_creds(self) -> Self {
78 if let Ok(triple) = get_triple() {
79 #[cfg(target_arch = "wasm32")]
80 {
81 self.fetch_credentials_include().header("Authorization", &triple.pack())
82 }
83 #[cfg(not(target_arch = "wasm32"))]
84 {
85 self.header("Authorization", &triple.pack())
86 }
87 } else {
88 #[cfg(target_arch = "wasm32")]
89 {
90 self.fetch_credentials_include()
91 }
92 #[cfg(not(target_arch = "wasm32"))]
93 {
94 self
95 }
96 }
97 }
98}
99
100pub trait MaybeTokensUpdate {
102 fn update_tokens(self) -> Self;
104}
105
106impl MaybeTokensUpdate for reqwest::Response {
107 fn update_tokens(self) -> Self {
108 #[cfg(target_arch = "wasm32")]
109 {
110 let mut old_triple = if let Ok(triple) = get_triple() {
111 triple
112 } else {
113 return self;
114 };
115 if let Some(new_act) = extract_header(&self, authnz_common::ACCESS_TOKEN) {
116 old_triple.set_act(new_act);
117 }
118 if let Some(new_cba) = extract_header(&self, authnz_common::CLIENT_TOKEN) {
119 old_triple.set_cba(Some(new_cba));
120 }
121 let _ = store_triple(&old_triple);
122 }
123 self
124 }
125}
126
127fn extract_and_decode_header(resp: &reqwest::Response, header_name: impl AsRef<str>) -> Option<Vec<u8>> {
128 resp.headers().get(header_name.as_ref()).and_then(|encoded| {
129 encoded
130 .to_str()
131 .ok()
132 .and_then(|str_encoded| authnz_common::base64_decode(str_encoded).ok())
133 })
134}
135
136fn extract_header(resp: &reqwest::Response, header_name: impl AsRef<str>) -> Option<&str> {
137 resp
138 .headers()
139 .get(header_name.as_ref())
140 .and_then(|header_val| header_val.to_str().ok())
141}
142
143fn auth_err_handler(builder: reqwest::RequestBuilder, bytes: &[u8]) -> CResult<reqwest::RequestBuilder> {
144 if let Ok(authorize_response) = serde_json::from_slice::<authnz_common::ApplicationAuthorizeResponse>(bytes)
145 && authorize_response.authorized
146 {
147 Ok(builder.include_creds())
148 } else if let Ok(err_resp) = serde_json::from_slice::<ErrorResponse>(bytes) {
149 Err(ClientError::from_str(err_resp.err))
150 } else {
151 Err(ClientError::from_str(format!("Unknown error: `{:?}`", String::from_utf8_lossy(bytes))))
152 }
153}
154
155impl Authorize for reqwest::RequestBuilder {
156 async fn authorize(self, endpoint: impl AsRef<str>) -> CResult<Self> {
158 let resp = reqwest::Client::new()
159 .post(endpoint.as_ref())
160 .include_creds()
161 .send()
162 .await
163 .map_err(ClientError::from)?
164 .update_tokens();
165
166 if let Some(challenge) = extract_and_decode_header(&resp, authnz_common::CLIENT_CHALLENGE_TOKEN)
167 && let Some(challenge_state) = extract_header(&resp, authnz_common::CLIENT_CHALLENGE_TOKEN)
168 {
169 let keypair = client_keypair()?;
170 let sign = keypair.sign_raw(&challenge);
171
172 let resp2 = reqwest::Client::new()
173 .post(endpoint.as_ref())
174 .include_creds()
175 .header(authnz_common::CLIENT_CHALLENGE_TOKEN, challenge_state)
176 .header(authnz_common::CLIENT_CHALLENGE_SIGN, authnz_common::base64_encode(&sign))
177 .send()
178 .await
179 .map_err(ClientError::from)?
180 .update_tokens()
181 .bytes()
182 .await
183 .map_err(ClientError::from)?;
184
185 return auth_err_handler(self, &resp2);
186 }
187
188 auth_err_handler(self, resp.bytes().await.map_err(ClientError::from)?.as_ref())
189 }
190}