enh: exp token check

This commit is contained in:
Timothy Jaeryang Baek 2025-04-24 03:12:22 +09:00
parent 91e758f3ec
commit 28ec3069de
3 changed files with 103 additions and 85 deletions

View File

@ -34,14 +34,17 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response from fastapi.responses import RedirectResponse, Response
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
from pydantic import BaseModel from pydantic import BaseModel
from open_webui.utils.misc import parse_duration, validate_email_format from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.auth import ( from open_webui.utils.auth import (
decode_token,
create_api_key, create_api_key,
create_token, create_token,
get_admin_user, get_admin_user,
get_verified_user, get_verified_user,
get_current_user, get_current_user,
get_password_hash, get_password_hash,
get_http_authorization_cred,
) )
from open_webui.utils.webhook import post_webhook from open_webui.utils.webhook import post_webhook
from open_webui.utils.access_control import get_permissions from open_webui.utils.access_control import get_permissions
@ -73,31 +76,13 @@ class SessionUserResponse(Token, UserResponse):
async def get_session_user( async def get_session_user(
request: Request, response: Response, user=Depends(get_current_user) request: Request, response: Response, user=Depends(get_current_user)
): ):
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
token = create_token( auth_header = request.headers.get("Authorization")
data={"id": user.id}, auth_token = get_http_authorization_cred(auth_header)
expires_delta=expires_delta, token = auth_token.credentials
)
datetime_expires_at = ( data = decode_token(token)
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc) expires_at = data.get("exp")
if expires_at
else None
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=datetime_expires_at,
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
)
user_permissions = get_permissions( user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS user.id, request.app.state.config.USER_PERMISSIONS
@ -289,11 +274,14 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
user = Auths.authenticate_user_by_trusted_header(email) user = Auths.authenticate_user_by_trusted_header(email)
if user: if user:
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
token = create_token( token = create_token(
data={"id": user.id}, data={"id": user.id},
expires_delta=parse_duration( expires_delta=expires_delta,
request.app.state.config.JWT_EXPIRES_IN
),
) )
# Set the cookie token # Set the cookie token
@ -301,6 +289,8 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
key="token", key="token",
value=token, value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
) )
user_permissions = get_permissions( user_permissions = get_permissions(
@ -310,6 +300,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
return { return {
"token": token, "token": token,
"token_type": "Bearer", "token_type": "Bearer",
"expires_at": expires_at,
"id": user.id, "id": user.id,
"email": user.email, "email": user.email,
"name": user.name, "name": user.name,

View File

@ -251,77 +251,79 @@
</div> </div>
{/if} {/if}
<div class="app relative"> {#if $user}
<div <div class="app relative">
class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900 h-screen max-h-[100dvh] overflow-auto flex flex-row justify-end" <div
> class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900 h-screen max-h-[100dvh] overflow-auto flex flex-row justify-end"
{#if !['user', 'admin'].includes($user?.role)} >
<AccountPending /> {#if !['user', 'admin'].includes($user?.role)}
{:else if localDBChats.length > 0} <AccountPending />
<div class="fixed w-full h-full flex z-50"> {:else if localDBChats.length > 0}
<div <div class="fixed w-full h-full flex z-50">
class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center" <div
> class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
<div class="m-auto pb-44 flex flex-col justify-center"> >
<div class="max-w-md"> <div class="m-auto pb-44 flex flex-col justify-center">
<div class="text-center dark:text-white text-2xl font-medium z-50"> <div class="max-w-md">
Important Update<br /> Action Required for Chat Log Storage <div class="text-center dark:text-white text-2xl font-medium z-50">
</div> Important Update<br /> Action Required for Chat Log Storage
</div>
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full"> <div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
{$i18n.t( {$i18n.t(
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through" "Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through"
)} )}
<span class="font-semibold dark:text-white" <span class="font-semibold dark:text-white"
>{$i18n.t('Settings')} > {$i18n.t('Chats')} > {$i18n.t('Import Chats')}</span >{$i18n.t('Settings')} > {$i18n.t('Chats')} > {$i18n.t('Import Chats')}</span
>. {$i18n.t( >. {$i18n.t(
'This ensures that your valuable conversations are securely saved to your backend database. Thank you!' 'This ensures that your valuable conversations are securely saved to your backend database. Thank you!'
)} )}
</div> </div>
<div class=" mt-6 mx-auto relative group w-fit"> <div class=" mt-6 mx-auto relative group w-fit">
<button <button
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm" class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
on:click={async () => { on:click={async () => {
let blob = new Blob([JSON.stringify(localDBChats)], { let blob = new Blob([JSON.stringify(localDBChats)], {
type: 'application/json' type: 'application/json'
}); });
saveAs(blob, `chat-export-${Date.now()}.json`); saveAs(blob, `chat-export-${Date.now()}.json`);
const tx = DB.transaction('chats', 'readwrite'); const tx = DB.transaction('chats', 'readwrite');
await Promise.all([tx.store.clear(), tx.done]); await Promise.all([tx.store.clear(), tx.done]);
await deleteDB('Chats'); await deleteDB('Chats');
localDBChats = []; localDBChats = [];
}} }}
> >
Download & Delete Download & Delete
</button> </button>
<button <button
class="text-xs text-center w-full mt-2 text-gray-400 underline" class="text-xs text-center w-full mt-2 text-gray-400 underline"
on:click={async () => { on:click={async () => {
localDBChats = []; localDBChats = [];
}}>{$i18n.t('Close')}</button }}>{$i18n.t('Close')}</button
> >
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> {/if}
{/if}
<Sidebar /> <Sidebar />
{#if loaded} {#if loaded}
<slot /> <slot />
{:else} {:else}
<div class="w-full flex-1 h-full flex items-center justify-center"> <div class="w-full flex-1 h-full flex items-center justify-center">
<Spinner /> <Spinner />
</div> </div>
{/if} {/if}
</div>
</div> </div>
</div> {/if}
<style> <style>
.loading { .loading {

View File

@ -54,6 +54,7 @@
const bc = new BroadcastChannel('active-tab-channel'); const bc = new BroadcastChannel('active-tab-channel');
let loaded = false; let loaded = false;
let tokenTimer = null;
const BREAKPOINT = 768; const BREAKPOINT = 768;
@ -443,6 +444,24 @@
} }
}; };
const checkTokenExpiry = () => {
const exp = $user?.expires_at; // token expiry time in unix timestamp
const now = Math.floor(Date.now() / 1000); // current time in unix timestamp
if (!exp) {
// If no expiry time is set, do nothing
return;
}
if (now >= exp) {
localStorage.removeItem('token');
// redirect to auth page
if ($page.url.pathname !== '/auth') {
goto(`/auth`);
}
}
};
onMount(async () => { onMount(async () => {
if (typeof window !== 'undefined' && window.applyTheme) { if (typeof window !== 'undefined' && window.applyTheme) {
window.applyTheme(); window.applyTheme();
@ -560,6 +579,12 @@
await user.set(sessionUser); await user.set(sessionUser);
await config.set(await getBackendConfig()); await config.set(await getBackendConfig());
// Set up the token expiry check
if (tokenTimer) {
clearInterval(tokenTimer);
}
tokenTimer = setInterval(checkTokenExpiry, 1000);
} else { } else {
// Redirect Invalid Session User to /auth Page // Redirect Invalid Session User to /auth Page
localStorage.removeItem('token'); localStorage.removeItem('token');