Beispiel #1
0
    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})
Beispiel #2
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
Beispiel #3
0
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']
Beispiel #4
0
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
Beispiel #5
0
    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"]
Beispiel #7
0
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"]
Beispiel #8
0
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
Beispiel #9
0
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)
Beispiel #10
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)
Beispiel #11
0
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
Beispiel #12
0
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
Beispiel #13
0
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"]
Beispiel #14
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
Beispiel #15
0
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
Beispiel #16
0
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
Beispiel #17
0
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", [])
Beispiel #18
0
    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")
Beispiel #19
0
 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
Beispiel #20
0
 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
Beispiel #21
0
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
Beispiel #22
0
    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__()
Beispiel #23
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
Beispiel #24
0
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
Beispiel #27
0
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
Beispiel #28
0
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
Beispiel #29
0
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"
Beispiel #30
0
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