def get(self): """Handle ``GET /login/fence/login``.""" # Check that the state passed back from IDP fence is the same as the # one stored previously. mismatched_state = ( "state" not in flask.request.args or "state" not in flask.session or flask.request.args["state"] != flask.session.pop("state", "")) if mismatched_state and not config.get("MOCK_AUTH"): raise Unauthorized( "Login flow was interrupted (state mismatch). Please go back to the" " login page for the original application to continue.") # Get the token response and log in the user. redirect_uri = flask.current_app.fence_client._get_session( ).redirect_uri tokens = flask.current_app.fence_client.fetch_access_token( redirect_uri, **flask.request.args.to_dict()) try: # For multi-Fence setup with two Fences >=5.0.0 id_token_claims = validate_jwt( tokens["id_token"], aud=self.client.client_id, scope={"openid"}, purpose="id", attempt_refresh=True, ) except JWTError: # Since fenceshib cannot be updated to issue "new-style" ID tokens # (where scopes are in the scope claim and aud is in the aud claim), # allow also "old-style" Fence ID tokens. id_token_claims = validate_jwt( tokens["id_token"], aud="openid", scope=None, purpose="id", attempt_refresh=True, ) username = id_token_claims["context"]["user"]["name"] email = id_token_claims["context"]["user"].get("email") login_user( username, IdentityProvider.fence, fence_idp=flask.session.get("fence_idp"), shib_idp=flask.session.get("shib_idp"), email=email, ) self.post_login() if config["REGISTER_USERS_ON"]: if not flask.g.user.additional_info.get("registration_info"): return flask.redirect(config["BASE_URL"] + flask.url_for("register.register_user")) if "redirect" in flask.session: return flask.redirect(flask.session.get("redirect")) return flask.jsonify({"username": username})
def create_user_access_token(keypair, api_key, expires_in): """ create access token given a user's api key Args: keypair: RSA keypair for signing jwt api_key: user created jwt token, the azp should match with user.id expires_in: expiration time in seconds Return: access token """ try: claims = validate_jwt(api_key, scope={"fence"}, purpose="api_key") # scopes = claims["scope"] ##### begin api key patch block ##### # TODO: In the next release, remove this block and uncomment line above. # Old API keys are not compatible with new validation # This is to help transition try: scopes = claims["scope"] except KeyError as e: scopes = claims["aud"] ##### end api key patch block ##### user = get_user_from_claims(claims) except Exception as e: raise Unauthorized(str(e)) return token.generate_signed_access_token( keypair.kid, keypair.private_key, user, expires_in, scopes ).token
def test_aud(client, oauth_client, id_token): """ Test that the audiences of the ID token contain the OAuth client id. """ id_claims = validate_jwt(id_token, {'openid'}) assert 'aud' in id_claims assert oauth_client.client_id in id_claims['aud']
def _get_valid_access_token(app, session, request): """ Return a valid access token. If at any point access token is determined invalid, this will return None. """ access_token = request.cookies.get(config["ACCESS_TOKEN_COOKIE_NAME"], None) if not access_token: return None try: valid_access_token = validate_jwt(access_token, purpose="access") except Exception as exc: return None # try to get user, exception means they're not logged in try: user = get_current_user(flask_session=session) except Unauthorized: return None # check that the current user is the one from the session and access_token user_sess_id = _get_user_id_from_session(session) token_user_id = _get_user_id_from_access_token(valid_access_token) if user.id != user_sess_id and user.username != user_sess_id: return None if user.id != token_user_id and user.username != token_user_id: # only invalid if the token id isn't the user's id OR username # since the username is also unique return None return access_token
def get(self): """Handle ``GET /login/fence/login``.""" # Check that the state passed back from IDP fence is the same as the # one stored previously. mismatched_state = ( "state" not in flask.request.args or "state" not in flask.session or flask.request.args["state"] != flask.session.pop("state", "")) if mismatched_state and not config.get("MOCK_AUTH"): raise Unauthorized( "Login flow was interrupted (state mismatch). Please go back to the" " login page for the original application to continue.") # Get the token response and log in the user. redirect_uri = flask.current_app.fence_client._get_session( ).redirect_uri tokens = flask.current_app.fence_client.fetch_access_token( redirect_uri, **flask.request.args.to_dict()) id_token_claims = validate_jwt(tokens["id_token"], aud={"openid"}, purpose="id", attempt_refresh=True) username = id_token_claims["context"]["user"]["name"] login_user( username, IdentityProvider.fence, fence_idp=flask.session.get("fence_idp"), shib_idp=flask.session.get("shib_idp"), ) self.post_login() if "redirect" in flask.session: return flask.redirect(flask.session.get("redirect")) return flask.jsonify({"username": username})
def test_id_token_hint(client, oauth_client): """ Test ``id_token_hint`` parameter when hinted user is logged in """ token_response = oauth2.get_token_response(client, oauth_client).json id_token = validate_jwt(token_response["id_token"], {"openid"}) # Now use that id_token as a hint to the authorize endpoint data = {"id_token_hint": str(id_token)} new_token_response = oauth2.get_token_response(client, oauth_client, code_request_data=data) new_id_token = validate_jwt(token_response["id_token"], {"openid"}) assert new_token_response.status_code == 200 assert new_id_token["sub"] == id_token["sub"]
def test_id_token_has_nonce(oauth_test_client): nonce = random_str(10) data = {"confirm": "yes", "nonce": nonce} oauth_test_client.authorize(data=data) response_json = oauth_test_client.token(data=data).response.json id_token = validate_jwt(response_json["id_token"]) assert "nonce" in id_token assert nonce == id_token["nonce"]
def test_same_claims(oauth_test_client, token_response_json): original_id_token = token_response_json["id_token"] original_claims = validate_jwt(original_id_token, {"openid"}) refresh_token = token_response_json["refresh_token"] refresh_token_response = oauth_test_client.refresh( refresh_token=refresh_token).response assert "id_token" in refresh_token_response.json new_claims = validate_jwt(refresh_token_response.json["id_token"], {"openid"}) assert original_claims["iss"] == new_claims["iss"] assert original_claims["sub"] == new_claims["sub"] assert original_claims["iat"] <= new_claims["iat"] assert original_claims["aud"] == new_claims["aud"] if "azp" in original_claims: assert original_claims["azp"] == new_claims["azp"] else: assert "azp" not in new_claims
def test_valid_session_valid_access_token_diff_user(app, test_user_a, test_user_b, db_session, monkeypatch): """ Test the case where a valid access token is in a cookie, but it's for a different user than the one logged in. Make sure that a new access token is created for the logged in user and the response doesn't contain info for the non-logged in user. """ monkeypatch.setitem(config, "MOCK_AUTH", False) user = db_session.query(User).filter_by(id=test_user_a["user_id"]).first() keypair = app.keypairs[0] test_session_jwt = create_session_token( keypair, config.get("SESSION_TIMEOUT"), context={ "username": user.username, "provider": "google" }, ) # different user's access token other_user = db_session.query(User).filter_by( id=test_user_b["user_id"]).first() test_access_jwt = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=other_user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], scopes=["openid", "user"], iss=config.get("BASE_URL"), ).token with app.test_client() as client: # manually set cookie for initial session client.set_cookie("localhost", config["SESSION_COOKIE_NAME"], test_session_jwt) client.set_cookie("localhost", config["ACCESS_TOKEN_COOKIE_NAME"], test_access_jwt) response = client.get("/user") cookies = _get_cookies_from_response(response) # either there's a new access_token in the response headers or the # previously set access token been changed access_token = (cookies.get("access_token", {}).get("access_token") or test_access_jwt) valid_access_token = validate_jwt(access_token, purpose="access") assert response.status_code == 200 response_user_id = response.json.get("user_id") or response.json.get( "sub") assert response_user_id == test_user_a["user_id"] user_id = valid_access_token.get("user_id") or valid_access_token.get( "sub") assert test_user_a["user_id"] == int(user_id)
def create_access_token(user, keypair, api_key, expires_in, scopes): try: claims = validate_jwt(api_key, aud=scopes, purpose='api_key') if not set(claims['aud']).issuperset(scopes): raise JWTError( 'cannot issue access token with scope beyond refresh token') except Exception as e: return flask.jsonify({'errors': e.message}) return token.generate_signed_access_token(keypair.kid, keypair.private_key, user, expires_in, scopes)
def test_same_claims(client, oauth_client, token_response_json): original_id_token = token_response_json['id_token'] original_claims = validate_jwt(original_id_token, {'openid'}) refresh_token = token_response_json['refresh_token'] refresh_token_response = oauth2.post_token_refresh( client, oauth_client, refresh_token ) assert 'id_token' in refresh_token_response.json new_claims = validate_jwt( refresh_token_response.json['id_token'], {'openid'} ) assert original_claims['iss'] == new_claims['iss'] assert original_claims['sub'] == new_claims['sub'] assert original_claims['iat'] <= new_claims['iat'] assert original_claims['aud'] == new_claims['aud'] if 'azp' in original_claims: assert original_claims['azp'] == new_claims['azp'] else: assert 'azp' not in new_claims
def test_id_token_contains_auth_time(oauth_test_client): """ Test that if ``max_age`` is included in the authentication request, then the ID token returned contains an ``auth_time`` claim. """ data = {"confirm": "yes", "max_age": 3600} oauth_test_client.authorize(data=data) id_token = oauth_test_client.token().id_token id_token_claims = validate_jwt(id_token, {"openid"}) assert "auth_time" in id_token_claims
def test_same_claims(oauth_test_client, token_response_json): original_id_token = token_response_json["id_token"] original_claims = validate_jwt(original_id_token) refresh_token = token_response_json["refresh_token"] refresh_token_response = oauth_test_client.refresh( refresh_token=refresh_token).response assert "id_token" in refresh_token_response.json new_claims = validate_jwt(refresh_token_response.json["id_token"]) assert original_claims["iss"] == new_claims["iss"] assert original_claims["sub"] == new_claims["sub"] assert original_claims["iat"] <= new_claims["iat"] assert original_claims["aud"] == new_claims["aud"] if "azp" in original_claims: assert original_claims["azp"] == new_claims["azp"] else: assert "azp" not in new_claims # Also test that custom (non-OIDC) scope claim is unchanged assert original_claims["scope"] == new_claims["scope"]
def create_access_token(user, keypair, api_key, expires_in, scopes): try: claims = validate_jwt(api_key, aud=scopes, purpose="api_key") if not set(claims["aud"]).issuperset(scopes): raise JWTError( "cannot issue access token with scope beyond refresh token") except Exception as e: return flask.jsonify({"errors": str(e)}) return token.generate_signed_access_token(keypair.kid, keypair.private_key, user, expires_in, scopes).token
def test_id_token_contains_auth_time(client, oauth_client): """ Test that if ``max_age`` is included in the authentication request, then the ID token returned contains an ``auth_time`` claim. """ data = {'max_age': 3600} token_response = oauth2.get_token_response(client, oauth_client, code_request_data=data).json id_token = validate_jwt(token_response['id_token'], {'openid'}) assert 'auth_time' in id_token
def test_acr_values(client, oauth_client): """ Test the very basic requirement that including the ``acr_values`` parameter does not cause any errors and the acr claim is represented in the resulting token. """ data = {'acr_values': ''} token_response = oauth2.get_token_response(client, oauth_client, code_request_data=data).json id_token = validate_jwt(token_response['id_token'], {'openid'}) assert 'acr' in id_token
def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): """ Return encoded visas after extracting and validating encoded passport Args: passport (string): encoded ga4gh passport pkey_cache (dict): app cache of public keys_dir Return: list: list of encoded GA4GH visas """ decoded_passport = {} passport_issuer, passport_kid = None, None if not pkey_cache: pkey_cache = {} try: passport_issuer = get_iss(passport) passport_kid = get_kid(passport) except Exception as e: logger.error( "Could not get issuer or kid from passport: {}. Discarding passport." .format(e)) # ignore malformed/invalid passports return [] public_key = pkey_cache.get(passport_issuer, {}).get(passport_kid) try: decoded_passport = validate_jwt( encoded_token=passport, public_key=public_key, attempt_refresh=True, require_purpose=False, scope={"openid"}, issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), options={ "require_iat": True, "require_exp": True, "verify_aud": False, }, ) if "sub" not in decoded_passport: raise JWTError(f"Passport is missing the 'sub' claim") except Exception as e: logger.error( "Passport failed validation: {}. Discarding passport.".format(e)) # ignore malformed/invalid passports return [] return decoded_passport.get("ga4gh_passport_v1", [])
def authenticate_refresh_token(self, refresh_token): """ Validate a refresh token. Required to implement this method for authlib. Args: refresh_token (str): refresh token as from a request Return: dict: the claims from the validated token """ return validate_jwt(refresh_token, purpose="refresh")
def _get_initial_session_token(self): keypair = current_app.keypairs[0] session_token = generate_signed_session_token( kid=keypair.kid, private_key=keypair.private_key, expires_in=current_app.config.get('SESSION_TIMEOUT').seconds, ) self._encoded_token = session_token initial_token = validate_jwt( session_token, aud={'fence'}, purpose='session', public_key=default_public_key(), ) return initial_token
def _get_initial_session_token(self): keypair = flask.current_app.keypairs[0] session_token = generate_signed_session_token( kid=keypair.kid, private_key=keypair.private_key, expires_in=config.get("SESSION_TIMEOUT"), ).token self._encoded_token = session_token initial_token = validate_jwt( session_token, aud={"fence"}, purpose="session", public_key=default_public_key(), ) return initial_token
def has_oauth(scope=None): scope = scope or set() scope.update({"openid"}) try: access_token_claims = validate_jwt(aud=scope, purpose="access") except JWTError as e: raise Unauthorized("failed to validate token: {}".format(e)) user_id = access_token_claims["sub"] user = current_session.query(User).filter_by(id=int(user_id)).first() if not user: raise Unauthorized("no user found with id: {}".format(user_id)) # set some application context for current user and client id flask.g.user = user # client_id should be None if the field doesn't exist or is empty flask.g.client_id = access_token_claims.get("azp") or None flask.g.token = access_token_claims
def __init__(self, session_token): self._encoded_token = session_token if session_token: try: jwt_info = validate_jwt(session_token, aud={"fence"}) except JWTError: # if session token is invalid, create a new # empty one silently jwt_info = self._get_initial_session_token() else: jwt_info = self._get_initial_session_token() self.session_token = jwt_info self.modified = False super(UserSession, self).__init__()
def create_user_access_token(keypair, api_key, expires_in): """ create access token given a user's api key Args: keypair: RSA keypair for signing jwt api_key: user created jwt token, the azp should match with user.id expires_in: expiration time in seconds Return: access token """ try: claims = validate_jwt(api_key, aud={"fence"}, purpose="api_key") scopes = claims["aud"] user = get_user_from_claims(claims) except Exception as e: raise Unauthorized(str(e)) return token.generate_signed_access_token(keypair.kid, keypair.private_key, user, expires_in, scopes).token
def test_id_token_has_nonce(client, oauth_client): nonce = random_str(10) data = { 'client_id': oauth_client.client_id, 'redirect_uri': oauth_client.url, 'response_type': 'code', 'scope': 'openid user', 'state': random_str(10), 'confirm': 'yes', 'nonce': nonce, } response_json = ( oauth2.get_token_response(client, oauth_client, code_request_data=data) .json ) id_token = validate_jwt(response_json['id_token'], {'openid'}) assert 'nonce' in id_token assert nonce == id_token['nonce']
def authenticate_refresh_token(self, refresh_token): """ Validate a refresh token. Required to implement this method for authlib. Args: refresh_token (str): refresh token as from a request Return: dict: the claims from the validated token """ try: if is_token_blacklisted(refresh_token): return except JWTError: return return validate_jwt(refresh_token, purpose='refresh')
def test_access_token_correct_fields(token_response): """ Test that the access token from the token response contains exactly the expected fields. """ encoded_access_token = token_response.json['access_token'] access_token = validate_jwt(encoded_access_token, {'openid'}) access_token_fields = set(access_token.keys()) expected_fields = { 'pur', 'iss', 'sub', 'aud', 'exp', 'iat', 'jti', 'context', } assert access_token_fields == expected_fields
def test_create_refresh_token_with_found_user( app, db_session, oauth_test_client, kid, rsa_private_key ): DB = config["DB"] username = "******" BASE_URL = config["BASE_URL"] scopes = "openid,user" expires_in = 3600 user = User(username=username) db_session.add(user) user = db_session.query(User).filter_by(username=username).first() jwt_result = JWTCreator( DB, BASE_URL, kid=kid, username=username, scopes=scopes, expires_in=expires_in, private_key=rsa_private_key, ).create_refresh_token() refresh_token_response = oauth_test_client.refresh( refresh_token=jwt_result.token ).response ret_claims = validate_jwt( refresh_token_response.json["id_token"], scope={"openid"}, ) assert jwt_result.claims["iss"] == ret_claims["iss"] assert jwt_result.claims["sub"] == ret_claims["sub"] assert jwt_result.claims["iat"] <= ret_claims["iat"] db_token = ( db_session.query(UserRefreshToken) .filter_by(jti=jwt_result.claims["jti"]) .first() ) assert db_token is not None
def test_access_token_correct_fields(token_response): """ Test that the access token from the token response contains exactly the expected fields. """ encoded_access_token = token_response.json["access_token"] access_token = validate_jwt(encoded_access_token, {"openid"}) access_token_fields = set(access_token.keys()) expected_fields = { "pur", "iss", "sub", "aud", "exp", "iat", "jti", "context", "azp", } assert access_token_fields == expected_fields
def test_id_token_hint_not_logged_in(app, client, oauth_client, monkeypatch): """ Test ``id_token_hint`` parameter when hinted user is not logged in. TODO: This should attempt to log the user in """ # test user is logged in right now token_response = oauth2.get_token_response(client, oauth_client).json id_token = validate_jwt(token_response["id_token"], {"openid"}) # don't mock auth so there isn't a logged in user any more monkeypatch.setitem(config, "MOCK_AUTH", False) # Now use that id_token as a hint to the authorize endpoint data = {"id_token_hint": str(id_token)} auth_response = oauth2.post_authorize(client, oauth_client, data=data, confirm=True) assert auth_response.status_code == 302 assert "Location" in auth_response.headers query_params = parse_qs(urlparse(auth_response.headers["Location"]).query) assert "error" in query_params assert query_params["error"][0] == "access_denied"
def test_id_token_required_fields(token_response): """ Test that the ID token returned in the token response is a valid JWT, and that it contains all of fields required by OIDC. """ assert "id_token" in token_response.json # Check that the ID token is a valid JWT. id_token = validate_jwt(token_response.json["id_token"], scope={"openid"}) # Check for required fields. assert "pur" in id_token and id_token["pur"] == "id" assert "iss" in id_token assert "sub" in id_token assert "aud" in id_token assert "exp" in id_token assert "iat" in id_token # Check for types on required fields. assert type(id_token["exp"]) is int assert type(id_token["iat"]) is int assert type(id_token["sub"]) is str assert type(id_token["iss"]) is str assert type(id_token["aud"]) is list