1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-21 22:29:39 +02:00

feat: OIDC: Call userinfo if no claims found in id token (#5228)

Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
Carter 2025-03-16 22:05:20 -05:00 committed by GitHub
parent 3b1a6280d6
commit d724f408cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 37 additions and 16 deletions

View file

@ -43,3 +43,6 @@ def mealie_registered_exceptions(t: Translator) -> dict:
class UserLockedOut(Exception): ... class UserLockedOut(Exception): ...
class MissingClaimException(Exception): ...

View file

@ -5,6 +5,7 @@ from sqlalchemy.orm.session import Session
from mealie.core import root_logger from mealie.core import root_logger
from mealie.core.config import get_app_settings from mealie.core.config import get_app_settings
from mealie.core.exceptions import MissingClaimException
from mealie.core.security.providers.auth_provider import AuthProvider from mealie.core.security.providers.auth_provider import AuthProvider
from mealie.db.models.users.users import AuthMethod from mealie.db.models.users.users import AuthMethod
from mealie.repos.all_repositories import get_repositories from mealie.repos.all_repositories import get_repositories
@ -25,7 +26,7 @@ class OpenIDProvider(AuthProvider[UserInfo]):
claims = self.data claims = self.data
if not claims: if not claims:
self._logger.error("[OIDC] No claims in the id_token") self._logger.error("[OIDC] No claims in the id_token")
return None raise MissingClaimException()
# Log all claims for debugging # Log all claims for debugging
self._logger.debug("[OIDC] Received claims:") self._logger.debug("[OIDC] Received claims:")
@ -38,13 +39,13 @@ class OpenIDProvider(AuthProvider[UserInfo]):
self.required_claims, self.required_claims,
claims.keys(), claims.keys(),
) )
return None raise MissingClaimException()
# Check for empty required claims # Check for empty required claims
for claim in self.required_claims: for claim in self.required_claims:
if not claims.get(claim): if not claims.get(claim):
self._logger.error("[OIDC] Required claim '%s' is empty", claim) self._logger.error("[OIDC] Required claim '%s' is empty", claim)
return None raise MissingClaimException()
repos = get_repositories(self.session, group_id=None, household_id=None) repos = get_repositories(self.session, group_id=None, household_id=None)

View file

@ -11,7 +11,7 @@ from starlette.datastructures import URLPath
from mealie.core import root_logger, security from mealie.core import root_logger, security
from mealie.core.config import get_app_settings from mealie.core.config import get_app_settings
from mealie.core.dependencies import get_current_user from mealie.core.dependencies import get_current_user
from mealie.core.exceptions import UserLockedOut from mealie.core.exceptions import MissingClaimException, UserLockedOut
from mealie.core.security.providers.openid_provider import OpenIDProvider from mealie.core.security.providers.openid_provider import OpenIDProvider
from mealie.core.security.security import get_auth_provider from mealie.core.security.security import get_auth_provider
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
@ -125,14 +125,24 @@ async def oauth_callback(request: Request, response: Response, session: Session
detail="Could not initialize OAuth client", detail="Could not initialize OAuth client",
) )
client = oauth.create_client("oidc") client = oauth.create_client("oidc")
token = await client.authorize_access_token(request) token = await client.authorize_access_token(request)
auth = None
try:
auth_provider = OpenIDProvider(session, token["userinfo"]) auth_provider = OpenIDProvider(session, token["userinfo"])
auth = auth_provider.authenticate() auth = auth_provider.authenticate()
except MissingClaimException:
try:
logger.debug("[OIDC] Claims not present in the ID token, pulling user info")
userinfo = await client.userinfo(token=token)
auth_provider = OpenIDProvider(session, userinfo)
auth = auth_provider.authenticate()
except MissingClaimException:
auth = None
if not auth: if not auth:
raise HTTPException( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
status_code=status.HTTP_401_UNAUTHORIZED,
)
access_token, duration = auth access_token, duration = auth
expires_in = duration.total_seconds() if duration else None expires_in = duration.total_seconds() if duration else None

View file

@ -1,8 +1,10 @@
import pytest
from pytest import MonkeyPatch, Session
import logging import logging
import pytest
from pytest import MonkeyPatch, Session
from mealie.core.config import get_app_settings from mealie.core.config import get_app_settings
from mealie.core.exceptions import MissingClaimException
from mealie.core.security.providers.openid_provider import OpenIDProvider from mealie.core.security.providers.openid_provider import OpenIDProvider
from mealie.repos.all_repositories import get_repositories from mealie.repos.all_repositories import get_repositories
from tests.utils.factories import random_email, random_string from tests.utils.factories import random_email, random_string
@ -12,13 +14,15 @@ from tests.utils.fixture_schemas import TestUser
def test_no_claims(): def test_no_claims():
auth_provider = OpenIDProvider(None, None) auth_provider = OpenIDProvider(None, None)
assert auth_provider.authenticate() is None with pytest.raises(MissingClaimException):
auth_provider.authenticate()
def test_empty_claims(): def test_empty_claims():
auth_provider = OpenIDProvider(None, {}) auth_provider = OpenIDProvider(None, {})
assert auth_provider.authenticate() is None with pytest.raises(MissingClaimException):
auth_provider.authenticate()
def test_empty_required_claims(): def test_empty_required_claims():
@ -30,14 +34,16 @@ def test_empty_required_claims():
} }
auth_provider = OpenIDProvider(None, data) auth_provider = OpenIDProvider(None, data)
assert auth_provider.authenticate() is None with pytest.raises(MissingClaimException):
auth_provider.authenticate()
def test_missing_claims(): def test_missing_claims():
data = {"preferred_username": "dude1"} data = {"preferred_username": "dude1"}
auth_provider = OpenIDProvider(None, data) auth_provider = OpenIDProvider(None, data)
assert auth_provider.authenticate() is None with pytest.raises(MissingClaimException):
auth_provider.authenticate()
def test_missing_groups_claim(monkeypatch: MonkeyPatch): def test_missing_groups_claim(monkeypatch: MonkeyPatch):
@ -51,7 +57,8 @@ def test_missing_groups_claim(monkeypatch: MonkeyPatch):
} }
auth_provider = OpenIDProvider(None, data) auth_provider = OpenIDProvider(None, data)
assert auth_provider.authenticate() is None with pytest.raises(MissingClaimException):
auth_provider.authenticate()
def test_missing_user_group(monkeypatch: MonkeyPatch): def test_missing_user_group(monkeypatch: MonkeyPatch):