Esempio n. 1
0
    def post(self, request: Request, *args, **kwargs) -> Response:
        try:
            token = request.META["HTTP_AUTHORIZATION"].split(" ", 1)[1]
        except (KeyError, IndexError):
            return self.respond(status=status.HTTP_400_BAD_REQUEST)

        state = request.data
        if not state:
            return self.respond(status=status.HTTP_400_BAD_REQUEST)

        key_id = jwt.peek_header(token).get("kid")
        if key_id:
            try:
                decoded_claims = authenticate_asymmetric_jwt(token, key_id)
                verify_claims(decoded_claims,
                              request.path,
                              request.GET,
                              method="POST")
            except AtlassianConnectValidationError:
                return self.respond(status=status.HTTP_400_BAD_REQUEST)

        data = JiraIntegrationProvider().build_integration(state)
        integration = ensure_integration("jira", data)

        # Sync integration metadata from Jira. This must be executed *after*
        # the integration has been installed on Jira as the access tokens will
        # not work until then.
        sync_metadata.apply_async(kwargs={"integration_id": integration.id},
                                  countdown=10)

        return self.respond()
Esempio n. 2
0
def test_peek_header(token: str) -> None:
    header = jwt_utils.peek_header(token)

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

    assert header == {"alg": "HS256", "typ": "JWT"}
Esempio n. 3
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
Esempio n. 4
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
Esempio n. 5
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
Esempio n. 6
0
    def post(self, request: Request, *args, **kwargs) -> Response:
        token = self.get_token(request)

        state = request.data
        if not state:
            return self.respond(status=status.HTTP_400_BAD_REQUEST)

        key_id = jwt.peek_header(token).get("kid")
        if key_id:
            decoded_claims = authenticate_asymmetric_jwt(token, key_id)
            verify_claims(decoded_claims, request.path, request.GET, method="POST")

        data = JiraIntegrationProvider().build_integration(state)
        integration = ensure_integration(self.provider, data)

        # Sync integration metadata from Jira. This must be executed *after*
        # the integration has been installed on Jira as the access tokens will
        # not work until then.
        sync_metadata.apply_async(kwargs={"integration_id": integration.id}, countdown=10)

        return self.respond()