Skip to content

Commit

Permalink
Merge pull request #58 from sebadob/impl-user-expiry
Browse files Browse the repository at this point in the history
Impl user expiry
  • Loading branch information
sebadob authored Sep 26, 2023
2 parents e717918 + 33a70fa commit e63d1ce
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 104 deletions.
42 changes: 41 additions & 1 deletion frontend/src/components/admin/users/UserInfo.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<script>
import * as yup from "yup";
import {extractFormErrors, formatDateFromTs} from "../../../utils/helpers.js";
import {
extractFormErrors,
formatDateFromTs,
formatDateToDateInput,
formatUtcTsFromDateInput
} from "../../../utils/helpers.js";
import Switch from "$lib/Switch.svelte";
import {globalGroupsNames, globalRolesNames} from "../../../stores/admin.js";
import Button from "$lib/Button.svelte";
Expand All @@ -20,6 +25,8 @@
let success = false;
let timer;
let language = user.language.toUpperCase();
let limitLifetime = !!user.user_expires;
let userExpires = limitLifetime ? formatDateFromTs(user.user_expires, true) : undefined;
let allRoles = [];
globalRolesNames.subscribe(rls => {
Expand Down Expand Up @@ -72,8 +79,18 @@
groups: user.groups,
enabled: user.enabled,
email_verified: user.email_verified,
user_expires: null,
};
if (limitLifetime) {
let d = formatUtcTsFromDateInput(userExpires);
if (!d) {
err = 'Invalid Date Input: User Expires';
return;
}
req.user_expires = d;
}
let res = await putUser(user.id, req);
if (res.ok) {
success = true;
Expand Down Expand Up @@ -197,6 +214,29 @@
/>
</div>

<!-- Limit Lifetime -->
<div class="unit" style:margin-top="12px">
<div class="label font-label">
LIMIT LIFETIME
</div>
<div class="value">
<Switch bind:selected={limitLifetime}/>
</div>
</div>
{#if limitLifetime}
<Input
type="datetime-local"
step="60"
width="18rem"
bind:value={userExpires}
on:input={validateForm}
min={new Date().toISOString().split('.')[0]}
max="2099-01-01T00:00"
>
USER EXPIRES
</Input>
{/if}

<!-- Last Login-->
<div class="unit" style:margin-top="12px">
<div class="label font-label">
Expand Down
61 changes: 38 additions & 23 deletions frontend/src/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@ import {
import sjcl from "sjcl";
import { decode, encode } from "base64-arraybuffer";

export function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

function deleteCookie(name) {
document.cookie = name + '=; Max-Age=-1;';
}
// export function getCookie(cname) {
// let name = cname + "=";
// let decodedCookie = decodeURIComponent(document.cookie);
// let ca = decodedCookie.split(';');
// for (let i = 0; i < ca.length; i++) {
// let c = ca[i];
// while (c.charAt(0) === ' ') {
// c = c.substring(1);
// }
// if (c.indexOf(name) === 0) {
// return c.substring(name.length, c.length);
// }
// }
// return "";
// }
//
// function deleteCookie(name) {
// document.cookie = name + '=; Max-Age=-1;';
// }

export function extractFormErrors(err) {
return err.inner.reduce((acc, err) => {
Expand Down Expand Up @@ -134,12 +134,24 @@ export const computePow = (powChallenge) => {
}


export const dateFromUtcTs = (ts) => {
const utcOffsetMinutes = -new Date().getTimezoneOffset();
return new Date((ts + utcOffsetMinutes * 60) * 1000);
// export const dateFromUtcTs = (ts) => {
// const utcOffsetMinutes = -new Date().getTimezoneOffset();
// return new Date((ts + utcOffsetMinutes * 60) * 1000);
// }

export const formatDateToDateInput = date => {
return date.toISOString().split('.')[0];
}

export const formatUtcTsFromDateInput = inputDate => {
let d = Date.parse(inputDate);
if (isNaN(d)) {
return;
}
return d / 1000;
}

export const formatDateFromTs = (ts) => {
export const formatDateFromTs = (ts, fmtIso) => {
const utcOffsetMinutes = -new Date().getTimezoneOffset();
const d = new Date((ts + utcOffsetMinutes * 60) * 1000);

Expand All @@ -166,6 +178,9 @@ export const formatDateFromTs = (ts) => {
sc = '0' + sc;
}

if (fmtIso) {
return `${yyyy}-${mm}-${dd}T${hr}:${mn}:${sc}`;
}
return `${yyyy}/${mm}/${dd} ${hr}:${mn}:${sc}`;
}

Expand Down
2 changes: 1 addition & 1 deletion rauthy-handlers/src/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub async fn get_authorize(
.body(body));
}

let session = Session::new(None, *SESSION_LIFETIME, real_ip_from_req(&req));
let session = Session::new(*SESSION_LIFETIME, real_ip_from_req(&req));
session.save(&data).await?;

let mut action = FrontendAction::None;
Expand Down
123 changes: 88 additions & 35 deletions rauthy-main/src/schedulers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use rauthy_common::DbType;
use rauthy_models::app_state::{AppState, DbPool};
use rauthy_models::email::send_pwd_reset_info;
use rauthy_models::entity::jwk::Jwk;
use rauthy_models::entity::refresh_tokens::RefreshToken;
use rauthy_models::entity::sessions::Session;
use rauthy_models::entity::users::User;
use rauthy_models::migration::backup_db;
use redhac::cache_del;
Expand All @@ -16,13 +18,6 @@ use std::time::Duration;
use tokio::time;
use tracing::{debug, error, info};

/**
The main scheduler function, where everything is defined.<br>
I runs in its own thread and executes the schedulers synchronously - no async needed here.
One important value is the SCHED_SLEEP_DURATION. This affects the graceful shutdown time of the
application, if it is too high.
*/
pub async fn scheduler_main(data: web::Data<AppState>) {
info!("Starting schedulers");

Expand All @@ -31,7 +26,8 @@ pub async fn scheduler_main(data: web::Data<AppState>) {
tokio::spawn(refresh_tokens_cleanup(data.db.clone()));
tokio::spawn(sessions_cleanup(data.db.clone()));
tokio::spawn(jwks_cleanup(data.clone()));
tokio::spawn(password_expiry_checker(data));
tokio::spawn(password_expiry_checker(data.clone()));
tokio::spawn(user_expiry_checker(data));
}

// TODO -> adapt to RDBMS
Expand Down Expand Up @@ -68,33 +64,6 @@ pub async fn db_backup(db: DbPool) {
}
}

// // Cleans up old / expired / already used Authorization Codes
// // Cleanup inside the cache is done automatically with a max entry lifetime of 300 seconds
// pub async fn auth_codes_cleanup(data: web::Data<AppState>) {
// let mut interval = time::interval(Duration::from_secs(60 * 17));
//
// loop {
// interval.tick().await;
//
// debug!("Running auth_codes_cleanup scheduler");
// match DATA_STORE.scan_cf(Cf::AuthCodes).await {
// Ok(bytes) => {
// let del = bytes
// .iter()
// .map(|(_, b)| bincode::deserialize::<AuthCode>(b).unwrap())
// .filter(|code| code.exp < chrono::Local::now().naive_local())
// .collect::<Vec<AuthCode>>();
// for code in del {
// if let Err(err) = code.delete(&data).await {
// error!("Auth Code Cleanup Error: {}", err.message);
// }
// }
// }
// Err(err) => error!("AuthCodes cleanup scheduler error: {}", err.message),
// }
// }
// }

// Cleans up old / expired magic links and deletes users, that have never used their
// 'set first ever password' magic link to keep the database clean in case of an open user registration.
// Runs every 6 hours.
Expand Down Expand Up @@ -189,6 +158,90 @@ pub async fn password_expiry_checker(data: web::Data<AppState>) {
}
}

// Checks for expired users
pub async fn user_expiry_checker(data: web::Data<AppState>) {
let secs = env::var("SCHED_USER_EXP_MINS")
.unwrap_or_else(|_| "60".to_string())
.parse::<u64>()
.expect("Cannot parse 'SCHED_USER_EXP_MINS' to u64");
let mut interval = time::interval(Duration::from_secs(secs * 60));
let cleanup_after_secs = env::var("SCHED_USER_EXP_DELETE_MINS")
.map(|s| {
s.parse::<u64>()
.expect("Cannot parse 'SCHED_USER_EXP_DELETE_MINS' to u64")
* 60
})
.ok();
if cleanup_after_secs.is_none() {
info!("Auto cleanup for expired users disabled");
}

loop {
interval.tick().await;
debug!("Running user_expiry_checker scheduler");

match User::find_expired(&data).await {
Ok(users) => {
let now = OffsetDateTime::now_utc().unix_timestamp();
// could possibly be optimized (if necessary) by collecting all IDs and use a
// non-prepared statement
for user in users {
debug!("Found expired user {}: {}", user.id, user.email);

let exp_ts = if let Some(ts) = user.user_expires {
if now < ts {
error!("Got not yet expired user in user_expiry_checker - this should never happen");
continue;
}
ts
} else {
error!("Got non-expiring user in user_expiry_checker - this should never happen");
continue;
};

// invalidate all sessions
if let Err(err) = Session::invalidate_for_user(&data, &user.id).await {
error!(
"Error invalidating sessions for user {}: {:?}",
user.id, err
);
}

// invalidate all refresh tokens
if let Err(err) = RefreshToken::invalidate_for_user(&data, &user.id).await {
error!(
"Error invalidating refresh tokens for user {}: {:?}",
user.id, err
);
}

// possibly auto-cleanup expired user
if let Some(secs) = cleanup_after_secs {
let expired_since_secs = (exp_ts - now).abs() as u64;
if expired_since_secs > secs {
info!(
"Auto cleanup for user {} after being expired for {} minutes",
user.id,
expired_since_secs / 60
);
if let Err(err) = user.delete(&data).await {
error!(
"Error during auto cleanup - deleting user {}: {:?}",
user.id, err
);
}
}
}
}
}

Err(err) => {
error!("user_expiry_checker error: {}", err.message);
}
};
}
}

// Cleans up old / expired / already used Refresh Tokens
pub async fn refresh_tokens_cleanup(db: DbPool) {
let mut interval = time::interval(Duration::from_secs(3600 * 3));
Expand Down
Loading

0 comments on commit e63d1ce

Please sign in to comment.