httpd: Remove session serialization, improve tests
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
parent
58eeb3ab91
commit
d9f878bc45
|
|
@ -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"] }
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue