feature/specify-valid-client-ids (#808)

* allow additional valid client ids to be specified for the purpose of token validation

* import the correct typedict and notrequired for python 3.10

* remove HEY

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2023-12-13 15:44:59 -05:00 committed by GitHub
parent 5cceb52756
commit 7234f0f181
3 changed files with 39 additions and 5 deletions

View File

@ -234,6 +234,7 @@ def setup_config(app: Flask) -> None:
"uri": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL"), "uri": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL"),
"client_id": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID"), "client_id": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID"),
"client_secret": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_SECRET_KEY"), "client_secret": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_SECRET_KEY"),
"additional_valid_client_ids": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_ADDITIONAL_VALID_CLIENT_IDS"),
} }
] ]

View File

@ -106,6 +106,15 @@ else:
SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL = url_config SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL = url_config
config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID", default="spiffworkflow-backend") config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID", default="spiffworkflow-backend")
config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_SECRET_KEY", default="JXeQExm0JhQPLumgHtIIqf52bDalHz0q") config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_SECRET_KEY", default="JXeQExm0JhQPLumgHtIIqf52bDalHz0q")
# comma-separated list of client ids that can be successfully validated against.
# useful for api users that will login to a different client on the same realm but from something external to backend.
# Example:
# client-A is configured as the main client id in backend
# client-B is for api users who will authenticate directly with keycloak
# if client-B is added to this list, then an api user can auth with keycloak
# and use that token successfully with backend
config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_ADDITIONAL_VALID_CLIENT_IDS")
else: else:
SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS = [ SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS = [
{ {
@ -114,6 +123,7 @@ else:
"uri": "http://localhost:7002/realms/spiffworkflow", "uri": "http://localhost:7002/realms/spiffworkflow",
"client_id": "spiffworkflow-backend", "client_id": "spiffworkflow-backend",
"client_secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q", "client_secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q",
"additional_valid_client_ids": None,
} }
] ]

View File

@ -1,11 +1,18 @@
import base64 import base64
import enum import enum
import json import json
import sys
import time import time
from hashlib import sha256 from hashlib import sha256
from hmac import HMAC from hmac import HMAC
from hmac import compare_digest from hmac import compare_digest
from typing import TypedDict
if sys.version_info < (3, 11):
from typing_extensions import NotRequired
from typing_extensions import TypedDict
else:
from typing import NotRequired
from typing import TypedDict
import jwt import jwt
import requests import requests
@ -36,6 +43,7 @@ class AuthenticationOptionForApi(TypedDict):
identifier: str identifier: str
label: str label: str
uri: str uri: str
additional_valid_client_ids: NotRequired[str]
class AuthenticationOption(AuthenticationOptionForApi): class AuthenticationOption(AuthenticationOptionForApi):
@ -163,6 +171,24 @@ class AuthenticationService:
auth_token_object: dict = json.loads(response.text) auth_token_object: dict = json.loads(response.text)
return auth_token_object return auth_token_object
@classmethod
def is_valid_azp(cls, authentication_identifier: str, azp: str | None) -> bool:
# not all open id token include an azp so only check if present
if azp is None:
return True
valid_client_ids = [cls.client_id(authentication_identifier), "account"]
if (
"additional_valid_client_ids" in cls.authentication_option_for_identifier(authentication_identifier)
and cls.authentication_option_for_identifier(authentication_identifier)["additional_valid_client_ids"] is not None
):
additional_valid_client_ids = cls.authentication_option_for_identifier(authentication_identifier)[
"additional_valid_client_ids"
].split(",")
additional_valid_client_ids = [value.strip() for value in additional_valid_client_ids]
valid_client_ids = valid_client_ids + additional_valid_client_ids
return azp in valid_client_ids
@classmethod @classmethod
def validate_id_or_access_token(cls, token: str, authentication_identifier: str) -> bool: def validate_id_or_access_token(cls, token: str, authentication_identifier: str) -> bool:
"""Https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation.""" """Https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation."""
@ -198,10 +224,7 @@ class AuthenticationService:
f"TOKEN INVALID because audience '{aud}' does not match client id '{cls.client_id(authentication_identifier)}'" f"TOKEN INVALID because audience '{aud}' does not match client id '{cls.client_id(authentication_identifier)}'"
) )
valid = False valid = False
elif azp and azp not in ( elif not cls.is_valid_azp(authentication_identifier, azp):
cls.client_id(authentication_identifier),
"account",
):
current_app.logger.error( current_app.logger.error(
f"TOKEN INVALID because azp '{azp}' does not match client id '{cls.client_id(authentication_identifier)}'" f"TOKEN INVALID because azp '{azp}' does not match client id '{cls.client_id(authentication_identifier)}'"
) )