Beispiel #1
0
def get_integration_from_token(token):
    """
    When we create a jira server integration we create a webhook that contains
    a JWT in the URL. We use that JWT to locate the matching sentry integration later
    as Jira doesn't have any additional fields we can embed information in.
    """
    if not token:
        raise ValueError("Token was empty")

    try:
        unvalidated = jwt.peek_claims(token)
    except jwt.DecodeError:
        raise ValueError("Could not decode JWT token")
    if "id" not in unvalidated:
        raise ValueError("Token did not contain `id`")
    try:
        integration = Integration.objects.get(provider="jira_server", external_id=unvalidated["id"])
    except Integration.DoesNotExist:
        raise ValueError("Could not find integration for token")
    try:
        jwt.decode(token, integration.metadata["webhook_secret"])
    except Exception as err:
        raise ValueError(f"Could not validate JWT. Got {err}")

    return integration
Beispiel #2
0
def test_decode(token: str) -> None:
    claims = jwt_utils.decode(token, "secret")
    assert claims == {"iss": "me"}

    for key, value in claims.items():
        assert isinstance(key, str)
        assert isinstance(value, str)

    claims["aud"] = "you"
    token = jwt_utils.encode(claims, "secret")

    with pytest.raises(pyjwt.exceptions.InvalidAudienceError):
        jwt_utils.decode(token, "secret")
Beispiel #3
0
def get_jira_auth_from_request(request):
    # https://developer.atlassian.com/static/connect/docs/latest/concepts/authentication.html
    # Extract the JWT token from the request's jwt query
    # parameter or the authorization header.
    token = request.GET.get("jwt")
    if token is None:
        raise ApiError("No token parameter")
    # Decode the JWT token, without verification. This gives
    # you a header JSON object, a claims JSON object, and a signature.
    decoded = jwt.peek_claims(token)
    # Extract the issuer ('iss') claim from the decoded, unverified
    # claims object. This is the clientKey for the tenant - an identifier
    # for the Atlassian application making the call
    issuer = decoded["iss"]
    # Look up the sharedSecret for the clientKey, as stored
    # by the add-on during the installation handshake
    from sentry_plugins.jira_ac.models import JiraTenant

    jira_auth = JiraTenant.objects.get(client_key=issuer)
    # Verify the signature with the sharedSecret and
    # the algorithm specified in the header's alg field.
    decoded_verified = jwt.decode(token, jira_auth.secret)
    # Verify the query has not been tampered by Creating a Query Hash
    # and comparing it against the qsh claim on the verified token.

    # TODO: probably shouldn't need to hardcode get... for post maybe
    # the secret should just be a hidden field in the form ?
    qsh = get_query_hash(request.path, "GET", request.GET)
    # qsh = get_query_hash(request.path, request.method, request.GET)
    if qsh != decoded_verified["qsh"]:
        raise ApiError("Query hash mismatch")

    return jira_auth
Beispiel #4
0
def test_rsa_key_from_jwk_pubkey(rsa_token: str) -> None:
    key = jwt_utils.rsa_key_from_jwk(json.dumps(RSA_PUB_JWK))
    assert key
    assert isinstance(key, str)

    claims = jwt_utils.decode(rsa_token, key, algorithms=["RS256"])
    assert claims == {"iss": "me"}
Beispiel #5
0
def get_integration_from_jwt(token, path, provider, query_params, method="GET"):
    # https://developer.atlassian.com/static/connect/docs/latest/concepts/authentication.html
    # Extract the JWT token from the request's jwt query
    # parameter or the authorization header.
    if token is None:
        raise AtlassianConnectValidationError("No token parameter")
    # Decode the JWT token, without verification. This gives
    # you a header JSON object, a claims JSON object, and a signature.
    decoded = jwt.peek_claims(token)
    # Extract the issuer ('iss') claim from the decoded, unverified
    # claims object. This is the clientKey for the tenant - an identifier
    # for the Atlassian application making the call
    issuer = decoded["iss"]
    # Look up the sharedSecret for the clientKey, as stored
    # by the add-on during the installation handshake
    try:
        integration = Integration.objects.get(provider=provider, external_id=issuer)
    except Integration.DoesNotExist:
        raise AtlassianConnectValidationError("No integration found")
    # Verify the signature with the sharedSecret and the algorithm specified in the header's
    # alg field.  We only need the token + shared secret and do not want to provide an
    # audience to the JWT validation that is require to match.  Bitbucket does give us an
    # audience claim however, so disable verification of this.
    decoded_verified = jwt.decode(token, integration.metadata["shared_secret"], audience=False)
    # Verify the query has not been tampered by Creating a Query Hash
    # and comparing it against the qsh claim on the verified token.

    qsh = get_query_hash(path, method, query_params)
    if qsh != decoded_verified["qsh"]:
        raise AtlassianConnectValidationError("Query hash mismatch")

    return integration
Beispiel #6
0
def verify_signature(request):
    # docs for jwt authentication here: https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0#bot-to-connector
    token = request.META.get("HTTP_AUTHORIZATION", "").replace("Bearer ", "")
    if not token:
        logger.error("msteams.webhook.no-auth-header")
        raise NotAuthenticated("Authorization header required")

    try:
        jwt.peek_claims(token)
    except jwt.DecodeError:
        logger.error("msteams.webhook.invalid-token-no-verify")
        raise AuthenticationFailed("Could not decode JWT token")

    # get the open id config and jwks
    client = MsTeamsJwtClient()
    open_id_config = client.get_open_id_config()
    jwks = client.get_cached(open_id_config["jwks_uri"])

    # create a mapping of all the keys
    # taken from: https://renzolucioni.com/verifying-jwts-with-jwks-and-pyjwt/
    public_keys = {}
    for jwk in jwks["keys"]:
        kid = jwk["kid"]
        public_keys[kid] = jwt.rsa_key_from_jwk(json.dumps(jwk))

    kid = jwt.peek_header(token)["kid"]
    key = public_keys[kid]

    try:
        decoded = jwt.decode(
            token,
            key,
            audience=options.get("msteams.client-id"),
            algorithms=open_id_config["id_token_signing_alg_values_supported"],
        )
    except Exception as err:
        logger.error("msteams.webhook.invalid-token-with-verify")
        raise AuthenticationFailed(f"Could not validate JWT. Got {err}")

    # now validate iss, service url, and expiration
    if decoded.get("iss") != "https://api.botframework.com":
        logger.error("msteams.webhook.invalid-iss")
        raise AuthenticationFailed("The field iss does not match")

    if decoded.get("serviceurl") != request.data.get("serviceUrl"):
        logger.error("msteams.webhook.invalid-service_url")
        raise AuthenticationFailed("The field serviceUrl does not match")

    if int(time.time()) > decoded["exp"] + CLOCK_SKEW:
        logger.error("msteams.webhook.expired-token")
        raise AuthenticationFailed("Token is expired")

    return True
Beispiel #7
0
def get_integration_from_jwt(
    token: Optional[str],
    path: str,
    provider: str,
    query_params: Optional[Mapping[str, str]],
    method: str = "GET",
) -> Integration:
    # https://developer.atlassian.com/static/connect/docs/latest/concepts/authentication.html
    # Extract the JWT token from the request's jwt query
    # parameter or the authorization header.
    if token is None:
        raise AtlassianConnectValidationError("No token parameter")
    # Decode the JWT token, without verification. This gives
    # you a header JSON object, a claims JSON object, and a signature.
    claims = jwt.peek_claims(token)
    headers = jwt.peek_header(token)

    # Extract the issuer ('iss') claim from the decoded, unverified
    # claims object. This is the clientKey for the tenant - an identifier
    # for the Atlassian application making the call
    issuer = claims.get("iss")
    # Look up the sharedSecret for the clientKey, as stored
    # by the add-on during the installation handshake
    try:
        integration = Integration.objects.get(provider=provider,
                                              external_id=issuer)
    except Integration.DoesNotExist:
        raise AtlassianConnectValidationError("No integration found")
    # Verify the signature with the sharedSecret and the algorithm specified in the header's
    # alg field.  We only need the token + shared secret and do not want to provide an
    # audience to the JWT validation that is require to match.  Bitbucket does give us an
    # audience claim however, so disable verification of this.
    key_id = headers.get("kid")
    try:
        # We only authenticate asymmetrically (through the CDN) if the event provides a key ID
        # in its JWT headers. This should only appear for install/uninstall events.

        decoded_claims = (authenticate_asymmetric_jwt(
            token, key_id) if key_id else jwt.decode(
                token, integration.metadata["shared_secret"], audience=False))
    except InvalidSignatureError:
        raise AtlassianConnectValidationError("Signature is invalid")

    verify_claims(decoded_claims, path, query_params, method)

    return integration
Beispiel #8
0
def test_encode(token: str) -> None:
    headers = {
        "alg": "HS256",
        "typ": "JWT",
    }
    claims = {
        "iss": "me",
    }
    key = "secret"

    encoded = jwt_utils.encode(claims, key, headers=headers)
    assert isinstance(encoded, str)

    assert encoded.count(".") == 2
    assert encoded == token

    decoded_claims = jwt_utils.decode(encoded, key)
    assert decoded_claims == claims
Beispiel #9
0
def authenticate_asymmetric_jwt(token: Optional[str],
                                key_id: str) -> Optional[Mapping[str, str]]:
    """
    Allows for Atlassian Connect installation lifecycle security improvements (i.e. verified senders)
    See: https://community.developer.atlassian.com/t/action-required-atlassian-connect-installation-lifecycle-security-improvements/49046
    """
    if token is None:
        raise AtlassianConnectValidationError("No token parameter")
    headers = jwt.peek_header(token)
    key_response = requests.get(
        f"https://connect-install-keys.atlassian.com/{key_id}")
    public_key = key_response.content.decode("utf-8").strip()
    decoded_claims = jwt.decode(token,
                                public_key,
                                audience=absolute_uri(),
                                algorithms=[headers.get("alg")])
    if not decoded_claims:
        raise AtlassianConnectValidationError(
            "Unable to verify asymmetric installation JWT")
    return decoded_claims
Beispiel #10
0
def test_decode_audience() -> None:
    payload = {
        "iss": "me",
        "aud": "you",
    }
    token = jwt_utils.encode(payload, "secret")

    with pytest.raises(pyjwt.exceptions.InvalidAudienceError):
        jwt_utils.decode(token, "secret")

    claims = jwt_utils.decode(token, "secret", audience="you")
    assert claims == payload

    with pytest.raises(pyjwt.exceptions.InvalidAudienceError):
        jwt_utils.decode(token, "secret", audience="wrong")

    claims = jwt_utils.decode(token, "secret", audience=False)
    assert claims == payload
Beispiel #11
0
def test_decode_pub(rsa_token: str) -> None:
    claims = jwt_utils.decode(rsa_token, RS256_PUB_KEY, algorithms=["RS256"])
    assert claims == {"iss": "me"}