Skip to content

Commit de74688

Browse files
authored
Merge pull request #1450 from rommapp/fix/improve-oidc-email-verified-check
fix: Improve OIDC email verified check
2 parents 415c7a7 + 8c8cd75 commit de74688

File tree

2 files changed

+137
-2
lines changed

2 files changed

+137
-2
lines changed

backend/handler/auth/base_handler.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any
44

55
from config import OIDC_ENABLED, ROMM_AUTH_SECRET_KEY
6+
from decorators.auth import oauth
67
from exceptions.auth_exceptions import OAuthCredentialsException, UserDisabledException
78
from fastapi import HTTPException, status
89
from handler.auth.constants import ALGORITHM, DEFAULT_OAUTH_TOKEN_EXPIRY
@@ -125,7 +126,18 @@ async def get_current_active_user_from_openid_token(self, token: Any):
125126
status_code=status.HTTP_400_BAD_REQUEST,
126127
detail="Email is missing from token.",
127128
)
128-
if userinfo.get("email_verified", None) is not True:
129+
130+
metadata = await oauth.openid.load_server_metadata()
131+
claims_supported = metadata.get("claims_supported")
132+
is_email_verified = userinfo.get("email_verified", None)
133+
134+
# Fail if email is explicitly unverified, or `email_verified` is a supported claim and
135+
# email is not explicitly verified.
136+
if is_email_verified is False or (
137+
claims_supported
138+
and "email_verified" in claims_supported
139+
and is_email_verified is not True
140+
):
129141
log.error("Email is not verified.")
130142
raise HTTPException(
131143
status_code=status.HTTP_400_BAD_REQUEST,

backend/handler/auth/tests/test_oidc.py

+124-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from unittest.mock import MagicMock
22

33
import pytest
4+
from authlib.integrations.starlette_client.apps import StarletteOAuth2App
45
from fastapi import HTTPException
56
from handler.auth.base_handler import OpenIDHandler
67
from joserfc.jwt import Token
@@ -50,6 +51,67 @@ def mock_token():
5051
}
5152

5253

54+
@pytest.fixture
55+
def mock_openid_configuration():
56+
return {
57+
"issuer": "https://authentik.example.com/application/o/romm/",
58+
"authorization_endpoint": "https://authentik.example.com/application/o/authorize/",
59+
"token_endpoint": "https://authentik.example.com/application/o/token/",
60+
"userinfo_endpoint": "https://authentik.example.com/application/o/userinfo/",
61+
"end_session_endpoint": "https://authentik.example.com/application/o/romm/end-session/",
62+
"introspection_endpoint": "https://authentik.example.com/application/o/introspect/",
63+
"revocation_endpoint": "https://authentik.example.com/application/o/revoke/",
64+
"device_authorization_endpoint": "https://authentik.example.com/application/o/device/",
65+
"response_types_supported": [
66+
"code",
67+
"id_token",
68+
"id_token token",
69+
"code token",
70+
"code id_token",
71+
"code id_token token",
72+
],
73+
"response_modes_supported": ["query", "fragment", "form_post"],
74+
"jwks_uri": "https://authentik.example.com/application/o/romm/jwks/",
75+
"grant_types_supported": [
76+
"authorization_code",
77+
"refresh_token",
78+
"implicit",
79+
"client_credentials",
80+
"password",
81+
"urn:ietf:params:oauth:grant-type:device_code",
82+
],
83+
"id_token_signing_alg_values_supported": ["RS256"],
84+
"subject_types_supported": ["public"],
85+
"token_endpoint_auth_methods_supported": [
86+
"client_secret_post",
87+
"client_secret_basic",
88+
],
89+
"acr_values_supported": ["goauthentik.io/providers/oauth2/default"],
90+
"scopes_supported": ["openid", "email", "profile"],
91+
"request_parameter_supported": False,
92+
"claims_supported": [
93+
"sub",
94+
"iss",
95+
"aud",
96+
"exp",
97+
"iat",
98+
"auth_time",
99+
"acr",
100+
"amr",
101+
"nonce",
102+
"email",
103+
"email_verified",
104+
"name",
105+
"given_name",
106+
"preferred_username",
107+
"nickname",
108+
"groups",
109+
],
110+
"claims_parameter_supported": False,
111+
"code_challenge_methods_supported": ["plain", "S256"],
112+
}
113+
114+
53115
async def test_oidc_disabled(mock_oidc_disabled, mock_token):
54116
"""Test that OIDC is disabled."""
55117
oidc_handler = OpenIDHandler()
@@ -60,7 +122,9 @@ async def test_oidc_disabled(mock_oidc_disabled, mock_token):
60122
assert userinfo is None
61123

62124

63-
async def test_oidc_valid_token_decoding(mocker, mock_oidc_enabled, mock_token):
125+
async def test_oidc_valid_token_decoding(
126+
mocker, mock_oidc_enabled, mock_token, mock_openid_configuration
127+
):
64128
"""Test token decoding with valid RSA key and token."""
65129
mock_jwt_payload = Token(
66130
header={"alg": "RS256"},
@@ -70,6 +134,65 @@ async def test_oidc_valid_token_decoding(mocker, mock_oidc_enabled, mock_token):
70134
mocker.patch(
71135
"handler.database.db_user_handler.get_user_by_email", return_value=mock_user
72136
)
137+
mocker.patch.object(
138+
StarletteOAuth2App,
139+
"load_server_metadata",
140+
return_value=mock_openid_configuration,
141+
)
142+
143+
oidc_handler = OpenIDHandler()
144+
user, userinfo = await oidc_handler.get_current_active_user_from_openid_token(
145+
mock_token
146+
)
147+
148+
assert user is not None
149+
assert userinfo is not None
150+
151+
assert user == mock_user
152+
assert userinfo.get("email") == mock_jwt_payload.claims.get("email")
153+
154+
155+
async def test_oidc_token_unverified_email(
156+
mocker, mock_oidc_enabled, mock_token, mock_openid_configuration
157+
):
158+
"""Test token decoding for unverified email."""
159+
mocker.patch.object(
160+
StarletteOAuth2App,
161+
"load_server_metadata",
162+
return_value=mock_openid_configuration,
163+
)
164+
165+
unverified_token = mock_token
166+
unverified_token["userinfo"]["email_verified"] = False
167+
168+
oidc_handler = OpenIDHandler()
169+
with pytest.raises(HTTPException):
170+
await oidc_handler.get_current_active_user_from_openid_token(unverified_token)
171+
172+
173+
async def test_oidc_token_without_email_verified_claim(
174+
mocker, mock_oidc_enabled, mock_token, mock_openid_configuration
175+
):
176+
"""Test token decoding with server not supporting email_verified claim."""
177+
mock_jwt_payload = Token(
178+
header={"alg": "RS256"},
179+
claims={"iss": OIDC_SERVER_APPLICATION_URL, "email": "[email protected]"},
180+
)
181+
mock_user = MagicMock(enabled=True)
182+
mocker.patch(
183+
"handler.database.db_user_handler.get_user_by_email", return_value=mock_user
184+
)
185+
186+
openid_conf = mock_openid_configuration
187+
openid_conf["claims_supported"] = openid_conf["claims_supported"].remove(
188+
"email_verified"
189+
)
190+
mocker.patch.object(
191+
StarletteOAuth2App, "load_server_metadata", return_value=openid_conf
192+
)
193+
194+
unverified_token = mock_token
195+
del unverified_token["userinfo"]["email_verified"]
73196

74197
oidc_handler = OpenIDHandler()
75198
user, userinfo = await oidc_handler.get_current_active_user_from_openid_token(

0 commit comments

Comments
 (0)