``` from typing import Any, Dict import requests from swh.deposit.exception import ( AuthenticationError, AuthenticationMisconfigurationError, BadRequestError, ) def token_url(auth_server: str) -> str: return f"{auth_server.rstrip('/')}/protocol/openid-connect/token" def authenticate(auth_server: str, username: str, password: str) -> Dict[str, Any]: """Login and create new offline OpenID Connect session. Args: auth_server: Keycloak authentication server url including the realm (e.g https://auth.softwareheritage.org/SoftwareHeritage) username: an existing username in the realm password: password associated to username Returns: The OpenID Connect session info """ response = requests.post( url=token_url(auth_server), data={ "grant_type": "password", "client_id": "swh-deposit", "scope": "openid offline_access", "username": username, "password": password, }, ) if response.ok: data = response.json() import pdb; pdb.set_trace() return data try: # try to parse some specific detail coming from keycloak if any data = response.json() except Exception: # No specific error (e.g. not found, ...), plainly raise the error response.raise_for_status() # Specific keycloak authentication issue map_error_code = { "invalid_grant": AuthenticationMisconfigurationError, "unsupported_grant_type": AuthenticationMisconfigurationError, "invalid_scope": AuthenticationMisconfigurationError, "invalid_client": AuthenticationError, "unauthorized_client": AuthenticationError, } error_code = data["error"] error_description = data.get("error_description", "") exception_factory = map_error_code.get(error_code, BadRequestError) raise exception_factory(f"{error_code}: {error_description}") url = "http://keycloak:8080/keycloak/auth/realms/SoftwareHeritage" username = "hal" password = "test" authenticate(url, username, password) ``` Finally! ``` In [7]: response = requests.post( ...: url=token_url(auth_server), ...: data={ ...: "grant_type": "password", ...: "client_id": "deposit", ...: "scope": "offline_access", ...: "username": username, ...: "password": password, ...: }, ...: ) ...: In [8]: response.json() Out[8]: {'error': 'invalid_client', 'error_description': 'INVALID_CREDENTIALS: Invalid client credentials'} In [9]: response = requests.post( ...: url=token_url(auth_server), ...: data={ ...: "grant_type": "password", ...: "client_id": "swh-deposit", ...: "scope": "openid offline_access", ...: "username": username, ...: "password": password, ...: }, ...: ) ...: In [10]: response.ok Out[10]: True In [12]: response.json() Out[12]: {'access_token': 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJtSzh2S28tNTlNZ0xZc0EyU1hvNDhYb1NoV2g4NXZMWFN0blE3d3M2Yl9ZIn0.eyJleHAiOjE2MTQwODcwOTYsImlhdCI6MTYxNDA4Njc5NiwianRpIjoiNGE3NzgzNWYtNmQwYy00MmMxLWFlOTYtOTAwOGRiMWZlNDgxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDgwL2tleWNsb2FrL2F1dGgvcmVhbG1zL1NvZnR3YXJlSGVyaXRhZ2UiLCJhdWQiOlsic3doLWRlcG9zaXQiLCJhY2NvdW50Il0sInN1YiI6ImM5N2NlMTM2LWZlMTQtNDFiOC04ODcxLTcxNGM4ZWUzNWVhNiIsInR5cCI6IkJlYXJlciIsImF6cCI6InN3aC1kZXBvc2l0Iiwic2Vzc2lvbl9zdGF0ZSI6IjI5MWJjMjQ0LTcyNGQtNDMyNC1iNGE4LTI0Y2RlMjc3OTMxZiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDo1MDA2Il0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsic3doLWRlcG9zaXQiOnsicm9sZXMiOlsic3doLmRlcG9zaXQuYXBpIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiSEFMIEFJIiwiZ3JvdXBzIjpbXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiaGFsIiwiZ2l2ZW5fbmFtZSI6IkhBTCIsImZhbWlseV9uYW1lIjoiQUkifQ.AXd_8vRsezWuBXdz380NvIOO2NDPTsi3zrTQam5Kn22_TIDVKX2dXY9qUIfm8S4c-WKYXxooTeA9qu-p5fHrVUddNFjKli4X23Fl05KvT4h8bgljlWD00XP2BWD87_5zxZaBolNQMa-4krNtfoTrAzXpQptmjDG4cJA9EbHYv8pb0DSgCv2gdZC94L8xxliQvJXN5X7Xuu8wstCJpSitT-AyTqtBwxoBuKhxy457RIdxRstjFSjwZYstRbJI8q7OtQ929VfznZsvbBmrzucrbUsgyqlCdO7p_D_aLNpv7uQ1cZdyAhNYFVXyIrJ1csRd9CR3rzp-Sx_JYZ1dXn4NlA', 'expires_in': 300, 'refresh_expires_in': 0, 'refresh_token': 'eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzNjExNTc2YS05OWFlLTRhMmMtYWJlOS1kZmQyNmY2ZGRlN2UifQ.eyJpYXQiOjE2MTQwODY3OTYsImp0aSI6ImQ3NDIyYWYzLTMxOTUtNDVkNC1hZTdjLWQ2YWU3ZmU0MWQ0ZSIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTA4MC9rZXljbG9hay9hdXRoL3JlYWxtcy9Tb2Z0d2FyZUhlcml0YWdlIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDgwL2tleWNsb2FrL2F1dGgvcmVhbG1zL1NvZnR3YXJlSGVyaXRhZ2UiLCJzdWIiOiJjOTdjZTEzNi1mZTE0LTQxYjgtODg3MS03MTRjOGVlMzVlYTYiLCJ0eXAiOiJPZmZsaW5lIiwiYXpwIjoic3doLWRlcG9zaXQiLCJzZXNzaW9uX3N0YXRlIjoiMjkxYmMyNDQtNzI0ZC00MzI0LWI0YTgtMjRjZGUyNzc5MzFmIiwic2NvcGUiOiJvcGVuaWQgb2ZmbGluZV9hY2Nlc3MgcHJvZmlsZSBlbWFpbCJ9.itjGoq7M2OcD6zVpn1339HRJYPRIAwNN5ms6-8kuYW0', 'token_type': 'Bearer', 'id_token': 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJtSzh2S28tNTlNZ0xZc0EyU1hvNDhYb1NoV2g4NXZMWFN0blE3d3M2Yl9ZIn0.eyJleHAiOjE2MTQwODcwOTYsImlhdCI6MTYxNDA4Njc5NiwiYXV0aF90aW1lIjowLCJqdGkiOiIyYjUyYTU4My0xMmY4LTRhZTQtYjY4OC0yNzJhYjA5MDJkZGEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwODAva2V5Y2xvYWsvYXV0aC9yZWFsbXMvU29mdHdhcmVIZXJpdGFnZSIsImF1ZCI6InN3aC1kZXBvc2l0Iiwic3ViIjoiYzk3Y2UxMzYtZmUxNC00MWI4LTg4NzEtNzE0YzhlZTM1ZWE2IiwidHlwIjoiSUQiLCJhenAiOiJzd2gtZGVwb3NpdCIsInNlc3Npb25fc3RhdGUiOiIyOTFiYzI0NC03MjRkLTQzMjQtYjRhOC0yNGNkZTI3NzkzMWYiLCJhdF9oYXNoIjoiMXV5djEtVVRha0g2R2QzTUxwUmViUSIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJIQUwgQUkiLCJncm91cHMiOltdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJoYWwiLCJnaXZlbl9uYW1lIjoiSEFMIiwiZmFtaWx5X25hbWUiOiJBSSJ9.AsthzCrFxoR96WACS3NXkFYsCV7rjM7kr3PM0f1_4DWa2mvgjCKoihtPhFnnxWOWNFmWm5rVfONoosjiArOI_Lh7EbVyQi2D2JpaLebPc725Ww2ARqNlUcepm_FyXRG3eQCmwnHWht37j91Jt3Q8FbWcJYHjIJgjg0tf2YMvdKLxEZHrl45GaqH_sJIF20pVDMd8vzRPiFQmixzwp_oqPvdpQ7vosk5BI6fanUMC_ZE1GzVicmW90bEMxVYAL8Sf8JLoXhkN2x6GEp_321C24DsBiIDHo3o6kafOuieJbYA7qdIgDztTTf-I042z0Uxa2xEJljWn5_Livi9fQh4qQg', 'not-before-policy': 0, 'session_state': '291bc244-724d-4324-b4a8-24cde277931f', 'scope': 'openid offline_access profile email'} ```