Skip to content

Commit b0f3624

Browse files
committed
Remove unused userinfo and online token validation (#3239)
* Remove unused userinfo and online token validation - Removes the get_userinfo functionality from the auth module - Removes the online token validation capability from the server - OIDC token validation will only happen locally PBENCH-1074
1 parent c64793f commit b0f3624

File tree

3 files changed

+13
-271
lines changed

3 files changed

+13
-271
lines changed

lib/pbench/server/auth/__init__.py

Lines changed: 1 addition & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -362,78 +362,14 @@ def __init__(
362362
pem_public_key += "-----END PUBLIC KEY-----\n"
363363
self._pem_public_key = pem_public_key
364364

365-
well_known_endpoint = ".well-known/openid-configuration"
366-
well_known_uri = f"realms/{self._realm_name}/{well_known_endpoint}"
367-
endpoints_json = self._connection.get(well_known_uri).json()
368-
369-
try:
370-
self._userinfo_endpoint = endpoints_json["userinfo_endpoint"]
371-
self._tokeninfo_endpoint = endpoints_json["introspection_endpoint"]
372-
except KeyError as e:
373-
raise OpenIDClientError(
374-
HTTPStatus.BAD_GATEWAY,
375-
f"Missing endpoint {e!r} at {well_known_uri}; Endpoints found:"
376-
f" {endpoints_json}",
377-
)
378-
379365
def __repr__(self):
380366
return (
381367
f"OpenIDClient(server_url={self._connection.server_url}, "
382368
f"client_id={self.client_id}, realm_name={self._realm_name}, "
383369
f"headers={self._connection.headers})"
384370
)
385371

386-
def token_introspect_online(self, token: str) -> Optional[JSON]:
387-
"""The introspection endpoint is used to retrieve the active state of a
388-
token.
389-
390-
It can only be invoked by confidential clients.
391-
392-
The introspected JWT token contains the claims specified in
393-
https://tools.ietf.org/html/rfc7662.
394-
395-
Note: this is not supposed to be used in production, instead rely on
396-
offline token validation because of security concerns mentioned in
397-
https://www.rfc-editor.org/rfc/rfc7662.html#section-4.
398-
399-
Args:
400-
token : token value to introspect
401-
402-
Returns:
403-
Extracted token information
404-
{
405-
"aud": <targeted_audience_id>,
406-
"email_verified": <boolean>,
407-
"expires_in": <number_of_seconds>,
408-
"access_type": "offline",
409-
"exp": <unix_timestamp>,
410-
"azp": <client_id>,
411-
"scope": <scope_string>, # "openid email profile"
412-
"email": <user_email>,
413-
"sub": <user_id>
414-
}
415-
"""
416-
if not self._tokeninfo_endpoint:
417-
return None
418-
419-
payload = {
420-
"client_id": self.client_id,
421-
"client_secret": self._client_secret_key,
422-
"token": token,
423-
}
424-
token_payload = self._connection.post(
425-
self._tokeninfo_endpoint, data=payload
426-
).json()
427-
428-
audience = token_payload.get("aud")
429-
if not audience or self.client_id not in audience:
430-
# If our client is not an intended audience for the token,
431-
# we will not verify the token.
432-
token_payload = None
433-
434-
return token_payload
435-
436-
def token_introspect_offline(self, token: str) -> JSON:
372+
def token_introspect(self, token: str) -> JSON:
437373
"""Utility method to decode access/Id tokens using the public key
438374
provided by the identity provider.
439375
@@ -481,31 +417,3 @@ def token_introspect_offline(self, token: str) -> JSON:
481417
jwt.InvalidAudienceError,
482418
) as exc:
483419
raise OpenIDTokenInvalid() from exc
484-
485-
def get_userinfo(self, token: str = None) -> JSON:
486-
"""The userinfo endpoint returns standard claims about the authenticated
487-
user, and is protected by a bearer token.
488-
489-
FIXME - This method appears unused in the rest of the code.
490-
491-
http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
492-
493-
Args:
494-
token : Valid token to extract the userinfo
495-
496-
Returns:
497-
Userinfo payload
498-
{
499-
"family_name": <surname>,
500-
"sub": <user_id>,
501-
"picture": <URL>,
502-
"locale": <locale_name>,
503-
"email_verified": <boolean>,
504-
"given_name": <given_name>,
505-
"email": <email_address>,
506-
"hd": <domain_name>,
507-
"name": <full_name>
508-
}
509-
"""
510-
headers = {"Authorization": f"Bearer {token}"} if token else None
511-
return self._connection.get(self._userinfo_endpoint, headers=headers).json()

lib/pbench/server/auth/auth.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,7 @@
99
import jwt
1010

1111
from pbench.server import PbenchServerConfig
12-
from pbench.server.auth import (
13-
InternalUser,
14-
OpenIDClient,
15-
OpenIDClientError,
16-
OpenIDTokenInvalid,
17-
)
12+
from pbench.server.auth import InternalUser, OpenIDClient, OpenIDTokenInvalid
1813
from pbench.server.database.models.auth_tokens import AuthToken
1914
from pbench.server.database.models.users import User
2015

@@ -225,28 +220,15 @@ def verify_auth_oidc(auth_token: str) -> Optional[InternalUser]:
225220
InternalUser object if the verification succeeds, None on failure.
226221
"""
227222
try:
228-
token_payload = oidc_client.token_introspect_offline(token=auth_token)
223+
token_payload = oidc_client.token_introspect(token=auth_token)
229224
except OpenIDTokenInvalid:
230225
token_payload = None
231226
except Exception:
232227
current_app.logger.exception(
233228
"Unexpected exception occurred while verifying the auth token {}",
234229
auth_token,
235230
)
236-
237-
# Offline token verification resulted in some unexpected error,
238-
# perform the online token verification.
239-
240-
# Note: Online verification should NOT be performed frequently, and it
241-
# is only allowed for non-public clients.
242-
try:
243-
token_payload = oidc_client.token_introspect_online(token=auth_token)
244-
except OpenIDClientError as exc:
245-
current_app.logger.debug(
246-
"Can not perform OIDC online token verification, '{}'", exc
247-
)
248-
token_payload = None
249-
231+
token_payload = None
250232
return (
251233
None
252234
if token_payload is None

lib/pbench/test/unit/server/auth/test_auth.py

Lines changed: 9 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -352,35 +352,10 @@ def __init__(
352352
def get(self, path: str, **kwargs) -> MockResponse:
353353
if path.endswith(f"realms/{realm_name}"):
354354
json_d = {"public_key": public_key}
355-
elif path.endswith(f"realms/{realm_name}/.well-known/openid-configuration"):
356-
json_d = {
357-
"userinfo_endpoint": f"{self.server_url}/userinfo",
358-
"introspection_endpoint": f"{self.server_url}/introspection",
359-
}
360-
if client_id == "us-missing-userinfo-ep":
361-
del json_d["userinfo_endpoint"]
362-
elif client_id == "us-missing-introspection-ep":
363-
del json_d["introspection_endpoint"]
364-
elif client_id == "us-empty-tokeninfo-ep":
365-
json_d["introspection_endpoint"] = ""
366-
elif path.endswith("userinfo"):
367-
json_d = {"path": path, "kwargs": kwargs}
368355
else:
369356
raise Exception(f"MockConnection: unrecognized .get(path={path})")
370357
return MockResponse(json_d)
371358

372-
def post(self, path: str, data: Dict, **kwargs) -> MockResponse:
373-
if path.endswith("/introspection"):
374-
if client_id == "us-raise-exc":
375-
raise OpenIDClientError(400, "Introspection failed")
376-
cid = "other-client" if client_id == "us-other-aud" else client_id
377-
json_d = {"aud": [cid], "data": data}
378-
if client_id == "us-token-payload":
379-
json_d["sub"] = "67890"
380-
else:
381-
raise Exception(f"MockConnection: unrecognized .post(path={path})")
382-
return MockResponse(json_d)
383-
384359
monkeypatch.setattr(pbench.server.auth, "Connection", MockConnection)
385360
return config
386361

@@ -436,61 +411,8 @@ def test_construct_oidc_client_succ(self, monkeypatch):
436411
oidc_client._pem_public_key
437412
== f"-----BEGIN PUBLIC KEY-----\n{public_key}\n-----END PUBLIC KEY-----\n"
438413
)
439-
assert oidc_client._userinfo_endpoint.endswith("/userinfo")
440-
assert oidc_client._tokeninfo_endpoint.endswith("/introspection")
441-
442-
def test_construct_oidc_client_fail_ep(self, monkeypatch):
443-
"""Verify .construct_oidc_client() failure mode where OpenIDClientError
444-
is raised
445-
"""
446-
# First failure should be caused by missing "userinfo" endpoint.
447-
client_id = "us-missing-userinfo-ep"
448-
config = mock_connection(monkeypatch, client_id)
449-
with pytest.raises(OpenIDClientError):
450-
OpenIDClient.construct_oidc_client(config)
451-
452-
# Second failure should be caused by missing "introspection" endpoint.
453-
client_id = "us-missing-introspection-ep"
454-
config = mock_connection(monkeypatch, client_id)
455-
with pytest.raises(OpenIDClientError):
456-
OpenIDClient.construct_oidc_client(config)
457-
458-
def test_token_introspect_online_no_ep(self, monkeypatch):
459-
"""Verify .token_introspect_online() with empty token information end
460-
point
461-
"""
462-
client_id = "us-empty-tokeninfo-ep"
463-
public_key = "opqrstu"
464-
config = mock_connection(monkeypatch, client_id, public_key)
465-
oidc_client = OpenIDClient.construct_oidc_client(config)
466-
json_d = oidc_client.token_introspect_online("my-token")
467-
assert json_d is None
468-
469-
def test_token_introspect_online_not_aud(self, monkeypatch):
470-
"""Verify .token_introspect_online() with different audience."""
471-
client_id = "us-other-aud"
472-
public_key = "vwxyz01"
473-
config = mock_connection(monkeypatch, client_id, public_key)
474-
oidc_client = OpenIDClient.construct_oidc_client(config)
475-
json_d = oidc_client.token_introspect_online("my-token")
476-
assert json_d is None, f"{json_d!r}"
477414

478-
def test_token_introspect_online_succ(self, monkeypatch):
479-
"""Verify .token_introspect_online() with different audience."""
480-
client_id = "us"
481-
public_key = "2345678"
482-
config = mock_connection(monkeypatch, client_id, public_key)
483-
secret = config["openid-connect"]["secret"]
484-
oidc_client = OpenIDClient.construct_oidc_client(config)
485-
json_d = oidc_client.token_introspect_online("my-token")
486-
assert json_d["aud"] == [client_id]
487-
assert json_d["data"] == {
488-
"client_id": client_id,
489-
"client_secret": secret,
490-
"token": "my-token",
491-
}, f"{json_d!r}"
492-
493-
def test_token_introspect_offline_succ(self, monkeypatch, rsa_keys):
415+
def test_token_introspect_succ(self, monkeypatch, rsa_keys):
494416
"""Verify .token_introspect_offline() success path"""
495417
client_id = "us"
496418
token, expected_payload = gen_rsa_token(client_id, rsa_keys["private_key"])
@@ -514,10 +436,10 @@ def test_token_introspect_offline_succ(self, monkeypatch, rsa_keys):
514436
)
515437

516438
# This is the target test case.
517-
response = oidc_client.token_introspect_offline(token)
439+
response = oidc_client.token_introspect(token)
518440
assert response == expected_payload
519441

520-
def test_token_introspect_offline_exp(self, monkeypatch, rsa_keys):
442+
def test_token_introspect_exp(self, monkeypatch, rsa_keys):
521443
"""Verify .token_introspect_offline() failure via expiration"""
522444
client_id = "us"
523445
token, expected_payload = gen_rsa_token(
@@ -531,12 +453,12 @@ def test_token_introspect_offline_exp(self, monkeypatch, rsa_keys):
531453
oidc_client = OpenIDClient.construct_oidc_client(config)
532454

533455
with pytest.raises(OpenIDTokenInvalid) as exc:
534-
oidc_client.token_introspect_offline(token)
456+
oidc_client.token_introspect(token)
535457
assert (
536458
str(exc.value.__cause__) == "Signature has expired"
537459
), f"{exc.value.__cause__}"
538460

539-
def test_token_introspect_offline_aud(self, monkeypatch, rsa_keys):
461+
def test_token_introspect_aud(self, monkeypatch, rsa_keys):
540462
"""Verify .token_introspect_offline() failure via audience error"""
541463
client_id = "us"
542464
token, expected_payload = gen_rsa_token(client_id, rsa_keys["private_key"])
@@ -547,10 +469,10 @@ def test_token_introspect_offline_aud(self, monkeypatch, rsa_keys):
547469
oidc_client = OpenIDClient.construct_oidc_client(config)
548470

549471
with pytest.raises(OpenIDTokenInvalid) as exc:
550-
oidc_client.token_introspect_offline(token)
472+
oidc_client.token_introspect(token)
551473
assert str(exc.value.__cause__) == "Invalid audience", f"{exc.value.__cause__}"
552474

553-
def test_token_introspect_offline_sig(self, monkeypatch, rsa_keys):
475+
def test_token_introspect_sig(self, monkeypatch, rsa_keys):
554476
"""Verify .token_introspect_offline() failure via signature error"""
555477
client_id = "us"
556478
token, expected_payload = gen_rsa_token(client_id, rsa_keys["private_key"])
@@ -564,27 +486,11 @@ def test_token_introspect_offline_sig(self, monkeypatch, rsa_keys):
564486

565487
with pytest.raises(OpenIDTokenInvalid) as exc:
566488
# Make the signature invalid.
567-
oidc_client.token_introspect_offline(token + "1")
489+
oidc_client.token_introspect(token + "1")
568490
assert (
569491
str(exc.value.__cause__) == "Signature verification failed"
570492
), f"{exc.value.__cause__}"
571493

572-
def test_get_userinfo(self, monkeypatch):
573-
"""Verify .get_userinfo() properly invokes Connection.get()"""
574-
# Mock the Connection object and generate an OpenIDClient object.
575-
client_id = "us"
576-
config = mock_connection(monkeypatch, client_id)
577-
oidc_client = OpenIDClient.construct_oidc_client(config)
578-
579-
# Ensure .get_userinfo() invokes Connection.get() with the correct
580-
# parameters.
581-
token = "the-token"
582-
json_d = oidc_client.get_userinfo(token)
583-
assert json_d == {
584-
"kwargs": {"headers": {"Authorization": "Bearer the-token"}},
585-
"path": "https://example.com/userinfo",
586-
}
587-
588494

589495
@dataclass
590496
class MockRequest:
@@ -776,61 +682,7 @@ def tio_exc(token: str) -> JSON:
776682
app = Flask("test-verify-auth-oidc-offline-invalid")
777683
app.logger = make_logger
778684
with app.app_context():
779-
monkeypatch.setattr(oidc_client, "token_introspect_offline", tio_exc)
780-
user = Auth.verify_auth(token)
781-
782-
assert user is None
783-
784-
def test_verify_auth_oidc_online(self, monkeypatch, rsa_keys, make_logger):
785-
"""Verify OIDC token online verification works via Auth.verify_auth()"""
786-
# Use the client ID to direct the MockConnection class to return a
787-
# token payload.
788-
client_id = "us-token-payload"
789-
token, expected_payload = gen_rsa_token(client_id, rsa_keys["private_key"])
790-
791-
# Mock the Connection object and generate an OpenIDClient object,
792-
# installing it as Auth module's OIDC client.
793-
config = mock_connection(
794-
monkeypatch, client_id, public_key=rsa_keys["public_key"]
795-
)
796-
oidc_client = OpenIDClient.construct_oidc_client(config)
797-
monkeypatch.setattr(Auth, "oidc_client", oidc_client)
798-
799-
def tio_exc(token: str) -> JSON:
800-
raise Exception("Token introspection offline failed for some reason")
801-
802-
app = Flask("test-verify-auth-oidc-online")
803-
app.logger = make_logger
804-
with app.app_context():
805-
monkeypatch.setattr(oidc_client, "token_introspect_offline", tio_exc)
806-
user = Auth.verify_auth(token)
807-
808-
assert user.id == "67890"
809-
810-
def test_verify_auth_oidc_online_fail(self, monkeypatch, rsa_keys, make_logger):
811-
"""Verify OIDC token online verification via Auth.verify_auth() fails
812-
returning None
813-
"""
814-
# Use the client ID to direct the online token introspection to raise
815-
# an OpenIDClientError.
816-
client_id = "us-raise-exc"
817-
token, expected_payload = gen_rsa_token(client_id, rsa_keys["private_key"])
818-
819-
# Mock the Connection object and generate an OpenIDClient object,
820-
# installing it as Auth module's OIDC client.
821-
config = mock_connection(
822-
monkeypatch, client_id, public_key=rsa_keys["public_key"]
823-
)
824-
oidc_client = OpenIDClient.construct_oidc_client(config)
825-
monkeypatch.setattr(Auth, "oidc_client", oidc_client)
826-
827-
def tio_exc(token: str) -> JSON:
828-
raise Exception("Token introspection offline failed for some reason")
829-
830-
app = Flask("test-verify-auth-oidc-online-fail")
831-
app.logger = make_logger
832-
with app.app_context():
833-
monkeypatch.setattr(oidc_client, "token_introspect_offline", tio_exc)
685+
monkeypatch.setattr(oidc_client, "token_introspect", tio_exc)
834686
user = Auth.verify_auth(token)
835687

836688
assert user is None

0 commit comments

Comments
 (0)