Skip to main content

authnz_client_sdk/
lib.rs

1//! Client SDK for Authnz Authorization Server.
2//!
3//! This crate contains functions to work with client signatures and token persistance.
4
5#![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
20/// Gets or generates client-side keypair.
21pub 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
38/// Stores token triple to authorize user.
39pub 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
47/// Gets token triple to authorize user.
48pub 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)]
61/// Authorization's extension trait.
62pub trait Authorize
63where
64  Self: Sized,
65{
66  /// Make sure that client is authorized before request.
67  async fn authorize(self, endpoint: impl AsRef<str>) -> CResult<Self>;
68}
69
70/// Platform-aware authorization credentials' extension trait.
71pub trait ClientPlatformAware {
72  /// Include credentials if they stored in LocalStorage.
73  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
100/// Tokens' extension trait.
101pub trait MaybeTokensUpdate {
102  /// Update and persist tokens, if needed.
103  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  /// Automatically gets token if persisted.
157  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}