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
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
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
def test_peek_claims(token: str) -> None: claims = jwt_utils.peek_claims(token) assert claims == {"iss": "me"} for key, value in claims.items(): assert isinstance(key, str) assert isinstance(value, str)
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
def webhook_response(request): # Ensure the webhook token contains our integration # external id data = json.loads(request.body) url = data["url"] token = url.split("/")[-2] token_data = jwt.peek_claims(token) assert "id" in token_data assert token_data["id"] == expected_id return (204, {}, "")
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