From 94ef46b29b6f6d400bc4dc07702b931284781b0b Mon Sep 17 00:00:00 2001
From: Erik Cederstrand <erik@adamatics.com>
Date: Tue, 31 May 2022 12:53:49 +0200
Subject: [PATCH 1/2] feat: Support Token Exchange. Fixes #305

---
 CHANGELOG.md                    |  1 +
 README.md                       |  3 +++
 src/keycloak/keycloak_openid.py | 24 ++++++++++++++++++++++++
 3 files changed, 28 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72b757b..4916c27 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,3 +46,4 @@ All notable changes to this project will be documented in this file.
  ## [master]
 
  * Renamed `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known`
+ * Add `KeycloakOpenID.token_exchange` to support Token Exchange
diff --git a/README.md b/README.md
index 85e3d34..d3572f5 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,9 @@ config_well_known = keycloak_openid.well_known()
 token = keycloak_openid.token("user", "password")
 token = keycloak_openid.token("user", "password", totp="012345")
 
+# Get token using Token Exchange
+token = keycloak_openid.exchange_token(token['access_token'], "my_client", "other_client", "some_user")
+
 # Get Userinfo
 userinfo = keycloak_openid.userinfo(token['access_token'])
 
diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py
index e73e963..a1f1f0a 100644
--- a/src/keycloak/keycloak_openid.py
+++ b/src/keycloak/keycloak_openid.py
@@ -254,6 +254,30 @@ class KeycloakOpenID:
         data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
         return raise_error_from_response(data_raw, KeycloakGetError)
 
+    def exchange_token(self, token: str, client_id: str, audience: str, subject: str) -> dict:
+        """
+        Use a token to obtain an entirely different token. See
+        https://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange
+
+        :param token:
+        :param client_id:
+        :param audience:
+        :param subject:
+        :return:
+        """
+        params_path = {"realm-name": self.realm_name}
+        payload = {
+            "grant_type": ["urn:ietf:params:oauth:grant-type:token-exchange"],
+            "client_id": client_id,
+            "subject_token": token,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:refresh_token",
+            "audience": audience,
+            "requested_subject": subject,
+        }
+        payload = self._add_secret_key(payload)
+        data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
+        return raise_error_from_response(data_raw, KeycloakGetError)
+
     def userinfo(self, token):
         """
         The userinfo endpoint returns standard claims about the authenticated user,

From b6990276c36fab64ba0a5406a6f22e68c51868c2 Mon Sep 17 00:00:00 2001
From: Erik Cederstrand <erik@adamatics.com>
Date: Tue, 31 May 2022 13:02:22 +0200
Subject: [PATCH 2/2] ci: fix docs generation

---
 docs/source/conf.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/source/conf.py b/docs/source/conf.py
index 2b67d12..b60a1c9 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -77,7 +77,7 @@ release = "0.0.0"
 #
 # This is also used if you do content translation via gettext catalogs.
 # Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.