mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-01-12 10:34:17 +00:00
Keycloak realm with groups (#2002)
* added a group and mapper to keycloak w/ burnettk * accept an internal uri to keycloak w/ burnettk * pyl w/ burnettk * the only time we ever use internal arg to open_id_endpoint_for_name we want it True * protect users of openid urls from internal urls * allow port 8000/8001 for docker and avoid public urls when using requests again * allow 8001 frontend in docker compose post logout redirect url --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com> Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
parent
b8c7aa991d
commit
d2c5c27bdc
2
.github/workflows/build_docker_images.yml
vendored
2
.github/workflows/build_docker_images.yml
vendored
@ -30,7 +30,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- spiffui/newui
|
||||
- keycloak-realm-with-groups
|
||||
tags: [v*]
|
||||
|
||||
jobs:
|
||||
|
@ -1,17 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
function error_handler() {
|
||||
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
echo >&2 "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
exit "$2"
|
||||
}
|
||||
trap 'error_handler ${LINENO} $?' ERR
|
||||
set -o errtrace -o errexit -o nounset -o pipefail
|
||||
|
||||
script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
script_dir="$(
|
||||
cd -- "$(dirname "$0")" >/dev/null 2>&1
|
||||
pwd -P
|
||||
)"
|
||||
|
||||
realms="$*"
|
||||
if [[ -z "$realms" ]]; then
|
||||
realms="spiffworkflow-realm"
|
||||
realms="spiffworkflow-local-realm"
|
||||
fi
|
||||
|
||||
docker_container_path=/tmp/hey
|
||||
@ -20,8 +23,8 @@ docker exec keycloak rm -rf "$docker_container_path"
|
||||
docker exec keycloak /opt/keycloak/bin/kc.sh export --dir "${docker_container_path}" --users realm_file || echo ''
|
||||
docker cp "keycloak:${docker_container_path}" "$local_tmp_dir"
|
||||
|
||||
for realm in $realms ; do
|
||||
if ! grep -Eq '\-realm$' <<< "$realm"; then
|
||||
for realm in $realms; do
|
||||
if ! grep -Eq '\-realm$' <<<"$realm"; then
|
||||
realm="${realm}-realm"
|
||||
fi
|
||||
cp "${local_tmp_dir}/hey/${realm}.json" "${script_dir}/../realm_exports/"
|
||||
|
@ -441,7 +441,17 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"groups": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": "a14e6081-ffd4-4925-9364-4916bcf74931",
|
||||
"name": "group_keycloak",
|
||||
"path": "/group_keycloak",
|
||||
"attributes": {},
|
||||
"realmRoles": [],
|
||||
"clientRoles": {},
|
||||
"subGroups": []
|
||||
}
|
||||
],
|
||||
"defaultRole": {
|
||||
"id": "c9f0ff93-642d-402b-965a-04d70719886b",
|
||||
"name": "default-roles-spiffworkflow",
|
||||
@ -458,7 +468,11 @@
|
||||
"otpPolicyLookAheadWindow": 1,
|
||||
"otpPolicyPeriod": 30,
|
||||
"otpPolicyCodeReusable": false,
|
||||
"otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName"],
|
||||
"otpSupportedApplications": [
|
||||
"totpAppMicrosoftAuthenticatorName",
|
||||
"totpAppFreeOTPName",
|
||||
"totpAppGoogleName"
|
||||
],
|
||||
"webAuthnPolicyRpEntityName": "keycloak",
|
||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||
"webAuthnPolicyRpId": "",
|
||||
@ -493,7 +507,7 @@
|
||||
"id": "644ff652-31d1-4349-9340-ce3b5fb76b5c",
|
||||
"type": "password",
|
||||
"createdDate": 1676302139645,
|
||||
"secretData": "{\"value\":\"P6nrEJgmgdL+HbNA9tPkOyHYaLAqLg6cXh27ACgVQiOox4qwNE8Iv1L4ssispV4gsmuHiY+alio/2UMuyMGvEw==\",\"salt\":\"g214ckcAyig59U/3Oqwq4w==\",\"additionalParameters\":{}}",
|
||||
"secretData": "{\"value\":\"nhHncfQqkY1vhJE8QxjD3hwpznBnZNc5+OxS79OFKJc=\",\"salt\":\"EradsPFeRiQ3oMAzGqfWBw==\",\"additionalParameters\":{}}",
|
||||
"credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
|
||||
}
|
||||
],
|
||||
@ -504,7 +518,7 @@
|
||||
"realm-management": ["realm-admin"]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
"groups": ["/group_keycloak"]
|
||||
},
|
||||
{
|
||||
"id": "4c436296-8471-4105-b551-80eee96b43bb",
|
||||
@ -651,6 +665,42 @@
|
||||
"realmRoles": ["default-roles-spiffworkflow"],
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
},
|
||||
{
|
||||
"id": "d2a86264-e3ca-4a82-99b8-0a6cd714ded4",
|
||||
"createdTimestamp": 1722019421345,
|
||||
"username": "service-account-spiffworkflow-backend",
|
||||
"enabled": true,
|
||||
"totp": false,
|
||||
"emailVerified": false,
|
||||
"serviceAccountClientId": "spiffworkflow-backend",
|
||||
"credentials": [],
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-spiffworkflow"],
|
||||
"clientRoles": {
|
||||
"spiffworkflow-backend": ["uma_protection"]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
},
|
||||
{
|
||||
"id": "bae51d14-9d82-468f-9e9d-1cd2cb507f15",
|
||||
"createdTimestamp": 1722019421396,
|
||||
"username": "service-account-withauth",
|
||||
"enabled": true,
|
||||
"totp": false,
|
||||
"emailVerified": false,
|
||||
"serviceAccountClientId": "withAuth",
|
||||
"credentials": [],
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-spiffworkflow"],
|
||||
"clientRoles": {
|
||||
"withAuth": ["uma_protection"]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
],
|
||||
"scopeMappings": [
|
||||
@ -1031,9 +1081,8 @@
|
||||
"secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q",
|
||||
"redirectUris": [
|
||||
"http://localhost:7000/*",
|
||||
"https://replace-me-with-spiff-backend-host-and-path/*",
|
||||
"http://67.205.133.116:7000/*",
|
||||
"http://167.172.242.138:7000/*"
|
||||
"http://localhost:8000/*",
|
||||
"https://for-local-dev.spiffworkflow.org/*"
|
||||
],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
@ -1051,7 +1100,7 @@
|
||||
"saml.force.post.binding": "false",
|
||||
"saml.multivalued.roles": "false",
|
||||
"frontchannel.logout.session.required": "false",
|
||||
"post.logout.redirect.uris": "https://replace-me-with-spiff-frontend-host-and-path/*##http://localhost:7001/*",
|
||||
"post.logout.redirect.uris": "https://replace-me-with-spiff-frontend-host-and-path/*##http://localhost:7001/*##http://localhost:8001/*",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false",
|
||||
"saml.server.signature.keyinfo.ext": "false",
|
||||
@ -1320,9 +1369,7 @@
|
||||
"redirectUris": [
|
||||
"https://api.unused-for-local-dev.spiffworkflow.org/*",
|
||||
"http://localhost:7001/*",
|
||||
"http://67.205.133.116:7000/*",
|
||||
"http://167.172.242.138:7001/*",
|
||||
"https://api.demo.spiffworkflow.org/*"
|
||||
"http://localhost:8001/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"notBefore": 0,
|
||||
@ -1391,10 +1438,7 @@
|
||||
"secret": "6o8kIKQznQtejHOdRhWeKorBJclMGcgA",
|
||||
"redirectUris": [
|
||||
"https://api.unused-for-local-dev.spiffworkflow.org/*",
|
||||
"http://localhost:7001/*",
|
||||
"http://67.205.133.116:7000/*",
|
||||
"http://167.172.242.138:7001/*",
|
||||
"https://api.demo.spiffworkflow.org/*"
|
||||
"http://localhost:8001/*"
|
||||
],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
@ -1712,6 +1756,20 @@
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "75c10b23-3a63-4aa6-99cc-7c8b7645d32f",
|
||||
"name": "groups",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-group-membership-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"full.path": "true",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "groups",
|
||||
"userinfo.token.claim": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b05e679c-e00e-427e-8e47-0a4fd411c7a6",
|
||||
"name": "updated at",
|
||||
@ -2071,6 +2129,7 @@
|
||||
"browserSecurityHeaders": {
|
||||
"contentSecurityPolicyReportOnly": "",
|
||||
"xContentTypeOptions": "nosniff",
|
||||
"referrerPolicy": "no-referrer",
|
||||
"xRobotsTag": "none",
|
||||
"xFrameOptions": "SAMEORIGIN",
|
||||
"contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
|
||||
@ -2106,14 +2165,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper"
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -2135,14 +2194,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper"
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -2281,40 +2340,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7675760b-666a-4b8c-a9b8-da1e01c207fe",
|
||||
"alias": "Authentication Options",
|
||||
"description": "Authentication options.",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": false,
|
||||
"builtIn": true,
|
||||
"authenticationExecutions": [
|
||||
{
|
||||
"authenticator": "basic-auth",
|
||||
"authenticatorFlow": false,
|
||||
"requirement": "REQUIRED",
|
||||
"priority": 10,
|
||||
"autheticatorFlow": false,
|
||||
"userSetupAllowed": false
|
||||
},
|
||||
{
|
||||
"authenticator": "basic-auth-otp",
|
||||
"authenticatorFlow": false,
|
||||
"requirement": "DISABLED",
|
||||
"priority": 20,
|
||||
"autheticatorFlow": false,
|
||||
"userSetupAllowed": false
|
||||
},
|
||||
{
|
||||
"authenticator": "auth-spnego",
|
||||
"authenticatorFlow": false,
|
||||
"requirement": "DISABLED",
|
||||
"priority": 30,
|
||||
"autheticatorFlow": false,
|
||||
"userSetupAllowed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "34e18ea8-f515-46dc-9dbf-5b79f8154564",
|
||||
"alias": "Browser - Conditional OTP",
|
||||
@ -2687,32 +2712,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "70ef5a26-e3bb-4ba7-a05a-d205b0a3836c",
|
||||
"alias": "http challenge",
|
||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": true,
|
||||
"builtIn": true,
|
||||
"authenticationExecutions": [
|
||||
{
|
||||
"authenticator": "no-cookie-redirect",
|
||||
"authenticatorFlow": false,
|
||||
"requirement": "REQUIRED",
|
||||
"priority": 10,
|
||||
"autheticatorFlow": false,
|
||||
"userSetupAllowed": false
|
||||
},
|
||||
{
|
||||
"authenticatorFlow": true,
|
||||
"requirement": "REQUIRED",
|
||||
"priority": 20,
|
||||
"autheticatorFlow": true,
|
||||
"flowAlias": "Authentication Options",
|
||||
"userSetupAllowed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "89abf09a-bfb4-4dea-b164-ca7c563b4009",
|
||||
"alias": "registration",
|
||||
@ -2862,9 +2861,9 @@
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"alias": "terms_and_conditions",
|
||||
"alias": "TERMS_AND_CONDITIONS",
|
||||
"name": "Terms and Conditions",
|
||||
"providerId": "terms_and_conditions",
|
||||
"providerId": "TERMS_AND_CONDITIONS",
|
||||
"enabled": false,
|
||||
"defaultAction": false,
|
||||
"priority": 20,
|
||||
@ -2940,7 +2939,7 @@
|
||||
"parRequestUriLifespan": "60",
|
||||
"clientSessionMaxLifespan": "0"
|
||||
},
|
||||
"keycloakVersion": "20.0.1",
|
||||
"keycloakVersion": "22.0.4",
|
||||
"userManagedAccessAllowed": false,
|
||||
"clientProfiles": {
|
||||
"profiles": []
|
||||
|
@ -241,12 +241,13 @@ def setup_config(app: Flask) -> None:
|
||||
if app.config.get("SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS") is None:
|
||||
app.config["SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS"] = [
|
||||
{
|
||||
"identifier": "default",
|
||||
"label": "Default",
|
||||
"uri": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL"),
|
||||
"additional_valid_client_ids": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_ADDITIONAL_VALID_CLIENT_IDS"),
|
||||
"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"),
|
||||
"identifier": "default",
|
||||
"internal_uri": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_INTERNAL_URL"),
|
||||
"label": "Default",
|
||||
"uri": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL"),
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -134,6 +134,7 @@ 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")
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_INTERNAL_URL")
|
||||
|
||||
# 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.
|
||||
@ -148,6 +149,7 @@ else:
|
||||
{
|
||||
"identifier": "default",
|
||||
"label": "Default",
|
||||
"internal_uri": "http://localhost:7002/realms/spiffworkflow-local",
|
||||
"uri": "http://localhost:7002/realms/spiffworkflow-local",
|
||||
"client_id": "spiffworkflow-backend",
|
||||
"client_secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q",
|
||||
|
@ -72,6 +72,7 @@ class AuthenticationProviderTypes(enum.Enum):
|
||||
class AuthenticationOptionForApi(TypedDict):
|
||||
identifier: str
|
||||
label: str
|
||||
internal_uri: NotRequired[str]
|
||||
uri: str
|
||||
additional_valid_client_ids: NotRequired[str]
|
||||
|
||||
@ -122,9 +123,14 @@ class AuthenticationService:
|
||||
return [cls.client_id(authentication_identifier), "account"]
|
||||
|
||||
@classmethod
|
||||
def server_url(cls, authentication_identifier: str) -> str:
|
||||
def server_url(cls, authentication_identifier: str, internal: bool = False) -> str:
|
||||
"""Returns the server url from the config."""
|
||||
config: str = cls.authentication_option_for_identifier(authentication_identifier)["uri"]
|
||||
auth_config = cls.authentication_option_for_identifier(authentication_identifier)
|
||||
uri_key = "uri"
|
||||
if internal:
|
||||
if "internal_uri" in auth_config and auth_config["internal_uri"] is not None:
|
||||
uri_key = "internal_uri"
|
||||
config: str = auth_config[uri_key] # type: ignore
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
@ -134,15 +140,16 @@ class AuthenticationService:
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def open_id_endpoint_for_name(cls, name: str, authentication_identifier: str) -> str:
|
||||
def open_id_endpoint_for_name(cls, name: str, authentication_identifier: str, internal: bool = False) -> str:
|
||||
"""All openid systems provide a mapping of static names to the full path of that endpoint."""
|
||||
appropriate_server_url = cls.server_url(authentication_identifier)
|
||||
openid_config_url = f"{appropriate_server_url}/.well-known/openid-configuration"
|
||||
|
||||
if authentication_identifier not in cls.ENDPOINT_CACHE:
|
||||
cls.ENDPOINT_CACHE[authentication_identifier] = {}
|
||||
if authentication_identifier not in cls.JSON_WEB_KEYSET_CACHE:
|
||||
cls.JSON_WEB_KEYSET_CACHE[authentication_identifier] = {}
|
||||
|
||||
internal_server_url = cls.server_url(authentication_identifier, internal=True)
|
||||
openid_config_url = f"{internal_server_url}/.well-known/openid-configuration"
|
||||
if name not in AuthenticationService.ENDPOINT_CACHE[authentication_identifier]:
|
||||
try:
|
||||
response = safe_requests.get(openid_config_url, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
|
||||
@ -151,7 +158,14 @@ class AuthenticationService:
|
||||
raise OpenIdConnectionError(f"Cannot connect to given open id url: {openid_config_url}") from ce
|
||||
if name not in AuthenticationService.ENDPOINT_CACHE[authentication_identifier]:
|
||||
raise Exception(f"Unknown OpenID Endpoint: {name}. Tried to get from {openid_config_url}")
|
||||
|
||||
config: str = AuthenticationService.ENDPOINT_CACHE[authentication_identifier].get(name, "")
|
||||
|
||||
external_server_url = cls.server_url(authentication_identifier)
|
||||
if internal is False:
|
||||
if internal_server_url != external_server_url:
|
||||
config = config.replace(internal_server_url, external_server_url)
|
||||
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
@ -166,7 +180,7 @@ class AuthenticationService:
|
||||
|
||||
@classmethod
|
||||
def jwks_public_key_for_key_id(cls, authentication_identifier: str, key_id: str) -> JWKSKeyConfig:
|
||||
jwks_uri = cls.open_id_endpoint_for_name("jwks_uri", authentication_identifier)
|
||||
jwks_uri = cls.open_id_endpoint_for_name("jwks_uri", authentication_identifier, internal=True)
|
||||
jwks_configs = cls.get_jwks_config_from_uri(jwks_uri)
|
||||
json_key_configs: JWKSKeyConfig | None = cls.get_key_config(jwks_configs, key_id)
|
||||
if not json_key_configs:
|
||||
@ -303,7 +317,9 @@ class AuthenticationService:
|
||||
"redirect_uri": f"{self.get_backend_url()}{redirect_url}",
|
||||
}
|
||||
|
||||
request_url = self.open_id_endpoint_for_name("token_endpoint", authentication_identifier=authentication_identifier)
|
||||
request_url = self.open_id_endpoint_for_name(
|
||||
"token_endpoint", authentication_identifier=authentication_identifier, internal=True
|
||||
)
|
||||
|
||||
response = requests.post(request_url, data=data, headers=headers, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
|
||||
auth_token_object: dict = json.loads(response.text)
|
||||
@ -432,7 +448,9 @@ class AuthenticationService:
|
||||
"client_secret": cls.secret_key(authentication_identifier),
|
||||
}
|
||||
|
||||
request_url = cls.open_id_endpoint_for_name("token_endpoint", authentication_identifier=authentication_identifier)
|
||||
request_url = cls.open_id_endpoint_for_name(
|
||||
"token_endpoint", authentication_identifier=authentication_identifier, internal=True
|
||||
)
|
||||
|
||||
response = requests.post(request_url, data=data, headers=headers, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
|
||||
auth_token_object: dict = json.loads(response.text)
|
||||
|
Loading…
x
Reference in New Issue
Block a user