|
|
@ -27,24 +27,38 @@ from jose import jwt |
|
|
|
|
|
|
|
from .authorization import Authorization |
|
|
|
from .connection import ConnectionManager |
|
|
|
from .exceptions import raise_error_from_response, KeycloakGetError, \ |
|
|
|
KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, KeycloakDeprecationError |
|
|
|
from .exceptions import ( |
|
|
|
KeycloakAuthorizationConfigError, |
|
|
|
KeycloakDeprecationError, |
|
|
|
KeycloakGetError, |
|
|
|
KeycloakInvalidTokenError, |
|
|
|
KeycloakRPTNotFound, |
|
|
|
raise_error_from_response, |
|
|
|
) |
|
|
|
from .urls_patterns import ( |
|
|
|
URL_REALM, |
|
|
|
URL_AUTH, |
|
|
|
URL_CERTS, |
|
|
|
URL_ENTITLEMENT, |
|
|
|
URL_INTROSPECT, |
|
|
|
URL_LOGOUT, |
|
|
|
URL_REALM, |
|
|
|
URL_TOKEN, |
|
|
|
URL_USERINFO, |
|
|
|
URL_WELL_KNOWN, |
|
|
|
URL_LOGOUT, |
|
|
|
URL_CERTS, |
|
|
|
URL_ENTITLEMENT, |
|
|
|
URL_INTROSPECT |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
class KeycloakOpenID: |
|
|
|
|
|
|
|
def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True, custom_headers=None, proxies=None): |
|
|
|
def __init__( |
|
|
|
self, |
|
|
|
server_url, |
|
|
|
realm_name, |
|
|
|
client_id, |
|
|
|
client_secret_key=None, |
|
|
|
verify=True, |
|
|
|
custom_headers=None, |
|
|
|
proxies=None, |
|
|
|
): |
|
|
|
""" |
|
|
|
|
|
|
|
:param server_url: Keycloak server url |
|
|
@ -62,11 +76,9 @@ class KeycloakOpenID: |
|
|
|
if custom_headers is not None: |
|
|
|
# merge custom headers to main headers |
|
|
|
headers.update(custom_headers) |
|
|
|
self._connection = ConnectionManager(base_url=server_url, |
|
|
|
headers=headers, |
|
|
|
timeout=60, |
|
|
|
verify=verify, |
|
|
|
proxies=proxies) |
|
|
|
self._connection = ConnectionManager( |
|
|
|
base_url=server_url, headers=headers, timeout=60, verify=verify, proxies=proxies |
|
|
|
) |
|
|
|
|
|
|
|
self._authorization = Authorization() |
|
|
|
|
|
|
@ -138,7 +150,7 @@ class KeycloakOpenID: |
|
|
|
:param kwargs: |
|
|
|
:return: |
|
|
|
""" |
|
|
|
if method_token_info == 'introspect': |
|
|
|
if method_token_info == "introspect": |
|
|
|
token_info = self.introspect(token) |
|
|
|
else: |
|
|
|
token_info = self.decode_token(token, **kwargs) |
|
|
@ -146,11 +158,11 @@ class KeycloakOpenID: |
|
|
|
return token_info |
|
|
|
|
|
|
|
def well_know(self): |
|
|
|
""" The most important endpoint to understand is the well-known configuration |
|
|
|
endpoint. It lists endpoints and other configuration options relevant to |
|
|
|
the OpenID Connect implementation in Keycloak. |
|
|
|
"""The most important endpoint to understand is the well-known configuration |
|
|
|
endpoint. It lists endpoints and other configuration options relevant to |
|
|
|
the OpenID Connect implementation in Keycloak. |
|
|
|
|
|
|
|
:return It lists endpoints and other configuration options relevant. |
|
|
|
:return It lists endpoints and other configuration options relevant. |
|
|
|
""" |
|
|
|
|
|
|
|
params_path = {"realm-name": self.realm_name} |
|
|
@ -165,12 +177,23 @@ class KeycloakOpenID: |
|
|
|
|
|
|
|
:return: |
|
|
|
""" |
|
|
|
params_path = {"authorization-endpoint": self.well_know()['authorization_endpoint'], |
|
|
|
"client-id": self.client_id, |
|
|
|
"redirect-uri": redirect_uri} |
|
|
|
params_path = { |
|
|
|
"authorization-endpoint": self.well_know()["authorization_endpoint"], |
|
|
|
"client-id": self.client_id, |
|
|
|
"redirect-uri": redirect_uri, |
|
|
|
} |
|
|
|
return URL_AUTH.format(**params_path) |
|
|
|
|
|
|
|
def token(self, username="", password="", grant_type=["password"], code="", redirect_uri="", totp=None, **extra): |
|
|
|
def token( |
|
|
|
self, |
|
|
|
username="", |
|
|
|
password="", |
|
|
|
grant_type=["password"], |
|
|
|
code="", |
|
|
|
redirect_uri="", |
|
|
|
totp=None, |
|
|
|
**extra |
|
|
|
): |
|
|
|
""" |
|
|
|
The token endpoint is used to obtain tokens. Tokens can either be obtained by |
|
|
|
exchanging an authorization code or by supplying credentials directly depending on |
|
|
@ -188,9 +211,14 @@ class KeycloakOpenID: |
|
|
|
:return: |
|
|
|
""" |
|
|
|
params_path = {"realm-name": self.realm_name} |
|
|
|
payload = {"username": username, "password": password, |
|
|
|
"client_id": self.client_id, "grant_type": grant_type, |
|
|
|
"code": code, "redirect_uri": redirect_uri} |
|
|
|
payload = { |
|
|
|
"username": username, |
|
|
|
"password": password, |
|
|
|
"client_id": self.client_id, |
|
|
|
"grant_type": grant_type, |
|
|
|
"code": code, |
|
|
|
"redirect_uri": redirect_uri, |
|
|
|
} |
|
|
|
if extra: |
|
|
|
payload.update(extra) |
|
|
|
|
|
|
@ -198,8 +226,7 @@ class KeycloakOpenID: |
|
|
|
payload["totp"] = totp |
|
|
|
|
|
|
|
payload = self._add_secret_key(payload) |
|
|
|
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), |
|
|
|
data=payload) |
|
|
|
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) |
|
|
|
return raise_error_from_response(data_raw, KeycloakGetError) |
|
|
|
|
|
|
|
def refresh_token(self, refresh_token, grant_type=["refresh_token"]): |
|
|
@ -216,10 +243,13 @@ class KeycloakOpenID: |
|
|
|
:return: |
|
|
|
""" |
|
|
|
params_path = {"realm-name": self.realm_name} |
|
|
|
payload = {"client_id": self.client_id, "grant_type": grant_type, "refresh_token": refresh_token} |
|
|
|
payload = { |
|
|
|
"client_id": self.client_id, |
|
|
|
"grant_type": grant_type, |
|
|
|
"refresh_token": refresh_token, |
|
|
|
} |
|
|
|
payload = self._add_secret_key(payload) |
|
|
|
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), |
|
|
|
data=payload) |
|
|
|
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) |
|
|
|
return raise_error_from_response(data_raw, KeycloakGetError) |
|
|
|
|
|
|
|
def userinfo(self, token): |
|
|
@ -250,8 +280,7 @@ class KeycloakOpenID: |
|
|
|
payload = {"client_id": self.client_id, "refresh_token": refresh_token} |
|
|
|
|
|
|
|
payload = self._add_secret_key(payload) |
|
|
|
data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), |
|
|
|
data=payload) |
|
|
|
data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload) |
|
|
|
|
|
|
|
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) |
|
|
|
|
|
|
@ -268,7 +297,7 @@ class KeycloakOpenID: |
|
|
|
params_path = {"realm-name": self.realm_name} |
|
|
|
data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) |
|
|
|
return raise_error_from_response(data_raw, KeycloakGetError) |
|
|
|
|
|
|
|
|
|
|
|
def public_key(self): |
|
|
|
""" |
|
|
|
The public key is exposed by the realm page directly. |
|
|
@ -277,32 +306,31 @@ class KeycloakOpenID: |
|
|
|
""" |
|
|
|
params_path = {"realm-name": self.realm_name} |
|
|
|
data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) |
|
|
|
return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] |
|
|
|
|
|
|
|
return raise_error_from_response(data_raw, KeycloakGetError)["public_key"] |
|
|
|
|
|
|
|
def entitlement(self, token, resource_server_id): |
|
|
|
""" |
|
|
|
Client applications can use a specific endpoint to obtain a special security token |
|
|
|
called a requesting party token (RPT). This token consists of all the entitlements |
|
|
|
(or permissions) for a user as a result of the evaluation of the permissions and authorization |
|
|
|
policies associated with the resources being requested. With an RPT, client applications can |
|
|
|
gain access to protected resources at the resource server. |
|
|
|
(or permissions) for a user as a result of the evaluation of the permissions and |
|
|
|
authorization policies associated with the resources being requested. With an RPT, |
|
|
|
client applications can gain access to protected resources at the resource server. |
|
|
|
|
|
|
|
:return: |
|
|
|
""" |
|
|
|
self.connection.add_param_headers("Authorization", "Bearer " + token) |
|
|
|
params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id} |
|
|
|
data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path)) |
|
|
|
|
|
|
|
if data_raw.status_code == 404: |
|
|
|
|
|
|
|
if data_raw.status_code == 404: |
|
|
|
return raise_error_from_response(data_raw, KeycloakDeprecationError) |
|
|
|
|
|
|
|
return raise_error_from_response(data_raw, KeycloakGetError) |
|
|
|
|
|
|
|
def introspect(self, token, rpt=None, token_type_hint=None): |
|
|
|
""" |
|
|
|
The introspection endpoint is used to retrieve the active state of a token. It is can only be |
|
|
|
invoked by confidential clients. |
|
|
|
The introspection endpoint is used to retrieve the active state of a token. |
|
|
|
It is can only be invoked by confidential clients. |
|
|
|
|
|
|
|
https://tools.ietf.org/html/rfc7662 |
|
|
|
|
|
|
@ -316,7 +344,7 @@ class KeycloakOpenID: |
|
|
|
|
|
|
|
payload = {"client_id": self.client_id, "token": token} |
|
|
|
|
|
|
|
if token_type_hint == 'requesting_party_token': |
|
|
|
if token_type_hint == "requesting_party_token": |
|
|
|
if rpt: |
|
|
|
payload.update({"token": rpt, "token_type_hint": token_type_hint}) |
|
|
|
self.connection.add_param_headers("Authorization", "Bearer " + token) |
|
|
@ -325,12 +353,11 @@ class KeycloakOpenID: |
|
|
|
|
|
|
|
payload = self._add_secret_key(payload) |
|
|
|
|
|
|
|
data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), |
|
|
|
data=payload) |
|
|
|
data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload) |
|
|
|
|
|
|
|
return raise_error_from_response(data_raw, KeycloakGetError) |
|
|
|
|
|
|
|
def decode_token(self, token, key, algorithms=['RS256'], **kwargs): |
|
|
|
def decode_token(self, token, key, algorithms=["RS256"], **kwargs): |
|
|
|
""" |
|
|
|
A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data |
|
|
|
structure that represents a cryptographic key. This specification |
|
|
@ -347,8 +374,7 @@ class KeycloakOpenID: |
|
|
|
:return: |
|
|
|
""" |
|
|
|
|
|
|
|
return jwt.decode(token, key, algorithms=algorithms, |
|
|
|
audience=self.client_id, **kwargs) |
|
|
|
return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) |
|
|
|
|
|
|
|
def load_authorization_config(self, path): |
|
|
|
""" |
|
|
@ -357,12 +383,12 @@ class KeycloakOpenID: |
|
|
|
:param path: settings file (json) |
|
|
|
:return: |
|
|
|
""" |
|
|
|
authorization_file = open(path, 'r') |
|
|
|
authorization_file = open(path, "r") |
|
|
|
authorization_json = json.loads(authorization_file.read()) |
|
|
|
self.authorization.load_config(authorization_json) |
|
|
|
authorization_file.close() |
|
|
|
|
|
|
|
def get_policies(self, token, method_token_info='introspect', **kwargs): |
|
|
|
def get_policies(self, token, method_token_info="introspect", **kwargs): |
|
|
|
""" |
|
|
|
Get policies by user token |
|
|
|
|
|
|
@ -377,12 +403,10 @@ class KeycloakOpenID: |
|
|
|
|
|
|
|
token_info = self._token_info(token, method_token_info, **kwargs) |
|
|
|
|
|
|
|
if method_token_info == 'introspect' and not token_info['active']: |
|
|
|
raise KeycloakInvalidTokenError( |
|
|
|
"Token expired or invalid." |
|
|
|
) |
|
|
|
if method_token_info == "introspect" and not token_info["active"]: |
|
|
|
raise KeycloakInvalidTokenError("Token expired or invalid.") |
|
|
|
|
|
|
|
user_resources = token_info['resource_access'].get(self.client_id) |
|
|
|
user_resources = token_info["resource_access"].get(self.client_id) |
|
|
|
|
|
|
|
if not user_resources: |
|
|
|
return None |
|
|
@ -390,13 +414,13 @@ class KeycloakOpenID: |
|
|
|
policies = [] |
|
|
|
|
|
|
|
for policy_name, policy in self.authorization.policies.items(): |
|
|
|
for role in user_resources['roles']: |
|
|
|
for role in user_resources["roles"]: |
|
|
|
if self._build_name_role(role) in policy.roles: |
|
|
|
policies.append(policy) |
|
|
|
|
|
|
|
return list(set(policies)) |
|
|
|
|
|
|
|
def get_permissions(self, token, method_token_info='introspect', **kwargs): |
|
|
|
def get_permissions(self, token, method_token_info="introspect", **kwargs): |
|
|
|
""" |
|
|
|
Get permission by user token |
|
|
|
|
|
|
@ -413,12 +437,10 @@ class KeycloakOpenID: |
|
|
|
|
|
|
|
token_info = self._token_info(token, method_token_info, **kwargs) |
|
|
|
|
|
|
|
if method_token_info == 'introspect' and not token_info['active']: |
|
|
|
raise KeycloakInvalidTokenError( |
|
|
|
"Token expired or invalid." |
|
|
|
) |
|
|
|
if method_token_info == "introspect" and not token_info["active"]: |
|
|
|
raise KeycloakInvalidTokenError("Token expired or invalid.") |
|
|
|
|
|
|
|
user_resources = token_info['resource_access'].get(self.client_id) |
|
|
|
user_resources = token_info["resource_access"].get(self.client_id) |
|
|
|
|
|
|
|
if not user_resources: |
|
|
|
return None |
|
|
@ -426,7 +448,7 @@ class KeycloakOpenID: |
|
|
|
permissions = [] |
|
|
|
|
|
|
|
for policy_name, policy in self.authorization.policies.items(): |
|
|
|
for role in user_resources['roles']: |
|
|
|
for role in user_resources["roles"]: |
|
|
|
if self._build_name_role(role) in policy.roles: |
|
|
|
permissions += policy.permissions |
|
|
|
|
|
|
|