Skip to content

Commit

Permalink
query token introspection endpoint if userinfo is not available, to b…
Browse files Browse the repository at this point in the history
…e compatible with client_credentials type clients
  • Loading branch information
indy-independence committed Feb 22, 2024
1 parent f4a82c8 commit 6745c6d
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 27 deletions.
13 changes: 10 additions & 3 deletions src/cnaas_nms/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from cnaas_nms.api.system import api as system_api
from cnaas_nms.app_settings import api_settings, auth_settings
from cnaas_nms.tools.log import get_logger
from cnaas_nms.tools.security import get_oauth_userinfo, oauth_required
from cnaas_nms.tools.security import get_oauth_token_info, oauth_required
from cnaas_nms.version import __api_version__

logger = get_logger()
Expand Down Expand Up @@ -191,7 +191,7 @@ def socketio_on_connect():
if auth_settings.OIDC_ENABLED:
try:
token = oauth_required.get_token_validator("bearer").authenticate_token(token_string)
user = get_oauth_userinfo(token)["email"]
user = get_oauth_token_info(token)["email"]
except InvalidTokenError as e:
logger.debug("InvalidTokenError: " + format(e))
return False
Expand Down Expand Up @@ -239,7 +239,14 @@ def log_request(response):
if auth_settings.OIDC_ENABLED:
token_string = request.headers.get("Authorization").split(" ")[-1]
token = oauth_required.get_token_validator("bearer").authenticate_token(token_string)
user = "User: {}, ".format(get_oauth_userinfo(token)["email"])
token_info = get_oauth_token_info(token)
if "email" in token_info:
user = "User: {} (email), ".format(get_oauth_token_info(token)["email"])
elif "client_id" in token_info:
user = "User: {} (client_id), ".format(get_oauth_token_info(token)["client_id"])
else:
logger.warning("Could not get user info from token")
raise ValueError
else:
token_string = request.headers.get("Authorization").split(" ")[-1]
user = "User: {}, ".format(decode_token(token_string).get("sub"))
Expand Down
4 changes: 2 additions & 2 deletions src/cnaas_nms/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from cnaas_nms.app_settings import auth_settings
from cnaas_nms.tools.log import get_logger
from cnaas_nms.tools.rbac.rbac import get_permissions_user
from cnaas_nms.tools.security import get_identity, get_oauth_userinfo, login_required, login_required_all_permitted
from cnaas_nms.tools.security import get_identity, get_oauth_token_info, login_required, login_required_all_permitted
from cnaas_nms.version import __api_version__

logger = get_logger()
Expand Down Expand Up @@ -155,7 +155,7 @@ def get(self):
if not permissions_rules:
logger.debug("No permissions defined, so nobody is permitted to do any api calls.")
return []
user_info = get_oauth_userinfo(current_token)
user_info = get_oauth_token_info(current_token)
permissions_of_user = get_permissions_user(permissions_rules, user_info)
return permissions_of_user

Expand Down
8 changes: 7 additions & 1 deletion src/cnaas_nms/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,13 @@ def thread_websocket_events():
t_websocket_events = threading.Thread(target=thread_websocket_events)
t_websocket_events.start()

app.socketio.run(get_app(), debug=True, host=api_settings.HOST)
app.socketio.run(
get_app(),
debug=True,
host=api_settings.HOST,
certfile="/home/johanmarcusson/python/cnaas-nms/ssl/cert.pem",
keyfile="/home/johanmarcusson/python/cnaas-nms/ssl/key.pem",
)
stop_websocket_threads = True
t_websocket_events.join()

Expand Down
63 changes: 42 additions & 21 deletions src/cnaas_nms/tools/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from jose import exceptions, jwt
from jwt.exceptions import ExpiredSignatureError, InvalidAudienceError, InvalidKeyError, InvalidTokenError
from redis.exceptions import RedisError
from requests.auth import HTTPBasicAuth

from cnaas_nms.app_settings import api_settings, auth_settings
from cnaas_nms.db.session import redis_session
Expand Down Expand Up @@ -52,15 +53,16 @@ def get_jwt_identity():
return get_jwt_identity_orig() if api_settings.JWT_ENABLED else "admin"


def get_oauth_userinfo(token: Token) -> Any:
"""Give back the user info of the OAUTH account
def get_oauth_token_info(token: Token) -> Any:
"""Give back the details about the token from userinfo or introspection
If OIDC is disabled, we return None.
We do an api call to request userinfo. This gives back all the userinfo.
For authorization code access_tokens we can use userinfo endpoint,
for client_credentials we can use introspection endpoint.
Returns:
resp.json(): Object of the user info
resp.json(): Object of the user info or introspection
"""
# For now unnecessary, useful when we only use one log in method
Expand All @@ -87,15 +89,31 @@ def get_oauth_userinfo(token: Token) -> Any:
except requests.exceptions.ConnectionError:
raise ConnectionError("OIDC metadata unavailable")
user_info_endpoint = metadata.json()["userinfo_endpoint"]
data = {"token_type_hint": "access_token"}
headers = {"Authorization": "Bearer " + token.token_string}
introspection_endpoint = metadata.json()["introspection_endpoint"]
userinfo_data = {"token_type_hint": "access_token"}
userinfo_headers = {"Authorization": "Bearer " + token.token_string}
introspect_data = {"token": token.token_string}
introspect_auth = HTTPBasicAuth(auth_settings.OIDC_CLIENT_ID, auth_settings.OIDC_CLIENT_SECRET)
token_info: str = ""

try:
resp = s.post(user_info_endpoint, data=data, headers=headers)
resp.raise_for_status()
resp.json()
userinfo_resp = s.post(user_info_endpoint, data=userinfo_data, headers=userinfo_headers)
if userinfo_resp.status_code in [401, 403, 404]:
introspect_resp = s.post(introspection_endpoint, data=introspect_data, auth=introspect_auth)
introspect_resp.raise_for_status()
introspect_json = introspect_resp.json()
if "active" in introspect_json and introspect_json["active"]:
token_info = introspect_resp.text
else:
raise InvalidTokenError("Token is no longer active")
else:
userinfo_resp.raise_for_status()
userinfo_resp.json()
token_info = userinfo_resp.text

with redis_session() as redis:
if "exp" in token.decoded_token:
redis.hsetnx(REDIS_OAUTH_USERINFO_KEY, token.decoded_token["sub"], resp.text)
redis.hsetnx(REDIS_OAUTH_USERINFO_KEY, token.decoded_token["sub"], token_info)
# expire hash at access_token expiry time or 1 hour from now
# (whichever is sooner)
# Entire hash is expired, since redis does not support expiry on individual keys
Expand All @@ -107,15 +125,15 @@ def get_oauth_userinfo(token: Token) -> Any:
logger.debug("OIDC userinfo endpoint request not successful: " + body["error_description"])
raise InvalidTokenError(body["error_description"])
except (json.decoder.JSONDecodeError, KeyError):
logger.debug("OIDC userinfo endpoint request not successful: {}".format(str(e.response.content)))
raise InvalidTokenError(e.response.content)
logger.debug("OIDC userinfo endpoint request not successful: {}".format(str(e)))
raise InvalidTokenError(str(e))
except requests.exceptions.JSONDecodeError as e:
raise InvalidTokenError("Invalid JSON in userinfo response: {}".format(str(e)))
raise InvalidTokenError("Invalid JSON in userinfo/introspection response: {}".format(str(e)))
except RedisError as e:
logger.debug("Redis cache error: {}".format(str(e)))
except (TypeError, KeyError) as e:
logger.debug("Error while getting userinfo cache: {}".format(str(e)))
return resp.json()
return json.loads(token_info)


class MyBearerTokenValidator(BearerTokenValidator):
Expand Down Expand Up @@ -183,7 +201,7 @@ def authenticate_token(self, token_string: str):
except exceptions.JWTError:
# check if we can still get the user info
token = Token(token_string, None)
get_oauth_userinfo(token)
get_oauth_token_info(token)

return token

Expand Down Expand Up @@ -225,7 +243,7 @@ def validate_token(self, token, scopes, request: OAuth2Request):
if not permissions_rules:
logger.debug("No permissions defined, so nobody is permitted to do any api calls.")
raise InvalidAudienceError()
user_info = get_oauth_userinfo(token)
user_info = get_oauth_token_info(token)
permissions = get_permissions_user(permissions_rules, user_info)
if len(permissions) == 0:
raise InvalidAudienceError() # TODO: fix error type?
Expand All @@ -250,11 +268,14 @@ def get_oauth_identity() -> str:
# For now unnecersary, useful when we only use one log in method
if not auth_settings.OIDC_ENABLED:
return "Admin"
userinfo = get_oauth_userinfo(current_token)
if "email" not in userinfo:
logger.error("Email is a required claim for oauth")
raise KeyError("Email is a required claim for oauth")
return userinfo["email"]
token_info = get_oauth_token_info(current_token)
if "email" in token_info:
return token_info["email"]
elif "client_id" in token_info:
return token_info["client_id"]
else:
logger.error("Email or client_id is a required claim for oauth")
raise KeyError("Email or client_id is a required claim for oauth")


# check which method we use to log in and load vars needed for that
Expand Down

0 comments on commit 6745c6d

Please sign in to comment.