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()
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"}
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_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
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
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()