implements authentication/token retrieval/keyring
This commit is contained in:
@@ -53,7 +53,8 @@ pub enum Error {
|
||||
ClientError(String),
|
||||
HTTPError(hyper::Error),
|
||||
SerdeError(serde_json::Error),
|
||||
DecodeError,
|
||||
DecodeError(String),
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
@@ -192,7 +193,7 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
|
||||
|
||||
let body = || -> Body { serde_json::to_string(&credentials).unwrap().into() };
|
||||
let token: AuthResponse = self.request_with_body_retry("authenticate", Method::POST, body, false).await?;
|
||||
let token = JwtToken::new(&token.jwt).map_err(|_| Error::DecodeError)?;
|
||||
let token = JwtToken::new(&token.jwt).map_err(|e| Error::DecodeError(e.to_string()))?;
|
||||
|
||||
log::debug!("Saving token: {:?}", token);
|
||||
self.auth_store.set_token(token.clone()).await;
|
||||
@@ -239,7 +240,6 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
|
||||
}
|
||||
|
||||
async fn open_event_socket(&mut self) -> Result<WebsocketEventSocket, Self::Error> {
|
||||
use tungstenite::http::StatusCode;
|
||||
use tungstenite::handshake::client::Request as TungsteniteRequest;
|
||||
use tungstenite::handshake::client::generate_key;
|
||||
|
||||
@@ -259,21 +259,45 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
|
||||
.body(())
|
||||
.expect("Unable to build websocket request");
|
||||
|
||||
match &auth {
|
||||
Some(token) => {
|
||||
let header_value = token.to_header_value().to_str().unwrap().parse().unwrap(); // ugh
|
||||
request.headers_mut().insert("Authorization", header_value);
|
||||
}
|
||||
None => {
|
||||
log::warn!(target: "websocket", "Proceeding without auth token.");
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Websocket request: {:?}", request);
|
||||
|
||||
if let Some(token) = &auth {
|
||||
let header_value = token.to_header_value().to_str().unwrap().parse().unwrap(); // ugh
|
||||
request.headers_mut().insert("Authorization", header_value);
|
||||
match connect_async(request).await.map_err(Error::from) {
|
||||
Ok((socket, response)) => {
|
||||
log::debug!("Websocket connected: {:?}", response.status());
|
||||
Ok(WebsocketEventSocket::new(socket))
|
||||
}
|
||||
Err(e) => match e {
|
||||
Error::ClientError(ce) => match ce.as_str() {
|
||||
"HTTP error: 401 Unauthorized" | "Unauthorized" => {
|
||||
// Try to authenticate
|
||||
if let Some(credentials) = &self.auth_store.get_credentials().await {
|
||||
log::warn!("Websocket connection failed, attempting to authenticate");
|
||||
let new_token = self.authenticate(credentials.clone()).await?;
|
||||
self.auth_store.set_token(new_token).await;
|
||||
|
||||
// try again on the next attempt.
|
||||
return Err(Error::Unauthorized);
|
||||
} else {
|
||||
log::error!("Websocket unauthorized, no credentials provided");
|
||||
return Err(Error::ClientError("Unauthorized, no credentials provided".into()));
|
||||
}
|
||||
}
|
||||
_ => Err(Error::Unauthorized)
|
||||
}
|
||||
|
||||
_ => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
let (socket, response) = connect_async(request).await.map_err(Error::from)?;
|
||||
log::debug!("Websocket connected: {:?}", response.status());
|
||||
|
||||
if response.status() != StatusCode::SWITCHING_PROTOCOLS {
|
||||
return Err(Error::ClientError("Websocket connection failed".into()));
|
||||
}
|
||||
|
||||
Ok(WebsocketEventSocket::new(socket))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +408,7 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
|
||||
|
||||
// If JSON deserialization fails, try to interpret it as plain text
|
||||
// Unfortunately the server does return things like this...
|
||||
let s = str::from_utf8(&body).map_err(|_| Error::DecodeError)?;
|
||||
let s = str::from_utf8(&body).map_err(|e| Error::DecodeError(e.to_string()))?;
|
||||
serde_plain::from_str(s).map_err(|_| json_err)
|
||||
}
|
||||
}?;
|
||||
|
||||
@@ -26,11 +26,31 @@ enum ExpValue {
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
struct JwtPayload {
|
||||
exp: serde_json::Value,
|
||||
#[serde(deserialize_with = "deserialize_exp")]
|
||||
exp: i64,
|
||||
iss: Option<String>,
|
||||
user: Option<String>,
|
||||
}
|
||||
|
||||
fn deserialize_exp<'de, D>(deserializer: D) -> Result<i64, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ExpValue {
|
||||
String(String),
|
||||
Number(i64),
|
||||
}
|
||||
|
||||
match ExpValue::deserialize(deserializer)? {
|
||||
ExpValue::String(s) => s.parse().map_err(D::Error::custom),
|
||||
ExpValue::Number(n) => Ok(n),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct JwtToken {
|
||||
@@ -62,13 +82,7 @@ impl JwtToken {
|
||||
let payload: JwtPayload = serde_json::from_slice(&payload)?;
|
||||
|
||||
// Parse jwt expiration date
|
||||
// Annoyingly, because of my own fault, this could be either an integer or string.
|
||||
let exp: i64 = payload.exp.as_i64().unwrap_or_else(|| {
|
||||
let exp: String = payload.exp.as_str().unwrap().to_string();
|
||||
exp.parse().unwrap()
|
||||
});
|
||||
|
||||
let timestamp = DateTime::from_timestamp(exp, 0).unwrap().naive_utc();
|
||||
let timestamp = DateTime::from_timestamp(payload.exp, 0).unwrap().naive_utc();
|
||||
let expiration_date = DateTime::from_naive_utc_and_offset(timestamp, Utc);
|
||||
|
||||
Ok(JwtToken {
|
||||
@@ -84,9 +98,19 @@ impl JwtToken {
|
||||
// STUPID: My mock server uses a different encoding than the real server, so we have to
|
||||
// try both encodings here.
|
||||
|
||||
Self::decode_token_using_engine(token, general_purpose::STANDARD).or(
|
||||
log::debug!("Attempting to decode JWT token: {}", token);
|
||||
|
||||
let result = Self::decode_token_using_engine(token, general_purpose::STANDARD).or(
|
||||
Self::decode_token_using_engine(token, general_purpose::URL_SAFE_NO_PAD),
|
||||
)
|
||||
);
|
||||
|
||||
if let Err(ref e) = result {
|
||||
log::error!("Failed to decode JWT token: {}", e);
|
||||
log::error!("Token length: {}", token.len());
|
||||
log::error!("Token parts: {:?}", token.split('.').collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn dummy() -> Self {
|
||||
@@ -96,7 +120,7 @@ impl JwtToken {
|
||||
typ: "JWT".to_string(),
|
||||
},
|
||||
payload: JwtPayload {
|
||||
exp: serde_json::Value::Null,
|
||||
exp: 0,
|
||||
iss: None,
|
||||
user: None,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user