def wrapper(*f_args, **f_kwargs): if not hasattr(flask.current_app, "arborist"): raise Forbidden( "this fence instance is not configured with arborist;" " this endpoint is unavailable") if not flask.current_app.arborist.auth_request( jwt=get_jwt_header(), service="fence", methods=method, resources=resource, ): raise Forbidden( "user does not have privileges to access this endpoint") return f(*f_args, **f_kwargs)
def wrapper(*f_args, **f_kwargs): if not hasattr(flask.current_app, "arborist"): raise Forbidden( "this fence instance is not configured for role-based access" " control; this endpoint is unavailable") jwt = get_jwt_header() data = { "user": { "token": jwt }, "request": { "resource": resource, "action": { "service": "fence", "method": method }, }, } if not flask.current_app.arborist.auth_request(data=data): raise Forbidden( "user does not have privileges to access this endpoint") return f(*f_args, **f_kwargs)
def get_user_info(current_session, username): user = get_user(current_session, username) if user.is_admin: role = "admin" else: role = "user" groups = udm.get_user_groups(current_session, username)["groups"] info = { "user_id": user.id, # TODO deprecated, use 'sub' "sub": str(user.id), # getattr b/c the identity_provider sqlalchemy relationship could not exists (be None) "idp": getattr(user.identity_provider, "name", ""), "username": user.username, # TODO deprecated, use 'name' "name": user.username, "display_name": user.display_name, # TODO deprecated, use 'preferred_username' "preferred_username": user.display_name, "phone_number": user.phone_number, "email": user.email, "is_admin": user.is_admin, "role": role, "project_access": dict(user.project_access), "certificates_uploaded": [], "resources_granted": [], "groups": groups, "message": "", } if "fence_idp" in flask.session: info["fence_idp"] = flask.session["fence_idp"] if "shib_idp" in flask.session: info["shib_idp"] = flask.session["shib_idp"] # User SAs are stored in db with client_id = None primary_service_account = get_service_account(client_id=None, user_id=user.id) or {} primary_service_account_email = getattr(primary_service_account, "email", None) info["primary_google_service_account"] = primary_service_account_email if hasattr(flask.current_app, "arborist"): try: resources = flask.current_app.arborist.list_resources_for_user( user.username) auth_mapping = flask.current_app.arborist.auth_mapping( user.username) except ArboristError: logger.error( "request to arborist for user's resources failed; going to list empty" ) resources = [] auth_mapping = {} info["resources"] = resources info["authz"] = auth_mapping if user.tags is not None and len(user.tags) > 0: info["tags"] = {tag.key: tag.value for tag in user.tags} if user.application: info["resources_granted"] = user.application.resources_granted info["certificates_uploaded"] = [ c.name for c in user.application.certificates_uploaded ] info["message"] = user.application.message if flask.request.get_json(force=True, silent=True): requested_userinfo_claims = (flask.request.get_json(force=True).get( "claims", {}).get("userinfo", {})) optional_info = _get_optional_userinfo(user, requested_userinfo_claims) info.update(optional_info) # Include ga4gh passport visas if access token has ga4gh_passport_v1 in scope claim try: encoded_access_token = flask.g.access_token or get_jwt_header() except Unauthorized: # This only happens if a session token was present (since login_required did not throw an error) # but for some reason there was no access token in flask.g.access_token. # (Perhaps it was manually deleted by the user.) # In particular, a curl request made with no tokens shouldn't get here (bc of login_required). # So the request is probably from a browser. logger.warning( "Session token present but no access token found. " "Unable to check scopes in userinfo; some claims may not be included in response." ) encoded_access_token = None if encoded_access_token: at_scopes = jwt.decode(encoded_access_token, verify=False).get("scope", "") if "ga4gh_passport_v1" in at_scopes: info["ga4gh_passport_v1"] = [] return info
def validate_jwt(encoded_token=None, aud=None, scope={"openid"}, require_purpose=True, purpose=None, public_key=None, attempt_refresh=False, issuers=None, pkey_cache=None, **kwargs): """ Validate a JWT and return the claims. This wraps the authutils functions to work correctly for fence and correctly validate the token. Other functions in fence should call this function and not use any functions from authutils. Args: encoded_token (str): the base64 encoding of the token aud (Optional[str]): audience as which the app identifies, which the JWT will be expected to include in its ``aud`` claim. Optional; will default to issuer (config["BASE_URL"]). To skip aud validation, pass the following as a kwarg: options={"verify_aud": False} scope (Optional[Iterable[str]]): list of scopes each of which the token must satisfy; defaults to ``{'openid'}`` (minimum expected by OpenID provider). Explicitly set this to None to skip scope validation. purpose (Optional[str]): which purpose the token is supposed to be used for (access, refresh, or id) public_key (Optional[str]): public key to vaidate JWT with Return: dict: dictionary of claims from the validated JWT Raises: JWTError: if auth header is missing, decoding fails, or the JWT fails to satisfy any expectation """ if encoded_token is None: try: encoded_token = get_jwt_header() except Unauthorized as e: raise JWTError(e.message) assert (isinstance(scope, set) or isinstance(scope, list) or scope is None), "scope argument must be set or list or None" # Can't set arg default to config[x] in fn def, so doing it this way. if aud is None: aud = config["BASE_URL"] iss = config["BASE_URL"] if issuers is None: issuers = [iss] oidc_iss = (config.get("OPENID_CONNECT", {}).get("fence", {}).get("api_base_url", None)) if oidc_iss: issuers.append(oidc_iss) try: token_iss = jwt.decode(encoded_token, verify=False).get("iss") except jwt.InvalidTokenError as e: raise JWTError(e) attempt_refresh = attempt_refresh and (token_iss != iss) public_key = public_key or authutils.token.keys.get_public_key_for_token( encoded_token, attempt_refresh=attempt_refresh, pkey_cache=pkey_cache) try: claims = authutils.token.validate.validate_jwt( encoded_token=encoded_token, aud=aud, scope=scope, purpose=purpose, issuers=issuers, public_key=public_key, attempt_refresh=attempt_refresh, **kwargs) except authutils.errors.JWTError as e: ##### begin refresh token and API key patch block ##### # TODO: In the next release, remove this if/elif block and take the else block # back out of the else. # Old refresh tokens and API keys are not compatible with new validation, so to smooth # the transition, allow old style refresh tokens/API keys with this patch; # remove patch in next tag. Refresh tokens and API keys have default TTL of 30 days. from authutils.errors import JWTAudienceError unverified_claims = jwt.decode(encoded_token, verify=False) if unverified_claims.get("pur") == "refresh" and isinstance( e, JWTAudienceError): # Check everything else is fine minus the audience try: claims = authutils.token.validate.validate_jwt( encoded_token=encoded_token, aud="openid", scope=None, purpose="refresh", issuers=issuers, public_key=public_key, attempt_refresh=attempt_refresh, **kwargs) except Error as e: raise JWTError("Invalid refresh token: {}".format(e)) elif unverified_claims.get("pur") == "api_key" and isinstance( e, JWTAudienceError): # Check everything else is fine minus the audience try: claims = authutils.token.validate.validate_jwt( encoded_token=encoded_token, aud="fence", scope=None, purpose="api_key", issuers=issuers, public_key=public_key, attempt_refresh=attempt_refresh, **kwargs) except Error as e: raise JWTError("Invalid API key: {}".format(e)) else: ##### end refresh token, API key patch block ##### msg = "Invalid token : {}".format(str(e)) unverified_claims = jwt.decode(encoded_token, verify=False) if not unverified_claims.get( "scope") or "" in unverified_claims["scope"]: msg += "; was OIDC client configured with scopes?" raise JWTError(msg) if purpose: validate_purpose(claims, purpose) if require_purpose and "pur" not in claims: raise JWTError("token {} missing purpose (`pur`) claim".format( claims["jti"])) # For refresh tokens and API keys specifically, check that they are not # blacklisted. if require_purpose and (claims["pur"] == "refresh" or claims["pur"] == "api_key"): if is_blacklisted(claims["jti"]): raise JWTError("token is blacklisted") return claims
def validate_jwt( encoded_token=None, aud=None, purpose=None, public_key=None, attempt_refresh=False, **kwargs ): """ Validate a JWT and return the claims. This wraps the authutils functions to work correctly for fence and correctly validate the token. Other functions in fence should call this function and not use any functions from authutils. Args: encoded_token (str): the base64 encoding of the token aud (Optional[Iterable[str]]): list of audiences that the token must satisfy; defaults to ``{'openid'}`` (minimum expected by OpenID provider) purpose (Optional[str]): which purpose the token is supposed to be used for (access, refresh, or id) public_key (Optional[str]): public key to vaidate JWT with Return: dict: dictionary of claims from the validated JWT Raises: JWTError: if auth header is missing, decoding fails, or the JWT fails to satisfy any expectation """ if encoded_token is None: try: encoded_token = get_jwt_header() except Unauthorized as e: raise JWTError(e.message) aud = aud or {"openid"} aud = set(aud) iss = config["BASE_URL"] issuers = [iss] oidc_iss = ( config.get("OPENID_CONNECT", {}).get("fence", {}).get("api_base_url", None) ) if oidc_iss: issuers.append(oidc_iss) try: token_iss = jwt.decode(encoded_token, verify=False).get("iss") except jwt.InvalidTokenError as e: raise JWTError(e.message) attempt_refresh = attempt_refresh and (token_iss != iss) public_key = authutils.token.keys.get_public_key_for_token( encoded_token, attempt_refresh=attempt_refresh ) try: claims = authutils.token.validate.validate_jwt( encoded_token=encoded_token, aud=aud, purpose=purpose, issuers=issuers, public_key=public_key, attempt_refresh=attempt_refresh, **kwargs ) except authutils.errors.JWTError as e: msg = "Invalid token : {}".format(str(e)) unverified_claims = jwt.decode(encoded_token, verify=False) if "" in unverified_claims["aud"]: msg += "; was OIDC client configured with scopes?" raise JWTError(msg) if purpose: validate_purpose(claims, purpose) if "pur" not in claims: raise JWTError("token {} missing purpose (`pur`) claim".format(claims["jti"])) # For refresh tokens and API keys specifically, check that they are not # blacklisted. if claims["pur"] == "refresh" or claims["pur"] == "api_key": if is_blacklisted(claims["jti"]): raise JWTError("token is blacklisted") return claims