mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 19:30:38 +08:00
Add web-based authentication
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
committed by
Alexis Sellier
parent
71edcfe7f2
commit
932d8d2ed8
@@ -19,14 +19,12 @@ anyhow = { version = "1" }
|
||||
axum = { version = "0.6.2", default-features = false, features = ["headers", "json", "query", "tokio"] }
|
||||
axum-server = { version = "0.4.4", default-features = false }
|
||||
chrono = { version = "0.4.22" }
|
||||
ethers-core = { version = "1.0" }
|
||||
fastrand = { version = "1.7.0" }
|
||||
flate2 = { version = "1" }
|
||||
hyper = { version = "0.14.17", default-features = false }
|
||||
lexopt = { version = "0.2.1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
siwe = { version = "0.5" }
|
||||
thiserror = { version = "1" }
|
||||
time = { version = "0.3.17" }
|
||||
tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
@@ -73,7 +73,7 @@ pub fn router(ctx: Context) -> Router {
|
||||
CorsLayer::new()
|
||||
.max_age(Duration::from_secs(86400))
|
||||
.allow_origin(cors::Any)
|
||||
.allow_methods([Method::GET, Method::POST, Method::PUT])
|
||||
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::PUT])
|
||||
.allow_headers([CONTENT_TYPE, AUTHORIZATION]),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ethers_core::types::{Signature, H160};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use radicle::crypto::PublicKey;
|
||||
use serde::{Serialize, Serializer};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::api::error::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq, PartialOrd)]
|
||||
pub struct DateTime(pub OffsetDateTime);
|
||||
|
||||
impl Serialize for DateTime {
|
||||
@@ -16,26 +11,11 @@ impl Serialize for DateTime {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct AuthRequest {
|
||||
pub message: String,
|
||||
#[serde(deserialize_with = "deserialize_signature")]
|
||||
pub signature: Signature,
|
||||
}
|
||||
|
||||
fn deserialize_signature<'de, D>(deserializer: D) -> Result<Signature, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
let buf = String::deserialize(deserializer)?;
|
||||
Signature::from_str(&buf).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
pub enum AuthState {
|
||||
Authorized(Session),
|
||||
Unauthorized {
|
||||
nonce: String,
|
||||
expiration_time: DateTime,
|
||||
public_key: PublicKey,
|
||||
expires_at: DateTime,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,48 +23,7 @@ pub enum AuthState {
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Session {
|
||||
pub domain: String,
|
||||
pub address: H160,
|
||||
pub statement: Option<String>,
|
||||
pub uri: String,
|
||||
pub version: u64,
|
||||
pub chain_id: u64,
|
||||
pub nonce: String,
|
||||
pub public_key: PublicKey,
|
||||
pub issued_at: DateTime,
|
||||
pub expiration_time: Option<DateTime>,
|
||||
pub resources: Vec<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<siwe::Message> for Session {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(message: siwe::Message) -> Result<Session, Error> {
|
||||
Ok(Session {
|
||||
domain: message.domain.host().to_string(),
|
||||
address: H160(message.address),
|
||||
statement: None,
|
||||
uri: message.uri.to_string(),
|
||||
version: message.version as u64,
|
||||
chain_id: message.chain_id,
|
||||
nonce: message.nonce,
|
||||
issued_at: DateTime(message.issued_at.as_ref().to_owned()),
|
||||
expiration_time: message
|
||||
.expiration_time
|
||||
.map(|x| DateTime(x.as_ref().to_owned())),
|
||||
resources: message.resources.iter().map(|r| r.to_string()).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn test_auth_request_de() {
|
||||
let json = serde_json::json!({
|
||||
"message": "Hello World!",
|
||||
"signature": "20096c6ed2bcccb88c9cafbbbbda7a5a3cff6d0ca318c07faa58464083ca40a92f899fbeb26a4c763a7004b13fd0f1ba6c321d4e3a023e30f63c40d4154b99a41c"
|
||||
});
|
||||
|
||||
let _req: super::AuthRequest = serde_json::from_value(json).unwrap();
|
||||
}
|
||||
pub expires_at: DateTime,
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Env(#[from] std::env::VarError),
|
||||
|
||||
/// An error occurred while verifying the siwe message.
|
||||
/// Profile error.
|
||||
#[error(transparent)]
|
||||
SiweVerification(#[from] siwe::VerificationError),
|
||||
Profile(#[from] radicle::profile::Error),
|
||||
|
||||
/// An error occurred while parsing the siwe message.
|
||||
/// Crypto error.
|
||||
#[error(transparent)]
|
||||
SiweParse(#[from] siwe::ParseError),
|
||||
Crypto(#[from] radicle::crypto::Error),
|
||||
|
||||
/// Storage error.
|
||||
#[error(transparent)]
|
||||
@@ -60,8 +60,7 @@ impl IntoResponse for Error {
|
||||
let (status, msg) = match &self {
|
||||
Error::NotFound => (StatusCode::NOT_FOUND, None),
|
||||
Error::Auth(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
|
||||
Error::SiweParse(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
|
||||
Error::SiweVerification(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
|
||||
Error::Crypto(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
|
||||
Error::Git2(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Some(e.message().to_owned()),
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use std::{env, fs};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::Request;
|
||||
use axum::http::{Method, Request};
|
||||
use axum::Router;
|
||||
use serde_json::Value;
|
||||
use tower::ServiceExt;
|
||||
@@ -116,20 +116,42 @@ pub fn seed(dir: &Path) -> Context {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request(app: &Router, path: impl ToString) -> Response {
|
||||
pub async fn get(app: &Router, path: impl ToString) -> Response {
|
||||
Response(
|
||||
app.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(path.to_string())
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.oneshot(request(path, Method::GET, None))
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn post(app: &Router, path: impl ToString, body: Option<Body>) -> Response {
|
||||
Response(
|
||||
app.clone()
|
||||
.oneshot(request(path, Method::POST, body))
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn put(app: &Router, path: impl ToString, body: Option<Body>) -> Response {
|
||||
Response(
|
||||
app.clone()
|
||||
.oneshot(request(path, Method::PUT, body))
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn request(path: impl ToString, method: Method, body: Option<Body>) -> Request<Body> {
|
||||
Request::builder()
|
||||
.method(method)
|
||||
.uri(path.to_string())
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body.unwrap_or_else(Body::empty))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub struct Response(axum::response::Response);
|
||||
|
||||
impl Response {
|
||||
|
||||
@@ -69,13 +69,13 @@ mod routes {
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::api::test::{self, request, HEAD};
|
||||
use crate::api::test::{self, get, HEAD};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delegates_projects() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(
|
||||
let response = get(
|
||||
&app,
|
||||
"/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects",
|
||||
)
|
||||
|
||||
@@ -465,13 +465,13 @@ mod routes {
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::api::test::{self, request, HEAD, HEAD_1};
|
||||
use crate::api::test::{self, get, HEAD, HEAD_1};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_projects_root() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(&app, "/projects").await;
|
||||
let response = get(&app, "/projects").await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
@@ -494,7 +494,7 @@ mod routes {
|
||||
async fn test_projects() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp").await;
|
||||
let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp").await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
@@ -515,7 +515,7 @@ mod routes {
|
||||
async fn test_projects_commits_root() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/commits").await;
|
||||
let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/commits").await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FOUND);
|
||||
assert_eq!(
|
||||
@@ -637,7 +637,7 @@ mod routes {
|
||||
async fn test_projects_commits() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(
|
||||
let response = get(
|
||||
&app,
|
||||
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/commits/{HEAD}"),
|
||||
)
|
||||
@@ -703,7 +703,7 @@ mod routes {
|
||||
async fn test_projects_tree() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(
|
||||
let response = get(
|
||||
&app,
|
||||
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/tree/{HEAD}/"),
|
||||
)
|
||||
@@ -750,7 +750,7 @@ mod routes {
|
||||
)
|
||||
);
|
||||
|
||||
let response = request(
|
||||
let response = get(
|
||||
&app,
|
||||
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/tree/{HEAD}/dir1"),
|
||||
)
|
||||
@@ -796,7 +796,7 @@ mod routes {
|
||||
async fn test_projects_remotes_root() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes").await;
|
||||
let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes").await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
@@ -817,7 +817,7 @@ mod routes {
|
||||
async fn test_projects_remotes() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi").await;
|
||||
let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi").await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
@@ -836,7 +836,7 @@ mod routes {
|
||||
async fn test_projects_blob() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(
|
||||
let response = get(
|
||||
&app,
|
||||
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/blob/{HEAD}/README"),
|
||||
)
|
||||
@@ -872,7 +872,7 @@ mod routes {
|
||||
async fn test_projects_readme() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(
|
||||
let response = get(
|
||||
&app,
|
||||
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/readme/{HEAD}"),
|
||||
)
|
||||
@@ -908,7 +908,7 @@ mod routes {
|
||||
async fn test_projects_issues_root() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues").await;
|
||||
let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues").await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,131 +1,135 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::env;
|
||||
use std::iter::repeat_with;
|
||||
use std::str::FromStr;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{get, post};
|
||||
use axum::routing::{post, put};
|
||||
use axum::{Json, Router};
|
||||
use ethers_core::utils::hex;
|
||||
use hyper::http::uri::Authority;
|
||||
use radicle::crypto::{PublicKey, Signature};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use siwe::Message;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
use crate::api::auth::{AuthRequest, AuthState, DateTime, Session};
|
||||
use crate::api::auth::{AuthState, DateTime, Session};
|
||||
use crate::api::axum_extra::Path;
|
||||
use crate::api::error::Error;
|
||||
use crate::api::Context;
|
||||
|
||||
pub const UNAUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::seconds(60);
|
||||
pub const AUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::weeks(1);
|
||||
|
||||
pub fn router(ctx: Context) -> Router {
|
||||
Router::new()
|
||||
.route("/sessions", post(session_create_handler))
|
||||
.route(
|
||||
"/sessions/:id",
|
||||
get(session_get_handler).put(session_signin_handler),
|
||||
)
|
||||
.route("/sessions/:id", put(session_signin_handler))
|
||||
.with_state(ctx)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct AuthChallenge {
|
||||
sig: Signature,
|
||||
pk: PublicKey,
|
||||
}
|
||||
|
||||
/// Create session.
|
||||
/// `POST /sessions`
|
||||
async fn session_create_handler(State(ctx): State<Context>) -> impl IntoResponse {
|
||||
let rng = fastrand::Rng::new();
|
||||
let session_id = repeat_with(|| rng.alphanumeric())
|
||||
.take(32)
|
||||
.collect::<String>();
|
||||
let signer = ctx.profile.signer().map_err(Error::from)?;
|
||||
let expiration_time = OffsetDateTime::now_utc()
|
||||
.checked_add(UNAUTHORIZED_SESSIONS_EXPIRATION)
|
||||
.unwrap();
|
||||
let auth_state = AuthState::Unauthorized {
|
||||
public_key: *signer.public_key(),
|
||||
expires_at: DateTime(expiration_time),
|
||||
};
|
||||
let mut sessions = ctx.sessions.write().await;
|
||||
let (session_id, nonce) = create_session(&mut sessions, DateTime(expiration_time));
|
||||
sessions.insert(session_id.clone(), auth_state);
|
||||
|
||||
Json(json!({ "id": session_id, "nonce": nonce }))
|
||||
}
|
||||
|
||||
/// Get session.
|
||||
/// `GET /sessions/:id`
|
||||
async fn session_get_handler(
|
||||
State(ctx): State<Context>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let sessions = ctx.sessions.read().await;
|
||||
let session = sessions.get(&id).ok_or(Error::NotFound)?;
|
||||
|
||||
match session {
|
||||
AuthState::Authorized(session) => {
|
||||
Ok::<_, Error>(Json(json!({ "id": id, "session": session })))
|
||||
}
|
||||
AuthState::Unauthorized {
|
||||
nonce,
|
||||
expiration_time,
|
||||
} => Ok::<_, Error>(Json(
|
||||
json!({ "id": id, "nonce": nonce, "expirationTime": expiration_time }),
|
||||
)),
|
||||
}
|
||||
Ok::<_, Error>(Json(
|
||||
json!({"sessionId": session_id, "publicKey": signer.public_key()}),
|
||||
))
|
||||
}
|
||||
|
||||
/// Update session.
|
||||
/// `PUT /sessions/:id`
|
||||
async fn session_signin_handler(
|
||||
State(ctx): State<Context>,
|
||||
Path(id): Path<String>,
|
||||
Json(request): Json<AuthRequest>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(request): Json<AuthChallenge>,
|
||||
) -> impl IntoResponse {
|
||||
// Get unauthenticated session data, return early if not found
|
||||
let mut sessions = ctx.sessions.write().await;
|
||||
let session = sessions.get(&id).ok_or(Error::NotFound)?;
|
||||
|
||||
if let AuthState::Unauthorized { nonce, .. } = session {
|
||||
let message = Message::from_str(request.message.as_str()).map_err(Error::from)?;
|
||||
|
||||
let host = env::var("RADICLE_DOMAIN").map_err(Error::from)?;
|
||||
|
||||
// Validate nonce
|
||||
if *nonce != message.nonce {
|
||||
return Err(Error::Auth("Invalid nonce"));
|
||||
let session = sessions.get(&session_id).ok_or(Error::NotFound)?;
|
||||
if let AuthState::Unauthorized {
|
||||
public_key,
|
||||
expires_at,
|
||||
} = session
|
||||
{
|
||||
if public_key != &request.pk {
|
||||
return Err(Error::Auth("Invalid public key"));
|
||||
}
|
||||
|
||||
// Verify that domain is the correct one
|
||||
let authority = Authority::from_str(&host).map_err(|_| Error::Auth("Invalid host"))?;
|
||||
if authority != message.domain {
|
||||
return Err(Error::Auth("Invalid domain"));
|
||||
if expires_at <= &DateTime(OffsetDateTime::now_utc()) {
|
||||
return Err(Error::Auth("Session expired"));
|
||||
}
|
||||
|
||||
// Verifies the following:
|
||||
// - AuthRequest sig matches the address passed in the AuthRequest message.
|
||||
// - expirationTime is not in the past.
|
||||
// - notBefore time is in the future.
|
||||
message
|
||||
.verify(&request.signature.to_vec(), &Default::default())
|
||||
.await
|
||||
let payload = format!("{}:{}", session_id, request.pk);
|
||||
request
|
||||
.pk
|
||||
.verify(payload.as_bytes(), &request.sig)
|
||||
.map_err(Error::from)?;
|
||||
let expiration_time = OffsetDateTime::now_utc()
|
||||
.checked_add(AUTHORIZED_SESSIONS_EXPIRATION)
|
||||
.unwrap();
|
||||
let session = Session {
|
||||
public_key: request.pk,
|
||||
issued_at: DateTime(OffsetDateTime::now_utc()),
|
||||
expires_at: DateTime(expiration_time),
|
||||
};
|
||||
sessions.insert(session_id.clone(), AuthState::Authorized(session));
|
||||
|
||||
let session: Session = message.try_into()?;
|
||||
sessions.insert(id.clone(), AuthState::Authorized(session.clone()));
|
||||
|
||||
return Ok::<_, Error>(Json(json!({ "id": id, "session": session })));
|
||||
return Ok::<_, Error>(());
|
||||
}
|
||||
|
||||
Err(Error::Auth("Session already authorized"))
|
||||
}
|
||||
|
||||
fn create_session(
|
||||
map: &mut HashMap<String, AuthState>,
|
||||
expiration_time: DateTime,
|
||||
) -> (String, String) {
|
||||
let nonce = siwe::generate_nonce();
|
||||
#[cfg(test)]
|
||||
mod routes {
|
||||
use axum::body::Body;
|
||||
use axum::http::StatusCode;
|
||||
use radicle_cli::commands::rad_web::{self, SessionInfo};
|
||||
|
||||
// We generate a value from the RNG for the session id
|
||||
let rng = fastrand::Rng::new();
|
||||
let id = hex::encode(repeat_with(|| rng.u8(..)).take(32).collect::<Vec<u8>>());
|
||||
use crate::api::test::{self, post, put};
|
||||
|
||||
let auth_state = AuthState::Unauthorized {
|
||||
nonce: nonce.clone(),
|
||||
expiration_time,
|
||||
};
|
||||
#[tokio::test]
|
||||
async fn test_session() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test::seed(tmp.path());
|
||||
let app = super::router(ctx.to_owned());
|
||||
|
||||
map.insert(id.clone(), auth_state);
|
||||
// Create session
|
||||
let response = post(&app, "/sessions", None).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let json = response.json().await;
|
||||
let session_info: SessionInfo = serde_json::from_value(json).unwrap();
|
||||
|
||||
(id, nonce)
|
||||
// Create request body
|
||||
let signer = ctx.profile.signer().unwrap();
|
||||
let signature = rad_web::sign(signer, &session_info).unwrap();
|
||||
let body = serde_json::to_vec(&super::AuthChallenge {
|
||||
sig: signature,
|
||||
pk: session_info.public_key,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let response = put(
|
||||
&app,
|
||||
format!("/sessions/{}", session_info.session_id),
|
||||
Some(Body::from(body)),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ mod routes {
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::api::test::{self, request};
|
||||
use crate::api::test::{self, get};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stats() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let app = super::router(test::seed(tmp.path()));
|
||||
let response = request(&app, "/stats").await;
|
||||
let response = get(&app, "/stats").await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user