Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion web/server/codechecker_server/database/config_db_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ class OAuthToken(Base):
__tablename__ = 'oauth_tokens'

id = Column(Integer, autoincrement=True, primary_key=True)
provider = Column(String, nullable=False)
access_token = Column(String, nullable=False)
expires_at = Column(DateTime)
refresh_token = Column(String, nullable=False)
Expand All @@ -226,8 +227,9 @@ class OAuthToken(Base):
nullable=False,
index=True)

def __init__(self, access_token, expires_at, refresh_token,
def __init__(self, provider, access_token, expires_at, refresh_token,
auth_session_id):
self.provider = provider
self.access_token = access_token
self.expires_at = expires_at
self.refresh_token = refresh_token
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Keep oauth session alive

Revision ID: 6df2e06f8cf6
Revises: 635389f535cd
Create Date: 2026-09-02 23:04:07.756806
"""

from logging import getLogger

from alembic import op
import sqlalchemy as sa

# Revision identifiers, used by Alembic.
revision = '6df2e06f8cf6'
down_revision = '635389f535cd'
branch_labels = None
depends_on = None


def upgrade():
LOG = getLogger("migration/config")
# Nullable: existing rows predate this column and have no provider.
op.add_column(
'oauth_tokens',
sa.Column('provider', sa.String(), nullable=True))


def downgrade():
LOG = getLogger("migration/config")
op.drop_column('oauth_tokens', 'provider')
94 changes: 82 additions & 12 deletions web/server/codechecker_server/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
import re
import uuid

from datetime import datetime
from datetime import datetime, timedelta
import hashlib
from typing import Optional

from authlib.integrations.requests_client import OAuth2Session
from sqlalchemy.orm import sessionmaker

from codechecker_common.compatibility.multiprocessing import cpu_count
Expand Down Expand Up @@ -991,6 +992,7 @@ def create_session_oauth(self, provider: str,
.first()

oauth_token_session = OAuthToken(
provider=provider,
access_token=access_token,
expires_at=token_expires_at,
refresh_token=refresh_token,
Expand Down Expand Up @@ -1110,24 +1112,91 @@ def get_session(self, token) -> Optional[_Session]:
return None

for sess in self.__sessions:
if sess.is_alive and sess.token == token:
# If the session is alive but the should be re-validated.
if sess.is_refresh_time_expire:
sess.revalidate()
return sess
if sess.token == token:
if sess.is_alive:
# If the session is alive but the should be re-validated.
if sess.is_refresh_time_expire:
sess.revalidate()
return sess

if self.__try_refresh_oauth_session(token):
sess.last_access = datetime.now()
return sess
break

# Try to get a local session from the database.
local_session = self.__get_local_session_from_db(token)
if local_session and local_session.is_alive:
self.__sessions.append(local_session)
if local_session.is_refresh_time_expire:
local_session.revalidate()
return local_session
if local_session:
if local_session.is_alive:
self.__sessions.append(local_session)
if local_session.is_refresh_time_expire:
local_session.revalidate()
return local_session

if self.__try_refresh_oauth_session(token):
local_session.last_access = datetime.now()
self.__sessions.append(local_session)
return local_session

self.invalidate(token)

return None

def __try_refresh_oauth_session(self, token) -> bool:
"""
Silently renews an OAuth-backed session via its refresh token.
Returns True and bumps last_access on success, False otherwise.
"""
if not self.__is_method_enabled('oauth'):
return False

transaction = None
try:
transaction = self.__config_db_sessionmaker()
row = transaction.query(OAuthToken, SessionRecord) \
.join(SessionRecord,
OAuthToken.auth_session_id == SessionRecord.id) \
.filter(SessionRecord.token == token) \
.limit(1).one_or_none()

if not row or not row[0].provider:
return False

oauth_token, session_record = row
oauth_config = self.get_oauth_config(oauth_token.provider)
if not oauth_config or not oauth_config.get('enabled'):
return False

oauth2_session = OAuth2Session(
oauth_config['client_id'],
oauth_config['client_secret'],
scope=oauth_config['scope'])

new_token = oauth2_session.refresh_token(
oauth_config['token_url'],
refresh_token=oauth_token.refresh_token)

if not new_token.get('access_token'):
return False

now = datetime.now()
oauth_token.access_token = new_token['access_token']
oauth_token.refresh_token = new_token.get(
'refresh_token', oauth_token.refresh_token)
if new_token.get('expires_in') is not None:
oauth_token.expires_at = \
now + timedelta(seconds=new_token['expires_in'])
session_record.last_access = now

transaction.commit()
return True
except Exception as e:
LOG.debug("Silent OAuth session refresh failed: %s", str(e))
return False
finally:
if transaction:
transaction.close()

def invalidate_local_session(self, token):
"""
Remove a user's previous session from the local in memory store.
Expand Down Expand Up @@ -1173,5 +1242,6 @@ def __cleanup_sessions(self):
self.invalidate_local_session(s.token)

for s in self.__sessions[:]:
if not s.is_alive:
if not s.is_alive and not self.__try_refresh_oauth_session(
s.token):
self.invalidate(s.token)
12 changes: 8 additions & 4 deletions web/tests/functional/authentication/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,14 +298,18 @@ def test_oauth_token_session(self):
session = self.try_login("github", "admin_github", "admin")
self.assertTrue(session, "Authentication failed")

result = env.validate_oauth_token_session(session_factory, "github1",)
self.assertTrue(result, "Access_token wasn't inserted in Database")
result = env.validate_oauth_token_session(
session_factory, "github1", provider="github")
self.assertTrue(
result, "Access_token/provider wasn't inserted in Database")

session = self.try_login("google", "user_google", "user")
self.assertTrue(session, "Authentication failed")

result = env.validate_oauth_token_session(session_factory, "google3",)
self.assertTrue(result, "Access_token wasn't inserted in Database")
result = env.validate_oauth_token_session(
session_factory, "google3", provider="google")
self.assertTrue(
result, "Access_token/provider wasn't inserted in Database")

def test_oauth_regular_users(self):
"""
Expand Down
18 changes: 11 additions & 7 deletions web/tests/libtest/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,20 +561,24 @@ def create_sqlalchemy_session(workspace):
raise err


def validate_oauth_token_session(session_alchemy, access_token):
def validate_oauth_token_session(session_alchemy, access_token, provider=None):
"""
Helper function that returns bool depending
if the OAuth token exists
if the OAuth token exists. If 'provider' is given, also checks that
the stored token's provider matches it.
"""

access_token_db = None
with DBSession(session_alchemy) as session:
access_token_db, *_ = \
session.query(OAuthToken.access_token) \
row = session.query(OAuthToken.access_token, OAuthToken.provider) \
.filter(OAuthToken.access_token == access_token) \
.first()
return access_token_db is not None \
and access_token_db == access_token

if row is None:
return False

access_token_db, provider_db = row
return access_token_db == access_token \
and (provider is None or provider_db == provider)


def validate_oauth_session(session_alchemy, state):
Expand Down
Loading