httpd: Remove session serialization, improve tests

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez 2023-02-02 16:37:13 -03:00 committed by Alexis Sellier
parent 58eeb3ab91
commit d9f878bc45
No known key found for this signature in database
3 changed files with 48 additions and 44 deletions

View File

@ -26,7 +26,7 @@ lexopt = { version = "0.2.1" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] } serde_json = { version = "1", features = ["preserve_order"] }
thiserror = { version = "1" } thiserror = { version = "1" }
time = { version = "0.3.17" } time = { version = "0.3.17", features = ["parsing", "serde"] }
tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] } tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.3.4", default-features = false, features = ["trace", "cors", "set-header"] } tower-http = { version = "0.3.4", default-features = false, features = ["trace", "cors", "set-header"] }
tracing = { version = "0.1.37", default-features = false, features = ["std", "log"] } tracing = { version = "0.1.37", default-features = false, features = ["std", "log"] }

View File

@ -1,27 +1,21 @@
use radicle::crypto::PublicKey; use radicle::crypto::PublicKey;
use serde::{Serialize, Serializer}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::{serde::timestamp, OffsetDateTime};
#[derive(Clone, PartialEq, PartialOrd)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct DateTime(pub OffsetDateTime);
impl Serialize for DateTime {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&format!("{}", self.0))
}
}
#[derive(Clone, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum AuthState { pub enum AuthState {
Authorized, Authorized,
Unauthorized, Unauthorized,
} }
#[derive(Clone)] #[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Session { pub struct Session {
pub status: AuthState, pub status: AuthState,
pub public_key: PublicKey, pub public_key: PublicKey,
pub issued_at: DateTime, #[serde(with = "timestamp")]
pub expires_at: DateTime, pub issued_at: OffsetDateTime,
#[serde(with = "timestamp")]
pub expires_at: OffsetDateTime,
} }

View File

@ -4,12 +4,13 @@ use axum::extract::State;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::{post, put}; use axum::routing::{post, put};
use axum::{Json, Router}; use axum::{Json, Router};
use hyper::StatusCode;
use radicle::crypto::{PublicKey, Signature}; use radicle::crypto::{PublicKey, Signature};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use crate::api::auth::{AuthState, DateTime, Session}; use crate::api::auth::{AuthState, Session};
use crate::api::axum_extra::Path; use crate::api::axum_extra::Path;
use crate::api::error::Error; use crate::api::error::Error;
use crate::api::Context; use crate::api::Context;
@ -41,24 +42,26 @@ async fn session_create_handler(State(ctx): State<Context>) -> impl IntoResponse
.take(32) .take(32)
.collect::<String>(); .collect::<String>();
let signer = ctx.profile.signer().map_err(Error::from)?; let signer = ctx.profile.signer().map_err(Error::from)?;
let expiration_time = OffsetDateTime::now_utc()
.checked_add(UNAUTHORIZED_SESSIONS_EXPIRATION)
.unwrap();
let session = Session { let session = Session {
status: AuthState::Unauthorized, status: AuthState::Unauthorized,
public_key: *signer.public_key(), public_key: *signer.public_key(),
issued_at: DateTime(OffsetDateTime::now_utc()), issued_at: OffsetDateTime::now_utc(),
expires_at: DateTime(expiration_time), expires_at: OffsetDateTime::now_utc()
.checked_add(UNAUTHORIZED_SESSIONS_EXPIRATION)
.unwrap(),
}; };
let mut sessions = ctx.sessions.write().await; let mut sessions = ctx.sessions.write().await;
sessions.insert(session_id.clone(), session.clone()); sessions.insert(session_id.clone(), session.clone());
Ok::<_, Error>(Json(json!({ Ok::<_, Error>((
"sessionId": session_id, StatusCode::CREATED,
"publicKey": session.public_key, Json(json!({
"issuedAt": session.issued_at, "sessionId": session_id,
"expiresAt": session.expires_at "publicKey": session.public_key,
}))) "issuedAt": session.issued_at,
"expiresAt": session.expires_at
})),
))
} }
/// Get a session. /// Get a session.
@ -73,8 +76,8 @@ async fn session_handler(
Ok::<_, Error>(Json(json!({ Ok::<_, Error>(Json(json!({
"status": session.status, "status": session.status,
"publicKey": session.public_key, "publicKey": session.public_key,
"issuedAt": session.issued_at, "issuedAt": session.issued_at.unix_timestamp(),
"expiresAt": session.expires_at "expiresAt": session.expires_at.unix_timestamp()
}))) })))
} }
@ -86,12 +89,12 @@ async fn session_signin_handler(
Json(request): Json<AuthChallenge>, Json(request): Json<AuthChallenge>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let mut sessions = ctx.sessions.write().await; let mut sessions = ctx.sessions.write().await;
let session = sessions.get(&session_id).ok_or(Error::NotFound)?; let session = sessions.get_mut(&session_id).ok_or(Error::NotFound)?;
if session.status == AuthState::Unauthorized { if session.status == AuthState::Unauthorized {
if session.public_key != request.pk { if session.public_key != request.pk {
return Err(Error::Auth("Invalid public key")); return Err(Error::Auth("Invalid public key"));
} }
if session.expires_at <= DateTime(OffsetDateTime::now_utc()) { if session.expires_at <= OffsetDateTime::now_utc() {
return Err(Error::Auth("Session expired")); return Err(Error::Auth("Session expired"));
} }
let payload = format!("{}:{}", session_id, request.pk); let payload = format!("{}:{}", session_id, request.pk);
@ -99,16 +102,10 @@ async fn session_signin_handler(
.pk .pk
.verify(payload.as_bytes(), &request.sig) .verify(payload.as_bytes(), &request.sig)
.map_err(Error::from)?; .map_err(Error::from)?;
let expiration_time = OffsetDateTime::now_utc() session.status = AuthState::Authorized;
session.expires_at = OffsetDateTime::now_utc()
.checked_add(AUTHORIZED_SESSIONS_EXPIRATION) .checked_add(AUTHORIZED_SESSIONS_EXPIRATION)
.unwrap(); .unwrap();
let session = Session {
status: AuthState::Authorized,
public_key: request.pk,
issued_at: session.issued_at.to_owned(),
expires_at: DateTime(expiration_time),
};
sessions.insert(session_id.clone(), session);
return Ok::<_, Error>(Json(json!({ "success": true }))); return Ok::<_, Error>(Json(json!({ "success": true })));
} }
@ -122,7 +119,10 @@ mod routes {
use axum::http::StatusCode; use axum::http::StatusCode;
use radicle_cli::commands::rad_web::{self, SessionInfo}; use radicle_cli::commands::rad_web::{self, SessionInfo};
use crate::api::test::{self, get, post, put}; use crate::api::{
auth::{AuthState, Session},
test::{self, get, post, put},
};
#[tokio::test] #[tokio::test]
async fn test_session() { async fn test_session() {
@ -130,12 +130,19 @@ mod routes {
let ctx = test::seed(tmp.path()); let ctx = test::seed(tmp.path());
let app = super::router(ctx.to_owned()); let app = super::router(ctx.to_owned());
// Create session // Create session.
let response = post(&app, "/sessions", None).await; let response = post(&app, "/sessions", None).await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::CREATED);
let json = response.json().await; let json = response.json().await;
let session_info: SessionInfo = serde_json::from_value(json).unwrap(); let session_info: SessionInfo = serde_json::from_value(json).unwrap();
// Check that an unauthorized session has been created.
let response = get(&app, format!("/sessions/{}", session_info.session_id)).await;
assert_eq!(response.status(), StatusCode::OK);
let json = response.json().await;
let body: Session = serde_json::from_value(json).unwrap();
assert_eq!(body.status, AuthState::Unauthorized);
// Create request body // Create request body
let signer = ctx.profile.signer().unwrap(); let signer = ctx.profile.signer().unwrap();
let signature = rad_web::sign(signer, &session_info).unwrap(); let signature = rad_web::sign(signer, &session_info).unwrap();
@ -151,10 +158,13 @@ mod routes {
Some(Body::from(body)), Some(Body::from(body)),
) )
.await; .await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
// Check that session has been authorized.
let response = get(&app, format!("/sessions/{}", session_info.session_id)).await; let response = get(&app, format!("/sessions/{}", session_info.session_id)).await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let json = response.json().await;
let body: Session = serde_json::from_value(json).unwrap();
assert_eq!(body.status, AuthState::Authorized);
} }
} }