Skip to content

Commit

Permalink
Return the max session age in login responses
Browse files Browse the repository at this point in the history
This detail is needed by the client in order to correctly configure the
expiration times of any cookies used to store the session.
  • Loading branch information
jmmv committed Sep 4, 2023
1 parent 611b483 commit b939bdd
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 4 deletions.
5 changes: 5 additions & 0 deletions authn/src/driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ impl AuthnDriver {
}
}

/// Returns a reference to the authentication options provided at creation time.
pub(crate) fn opts(&self) -> &AuthnOptions {
&self.opts
}

/// Obtains the current time from the driver.
#[cfg(test)]
pub(crate) fn now_utc(&self) -> OffsetDateTime {
Expand Down
20 changes: 18 additions & 2 deletions authn/src/rest/api_login_post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ use axum::response::IntoResponse;
use axum::Json;
use iii_iv_core::rest::{EmptyBody, RestError};
use serde::{Deserialize, Serialize};
use std::time::Duration;

/// Message returned by the server after a successful login attempt.
#[derive(Debug, Deserialize, Serialize)]
pub struct LoginResponse {
/// Access token for this session.
pub access_token: AccessToken,

/// Maximum age of the created session. The client can use this to set up cookie expiration
/// times to match.
pub session_max_age: Duration,
}

/// POST handler for this API.
Expand All @@ -40,15 +45,23 @@ pub(crate) async fn handler(
) -> Result<impl IntoResponse, RestError> {
let (username, password) = get_basic_auth(&headers, driver.realm())?;

// The maximum session age is a property of the server, not the session. This might lead to a
// situation where this value changes in the server's configuration and the clients have session
// cookies with expiration times that don't match. That's OK because the clients need to be
// prepared to handle authentication problems and session revocation for any reason. But this
// is just a choice. We could as well store this value along each session in the database.
let session_max_age = driver.opts().session_max_age;

let session = driver.login(username, password).await?;
let response = LoginResponse { access_token: session.take_access_token() };
let response = LoginResponse { access_token: session.take_access_token(), session_max_age };

Ok(Json(response))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::driver::AuthnOptions;
use crate::rest::testutils::*;
use axum::http;
use iii_iv_core::rest::testutils::OneShotBuilder;
Expand All @@ -60,7 +73,9 @@ mod tests {

#[tokio::test]
async fn test_ok() {
let mut context = TestContextBuilder::new().build().await;
let opts =
AuthnOptions { session_max_age: Duration::from_secs(4182), ..Default::default() };
let mut context = TestContextBuilder::new().with_opts(opts).build().await;

context.create_whoami_user().await;

Expand All @@ -73,6 +88,7 @@ mod tests {

assert!(context.session_exists(&response.access_token).await);
assert!(context.user_exists(&context.whoami()).await);
assert_eq!(4182, response.session_max_age.as_secs());
}

#[tokio::test]
Expand Down
15 changes: 13 additions & 2 deletions authn/src/rest/testutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,18 @@ impl TestContext {
pub(crate) struct TestContextBuilder {
whoami: String,
activated_template: Option<&'static str>,
opts: AuthnOptions,
}

#[cfg(test)]
impl TestContextBuilder {
/// Initializes a new builder with the default test settings.
pub(crate) fn new() -> Self {
Self { whoami: "whoami".to_owned(), activated_template: None }
Self {
whoami: "whoami".to_owned(),
activated_template: None,
opts: AuthnOptions::default(),
}
}

/// Overrides the default activated template.
Expand All @@ -210,6 +215,12 @@ impl TestContextBuilder {
self
}

/// Overrides the default authentication options.
pub(crate) fn with_opts(mut self, opts: AuthnOptions) -> Self {
self.opts = opts;
self
}

/// Sets up the test environment with the configured settings.
pub(crate) async fn build(self) -> TestContext {
let db = Arc::from(iii_iv_core::db::sqlite::testutils::setup().await);
Expand All @@ -224,7 +235,7 @@ impl TestContextBuilder {
make_test_activation_template(),
Arc::from(BaseUrls::from_strs("http://localhost:1234/", None)),
"the-realm",
AuthnOptions::default(),
self.opts,
);
let app = Router::new().nest("/api/test", app(driver, self.activated_template));

Expand Down

0 comments on commit b939bdd

Please sign in to comment.