Skip to content

Commit

Permalink
Start to test the update of the expiration date in create_client()
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaskrause committed Oct 30, 2023
1 parent ef8c71b commit 1029c10
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 36 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ We recommend installing the following Cargo subcommands for developing annis-web

### Running the web server

When developing, you can run a webserver that is automatically re-compiled when
When developing, you can run a web server that is automatically re-compiled when
any of the source files changes.

```bash
Expand Down
59 changes: 35 additions & 24 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,17 @@ pub struct LoginInfo {
oauth_token: AnnisTokenResponse,

/// Unix time stamp when the session attached to this login information expires.
pub user_session_expiry: Option<i64>,
user_session_expiry: Option<i64>,

/// An authentificated HTTP client
client: reqwest::Client,
}

fn parse_unverified_username(token: &str) -> Result<Option<String>> {
let splitted: Vec<_> = token.splitn(3, '.').collect();

if let Some(raw_claims) = splitted.get(1) {
let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(raw_claims)?;
let claims: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&claims_json))?;
let mut user_name = None;
if let Some(claims) = claims.as_object() {
if let Some(preferred_username) = claims.get("preferred_username") {
user_name = preferred_username.as_str();
} else if let Some(sub) = claims.get("sub") {
user_name = sub.as_str();
}
}
Ok(user_name.map(str::to_string))
} else {
Err(AppError::JwtMissingPayload)
}
}

impl LoginInfo {
pub fn new(oauth_token: AnnisTokenResponse, user_session_expiry: Option<i64>) -> Result<Self> {
pub fn from_token(
oauth_token: AnnisTokenResponse,
user_session_expiry: Option<i64>,
) -> Result<Self> {
let mut default_headers = reqwest::header::HeaderMap::new();

let value =
Expand All @@ -64,6 +46,14 @@ impl LoginInfo {
Ok(result)
}

pub fn expires_unix(&self) -> Option<i64> {
self.user_session_expiry
}

pub fn set_expiration_unix(&mut self, exp: Option<i64>) {
self.user_session_expiry = exp;
}

pub fn renew_token(&mut self, oauth_token: AnnisTokenResponse) -> Result<()> {
self.oauth_token = oauth_token;
// Also recreate the HTTP client, because it needs to use the new bearer token as default header
Expand Down Expand Up @@ -96,6 +86,27 @@ impl LoginInfo {
}
}

fn parse_unverified_username(token: &str) -> Result<Option<String>> {
let splitted: Vec<_> = token.splitn(3, '.').collect();

if let Some(raw_claims) = splitted.get(1) {
let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(raw_claims)?;
let claims: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&claims_json))?;
let mut user_name = None;
if let Some(claims) = claims.as_object() {
if let Some(preferred_username) = claims.get("preferred_username") {
user_name = preferred_username.as_str();
} else if let Some(sub) = claims.get("sub") {
user_name = sub.as_str();
}
}
Ok(user_name.map(str::to_string))
} else {
Err(AppError::JwtMissingPayload)
}
}

async fn refresh_token_action(
refresh_instant: Instant,
refresh_token: RefreshToken,
Expand Down Expand Up @@ -222,7 +233,7 @@ mod tests {

app_state.login_info.insert(
session_id.to_string(),
LoginInfo::new(token.clone(), None).unwrap(),
LoginInfo::from_token(token.clone(), None).unwrap(),
);

let token_request_time = Instant::now();
Expand Down
65 changes: 58 additions & 7 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::auth::LoginInfo;
use crate::{config::CliConfig, errors::AppError, Result, TEMPLATES_DIR};
use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
use chrono::Utc;
use dashmap::DashMap;
Expand All @@ -10,8 +12,6 @@ use time::OffsetDateTime;
use tokio::{sync::mpsc::Receiver, task::JoinHandle};
use url::Url;

use crate::{auth::LoginInfo, config::CliConfig, errors::AppError, Result, TEMPLATES_DIR};

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct Session {
selected_corpora: BTreeSet<String>,
Expand Down Expand Up @@ -187,16 +187,17 @@ impl GlobalAppState {
self.login_info
.alter(&session.id().to_string(), |_, mut l| {
if let (Some(old_expiry), Some(new_expiry)) =
(l.user_session_expiry, session.expiration_time())
(l.expires_unix(), session.expiration_time())
{
// Check if the new expiration date is actually longer before replacing it
if old_expiry < new_expiry.unix_timestamp() {
l.user_session_expiry = Some(new_expiry.unix_timestamp());
l.set_expiration_unix(Some(new_expiry.unix_timestamp()));
}
} else {
// Use the new expiration date
l.user_session_expiry =
session.expiration_time().map(|t| t.unix_timestamp());
l.set_expiration_unix(
session.expiration_time().map(|t| t.unix_timestamp()),
);
}
l
});
Expand All @@ -214,11 +215,61 @@ impl GlobalAppState {
/// Cleans up ressources coupled to sessions that are expired or non-existing.
pub async fn cleanup(&self) {
self.login_info.retain(|_session_id, login_info| {
if let Some(expiry) = login_info.user_session_expiry {
if let Some(expiry) = login_info.expires_unix() {
Utc::now().timestamp() < expiry
} else {
true
}
});
}
}

#[cfg(test)]
mod tests {

use crate::config::CliConfig;

use super::*;

use oauth2::{basic::BasicTokenType, AccessToken, StandardTokenResponse};

#[test]
fn client_access_time_updated() {
let config = CliConfig::default();
let state = GlobalAppState::new(&config).unwrap();

// Create a session that should be updated when accessed
let now = OffsetDateTime::now_utc();

// The user session will only expire in 1 day
let session_expiration = now.checked_add(time::Duration::days(1)).unwrap();
let raw_session = tower_sessions::Session::new(Some(session_expiration));
let session_id = raw_session.id().to_string();

let mut session = Session::default();
session.session_id = session_id.clone();
session.session = raw_session;

let access_token = AccessToken::new("ABC".into());
let token_response = StandardTokenResponse::new(
access_token,
BasicTokenType::Bearer,
oauth2::EmptyExtraTokenFields {},
);
// Simulate an old access to the login info, which would trigger a cleanup
let expired_login_info =
LoginInfo::from_token(token_response, Some(now.unix_timestamp() - 1)).unwrap();

state
.login_info
.insert(session.session_id.clone(), expired_login_info.clone());

let session_arg = SessionArg::Session(session.clone());
state.create_client(&session_arg).unwrap();
// The user session expiration time must be updated
assert_eq!(
Some(session_expiration.unix_timestamp()),
state.login_info.get(&session_id).unwrap().expires_unix()
);
}
}
5 changes: 3 additions & 2 deletions src/views/oauth.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::auth::LoginInfo;
use crate::{
auth::{schedule_refresh_token, LoginInfo},
auth::schedule_refresh_token,
errors::AppError,
state::{GlobalAppState, Session},
Result,
Expand Down Expand Up @@ -99,7 +100,7 @@ async fn login_callback(
.await;
let token = token?;

let login_info = LoginInfo::new(
let login_info = LoginInfo::from_token(
token.clone(),
session.expiration_time().map(|t| t.unix_timestamp()),
)?;
Expand Down
5 changes: 3 additions & 2 deletions src/views/oauth/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ use tower::ServiceExt;
use tower_sessions::{sqlx::SqlitePool, Session, SessionRecord, SessionStore, SqliteStore};
use url::Url;

use crate::auth::LoginInfo;

use crate::{
auth::LoginInfo,
config::CliConfig,
state::GlobalAppState,
tests::{get_body, get_html},
Expand Down Expand Up @@ -104,7 +105,7 @@ async fn logout_removes_login_info() {
let (session_id, session_cookie, session_store) = create_dummy_session().await;

let state = Arc::new(GlobalAppState::new(&config).unwrap());
let l = LoginInfo::new(token_response, None).unwrap();
let l = LoginInfo::from_token(token_response, None).unwrap();
state.login_info.insert(session_id.clone(), l);

// Create an app with the prepared session store
Expand Down

0 comments on commit 1029c10

Please sign in to comment.