261 lines
7.9 KiB
Rust
261 lines
7.9 KiB
Rust
use std::sync::Arc;
|
|
|
|
use actix_jwt_session::*;
|
|
use actix_web::dev::ServiceResponse;
|
|
use actix_web::http::{Method, StatusCode};
|
|
use actix_web::web::{Data, Json};
|
|
use actix_web::HttpResponse;
|
|
use actix_web::{get, post};
|
|
use actix_web::{http::header::ContentType, test, App};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
struct Claims {
|
|
id: Uuid,
|
|
subject: String,
|
|
}
|
|
|
|
impl actix_jwt_session::Claims for Claims {
|
|
fn jti(&self) -> Uuid {
|
|
self.id
|
|
}
|
|
fn subject(&self) -> &str {
|
|
&self.subject
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn full_flow() {
|
|
let redis = {
|
|
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
|
RedisPool::new(
|
|
RedisConnectionManager::new(
|
|
redis::Client::open("redis://localhost:6379").expect("Fail to connect to redis"),
|
|
true,
|
|
None,
|
|
),
|
|
5,
|
|
)
|
|
};
|
|
|
|
let keys = JwtSigningKeys::generate(false).unwrap();
|
|
let (storage, factory) = SessionMiddlewareFactory::<Claims>::build(
|
|
Arc::new(keys.encoding_key),
|
|
Arc::new(keys.decoding_key),
|
|
Algorithm::EdDSA,
|
|
)
|
|
.with_redis_pool(redis.clone())
|
|
.with_jwt_header(JWT_HEADER_NAME)
|
|
.with_refresh_header(REFRESH_HEADER_NAME)
|
|
.with_jwt_cookie(JWT_COOKIE_NAME)
|
|
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
|
.finish();
|
|
|
|
let app = App::new()
|
|
.app_data(Data::new(storage.clone()))
|
|
.wrap(factory.clone())
|
|
.app_data(Data::new(redis.clone()))
|
|
.app_data(Data::new(JwtTtl(Duration::seconds(1))))
|
|
.app_data(Data::new(RefreshTtl(Duration::seconds(30))))
|
|
.service(sign_in)
|
|
.service(sign_out)
|
|
.service(session)
|
|
.service(refresh_session)
|
|
.service(root);
|
|
|
|
let app = actix_web::test::init_service(app).await;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Assert authorization is ignored when token is not needed
|
|
// -----------------------------------------------------------------------------
|
|
let res = test::call_service(
|
|
&app,
|
|
test::TestRequest::default()
|
|
.insert_header(ContentType::plaintext())
|
|
.to_request(),
|
|
)
|
|
.await;
|
|
assert!(res.status().is_success());
|
|
|
|
// -----------------------------------------------------------------
|
|
// Assert signed out when active session
|
|
// -----------------------------------------------------------------
|
|
let res = test::call_service(&app, session_request("", "").to_request()).await;
|
|
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
|
|
|
let origina_claims = Claims {
|
|
id: Uuid::new_v4(),
|
|
subject: "foo".to_string(),
|
|
};
|
|
|
|
// ----------------------------------------------
|
|
// Create session
|
|
// ----------------------------------------------
|
|
println!("-> Creating session");
|
|
let res = test::call_service(
|
|
&app,
|
|
test::TestRequest::default()
|
|
.uri("/session/sign-in")
|
|
.method(actix_web::http::Method::POST)
|
|
.insert_header(ContentType::json())
|
|
.set_json(&origina_claims)
|
|
.to_request(),
|
|
)
|
|
.await;
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
println!(" <- OK");
|
|
|
|
let auth_bearer = res
|
|
.headers()
|
|
.get(JWT_HEADER_NAME)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap();
|
|
let refresh_bearer = res
|
|
.headers()
|
|
.get(REFRESH_HEADER_NAME)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap();
|
|
|
|
// ----------------------------------------------
|
|
// Assert signed in
|
|
// ----------------------------------------------
|
|
println!("-> Assert signed in");
|
|
let res = test::call_service(
|
|
&app,
|
|
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
|
)
|
|
.await;
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
println!(" <- OK");
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
|
|
|
// ----------------------------------------------
|
|
// Access Token TTL expires
|
|
// ----------------------------------------------
|
|
println!("-> Access Token TTL expires");
|
|
let res = test::try_call_service(
|
|
&app,
|
|
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
|
)
|
|
.await;
|
|
expect_invalid_session(res);
|
|
println!(" <- OK");
|
|
|
|
// ----------------------------------------------
|
|
// Refresh token
|
|
// ----------------------------------------------
|
|
println!("-> Refresh token");
|
|
let res = test::call_service(
|
|
&app,
|
|
test::TestRequest::default()
|
|
.uri("/session/refresh")
|
|
.method(Method::GET)
|
|
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
|
.to_request(),
|
|
)
|
|
.await;
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
println!(" <- OK");
|
|
|
|
// ----------------------------------------------
|
|
// Logout
|
|
// ----------------------------------------------
|
|
println!("-> Logout");
|
|
let res = test::call_service(
|
|
&app,
|
|
test::TestRequest::default()
|
|
.uri("/session/sign-out")
|
|
.method(Method::POST)
|
|
.insert_header((JWT_HEADER_NAME, auth_bearer))
|
|
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
|
.to_request(),
|
|
)
|
|
.await;
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
println!(" <- OK");
|
|
|
|
// --------------------------------------------------------------
|
|
// Assert signed out - session destroyed
|
|
// --------------------------------------------------------------
|
|
println!("-> Assert signed out - session destroyed");
|
|
let res = test::try_call_service(
|
|
&app,
|
|
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
|
)
|
|
.await;
|
|
expect_invalid_session(res);
|
|
println!(" <- OK");
|
|
}
|
|
|
|
#[post("/session/sign-in")]
|
|
async fn sign_in(
|
|
store: Data<SessionStorage>,
|
|
claims: Json<Claims>,
|
|
jwt_ttl: Data<JwtTtl>,
|
|
refresh_ttl: Data<RefreshTtl>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
let claims = claims.into_inner();
|
|
let store = store.into_inner();
|
|
let pair = store
|
|
.clone()
|
|
.store(claims, *jwt_ttl.into_inner(), *refresh_ttl.into_inner())
|
|
.await
|
|
.unwrap();
|
|
Ok(HttpResponse::Ok()
|
|
.append_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
|
.append_header((REFRESH_HEADER_NAME, pair.refresh.encode().unwrap()))
|
|
.finish())
|
|
}
|
|
|
|
#[post("/session/sign-out")]
|
|
async fn sign_out(store: Data<SessionStorage>, auth: Authenticated<Claims>) -> HttpResponse {
|
|
let store = store.into_inner();
|
|
store.erase::<Claims>(auth.id).await.unwrap();
|
|
HttpResponse::Ok().finish()
|
|
}
|
|
|
|
#[get("/session/info")]
|
|
async fn session(auth: Authenticated<Claims>) -> HttpResponse {
|
|
HttpResponse::Ok().json(&*auth)
|
|
}
|
|
|
|
#[get("/session/refresh")]
|
|
async fn refresh_session(
|
|
auth: Authenticated<RefreshToken>,
|
|
storage: Data<SessionStorage>,
|
|
) -> HttpResponse {
|
|
let storage = storage.into_inner();
|
|
storage.refresh(auth.refresh_jti).await.unwrap();
|
|
HttpResponse::Ok().json(&*auth)
|
|
}
|
|
|
|
#[get("/")]
|
|
async fn root() -> HttpResponse {
|
|
HttpResponse::Ok().finish()
|
|
}
|
|
|
|
fn session_request(auth_bearer: &str, refresh_bearer: &str) -> actix_web::test::TestRequest {
|
|
let req = test::TestRequest::default()
|
|
.uri("/session/info")
|
|
.method(Method::GET);
|
|
if !auth_bearer.is_empty() {
|
|
req.insert_header((JWT_HEADER_NAME, auth_bearer))
|
|
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
|
} else {
|
|
req
|
|
}
|
|
}
|
|
|
|
fn expect_invalid_session(res: Result<ServiceResponse, actix_web::Error>) {
|
|
let err = res
|
|
.expect_err("Must be unauthorized")
|
|
.as_error::<actix_jwt_session::Error>()
|
|
.expect("Must be authorization error")
|
|
.clone();
|
|
assert_eq!(err, actix_jwt_session::Error::LoadError);
|
|
}
|