def test_oversized_access_token(app, rsa_private_key, test_user_a): """ Test that generate_signed_access_token raises JTWSizeError when the access token is over 4096 bytes. Here, the JWT is made to be large via the kid parameter in generate_signed_access_token. The scopes argument is ["openid", "user"] because there is currently no fixture for scopes in /tests/conftest.py, but default_claims() in /tests/utils/__init__.py sets aud = ["openid", "user"]. """ _, exp = iat_and_exp() with pytest.raises(JWTSizeError): generate_signed_access_token(oversized_junk(), rsa_private_key, test_user_a, exp, ["openid", "user"])
def test_passport_access_token(app, kid, rsa_private_key, test_user_a): """ Test that generate_signed_access_token is a valid GA4GH Passport Access Token as specified: https://github.com/ga4gh/data-security/blob/master/AAI/AAIConnectProfile.md#ga4gh-jwt-format The scopes argument is ["openid", "user", "ga4gh_passport_v1"] because there is currently no fixture for scopes in /tests/conftest.py, but default_claims() in /tests/utils/__init__.py sets aud = ["openid", "user"]. """ _, exp = iat_and_exp() jwt_token = generate_signed_access_token( kid, rsa_private_key, test_user_a, exp, ["openid", "user", "ga4gh_passport_v1"], client_id="client_a", ) payload = jwt.decode(jwt_token.token, verify=False) # assert required fields exist assert payload["iss"] is not None or "" assert payload["sub"] is not None or "" assert payload["iat"] is not None assert payload["exp"] == payload["iat"] + exp assert payload["scope"] == ["openid", "user", "ga4gh_passport_v1"] assert isinstance(payload["aud"], list) # assert client_id in audiences assert "client_a" in payload["aud"]
def _create_access_token_cookie(app, session, response, user): keypair = app.keypairs[0] scopes = config["SESSION_ALLOWED_SCOPES"] now = int(time.time()) expiration = now + config.get("ACCESS_TOKEN_EXPIRES_IN") # try to get from current session, if it's not there, we have to hit db linked_google_email = session.get("linked_google_email") if not linked_google_email: linked_google_email = get_linked_google_account_email(user.id) access_token = generate_signed_access_token( keypair.kid, keypair.private_key, user, config.get("ACCESS_TOKEN_EXPIRES_IN"), scopes, forced_exp_time=expiration, linked_google_email=linked_google_email, ).token domain = app.session_interface.get_cookie_domain(app) response.set_cookie( config["ACCESS_TOKEN_COOKIE_NAME"], access_token, expires=expiration, httponly=True, domain=domain, ) return response
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_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 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_valid_session_valid_access_token(app, db_session, test_user_a, test_user_b, monkeypatch): 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" }, ) test_access_jwt = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], scopes=["openid", "user"], iss=config.get("BASE_URL"), forced_exp_time=None, client_id=None, linked_google_email=None, ).token # Test that once the session is started, we have access to # the username with app.test_client() as client: # manually set cookie for initial session client.set_cookie( "localhost", config["SESSION_COOKIE_NAME"], test_session_jwt, httponly=True, samesite="Lax", ) client.set_cookie( "localhost", config["ACCESS_TOKEN_COOKIE_NAME"], test_access_jwt, httponly=True, samesite="Lax", ) response = client.get("/user") user_id = response.json.get("user_id") or response.json.get("sub") assert response.status_code == 200 assert user_id == user.id
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 encoded_jwt_function(private_key, user): """ Return an example JWT containing the claims and encoded with the private key. Args: private_key (str): private key user (userdatamodel.models.User): user object Return: str: JWT containing claims encoded with private key """ kid = peregrine.test_settings.JWT_KEYPAIR_FILES.keys()[0] scopes = ['openid'] token = generate_signed_access_token( kid, private_key, user, 3600, scopes, forced_exp_time=None, iss=app.config['USER_API'], ) return token.token
def encoded_jwt(private_key, user): """ Return an example JWT containing the claims and encoded with the private key. Args: private_key (str): private key user (userdatamodel.models.User): user object Return: str: JWT containing claims encoded with private key """ kid = JWT_KEYPAIR_FILES.keys()[0] scopes = ['openid'] return generate_signed_access_token(kid, private_key, user, 3600, scopes, forced_exp_time=None)
def create_access_token(self): """ Create a new access token. Return: JWTResult: result containing the encoded token and claims """ driver = SQLAlchemyDriver(self.db) with driver.session as current_session: user = (current_session.query(User).filter( func.lower(User.username) == self.username.lower()).first()) if not user: raise EnvironmentError("no user found with given username: " + self.username) return generate_signed_access_token( self.kid, self.private_key, user, self.expires_in, self.scopes, iss=self.base_url, )
def _create_access_token_cookie(app, response, user): keypair = app.keypairs[0] scopes = SESSION_ALLOWED_SCOPES now = datetime.now() expiration = int( (now + app.config.get('ACCESS_TOKEN_LIFETIME')).strftime('%s') ) timeout = datetime.fromtimestamp(expiration, pytz.utc) access_token = generate_signed_access_token( keypair.kid, keypair.private_key, user, app.config.get('ACCESS_TOKEN_LIFETIME').seconds, scopes, forced_exp_time=expiration ) domain = app.session_interface.get_cookie_domain(app) response.set_cookie( app.config['ACCESS_TOKEN_COOKIE_NAME'], access_token, expires=timeout, httponly=True, domain=domain ) return response
def generate_implicit_response(client, grant_type, include_access_token=True, expires_in=None, user=None, scope=None, nonce=None, **kwargs): # prevent those bothersome "not bound to session" errors if user not in current_session: user = current_session.query(User).filter_by(id=user.id).first() if not user: raise OIDCError("user not authenticated") keypair = flask.current_app.keypairs[0] linked_google_email = get_linked_google_account_email(user.id) linked_google_account_exp = get_linked_google_account_exp(user.id) if not isinstance(scope, list): scope = scope.split(" ") if not "user" in scope: scope.append("user") id_token = generate_signed_id_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=ACCESS_TOKEN_EXPIRES_IN, client_id=client.client_id, audiences=scope, nonce=nonce, linked_google_email=linked_google_email, linked_google_account_exp=linked_google_account_exp, ).token # ``expires_in`` is just the token expiration time. expires_in = ACCESS_TOKEN_EXPIRES_IN response = { "token_type": "Bearer", "id_token": id_token, "expires_in": expires_in, # "state" handled in authlib } if include_access_token: access_token = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=ACCESS_TOKEN_EXPIRES_IN, scopes=scope, client_id=client.client_id, linked_google_email=linked_google_email, ).token response["access_token"] = access_token return response
def __call__(self, client, grant_type, expires_in=None, scope=None, include_refresh_token=True, nonce=None, refresh_token=None, refresh_token_claims=None): """ Generate the token response, which looks like the following: { 'token_type': 'Bearer', 'id_token': 'eyJhb[...long encoded JWT...]OnoVQ', 'access_token': 'eyJhb[...long encoded JWT...]evfxA', 'refresh_token': 'eyJhb[ ... long encoded JWT ... ]KnLJA', 'expires_in': 1200, } This function will be called in authlib internals. Args: client: not used (would be used to determine expiration) grant_type: not used expires_in: not used (see expiration times configured above) scope (List[str]): list of requested scopes include_refresh_token: not used nonce (str): "nonsense" to include in ID token (see OIDC spec) refresh_token: for a refresh token grant, pass in the previous refresh token to return that same token again instead of generating a new one (otherwise this will let the refresh token refresh itself) refresh_token_claims (dict): also for a refresh token grant, pass the previous refresh token claims (to avoid having to encode or decode the refresh token here) """ # Find the ``User`` model. # The way to do this depends on the grant type. if grant_type == 'authorization_code': # For authorization code grant, get the code from either the query # string or the form data, and use that to look up the user. if flask.request.method == 'GET': code = flask.request.args.get('code') else: code = flask.request.form.get('code') user = (current_session.query(AuthorizationCode).filter_by( code=code).first().user) if grant_type == 'refresh_token': # For refresh token, the user ID is the ``sub`` field in the token. user = (current_session.query(User).filter_by( id=int(refresh_token_claims['sub'])).first()) keypair = flask.current_app.keypairs[0] id_token = generate_signed_id_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=self.ACCESS_TOKEN_EXPIRES_IN, client_id=client.client_id, audiences=scope, nonce=nonce, ) access_token = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=self.ACCESS_TOKEN_EXPIRES_IN, scopes=scope, ) # If ``refresh_token`` was passed (for instance from the refresh # grant), use that instead of generating a new one. if refresh_token is None: refresh_token, _ = generate_signed_refresh_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=self.REFRESH_TOKEN_EXPIRES_IN, scopes=scope, ) # ``expires_in`` is just the access token expiration time. expires_in = self.ACCESS_TOKEN_EXPIRES_IN return { 'token_type': 'Bearer', 'id_token': id_token, 'access_token': access_token, 'refresh_token': refresh_token, 'expires_in': expires_in, }
def generate_implicit_response(client, grant_type, include_access_token=True, expires_in=None, user=None, scope=None, nonce=None, **kwargs): # prevent those bothersome "not bound to session" errors if user not in current_session: user = current_session.query(User).filter_by(id=user.id).first() if not user: raise OIDCError("user not authenticated") keypair = flask.current_app.keypairs[0] linked_google_email = get_linked_google_account_email(user.id) linked_google_account_exp = get_linked_google_account_exp(user.id) if not isinstance(scope, list): scope = scope.split(" ") if not "user" in scope: scope.append("user") # ``expires_in`` is just the token expiration time. expires_in = config["ACCESS_TOKEN_EXPIRES_IN"] response = { "token_type": "Bearer", "expires_in": expires_in, # "state" handled in authlib } # don't provide user projects access in access_tokens for implicit flow # due to issues with "Location" header size during redirect (and b/c # of general deprecation of user access information in tokens) if include_access_token: access_token = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], scopes=scope, client_id=client.client_id, linked_google_email=linked_google_email, include_project_access=False, ).token response["access_token"] = access_token # don't provide user projects access in id_tokens for implicit flow # due to issues with "Location" header size during redirect (and b/c # of general deprecation of user access information in tokens) id_token = generate_signed_id_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], client_id=client.client_id, audiences=scope, nonce=nonce, linked_google_email=linked_google_email, linked_google_account_exp=linked_google_account_exp, include_project_access=False, auth_flow_type=AuthFlowTypes.IMPLICIT, access_token=access_token if include_access_token else None, ).token response["id_token"] = id_token return response
def generate_token_response(client, grant_type, expires_in=None, user=None, scope=None, include_refresh_token=True, nonce=None, refresh_token=None, refresh_token_claims=None, **kwargs): # prevent those bothersome "not bound to session" errors if user not in current_session: user = current_session.query(User).filter_by(id=user.id).first() if not user: # Find the ``User`` model. # The way to do this depends on the grant type. if grant_type == "authorization_code": # For authorization code grant, get the code from either the query # string or the form data, and use that to look up the user. if flask.request.method == "GET": code = flask.request.args.get("code") else: code = flask.request.form.get("code") user = (current_session.query(AuthorizationCode).filter_by( code=code).first().user) if grant_type == "refresh_token": # For refresh token, the user ID is the ``sub`` field in the token. user = (current_session.query(User).filter_by( id=int(refresh_token_claims["sub"])).first()) keypair = flask.current_app.keypairs[0] linked_google_email = get_linked_google_account_email(user.id) linked_google_account_exp = get_linked_google_account_exp(user.id) if not isinstance(scope, list): scope = scope.split(" ") access_token = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], scopes=scope, client_id=client.client_id, linked_google_email=linked_google_email, ).token id_token = generate_signed_id_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], client_id=client.client_id, audiences=scope, nonce=nonce, linked_google_email=linked_google_email, linked_google_account_exp=linked_google_account_exp, auth_flow_type=AuthFlowTypes.CODE, access_token=access_token, ).token # If ``refresh_token`` was passed (for instance from the refresh # grant), use that instead of generating a new one. if refresh_token is None: refresh_token = generate_signed_refresh_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["REFRESH_TOKEN_EXPIRES_IN"], scopes=scope, client_id=client.client_id, ).token # ``expires_in`` is just the access token expiration time. expires_in = config["ACCESS_TOKEN_EXPIRES_IN"] return { "token_type": "Bearer", "id_token": id_token, "access_token": access_token, "refresh_token": refresh_token, "expires_in": expires_in, }