Exemple #1
0
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"])
Exemple #2
0
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"]
Exemple #3
0
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
Exemple #4
0
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)
Exemple #6
0
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)
Exemple #7
0
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
Exemple #8
0
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
Exemple #9
0
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
Exemple #10
0
    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
Exemple #11
0
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)
Exemple #12
0
    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,
            )
Exemple #13
0
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
Exemple #14
0
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
Exemple #15
0
    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,
        }
Exemple #16
0
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
Exemple #17
0
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,
    }