Standalone verification of Keycloak access-token
Question:
I am using Keycloak to handle login and generate JWT tokens. I need to be able to verify the access token that I’m sending to my REST API service. Best practice is to use the JWT secret to verify the token directly rather than send it to the Keycloak server for verification. There are a lot of Java examples of doing this, but I need to be able to verify this using python or ruby.
I tried the following python signature verification but I get an error of ValueError: Could not unserialize key data.
I also tried entering the public key in the https://jwt.io debugger but also get an invalid signature.
#!/usr/bin/env python3
import jwt
# Public key from Keycloak realm -> Keys -> Public Key -> (view)
public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu77nUtVw7SIIcUTSiStzMPB7BGB/9eS+CpppsUaiyZyWCXlrALT3YdqneSlpX4Ta+0wvhOkKQtoSS8dCH8GIi7esAmfdHetHfRgeDXHAlXo8HIzshUzODg3ysT7j+Ha3eJsO+LNS/omHDhsarP8Z2eThW876iKJCCc/mB76a6u1e4Id+52K5lG++m8Pn4Gs+cqd2sKUKcMJ9CkJ6dBIdGlXHMoOHj4C33SPrEG/vEBv5cu0l5PP3RiBAuaZHpLKzfIiaLOpj/k4dD/weVt5gwTIJn16AEgPD7173Xef0HgoPlQInDFrJwsGpYCnIPZWSxRbvjKkya2Auj0QZyMCrXwIDAQAB"
# Keycloak JWT RS256 access-token
access_token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0LVlJOUlVc2R6NGM0SHoycXczT0xXZ0I0eHc2eFd4T29XdktVT2FvV3FzIn0.eyJqdGkiOiJjYzZkMjM5OS04ZmU5LTQzMWItYjZjMS05NWQxMWUxN2FiZGQiLCJleHAiOjE0ODE1NTQ2NTksIm5iZiI6MCwiaWF0IjoxNDgxNTU0MzU5LCJpc3MiOiJodHRwczovL3BtaS1rZXljbG9hay5zYnZpbXByb3Zlci5jb20vYXV0aC9yZWFsbXMvT3BlbkJFTCIsImF1ZCI6ImJlbG1nciIsInN1YiI6ImU1ODc2OGQxLWU3ODktNGU3Yi04ZGVlLWJjMzYxNzFkZDNhZCIsInR5cCI6IkJlYXJlciIsImF6cCI6ImJlbG1nciIsIm5vbmNlIjoiMzc2ZDdjZmUtZGZkNC00Yzg5LWJlZGEtZTlmOWNmYWNlMTNkIiwiYXV0aF90aW1lIjoxNDgxNTUzMTY0LCJzZXNzaW9uX3N0YXRlIjoiN2E2OTEzYzItZWJkZC00MTc2LWI4YTAtZDc2NzVhNDZkNmJjIiwiYWNyIjoiMSIsImNsaWVudF9zZXNzaW9uIjoiOTMyNGMzMjAtMmE4Zi00ODBlLTg5MjItZGQxNmFmMDQxZDdmIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sIm5hbWUiOiJXaWxsaWFtIEhheWVzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoid2lsbGlhbS5zLmhheWVzQGdtYWlsLmNvbSIsImdpdmVuX25hbWUiOiJXaWxsaWFtIiwiZmFtaWx5X25hbWUiOiJIYXllcyIsImVtYWlsIjoid2lsbGlhbS5zLmhheWVzQGdtYWlsLmNvbSJ9.Q7s-qTcJyH69Ebof8pQI1kZzeT8olwQnRJ06uas5TP2isacxOheHnJ9ixEvqTrr-iefmYMwx41jM68NCs6l8IBNHqv7t5-ediizx4ianMiXr7oZ_1oAT9hkLyrpv9iF2IZBtzNJz0GQAnDYe1moLOLuzqwvcUaWgmzRY95xvzo4kbE8OkeZiMpD_cDmp3_vKOsdn3B6ybJ9TXtea55A29pQzsvAM_6lHeyxTCisipOtu_ubnUOamkYSpxLwWZXgI1w7iz-igt-n7xtlFhUpra239yn9uly9iuBtlgnc3TFDmZn-XRq_PODDJNJeaQXDRaDqnRQhXsoObxCaPqXDQ3A"
access_token_json = jwt.decode(access_token, public_key)
print(access_token_json)
Answers:
To verify the access token I did the following things:
-
I reset the expiration timeframe so I didn’t have to worry about timeouts as a complicating factor.
-
I had to add the BEGIN/END header/footer to the pubkey (of which there are two different versions based on the encoding of the pubkey -e.g. ‘BEGIN RSA PUBLIC KEY’ is not the right version for the Keycloak pubkey encoding):
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu77nUtVw7SIIcUTSiStzMPB7BGB/9eS+CpppsUaiyZyWCXlrALT3YdqneSlpX4Ta+0wvhOkKQtoSS8dCH8GIi7esAmfdHetHfRgeDXHAlXo8HIzshUzODg3ysT7j+Ha3eJsO+LNS/omHDhsarP8Z2eThW876iKJCCc/mB76a6u1e4Id+52K5lG++m8Pn4Gs+cqd2sKUKcMJ9CkJ6dBIdGlXHMoOHj4C33SPrEG/vEBv5cu0l5PP3RiBAuaZHpLKzfIiaLOpj/k4dD/weVt5gwTIJn16AEgPD7173Xef0HgoPlQInDFrJwsGpYCnIPZWSxRbvjKkya2Auj0QZyMCrXwIDAQAB
-----END PUBLIC KEY-----
- Last – I had to add the audience=’belmgr’ to the jwt.decode for the python script
## Python script
#!/usr/bin/env python3
import jwt
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu77nUtVw7SIIcUTSiStzMPB7BGB/9eS+CpppsUaiyZyWCXlrALT3YdqneSlpX4Ta+0wvhOkKQtoSS8dCH8GIi7esAmfdHetHfRgeDXHAlXo8HIzshUzODg3ysT7j+Ha3eJsO+LNS/omHDhsarP8Z2eThW876iKJCCc/mB76a6u1e4Id+52K5lG++m8Pn4Gs+cqd2sKUKcMJ9CkJ6dBIdGlXHMoOHj4C33SPrEG/vEBv5cu0l5PP3RiBAuaZHpLKzfIiaLOpj/k4dD/weVt5gwTIJn16AEgPD7173Xef0HgoPlQInDFrJwsGpYCnIPZWSxRbvjKkya2Auj0QZyMCrXwIDAQAB
-----END PUBLIC KEY-----"""
access_token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0LVlJOUlVc2R6NGM0SHoycXczT0xXZ0I0eHc2eFd4T29XdktVT2FvV3FzIn0.eyJqdGkiOiJiMGFhZDllMC1lYmU5LTQ4ZDQtYTcxNC1iMWMyNjg4NmM2MDciLCJleHAiOjE0ODE5ODE0ODQsIm5iZiI6MCwiaWF0IjoxNDgxODk1MDg0LCJpc3MiOiJodHRwczovL3BtaS1rZXljbG9hay5zYnZpbXByb3Zlci5jb20vYXV0aC9yZWFsbXMvT3BlbkJFTCIsImF1ZCI6ImJlbG1nciIsInN1YiI6ImU1ODc2OGQxLWU3ODktNGU3Yi04ZGVlLWJjMzYxNzFkZDNhZCIsInR5cCI6IkJlYXJlciIsImF6cCI6ImJlbG1nciIsIm5vbmNlIjoiMzI3NjRhYWUtZmM1NC00MDlkLTgxM2EtNjhhNmM4YTNhYzI2IiwiYXV0aF90aW1lIjoxNDgxODk1MDgzLCJzZXNzaW9uX3N0YXRlIjoiMWQwNDMyMDQtNWNkYy00OTVjLWJlZWUtODIwZWJiMmRlNWUzIiwiYWNyIjoiMSIsImNsaWVudF9zZXNzaW9uIjoiMmEzZWY1MzgtN2MxOS00YzE3LTlmZTctYjQ3ZGNjNmM0ODQyIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sIm5hbWUiOiJXaWxsaWFtIEhheWVzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoid2lsbGlhbS5zLmhheWVzQGdtYWlsLmNvbSIsImdpdmVuX25hbWUiOiJXaWxsaWFtIiwiZmFtaWx5X25hbWUiOiJIYXllcyIsImVtYWlsIjoid2lsbGlhbS5zLmhheWVzQGdtYWlsLmNvbSJ9.DfC1c6BVBZ8Bgwu6CYGBsWp4T0dqltwAQ84E1Q0LdjFxvtVeDOF8rBIdgkr7rMCHObZWsEotljSR4BZzCvfDNmdk_25sedvi-ZHXTP0-nSeHczIXBstZ8p257A6-fEiIcG5CRoClHMI317bVGjNkzAV7l8kuBhr0bfrDedxpvKo3EQah4MrOF7-JXQGPAWlLDV1E9zsrT99Vm_XL58M-ur8q7N-B-CmOBV2GGsMEosTDK_-U-mattEN6PMNiG004Ryg0iPDM4-kr1AQsPE_wHBYf81_-vrqs7ec--0ShJYdC8-eBbuf9xVixNQVPRl7mnktaKA19YXdzdCwcQa6crw"
access_token_json = jwt.decode(access_token, public_key, audience='belmgr')
print(access_token_json)
I want to add to this knowledge in case anyone else is stuck on this.
The public_key value copied straight from Keycloak realm -> Keys -> Public Key -> (view) did not work. Instead, I had to get the public key that was exposed by Keycloak for the realm by using the URL:
https://<< my keycloak url >>/auth/realms/<< my realm >>/
This url returns json that includes a value for “public_key” for the realm “my realm”. Using THIS value of the public key in the way that’s been suggested in the selected answer worked.
However, you need to also make sure your audience value is correct with the decode call. For my use, the value “belmgr”. I found the audience I needed by using jwt.io, decoding the access_token, and in the payload data, I found:
{ ..."aud": "account",... }
Using “account” as the audience worked.
# wrong audience - FAIL
access_token_json = jwt.decode(access_token, public_key, audience='belmgr')
# right audience - SUCCESS
access_token_json = jwt.decode(access_token, public_key, audience='account')
A better way to validate the bearer token from keycloak is to use the decode_token method of KeycloakOpenID client.
from keycloak.keycloak_openid import KeycloakOpenID
KEYCLOAK_OPENID_CLIENT = KeycloakOpenID(server_url='keycloak_base_url',
client_id='client_id',
client_secret_key='client_secret',
realm_name='realm_name'
)
KEYCLOAK_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----n" + KEYCLOAK_OPENID_CLIENT.public_key() + "n-----END PUBLIC KEY-----"
options = {"verify_signature": True,"verify_aud": True, "verify_exp": True}
KEYCLOAK_OPENID_CLIENT.decode_token(access_token, key=KEYCLOAK_PUBLIC_KEY, options=options)
python package for client —https://pypi.org/project/python-keycloak/
NB: Please be sure to add your audience claim properly. For more details visit post Keycloak-gatekeeper: 'aud' claim and 'client_id' do not match
I am using Keycloak to handle login and generate JWT tokens. I need to be able to verify the access token that I’m sending to my REST API service. Best practice is to use the JWT secret to verify the token directly rather than send it to the Keycloak server for verification. There are a lot of Java examples of doing this, but I need to be able to verify this using python or ruby.
I tried the following python signature verification but I get an error of ValueError: Could not unserialize key data.
I also tried entering the public key in the https://jwt.io debugger but also get an invalid signature.
#!/usr/bin/env python3
import jwt
# Public key from Keycloak realm -> Keys -> Public Key -> (view)
public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu77nUtVw7SIIcUTSiStzMPB7BGB/9eS+CpppsUaiyZyWCXlrALT3YdqneSlpX4Ta+0wvhOkKQtoSS8dCH8GIi7esAmfdHetHfRgeDXHAlXo8HIzshUzODg3ysT7j+Ha3eJsO+LNS/omHDhsarP8Z2eThW876iKJCCc/mB76a6u1e4Id+52K5lG++m8Pn4Gs+cqd2sKUKcMJ9CkJ6dBIdGlXHMoOHj4C33SPrEG/vEBv5cu0l5PP3RiBAuaZHpLKzfIiaLOpj/k4dD/weVt5gwTIJn16AEgPD7173Xef0HgoPlQInDFrJwsGpYCnIPZWSxRbvjKkya2Auj0QZyMCrXwIDAQAB"
# Keycloak JWT RS256 access-token
access_token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0LVlJOUlVc2R6NGM0SHoycXczT0xXZ0I0eHc2eFd4T29XdktVT2FvV3FzIn0.eyJqdGkiOiJjYzZkMjM5OS04ZmU5LTQzMWItYjZjMS05NWQxMWUxN2FiZGQiLCJleHAiOjE0ODE1NTQ2NTksIm5iZiI6MCwiaWF0IjoxNDgxNTU0MzU5LCJpc3MiOiJodHRwczovL3BtaS1rZXljbG9hay5zYnZpbXByb3Zlci5jb20vYXV0aC9yZWFsbXMvT3BlbkJFTCIsImF1ZCI6ImJlbG1nciIsInN1YiI6ImU1ODc2OGQxLWU3ODktNGU3Yi04ZGVlLWJjMzYxNzFkZDNhZCIsInR5cCI6IkJlYXJlciIsImF6cCI6ImJlbG1nciIsIm5vbmNlIjoiMzc2ZDdjZmUtZGZkNC00Yzg5LWJlZGEtZTlmOWNmYWNlMTNkIiwiYXV0aF90aW1lIjoxNDgxNTUzMTY0LCJzZXNzaW9uX3N0YXRlIjoiN2E2OTEzYzItZWJkZC00MTc2LWI4YTAtZDc2NzVhNDZkNmJjIiwiYWNyIjoiMSIsImNsaWVudF9zZXNzaW9uIjoiOTMyNGMzMjAtMmE4Zi00ODBlLTg5MjItZGQxNmFmMDQxZDdmIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sIm5hbWUiOiJXaWxsaWFtIEhheWVzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoid2lsbGlhbS5zLmhheWVzQGdtYWlsLmNvbSIsImdpdmVuX25hbWUiOiJXaWxsaWFtIiwiZmFtaWx5X25hbWUiOiJIYXllcyIsImVtYWlsIjoid2lsbGlhbS5zLmhheWVzQGdtYWlsLmNvbSJ9.Q7s-qTcJyH69Ebof8pQI1kZzeT8olwQnRJ06uas5TP2isacxOheHnJ9ixEvqTrr-iefmYMwx41jM68NCs6l8IBNHqv7t5-ediizx4ianMiXr7oZ_1oAT9hkLyrpv9iF2IZBtzNJz0GQAnDYe1moLOLuzqwvcUaWgmzRY95xvzo4kbE8OkeZiMpD_cDmp3_vKOsdn3B6ybJ9TXtea55A29pQzsvAM_6lHeyxTCisipOtu_ubnUOamkYSpxLwWZXgI1w7iz-igt-n7xtlFhUpra239yn9uly9iuBtlgnc3TFDmZn-XRq_PODDJNJeaQXDRaDqnRQhXsoObxCaPqXDQ3A"
access_token_json = jwt.decode(access_token, public_key)
print(access_token_json)
To verify the access token I did the following things:
-
I reset the expiration timeframe so I didn’t have to worry about timeouts as a complicating factor.
-
I had to add the BEGIN/END header/footer to the pubkey (of which there are two different versions based on the encoding of the pubkey -e.g. ‘BEGIN RSA PUBLIC KEY’ is not the right version for the Keycloak pubkey encoding):
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu77nUtVw7SIIcUTSiStzMPB7BGB/9eS+CpppsUaiyZyWCXlrALT3YdqneSlpX4Ta+0wvhOkKQtoSS8dCH8GIi7esAmfdHetHfRgeDXHAlXo8HIzshUzODg3ysT7j+Ha3eJsO+LNS/omHDhsarP8Z2eThW876iKJCCc/mB76a6u1e4Id+52K5lG++m8Pn4Gs+cqd2sKUKcMJ9CkJ6dBIdGlXHMoOHj4C33SPrEG/vEBv5cu0l5PP3RiBAuaZHpLKzfIiaLOpj/k4dD/weVt5gwTIJn16AEgPD7173Xef0HgoPlQInDFrJwsGpYCnIPZWSxRbvjKkya2Auj0QZyMCrXwIDAQAB -----END PUBLIC KEY-----
- Last – I had to add the audience=’belmgr’ to the jwt.decode for the python script
## Python script #!/usr/bin/env python3 import jwt public_key = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu77nUtVw7SIIcUTSiStzMPB7BGB/9eS+CpppsUaiyZyWCXlrALT3YdqneSlpX4Ta+0wvhOkKQtoSS8dCH8GIi7esAmfdHetHfRgeDXHAlXo8HIzshUzODg3ysT7j+Ha3eJsO+LNS/omHDhsarP8Z2eThW876iKJCCc/mB76a6u1e4Id+52K5lG++m8Pn4Gs+cqd2sKUKcMJ9CkJ6dBIdGlXHMoOHj4C33SPrEG/vEBv5cu0l5PP3RiBAuaZHpLKzfIiaLOpj/k4dD/weVt5gwTIJn16AEgPD7173Xef0HgoPlQInDFrJwsGpYCnIPZWSxRbvjKkya2Auj0QZyMCrXwIDAQAB -----END PUBLIC KEY-----""" access_token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0LVlJOUlVc2R6NGM0SHoycXczT0xXZ0I0eHc2eFd4T29XdktVT2FvV3FzIn0.eyJqdGkiOiJiMGFhZDllMC1lYmU5LTQ4ZDQtYTcxNC1iMWMyNjg4NmM2MDciLCJleHAiOjE0ODE5ODE0ODQsIm5iZiI6MCwiaWF0IjoxNDgxODk1MDg0LCJpc3MiOiJodHRwczovL3BtaS1rZXljbG9hay5zYnZpbXByb3Zlci5jb20vYXV0aC9yZWFsbXMvT3BlbkJFTCIsImF1ZCI6ImJlbG1nciIsInN1YiI6ImU1ODc2OGQxLWU3ODktNGU3Yi04ZGVlLWJjMzYxNzFkZDNhZCIsInR5cCI6IkJlYXJlciIsImF6cCI6ImJlbG1nciIsIm5vbmNlIjoiMzI3NjRhYWUtZmM1NC00MDlkLTgxM2EtNjhhNmM4YTNhYzI2IiwiYXV0aF90aW1lIjoxNDgxODk1MDgzLCJzZXNzaW9uX3N0YXRlIjoiMWQwNDMyMDQtNWNkYy00OTVjLWJlZWUtODIwZWJiMmRlNWUzIiwiYWNyIjoiMSIsImNsaWVudF9zZXNzaW9uIjoiMmEzZWY1MzgtN2MxOS00YzE3LTlmZTctYjQ3ZGNjNmM0ODQyIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sIm5hbWUiOiJXaWxsaWFtIEhheWVzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoid2lsbGlhbS5zLmhheWVzQGdtYWlsLmNvbSIsImdpdmVuX25hbWUiOiJXaWxsaWFtIiwiZmFtaWx5X25hbWUiOiJIYXllcyIsImVtYWlsIjoid2lsbGlhbS5zLmhheWVzQGdtYWlsLmNvbSJ9.DfC1c6BVBZ8Bgwu6CYGBsWp4T0dqltwAQ84E1Q0LdjFxvtVeDOF8rBIdgkr7rMCHObZWsEotljSR4BZzCvfDNmdk_25sedvi-ZHXTP0-nSeHczIXBstZ8p257A6-fEiIcG5CRoClHMI317bVGjNkzAV7l8kuBhr0bfrDedxpvKo3EQah4MrOF7-JXQGPAWlLDV1E9zsrT99Vm_XL58M-ur8q7N-B-CmOBV2GGsMEosTDK_-U-mattEN6PMNiG004Ryg0iPDM4-kr1AQsPE_wHBYf81_-vrqs7ec--0ShJYdC8-eBbuf9xVixNQVPRl7mnktaKA19YXdzdCwcQa6crw" access_token_json = jwt.decode(access_token, public_key, audience='belmgr') print(access_token_json)
I want to add to this knowledge in case anyone else is stuck on this.
The public_key value copied straight from Keycloak realm -> Keys -> Public Key -> (view) did not work. Instead, I had to get the public key that was exposed by Keycloak for the realm by using the URL:
https://<< my keycloak url >>/auth/realms/<< my realm >>/
This url returns json that includes a value for “public_key” for the realm “my realm”. Using THIS value of the public key in the way that’s been suggested in the selected answer worked.
However, you need to also make sure your audience value is correct with the decode call. For my use, the value “belmgr”. I found the audience I needed by using jwt.io, decoding the access_token, and in the payload data, I found:
{ ..."aud": "account",... }
Using “account” as the audience worked.
# wrong audience - FAIL
access_token_json = jwt.decode(access_token, public_key, audience='belmgr')
# right audience - SUCCESS
access_token_json = jwt.decode(access_token, public_key, audience='account')
A better way to validate the bearer token from keycloak is to use the decode_token method of KeycloakOpenID client.
from keycloak.keycloak_openid import KeycloakOpenID
KEYCLOAK_OPENID_CLIENT = KeycloakOpenID(server_url='keycloak_base_url',
client_id='client_id',
client_secret_key='client_secret',
realm_name='realm_name'
)
KEYCLOAK_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----n" + KEYCLOAK_OPENID_CLIENT.public_key() + "n-----END PUBLIC KEY-----"
options = {"verify_signature": True,"verify_aud": True, "verify_exp": True}
KEYCLOAK_OPENID_CLIENT.decode_token(access_token, key=KEYCLOAK_PUBLIC_KEY, options=options)
python package for client —https://pypi.org/project/python-keycloak/
NB: Please be sure to add your audience claim properly. For more details visit post Keycloak-gatekeeper: 'aud' claim and 'client_id' do not match