mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
refac
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
@@ -12,6 +13,7 @@ from open_webui.models.users import User, UserModel, UserProfileImageResponse, U
|
||||
from open_webui.utils.validate import validate_profile_image_url
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy import Boolean, Column, String, Text, delete, select, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -109,12 +111,13 @@ class AuthsTable:
|
||||
role: str = 'pending',
|
||||
oauth: dict | None = None,
|
||||
db: AsyncSession | None = None,
|
||||
user_id: str | None = None,
|
||||
) -> UserModel | None:
|
||||
"""Create an Auth + User pair inside a single transaction."""
|
||||
async with get_async_db_context(db) as session:
|
||||
log.info('insert_new_auth')
|
||||
|
||||
new_id = str(uuid.uuid4())
|
||||
new_id = user_id or str(uuid.uuid4())
|
||||
|
||||
credential = Auth(
|
||||
id=new_id,
|
||||
@@ -124,17 +127,30 @@ class AuthsTable:
|
||||
)
|
||||
session.add(credential)
|
||||
|
||||
created_user = await Users.insert_new_user(
|
||||
new_id,
|
||||
name,
|
||||
email,
|
||||
profile_image_url,
|
||||
role,
|
||||
try:
|
||||
profile_image_url = validate_profile_image_url(profile_image_url)
|
||||
except ValueError:
|
||||
profile_image_url = '/user.png'
|
||||
|
||||
now = int(time.time())
|
||||
created_user = UserModel(
|
||||
id=new_id,
|
||||
email=email,
|
||||
name=name,
|
||||
role=role,
|
||||
profile_image_url=profile_image_url,
|
||||
last_active_at=now,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
oauth=oauth,
|
||||
db=session,
|
||||
)
|
||||
# persist both records
|
||||
await session.commit()
|
||||
session.add(User(**created_user.model_dump()))
|
||||
|
||||
try:
|
||||
await session.commit()
|
||||
except IntegrityError:
|
||||
await session.rollback()
|
||||
raise
|
||||
return created_user if credential and created_user else None
|
||||
|
||||
async def authenticate_user(
|
||||
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
@@ -9,7 +11,7 @@ import urllib
|
||||
import uuid
|
||||
from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp import BasicAuth, ClientSession
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from ldap3 import NONE, Connection, Server, Tls
|
||||
@@ -27,6 +29,7 @@ from open_webui.env import (
|
||||
ENABLE_OAUTH_TOKEN_EXCHANGE,
|
||||
OAUTH_TOKEN_EXCHANGE_RATE_LIMIT,
|
||||
OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW,
|
||||
OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS,
|
||||
WEBUI_AUTH,
|
||||
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||
WEBUI_AUTH_COOKIE_SECURE,
|
||||
@@ -35,6 +38,7 @@ from open_webui.env import (
|
||||
WEBUI_AUTH_TRUSTED_GROUPS_HEADER,
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER,
|
||||
WEBUI_AUTH_TRUSTED_ROLE_HEADER,
|
||||
WEBUI_SECRET_KEY,
|
||||
)
|
||||
from open_webui.internal.db import get_async_session
|
||||
from open_webui.models.auths import (
|
||||
@@ -77,6 +81,7 @@ from open_webui.utils.misc import parse_duration, validate_email_format
|
||||
from open_webui.utils.rate_limit import RateLimiter
|
||||
from open_webui.utils.redis import get_redis_client
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
router = APIRouter()
|
||||
@@ -98,6 +103,16 @@ token_exchange_rate_limiter = (
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
def trusted_header_user_id(email: str) -> str:
|
||||
digest = hmac.digest(
|
||||
WEBUI_SECRET_KEY.encode(),
|
||||
f'open-webui:trusted-header:{email.lower()}'.encode(),
|
||||
hashlib.sha256,
|
||||
)
|
||||
return str(uuid.UUID(bytes=digest[:16], version=4))
|
||||
|
||||
|
||||
ADMIN_CONFIG_KEYS = {
|
||||
'SHOW_ADMIN_DETAILS': 'auth.admin.show',
|
||||
'ADMIN_EMAIL': 'auth.admin.email',
|
||||
@@ -738,14 +753,18 @@ async def signin(
|
||||
pass
|
||||
|
||||
if not await Users.get_user_by_email(email.lower(), db=db):
|
||||
await signup_handler(
|
||||
request,
|
||||
email,
|
||||
str(uuid.uuid4()),
|
||||
name,
|
||||
db=db,
|
||||
source='trusted_header',
|
||||
)
|
||||
try:
|
||||
await signup_handler(
|
||||
request,
|
||||
email,
|
||||
str(uuid.uuid4()),
|
||||
name,
|
||||
db=db,
|
||||
source='trusted_header',
|
||||
user_id=trusted_header_user_id(email),
|
||||
)
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
user = await Auths.authenticate_user_by_email(email, db=db)
|
||||
if user:
|
||||
@@ -826,6 +845,7 @@ async def signup_handler(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
source: str = 'api',
|
||||
user_id: str | None = None,
|
||||
) -> UserModel:
|
||||
"""
|
||||
Core user-creation logic shared by the signup endpoint and
|
||||
@@ -846,6 +866,7 @@ async def signup_handler(
|
||||
profile_image_url=profile_image_url,
|
||||
role=await Config.get('ui.default_user_role'),
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||
@@ -1520,6 +1541,37 @@ class TokenExchangeForm(BaseModel):
|
||||
token: str # OAuth access token from external provider
|
||||
|
||||
|
||||
async def get_token_client_id(client, token: str) -> str | None:
|
||||
"""Return the OAuth client_id a token was minted for, when the provider supports introspection."""
|
||||
try:
|
||||
metadata = await client.load_server_metadata()
|
||||
introspection_endpoint = metadata.get('introspection_endpoint')
|
||||
if not introspection_endpoint:
|
||||
log.warning('Token exchange trusted-client check requires an introspection_endpoint')
|
||||
return None
|
||||
|
||||
async with ClientSession(trust_env=True) as session:
|
||||
async with session.post(
|
||||
introspection_endpoint,
|
||||
data={'token': token, 'token_type_hint': 'access_token'},
|
||||
auth=BasicAuth(client.client_id, client.client_secret or ''),
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
log.warning(f'Token introspection returned {r.status}')
|
||||
return None
|
||||
introspection = await r.json()
|
||||
|
||||
if not introspection.get('active'):
|
||||
log.warning('Token introspection reports the token is inactive')
|
||||
return None
|
||||
|
||||
return introspection.get('client_id')
|
||||
except Exception as e:
|
||||
log.warning(f'Token introspection failed: {e}')
|
||||
return None
|
||||
|
||||
|
||||
@router.post('/oauth/{provider}/token/exchange', response_model=SessionUserResponse)
|
||||
async def token_exchange(
|
||||
request: Request,
|
||||
@@ -1563,6 +1615,20 @@ async def token_exchange(
|
||||
detail=ERROR_MESSAGES.OAUTH_NOT_CONFIGURED(provider),
|
||||
)
|
||||
|
||||
if OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS:
|
||||
token_client_id = await get_token_client_id(client, form_data.token)
|
||||
if not token_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Unable to determine which client the token was issued to',
|
||||
)
|
||||
if token_client_id not in OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS:
|
||||
log.warning('Token exchange denied: token was issued to an untrusted client for %s', provider)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
# Validate the token by calling the userinfo endpoint
|
||||
try:
|
||||
token_data = {'access_token': form_data.token, 'token_type': 'Bearer'}
|
||||
|
||||
Reference in New Issue
Block a user