{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# OpenID Connect Flows With An Example Keycloak Setup\n", "\n", "This is a Python3 notebook that illustrates different OpenID Connect flows, using a local Keycloak instance as OpenID provider and some basic libraries to handle the HTTP interactions.\n", "\n", "You can skip the set up part and go straight to the flows:\n", "\n", "- [Client Credentials Flow](#Client-Credentials-Flow)\n", "- [Resource Owner Password Flow](#Resource-Owner-Password-Flow)\n", "- [Authorization Code Flow](#Authorization-Code-Flow)\n", "\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import base64\n", "import html\n", "import json\n", "import logging\n", "import re\n", "import urllib.parse\n", "import uuid\n", "\n", "import requests" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "logging.basicConfig(level=logging.INFO)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup of Keycloak instance\n", "\n", "We need a test/development Keycloak instance.\n", "For example, spin up a local Keycloak instance with Docker as follows:\n", "\n", " docker run --rm -it -p 9090:8080 \\\n", " -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin \\\n", " jboss/keycloak:7.0.0" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "keycloak_base_url = \"http://localhost:9090/auth\"\n", "admin_username = \"admin\"\n", "admin_password = \"admin\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Keycloak Admin API\n", "\n", "To be able to create clients and users through the Keycloak admin API, we first have to obtain an admin access token through OpenID, which we have to use a bearer token for other admin API requests. Let's wrap this stuff in a class." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class KeycloakAdmin:\n", " def __init__(self, base_url: str, username: str, password: str):\n", " r = requests.post(\n", " base_url + '/realms/master/protocol/openid-connect/token', \n", " data={\n", " \"username\": username,\n", " \"password\": password,\n", " \"grant_type\": \"password\",\n", " \"client_id\": \"admin-cli\"\n", " })\n", " r.raise_for_status()\n", " \n", " self.session = requests.Session()\n", " self.session.headers[\"Authorization\"]= \"Bearer \" + r.json()[\"access_token\"]\n", " self.admin_base_url = base_url + '/admin/realms/master'\n", " self.log = logging.getLogger(\"keycloak-admin\")\n", " \n", " def create_client(self, options: dict = None, prefix: str = \"myclient-\") -> str:\n", " client_id = prefix + uuid.uuid4().hex[:8]\n", " data = {\"id\": client_id}\n", " data.update(options)\n", " self.log.info(\"Creating client with settings {s!r}\".format(s=data))\n", " r = self.session.post(self.admin_base_url + '/clients', json=data)\n", " r.raise_for_status()\n", " return client_id\n", "\n", " def get_client_secret(self, client_id) -> str:\n", " r = self.session.get(self.admin_base_url + '/clients/{c}/client-secret'.format(c=client_id))\n", " r.raise_for_status()\n", " self.log.info(\"Client secret response: {r!r}\".format(r=r.text))\n", " client_secret = r.json()[\"value\"]\n", " return client_secret\n", " \n", " def create_user(self, prefix: str = \"John-\", password: str = \"j0hn\"):\n", " username = prefix + uuid.uuid4().hex[:8]\n", "\n", " r = self.session.post(\n", " self.admin_base_url + '/users', \n", " json={\n", " \"username\": username,\n", " \"credentials\": [\n", " {\"type\": \"password\", \"value\": password, \"temporary\": False},\n", " ],\n", " \"enabled\": True,\n", " }\n", " )\n", " r.raise_for_status()\n", " return username, password\n", "\n", "\n", "# And while we're at it,\n", "def jwt_decode(token: str):\n", " \"\"\"Poor man's JWT decoding\"\"\"\n", "\n", " def _decode(data: str) -> dict:\n", " decoded = base64.b64decode(data + '=' * (4 - len(data) % 4)).decode('ascii')\n", " return json.loads(decoded)\n", "\n", " header, payload, signature = token.split('.')\n", " return _decode(header), _decode(payload)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "keycloak_admin = KeycloakAdmin(keycloak_base_url, admin_username, admin_password)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## General Set Up" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To better see what is going on the HTTP level when doing OpenID Connect request, we'll add a `requests` hook that prints a bit of request and response info." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from IPython.display import HTML, display\n", "\n", "def _show_request_info(r, *args, **kwargs):\n", " req = r.request\n", " default_headers = requests.utils.default_headers()\n", " headers = {k:v for k,v in req.headers.items() if k not in default_headers}\n", " display(HTML('''
\n", " Did request: {m} {u} with
'''.format(\n", " m=req.method, u=req.url, \n", " b=req.body, h=headers,\n", " r=r.status_code\n", " )))\n", "\n", "session = requests.Session()\n", "session.hooks['response'].append(_show_request_info)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And while we're at it, define some additional small utilities." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def jwt_decode(token: str):\n", " \"\"\"Poor man's JWT decoding\"\"\"\n", "\n", " def _decode(data: str) -> dict:\n", " decoded = base64.b64decode(data + '=' * (4 - len(data) % 4)).decode('ascii')\n", " return json.loads(decoded)\n", "\n", " header, payload, signature = token.split('.')\n", " return _decode(header), _decode(payload)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## OpenID provider info\n", "\n", "Get OpenID provider info from the configuration document." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " Did request: GET http://localhost:9090/auth/realms/master/.well-known/openid-configuration with
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "dict_keys(['issuer', 'authorization_endpoint', 'token_endpoint', 'token_introspection_endpoint', 'userinfo_endpoint', 'end_session_endpoint', 'jwks_uri', 'check_session_iframe', 'grant_types_supported', 'response_types_supported', 'subject_types_supported', 'id_token_signing_alg_values_supported', 'id_token_encryption_alg_values_supported', 'id_token_encryption_enc_values_supported', 'userinfo_signing_alg_values_supported', 'request_object_signing_alg_values_supported', 'response_modes_supported', 'registration_endpoint', 'token_endpoint_auth_methods_supported', 'token_endpoint_auth_signing_alg_values_supported', 'claims_supported', 'claim_types_supported', 'claims_parameter_supported', 'scopes_supported', 'request_parameter_supported', 'request_uri_parameter_supported', 'code_challenge_methods_supported', 'tls_client_certificate_bound_access_tokens', 'introspection_endpoint'])" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "provider_info = session.get(\n", " keycloak_base_url + '/realms/master/.well-known/openid-configuration'\n", ").json()\n", "provider_info.keys()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Get the token endpoint URL." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'http://localhost:9090/auth/realms/master/protocol/openid-connect/token'" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "token_endpoint = provider_info[\"token_endpoint\"]\n", "token_endpoint" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Client Credentials Flow" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create a client in Keycloak with settings that allow enable Client Credentials Grant. We'll also need the client's secret." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:keycloak-admin:Creating client with settings {'id': 'myclient-5b8dfca4', 'serviceAccountsEnabled': True}\n", "INFO:keycloak-admin:Client secret response: '{\"type\":\"secret\",\"value\":\"7f39376a-0d3e-4a87-b2ef-a9718bee4a76\"}'\n" ] }, { "data": { "text/plain": [ "('myclient-5b8dfca4', '7f39376a-0d3e-4a87-b2ef-a9718bee4a76')" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cc_client = keycloak_admin.create_client(options={\n", " \"serviceAccountsEnabled\": True,\n", "})\n", "cc_client_secret = keycloak_admin.get_client_secret(cc_client)\n", "\n", "cc_client, cc_client_secret" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Do `client_credentials` token request." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " Did request: POST http://localhost:9090/auth/realms/master/protocol/openid-connect/token with
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "{'access_token': 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXVEdTMXJyN1pTY2RWU05DV1d5M0tiVUJXNFVaUDc2ZFZ1V1l4RTdYSnYwIn0.eyJqdGkiOiI0ODdiOTYzYy0yYzI0LTQwMmYtODE3OS02YjE1OTAxYjkxODEiLCJleHAiOjE1NzE3MzYwNTQsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjZiMTRkYTE2LWM5MjgtNDg4Ni05MmE2LTQyYmE3MjU1YzU0NSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15Y2xpZW50LTViOGRmY2E0IiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiMDNlODcxNmItZWRkZS00YWY4LTk5ZWEtNGI4ZDU5ODQ5OThhIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNsaWVudElkIjoibXljbGllbnQtNWI4ZGZjYTQiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtbXljbGllbnQtNWI4ZGZjYTQiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSIsImVtYWlsIjoic2VydmljZS1hY2NvdW50LW15Y2xpZW50LTViOGRmY2E0QHBsYWNlaG9sZGVyLm9yZyJ9.dplwSv8nByYO7Xh7O8Vl1EGAtXLP6W87KGzUOCwN73tCcAJbX3HhbYvP36f489dx1wj8n1Wb_FwNbASED1XGZb_pbps07YO68OlcJjkIafXsLXK98tSaQJ3hurn-lSa8DA3_-A5MnW-XR_7dFad1Guo0RSypv94ybZEFX8RMFWcUeVmsEkRLlAfP2d9WMnZ2N8d08jAr5FBbYedhNBX8VOWIm05Ho5hv8h7OtnS3fLVsfCtm36s6sQCYfGlfgfGc_bg1O9mtPHLfn5sf-j6SUBYpnfIBFXJBx4tChfd4Vryoa5tloqMiRsx1Xq96jiuWGQjLvbKjXTM3n_XB4UVuWQ',\n", " 'expires_in': 60,\n", " 'refresh_expires_in': 1800,\n", " 'refresh_token': 'eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyN2Y1Y2U0My0yMGViLTQ5YmQtYWNiZS04ZWJlMTE2OTllM2UifQ.eyJqdGkiOiJlYzg5OTE4ZC03MGM3LTQ1ZDktYjcwZi03MmI1ZGE3NTliYTUiLCJleHAiOjE1NzE3Mzc3OTQsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo5MDkwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6IjZiMTRkYTE2LWM5MjgtNDg4Ni05MmE2LTQyYmE3MjU1YzU0NSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJteWNsaWVudC01YjhkZmNhNCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjAzZTg3MTZiLWVkZGUtNGFmOC05OWVhLTRiOGQ1OTg0OTk4YSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIn0.SLiA9n7airjzgB5XbPw5XPE7uimRhTB4PIzhjGheJnU',\n", " 'token_type': 'bearer',\n", " 'not-before-policy': 0,\n", " 'session_state': '03e8716b-edde-4af8-99ea-4b8d5984998a',\n", " 'scope': 'email profile'}" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r = session.post(\n", " token_endpoint,\n", " data={\n", " \"grant_type\": \"client_credentials\",\n", " \"client_id\": cc_client,\n", " \"client_secret\": cc_client_secret,\n", " }\n", ")\n", "r.raise_for_status()\n", "r.json()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## JWT inspection\n", "\n", "Extract access token and inspect it (assuming it is a JWT token)." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "({'alg': 'RS256',\n", " 'typ': 'JWT',\n", " 'kid': 'WTGS1rr7ZScdVSNCWWy3KbUBW4UZP76dVuWYxE7XJv0'},\n", " {'jti': '487b963c-2c24-402f-8179-6b15901b9181',\n", " 'exp': 1571736054,\n", " 'nbf': 0,\n", " 'iat': 1571735994,\n", " 'iss': 'http://localhost:9090/auth/realms/master',\n", " 'aud': 'account',\n", " 'sub': '6b14da16-c928-4886-92a6-42ba7255c545',\n", " 'typ': 'Bearer',\n", " 'azp': 'myclient-5b8dfca4',\n", " 'auth_time': 0,\n", " 'session_state': '03e8716b-edde-4af8-99ea-4b8d5984998a',\n", " 'acr': '1',\n", " 'realm_access': {'roles': ['offline_access', 'uma_authorization']},\n", " 'resource_access': {'account': {'roles': ['manage-account',\n", " 'manage-account-links',\n", " 'view-profile']}},\n", " 'scope': 'email profile',\n", " 'clientHost': '172.17.0.1',\n", " 'email_verified': False,\n", " 'clientId': 'myclient-5b8dfca4',\n", " 'preferred_username': 'service-account-myclient-5b8dfca4',\n", " 'clientAddress': '172.17.0.1',\n", " 'email': 'service-account-myclient-5b8dfca4@placeholder.org'})" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "access_token = r.json()[\"access_token\"]\n", "jwt_decode(access_token)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Query `userinfo`\n", "\n", "Check the access token against the `userinfo` endpoint" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " Did request: GET http://localhost:9090/auth/realms/master/protocol/openid-connect/userinfo with
    \n", "
  • body None
  • \n", "
  • headers {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXVEdTMXJyN1pTY2RWU05DV1d5M0tiVUJXNFVaUDc2ZFZ1V1l4RTdYSnYwIn0.eyJqdGkiOiI0ODdiOTYzYy0yYzI0LTQwMmYtODE3OS02YjE1OTAxYjkxODEiLCJleHAiOjE1NzE3MzYwNTQsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjZiMTRkYTE2LWM5MjgtNDg4Ni05MmE2LTQyYmE3MjU1YzU0NSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15Y2xpZW50LTViOGRmY2E0IiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiMDNlODcxNmItZWRkZS00YWY4LTk5ZWEtNGI4ZDU5ODQ5OThhIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNsaWVudElkIjoibXljbGllbnQtNWI4ZGZjYTQiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtbXljbGllbnQtNWI4ZGZjYTQiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSIsImVtYWlsIjoic2VydmljZS1hY2NvdW50LW15Y2xpZW50LTViOGRmY2E0QHBsYWNlaG9sZGVyLm9yZyJ9.dplwSv8nByYO7Xh7O8Vl1EGAtXLP6W87KGzUOCwN73tCcAJbX3HhbYvP36f489dx1wj8n1Wb_FwNbASED1XGZb_pbps07YO68OlcJjkIafXsLXK98tSaQJ3hurn-lSa8DA3_-A5MnW-XR_7dFad1Guo0RSypv94ybZEFX8RMFWcUeVmsEkRLlAfP2d9WMnZ2N8d08jAr5FBbYedhNBX8VOWIm05Ho5hv8h7OtnS3fLVsfCtm36s6sQCYfGlfgfGc_bg1O9mtPHLfn5sf-j6SUBYpnfIBFXJBx4tChfd4Vryoa5tloqMiRsx1Xq96jiuWGQjLvbKjXTM3n_XB4UVuWQ'}
  • \n", "
  • ⇒ response 200
  • \n", "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "{'sub': '6b14da16-c928-4886-92a6-42ba7255c545',\n", " 'email_verified': False,\n", " 'preferred_username': 'service-account-myclient-5b8dfca4',\n", " 'email': 'service-account-myclient-5b8dfca4@placeholder.org'}" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r = session.get(\n", " provider_info[\"userinfo_endpoint\"], \n", " headers={\"Authorization\": \"Bearer %s\" % access_token}\n", ")\n", "r.raise_for_status()\n", "r.json()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Resource Owner Password Flow" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create a client that allows resource owner password flow." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:keycloak-admin:Creating client with settings {'id': 'myclient-9fcf2d4e', 'publicClient': True, 'directAccessGrantsEnabled': True}\n" ] }, { "data": { "text/plain": [ "'myclient-9fcf2d4e'" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pwd_client = keycloak_admin.create_client({\n", " \"publicClient\": True,\n", " \"directAccessGrantsEnabled\": True,\n", "})\n", "pwd_client" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "user, password = keycloak_admin.create_user()" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " Did request: POST http://localhost:9090/auth/realms/master/protocol/openid-connect/token with
    \n", "
  • body 'grant_type=password&username=John-d2c1d40b&password=j0hn&client_id=myclient-9fcf2d4e'
  • \n", "
  • headers {'Content-Length': '84', 'Content-Type': 'application/x-www-form-urlencoded'}
  • \n", "
  • ⇒ response 200
  • \n", "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "{'access_token': 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXVEdTMXJyN1pTY2RWU05DV1d5M0tiVUJXNFVaUDc2ZFZ1V1l4RTdYSnYwIn0.eyJqdGkiOiJkZjkxMjZmZi1lN2YyLTQxYzUtODdiMi1mMmZkMWQwMDM5OGQiLCJleHAiOjE1NzE3MzYwNTQsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImFhNGYxZjExLTk5N2EtNDY4Yi05ZDE1LTA1NmMyMjBjMTQ1NSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15Y2xpZW50LTlmY2YyZDRlIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiNGM5OWQ5ODAtZDVlOC00MzU3LTkxMjQtNjI4NWU3ZGVkNzM3IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqb2huLWQyYzFkNDBiIn0.SzQsIuL1O-IIdJIILGq4uAuh8ZXwTtdSiu7TdFZ3uwnM96FGwbGFxP9InlWd-QJijkd3P_01Dh0OP00HtSWkvBseuRKEMnPcGqhaTl89sMzKGxGgvAfVXJXTcsrTPjDA1jG1kJrd4Bqsafy-hNCsTTAo2t3KHKDAQcPRAq31-DqK4pbGDC6NP7y7CKs4e3LoDYOXdtE-IlUwqUTF05XHwhrjzickzS-hj2tnszK-PWndAcmDTljsjhENR_IBM9mjiQNNhqjgvykANSnJlDQxwZIPekFs0_yWoYPy7iAbcVGjCO6GaSHEM--bTdxVFemwHs4Zh7gXuC5IG0_YMxAmqQ',\n", " 'expires_in': 60,\n", " 'refresh_expires_in': 1800,\n", " 'refresh_token': 'eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyN2Y1Y2U0My0yMGViLTQ5YmQtYWNiZS04ZWJlMTE2OTllM2UifQ.eyJqdGkiOiIxZmUxMGY5Yi0wMWQ3LTQ3ZDktYjAxZi1iY2ZjOGZkNWNmZTIiLCJleHAiOjE1NzE3Mzc3OTQsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo5MDkwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6ImFhNGYxZjExLTk5N2EtNDY4Yi05ZDE1LTA1NmMyMjBjMTQ1NSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJteWNsaWVudC05ZmNmMmQ0ZSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjRjOTlkOTgwLWQ1ZTgtNDM1Ny05MTI0LTYyODVlN2RlZDczNyIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIn0.3GxXeXBFeImFuvuW1qJQ-a11dxAdXzHX8jOjboXYbbw',\n", " 'token_type': 'bearer',\n", " 'not-before-policy': 0,\n", " 'session_state': '4c99d980-d5e8-4357-9124-6285e7ded737',\n", " 'scope': 'email profile'}" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r = session.post(\n", " token_endpoint,\n", " data={\n", " \"grant_type\": \"password\",\n", " \"username\": user,\n", " \"password\": password,\n", " \"client_id\": pwd_client,\n", " }\n", ")\n", "r.raise_for_status()\n", "r.json()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## JWT inspection\n", "\n", "Extract access token and inspect it (assuming it is a JWT token)." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "({'alg': 'RS256',\n", " 'typ': 'JWT',\n", " 'kid': 'WTGS1rr7ZScdVSNCWWy3KbUBW4UZP76dVuWYxE7XJv0'},\n", " {'jti': 'df9126ff-e7f2-41c5-87b2-f2fd1d00398d',\n", " 'exp': 1571736054,\n", " 'nbf': 0,\n", " 'iat': 1571735994,\n", " 'iss': 'http://localhost:9090/auth/realms/master',\n", " 'aud': 'account',\n", " 'sub': 'aa4f1f11-997a-468b-9d15-056c220c1455',\n", " 'typ': 'Bearer',\n", " 'azp': 'myclient-9fcf2d4e',\n", " 'auth_time': 0,\n", " 'session_state': '4c99d980-d5e8-4357-9124-6285e7ded737',\n", " 'acr': '1',\n", " 'realm_access': {'roles': ['offline_access', 'uma_authorization']},\n", " 'resource_access': {'account': {'roles': ['manage-account',\n", " 'manage-account-links',\n", " 'view-profile']}},\n", " 'scope': 'email profile',\n", " 'email_verified': False,\n", " 'preferred_username': 'john-d2c1d40b'})" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "access_token = r.json()[\"access_token\"]\n", "jwt_decode(access_token)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Query `userinfo`\n", "\n", "Check the access token against the `userinfo` endpoint." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " Did request: GET http://localhost:9090/auth/realms/master/protocol/openid-connect/userinfo with
    \n", "
  • body None
  • \n", "
  • headers {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXVEdTMXJyN1pTY2RWU05DV1d5M0tiVUJXNFVaUDc2ZFZ1V1l4RTdYSnYwIn0.eyJqdGkiOiJkZjkxMjZmZi1lN2YyLTQxYzUtODdiMi1mMmZkMWQwMDM5OGQiLCJleHAiOjE1NzE3MzYwNTQsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImFhNGYxZjExLTk5N2EtNDY4Yi05ZDE1LTA1NmMyMjBjMTQ1NSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15Y2xpZW50LTlmY2YyZDRlIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiNGM5OWQ5ODAtZDVlOC00MzU3LTkxMjQtNjI4NWU3ZGVkNzM3IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqb2huLWQyYzFkNDBiIn0.SzQsIuL1O-IIdJIILGq4uAuh8ZXwTtdSiu7TdFZ3uwnM96FGwbGFxP9InlWd-QJijkd3P_01Dh0OP00HtSWkvBseuRKEMnPcGqhaTl89sMzKGxGgvAfVXJXTcsrTPjDA1jG1kJrd4Bqsafy-hNCsTTAo2t3KHKDAQcPRAq31-DqK4pbGDC6NP7y7CKs4e3LoDYOXdtE-IlUwqUTF05XHwhrjzickzS-hj2tnszK-PWndAcmDTljsjhENR_IBM9mjiQNNhqjgvykANSnJlDQxwZIPekFs0_yWoYPy7iAbcVGjCO6GaSHEM--bTdxVFemwHs4Zh7gXuC5IG0_YMxAmqQ'}
  • \n", "
  • ⇒ response 200
  • \n", "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "{'sub': 'aa4f1f11-997a-468b-9d15-056c220c1455',\n", " 'email_verified': False,\n", " 'preferred_username': 'john-d2c1d40b'}" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r = session.get(\n", " provider_info[\"userinfo_endpoint\"], \n", " headers={\"Authorization\": \"Bearer %s\" % access_token}\n", ")\n", "r.raise_for_status()\n", "r.json()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Authorization Code Flow\n", "\n", "Create a client that allows the Authorization Code flow" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:keycloak-admin:Creating client with settings {'id': 'myclient-ebda8bec', 'publicClient': False, 'redirectUris': ['https://example.com/redir']}\n", "INFO:keycloak-admin:Client secret response: '{\"type\":\"secret\",\"value\":\"b08a98e8-8eff-41e7-ba0a-7d6a42d5624c\"}'\n" ] }, { "data": { "text/plain": [ "'myclient-ebda8bec'" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "redirect_uri = \"https://example.com/redir\"\n", "ac_client = keycloak_admin.create_client({\n", " \"publicClient\": False,\n", " \"redirectUris\": [redirect_uri]\n", "})\n", "ac_client_secret = keycloak_admin.get_client_secret(ac_client)\n", "ac_client" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And create a user" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "user, password = keycloak_admin.create_user()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Start Authorization Code flow: we are forwarded to the OpenID provider (log in page)." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " Did request: GET http://localhost:9090/auth/realms/master/protocol/openid-connect/auth?response_type=code&client_id=myclient-ebda8bec&scope=openid&redirect_uri=https%3A%2F%2Fexample.com%2Fredir&state=foobar with
    \n", "
  • body None
  • \n", "
  • headers {}
  • \n", "
  • ⇒ response 200
  • \n", "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "r = session.get(\n", " url=provider_info[\"authorization_endpoint\"],\n", " params={\n", " \"response_type\": \"code\",\n", " \"client_id\": ac_client,\n", " \"scope\": \"openid\",\n", " \"redirect_uri\": redirect_uri,\n", " \"state\": \"foobar\",\n", " },\n", " headers={},\n", " allow_redirects=False\n", ")\n", "r.raise_for_status()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Extract the form action so we can submit the form (the requests session will take care of the cookies)." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'http://localhost:9090/auth/realms/master/login-actions/authenticate?session_code=nmC60BxdB7AYDTauUUN40p2Ek7OV7lREddQOfDKrBb0&execution=fdf3c29c-a1f3-4d24-82f8-00fb7a8b8115&client_id=myclient-ebda8bec&tab_id=OdTQc95vCT0'" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "form_action = html.unescape(re.search('\n", " Did request: POST http://localhost:9090/auth/realms/master/login-actions/authenticate?session_code=nmC60BxdB7AYDTauUUN40p2Ek7OV7lREddQOfDKrBb0&execution=fdf3c29c-a1f3-4d24-82f8-00fb7a8b8115&client_id=myclient-ebda8bec&tab_id=OdTQc95vCT0 with
    \n", "
  • body 'username=John-8d7b0df7&password=j0hn'
  • \n", "
  • headers {'Cookie': 'AUTH_SESSION_ID=712195f9-6dce-4379-9cd7-49c8583c2ecb.cacb8db48b6c; KC_RESTART=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyN2Y1Y2U0My0yMGViLTQ5YmQtYWNiZS04ZWJlMTE2OTllM2UifQ.eyJjaWQiOiJteWNsaWVudC1lYmRhOGJlYyIsInB0eSI6Im9wZW5pZC1jb25uZWN0IiwicnVyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcmVkaXIiLCJhY3QiOiJBVVRIRU5USUNBVEUiLCJub3RlcyI6eyJzY29wZSI6Im9wZW5pZCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTA5MC9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJyZXNwb25zZV90eXBlIjoiY29kZSIsInJlZGlyZWN0X3VyaSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcmVkaXIiLCJzdGF0ZSI6ImZvb2JhciJ9fQ.vbgffpCecauRHM7FztWsw0R3kN1n3Xq6fMGLYDXUG3w', 'Content-Length': '36', 'Content-Type': 'application/x-www-form-urlencoded'}
  • \n", "
  • ⇒ response 302
  • \n", "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "https://example.com/redir?state=foobar&session_state=712195f9-6dce-4379-9cd7-49c8583c2ecb&code=0c28e3db-08b5-423e-8432-d42a7dc1c538.712195f9-6dce-4379-9cd7-49c8583c2ecb.myclient-ebda8bec\n" ] }, { "data": { "text/plain": [ "['0c28e3db-08b5-423e-8432-d42a7dc1c538.712195f9-6dce-4379-9cd7-49c8583c2ecb.myclient-ebda8bec']" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r = session.post(\n", " form_action, \n", " data={\"username\": user, \"password\": password},\n", " allow_redirects=False\n", ")\n", "assert r.status_code == 302\n", "redirect = r.headers['Location']\n", "print(redirect)\n", "session.cookies.clear()\n", "\n", "redirect_params = urllib.parse.parse_qs(urllib.parse.urlparse(redirect).query)\n", "auth_code = redirect_params[\"code\"]\n", "auth_code" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Exchange authorization code for an access token." ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " Did request: POST http://localhost:9090/auth/realms/master/protocol/openid-connect/token with
    \n", "
  • body 'grant_type=authorization_code&client_id=myclient-ebda8bec&client_secret=b08a98e8-8eff-41e7-ba0a-7d6a42d5624c&redirect_uri=https%3A%2F%2Fexample.com%2Fredir&code=0c28e3db-08b5-423e-8432-d42a7dc1c538.712195f9-6dce-4379-9cd7-49c8583c2ecb.myclient-ebda8bec'
  • \n", "
  • headers {'Content-Length': '252', 'Content-Type': 'application/x-www-form-urlencoded'}
  • \n", "
  • ⇒ response 200
  • \n", "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "{'access_token': 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXVEdTMXJyN1pTY2RWU05DV1d5M0tiVUJXNFVaUDc2ZFZ1V1l4RTdYSnYwIn0.eyJqdGkiOiJmNjgzZGFiZC0xNDE1LTQ1NGUtYTFjZi0xMjEzZmRiZmNkYjYiLCJleHAiOjE1NzE3MzYwNTUsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjllMjMyYWM0LTk0NjUtNDRhNC1hY2RkLWJjYTBhMTcxMmQxOCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15Y2xpZW50LWViZGE4YmVjIiwiYXV0aF90aW1lIjoxNTcxNzM1OTk1LCJzZXNzaW9uX3N0YXRlIjoiNzEyMTk1ZjktNmRjZS00Mzc5LTljZDctNDljODU4M2MyZWNiIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwczovL2V4YW1wbGUuY29tIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiam9obi04ZDdiMGRmNyJ9.JKhhWaR2d1HxSL0MiJGMFAnuPSFAK9ia29aLotl5aEMKrRrF5pqk4lZbvVXRE4kAt2SONHZacAWRhlDmeapmNRitNbc2OQTCPGwQFsHPFxiV5B3VShl_4-JPNcbHxOt5NMz7aJ5I5uMym5-XNxZXAyHTb_AQMpd7OFiNtHRVCvrlUjt-klQRgvQZpK413pmLL4hBBIh7rjCi07rPtLoWa9og_vXzzhUIq8zfB19KNq_BjhJRhP2GUAlg-6Cu1SRpzdB5ZB3ljMpb45otxB7mVZ-TsqxPzK7hwboOK7F0zdF37SEAI5kJ-ShCBm4NtA_0W7qQch8wNl-KeM3-p5t6TA',\n", " 'expires_in': 60,\n", " 'refresh_expires_in': 1800,\n", " 'refresh_token': 'eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyN2Y1Y2U0My0yMGViLTQ5YmQtYWNiZS04ZWJlMTE2OTllM2UifQ.eyJqdGkiOiIyNWNhNDU1Yy0yZGQ5LTRiZDAtOWZhZi1hYWIyMGZhMGY4NzgiLCJleHAiOjE1NzE3Mzc3OTUsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo5MDkwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6IjllMjMyYWM0LTk0NjUtNDRhNC1hY2RkLWJjYTBhMTcxMmQxOCIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJteWNsaWVudC1lYmRhOGJlYyIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjcxMjE5NWY5LTZkY2UtNDM3OS05Y2Q3LTQ5Yzg1ODNjMmVjYiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSJ9.hRLfBXIV2AIVYQOt54a38AhqT3BhJUU1iky0BnAYMeU',\n", " 'token_type': 'bearer',\n", " 'id_token': 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXVEdTMXJyN1pTY2RWU05DV1d5M0tiVUJXNFVaUDc2ZFZ1V1l4RTdYSnYwIn0.eyJqdGkiOiJmMDcwNjJkOC1hYTNmLTQwZTgtOWNjMi05YmZiZWJkMWRmNWYiLCJleHAiOjE1NzE3MzYwNTUsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoibXljbGllbnQtZWJkYThiZWMiLCJzdWIiOiI5ZTIzMmFjNC05NDY1LTQ0YTQtYWNkZC1iY2EwYTE3MTJkMTgiLCJ0eXAiOiJJRCIsImF6cCI6Im15Y2xpZW50LWViZGE4YmVjIiwiYXV0aF90aW1lIjoxNTcxNzM1OTk1LCJzZXNzaW9uX3N0YXRlIjoiNzEyMTk1ZjktNmRjZS00Mzc5LTljZDctNDljODU4M2MyZWNiIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiam9obi04ZDdiMGRmNyJ9.feAZA3yuoNY1UxtprO5RzpQ8ZVphgj10wVg-NmXrowZok9sqgW7n7n77_E5bfQqd40v6Z99GsEUVj5uLCPMCwiGw7prlciQnq-RDT_jKiJ5gmA85LXNGiehDHFEKnkL0SYkMUvizKJ31yZddi6P_w4oNfyy05tkMtaApWKXz9KZO4paGHNMO4lK-Vmn3NLkSEP56Cqgj3I1OsZYqWdZqhIEC-8AxufH9QUKMEGD07SLDtbO0W_-GRmyyRzjk42FOtzU-Fb6B9Q5XUJomsLwhnGI1gzkjfj_FoMNwtfpVRfVsGu76FK7uFSFkS-_R3svpaese-5n7HWBxXnz5PricEQ',\n", " 'not-before-policy': 0,\n", " 'session_state': '712195f9-6dce-4379-9cd7-49c8583c2ecb',\n", " 'scope': 'openid email profile'}" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r = session.post(\n", " url=token_endpoint,\n", " data={\n", " \"grant_type\": \"authorization_code\",\n", " \"client_id\": ac_client,\n", " \"client_secret\": ac_client_secret,\n", " \"redirect_uri\": redirect_uri,\n", " \"code\": auth_code,\n", " },\n", " allow_redirects=False\n", ")\n", "r.raise_for_status()\n", "r.json()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## JWT inspection\n", "\n", "Extract access token and inspect it (assuming it is a JWT token)." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "({'alg': 'RS256',\n", " 'typ': 'JWT',\n", " 'kid': 'WTGS1rr7ZScdVSNCWWy3KbUBW4UZP76dVuWYxE7XJv0'},\n", " {'jti': 'f683dabd-1415-454e-a1cf-1213fdbfcdb6',\n", " 'exp': 1571736055,\n", " 'nbf': 0,\n", " 'iat': 1571735995,\n", " 'iss': 'http://localhost:9090/auth/realms/master',\n", " 'aud': 'account',\n", " 'sub': '9e232ac4-9465-44a4-acdd-bca0a1712d18',\n", " 'typ': 'Bearer',\n", " 'azp': 'myclient-ebda8bec',\n", " 'auth_time': 1571735995,\n", " 'session_state': '712195f9-6dce-4379-9cd7-49c8583c2ecb',\n", " 'acr': '1',\n", " 'allowed-origins': ['https://example.com'],\n", " 'realm_access': {'roles': ['offline_access', 'uma_authorization']},\n", " 'resource_access': {'account': {'roles': ['manage-account',\n", " 'manage-account-links',\n", " 'view-profile']}},\n", " 'scope': 'openid email profile',\n", " 'email_verified': False,\n", " 'preferred_username': 'john-8d7b0df7'})" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "access_token = r.json()[\"access_token\"]\n", "jwt_decode(access_token)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Query `userinfo`\n", "\n", "Check the access token against the `userinfo` endpoint." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " Did request: GET http://localhost:9090/auth/realms/master/protocol/openid-connect/userinfo with
    \n", "
  • body None
  • \n", "
  • headers {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXVEdTMXJyN1pTY2RWU05DV1d5M0tiVUJXNFVaUDc2ZFZ1V1l4RTdYSnYwIn0.eyJqdGkiOiJmNjgzZGFiZC0xNDE1LTQ1NGUtYTFjZi0xMjEzZmRiZmNkYjYiLCJleHAiOjE1NzE3MzYwNTUsIm5iZiI6MCwiaWF0IjoxNTcxNzM1OTk1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjllMjMyYWM0LTk0NjUtNDRhNC1hY2RkLWJjYTBhMTcxMmQxOCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15Y2xpZW50LWViZGE4YmVjIiwiYXV0aF90aW1lIjoxNTcxNzM1OTk1LCJzZXNzaW9uX3N0YXRlIjoiNzEyMTk1ZjktNmRjZS00Mzc5LTljZDctNDljODU4M2MyZWNiIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwczovL2V4YW1wbGUuY29tIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiam9obi04ZDdiMGRmNyJ9.JKhhWaR2d1HxSL0MiJGMFAnuPSFAK9ia29aLotl5aEMKrRrF5pqk4lZbvVXRE4kAt2SONHZacAWRhlDmeapmNRitNbc2OQTCPGwQFsHPFxiV5B3VShl_4-JPNcbHxOt5NMz7aJ5I5uMym5-XNxZXAyHTb_AQMpd7OFiNtHRVCvrlUjt-klQRgvQZpK413pmLL4hBBIh7rjCi07rPtLoWa9og_vXzzhUIq8zfB19KNq_BjhJRhP2GUAlg-6Cu1SRpzdB5ZB3ljMpb45otxB7mVZ-TsqxPzK7hwboOK7F0zdF37SEAI5kJ-ShCBm4NtA_0W7qQch8wNl-KeM3-p5t6TA'}
  • \n", "
  • ⇒ response 200
  • \n", "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "{'sub': '9e232ac4-9465-44a4-acdd-bca0a1712d18',\n", " 'email_verified': False,\n", " 'preferred_username': 'john-8d7b0df7'}" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r = session.get(\n", " provider_info[\"userinfo_endpoint\"], \n", " headers={\"Authorization\": \"Bearer %s\" % access_token}\n", ")\n", "r.raise_for_status()\n", "r.json()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 2 }