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"),
"client_id": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID"),
"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
config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID", default="spiffworkflow-backend")
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:
SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS = [
{
@ -114,6 +123,7 @@ else:
"uri": "http://localhost:7002/realms/spiffworkflow",
"client_id": "spiffworkflow-backend",
"client_secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q",
"additional_valid_client_ids": None,
}
]

View File

@ -1,11 +1,18 @@
import base64
import enum
import json
import sys
import time
from hashlib import sha256
from hmac import HMAC
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 requests
@ -36,6 +43,7 @@ class AuthenticationOptionForApi(TypedDict):
identifier: str
label: str
uri: str
additional_valid_client_ids: NotRequired[str]
class AuthenticationOption(AuthenticationOptionForApi):
@ -163,6 +171,24 @@ class AuthenticationService:
auth_token_object: dict = json.loads(response.text)
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
def validate_id_or_access_token(cls, token: str, authentication_identifier: str) -> bool:
"""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)}'"
)
valid = False
elif azp and azp not in (
cls.client_id(authentication_identifier),
"account",
):
elif not cls.is_valid_azp(authentication_identifier, azp):
current_app.logger.error(
f"TOKEN INVALID because azp '{azp}' does not match client id '{cls.client_id(authentication_identifier)}'"
)