use actix_web::{HttpRequest, HttpResponse};
use futures::{future::IntoFuture, Future};
use crate::biome::oauth::store::{OAuthUserSessionStore, OAuthUserSessionStoreError};
#[cfg(feature = "authorization")]
use crate::rest_api::auth::authorization::Permission;
use crate::rest_api::{
actix_web_1::{Method, ProtocolVersionRangeGuard, Resource},
auth::{AuthorizationHeader, BearerToken},
ErrorResponse, SPLINTER_PROTOCOL_VERSION,
};
const OAUTH_LOGOUT_MIN: u32 = 1;
pub fn make_logout_route(oauth_user_session_store: Box<dyn OAuthUserSessionStore>) -> Resource {
let resource = Resource::build("/oauth/logout").add_request_guard(
ProtocolVersionRangeGuard::new(OAUTH_LOGOUT_MIN, SPLINTER_PROTOCOL_VERSION),
);
#[cfg(feature = "authorization")]
{
resource.add_method(
Method::Get,
Permission::AllowAuthenticated,
move |req, _| {
let access_token = match get_access_token(req) {
Ok(access_token) => access_token,
Err(err_response) => return err_response,
};
Box::new(
match oauth_user_session_store.remove_session(&access_token) {
Ok(()) | Err(OAuthUserSessionStoreError::InvalidState(_)) => {
HttpResponse::Ok()
.json(json!({
"message": "User successfully logged out"
}))
.into_future()
}
Err(err) => {
error!("Unable to remove user session: {}", err);
HttpResponse::InternalServerError()
.json(ErrorResponse::internal_error())
.into_future()
}
},
)
},
)
}
#[cfg(not(feature = "authorization"))]
{
resource.add_method(Method::Get, move |req, _| {
let access_token = match get_access_token(req) {
Ok(access_token) => access_token,
Err(err_response) => return err_response,
};
Box::new(
match oauth_user_session_store.remove_session(&access_token) {
Ok(()) | Err(OAuthUserSessionStoreError::InvalidState(_)) => HttpResponse::Ok()
.json(json!({
"message": "User successfully logged out"
}))
.into_future(),
Err(err) => {
error!("Unable to remove user session: {}", err);
HttpResponse::InternalServerError()
.json(ErrorResponse::internal_error())
.into_future()
}
},
)
})
}
}
fn get_access_token(
req: HttpRequest,
) -> Result<String, Box<dyn Future<Item = HttpResponse, Error = actix_web::Error>>> {
let auth_header = match req
.headers()
.get("Authorization")
.map(|auth| auth.to_str())
.transpose()
{
Ok(Some(header_str)) => header_str,
Ok(None) => {
return Err(Box::new(
HttpResponse::Unauthorized()
.json(ErrorResponse::unauthorized())
.into_future(),
))
}
Err(_) => {
return Err(Box::new(
HttpResponse::BadRequest()
.json(ErrorResponse::bad_request(
"Authorization header must contain only visible ASCII characters",
))
.into_future(),
))
}
};
match auth_header.parse() {
Ok(AuthorizationHeader::Bearer(BearerToken::OAuth2(access_token))) => Ok(access_token),
Ok(_) | Err(_) => Err(Box::new(
HttpResponse::Unauthorized()
.json(ErrorResponse::unauthorized())
.into_future(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::{blocking::Client, StatusCode, Url};
use crate::biome::oauth::store::InsertableOAuthUserSessionBuilder;
use crate::biome::MemoryOAuthUserSessionStore;
use crate::rest_api::actix_web_1::{RestApiBuilder, RestApiShutdownHandle};
const SPLINTER_ACCESS_TOKEN: &str = "splinter_access_token";
#[test]
fn get_logout_existing_session() {
let session_store = MemoryOAuthUserSessionStore::new();
session_store
.add_session(
InsertableOAuthUserSessionBuilder::new()
.with_splinter_access_token(SPLINTER_ACCESS_TOKEN.into())
.with_subject("subject".into())
.with_oauth_access_token("oauth_access_token".into())
.build()
.expect("Failed to build session"),
)
.expect("Failed to add session");
let (shutdown_handle, join_handle, bind_url) =
run_rest_api_on_open_port(vec![make_logout_route(session_store.clone_box())]);
let url =
Url::parse(&format!("http://{}/oauth/logout", bind_url)).expect("Failed to parse URL");
let resp = Client::new()
.get(url)
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION)
.header(
"Authorization",
format!("Bearer OAuth2:{}", SPLINTER_ACCESS_TOKEN),
)
.send()
.expect("Failed to perform request");
assert_eq!(resp.status(), StatusCode::OK);
assert!(session_store
.get_session(SPLINTER_ACCESS_TOKEN)
.expect("Failed to check session")
.is_none());
shutdown_handle
.shutdown()
.expect("Unable to shutdown rest api");
join_handle.join().expect("Unable to join rest api thread");
}
#[test]
fn get_logout_non_existent_session() {
let session_store = MemoryOAuthUserSessionStore::new();
let (shutdown_handle, join_handle, bind_url) =
run_rest_api_on_open_port(vec![make_logout_route(session_store.clone_box())]);
let url =
Url::parse(&format!("http://{}/oauth/logout", bind_url)).expect("Failed to parse URL");
let resp = Client::new()
.get(url)
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION)
.header(
"Authorization",
format!("Bearer OAuth2:{}", SPLINTER_ACCESS_TOKEN),
)
.send()
.expect("Failed to perform request");
assert_eq!(resp.status(), StatusCode::OK);
shutdown_handle
.shutdown()
.expect("Unable to shutdown rest api");
join_handle.join().expect("Unable to join rest api thread");
}
fn run_rest_api_on_open_port(
resources: Vec<Resource>,
) -> (RestApiShutdownHandle, std::thread::JoinHandle<()>, String) {
#[cfg(not(feature = "https-bind"))]
let bind = "127.0.0.1:0";
#[cfg(feature = "https-bind")]
let bind = crate::rest_api::BindConfig::Http("127.0.0.1:0".into());
let result = RestApiBuilder::new()
.with_bind(bind)
.add_resources(resources.clone())
.build_insecure()
.expect("Failed to build REST API")
.run_insecure();
match result {
Ok((shutdown_handle, join_handle)) => {
let port = shutdown_handle.port_numbers()[0];
(shutdown_handle, join_handle, format!("127.0.0.1:{}", port))
}
Err(err) => panic!("Failed to run REST API: {}", err),
}
}
}