I’ve been trying to read/write to a pod via Python, with bearer tokens. I would prefer to use bearer tokens (as opposed to DPoP) if possible because there don’t seem to be any DPoP implementations for Python yet. (I understand in the future DPoP will be needed for bigger Solid apps because of some reasons having to do with separating resources and auth.)
I got so far to get an access token back, but a GET for ://agentydragon.solidcommunity.net/private
returns 403.
(Because I am new on this forum, the forum software is telling me I can’t have
2 links per post. So I’m just leaving out all the
https
to get around it,
there’s just no easy way to make this post have <=2 URLs…)
An OPTIONS for the same URL returns this header:
'WWW-Authenticate': 'Bearer realm="://solidcommunity.net", error="access_denied", error_description="Token does not pass the audience allow filter"'
I have found ://github.com/solid/node-solid-server/issues/1061, and ://github.com/solid/node-solid-server/issues/1082. From what I understand those have to do with the server giving access tokens with aud
set to just one element - my client ID.
Here’s my code. Please excuse the mess. I tried also using requests-oauthlib
and pyoidc
packages, which are less low-level than this code, but got the same issue.
_ISSUER = '://solidcommunity.net/'
_OID_CALLBACK_PATH = "/oid_callback"
import json
import jwt
import os
import re
import hashlib
import requests
import urllib
from absl import logging, app
from oic.oic import Client as OicClient
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
import base64
REDIRECT_URL = "://rai-local-test" + _OID_CALLBACK_PATH
def main(_):
oic_client = OicClient(client_authn_method=CLIENT_AUTHN_METHOD)
# Provider info discovery.
# ://pyoidc.readthedocs.io/en/latest/examples/rp.html#provider-info-discovery
provider_info = requests.get(_ISSUER +
".well-known/openid-configuration").json()
logging.info("Provider info: %s", provider_info)
# Client registration.
# ://pyoidc.readthedocs.io/en/latest/examples/rp.html#client-registration
registration_response = oic_client.register(
provider_info['registration_endpoint'], redirect_uris=[REDIRECT_URL])
logging.info("Registration response: %s", registration_response)
from authlib.integrations.requests_client import OAuth2Session
code_verifier = base64.urlsafe_b64encode(os.urandom(40)).decode('utf-8')
code_verifier = re.sub('[^a-zA-Z0-9]+', '', code_verifier)
code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge).decode('utf-8')
code_challenge = code_challenge.replace('=', '')
state = "foobarbaz"
query = urllib.parse.urlencode({
"code_challenge":
code_challenge,
"state":
state,
"response_type":
"code",
"redirect_uri":
REDIRECT_URL,
"code_challenge_method":
"S256",
"client_id":
registration_response['client_id'],
# offline_access: also asks for refresh token
"scope":
"openid offline_access",
})
url = provider_info['authorization_endpoint'] + '?' + query
logging.info("Visit: %s", url)
redirected_to = input("Redirected to: ")
# ://localhost:3000/oid_callback?code=d7ee1fc81c7bd3bd18f35b20d5ee3e5a&state=foobarbaz
query = urllib.parse.urlparse(redirected_to).query
redirect_params = urllib.parse.parse_qs(query)
logging.info("Redirect params: %s", redirect_params)
auth_code = redirect_params['code'][0]
# Exchange auth code for access token
resp = requests.post(url=provider_info['token_endpoint'],
data={
"grant_type": "authorization_code",
"client_id": registration_response['client_id'],
"redirect_uri": REDIRECT_URL,
"code": auth_code,
"code_verifier": code_verifier,
"state": state
},
allow_redirects=False)
result = resp.json()
logging.info("%s", result)
# decode access and ID token
def _b64_decode(data):
data += '=' * (4 - len(data) % 4)
return base64.b64decode(data).decode('utf-8')
def jwt_payload_decode(jwt):
_, payload, _ = jwt.split('.')
return json.loads(_b64_decode(payload))
decoded_access_token = jwt_payload_decode(result['access_token'])
decoded_id_token = jwt_payload_decode(result['id_token'])
logging.info("access token: %s", decoded_access_token)
logging.info("id token: %s", decoded_id_token)
resp = requests.options(
url="://agentydragon.solidcommunity.net/private",
headers={'Authorization': ('Bearer ' + result['access_token'])})
logging.info("%s", resp.headers)
resp = requests.get(
url="://agentydragon.solidcommunity.net/private",
headers={'Authorization': ('Bearer ' + result['access_token'])})
logging.info("%d %s", resp.status_code, resp.text)
Here’s output I got from running it, with a few bits redacted:
I0208 00:58:41.356953 139765113108288 authlib_solid_main.py:25] Provider info: {'issuer': '://solidcommunity.net', 'jwks_uri': '://solidcommunity.net/jwks', 'response_types_supported': ['code', 'code token', 'code id_token', 'id_token code', 'id_token', 'id_token token', 'code id_token token', 'none'], 'token_types_supported': ['legacyPop', 'dpop'], 'response_modes_supported': ['query', 'fragment'], 'grant_types_supported': ['authorization_code', 'implicit', 'refresh_token', 'client_credentials'], 'subject_types_supported': ['public'], 'id_token_signing_alg_values_supported': ['RS256'], 'token_endpoint_auth_methods_supported': 'client_secret_basic', 'token_endpoint_auth_signing_alg_values_supported': ['RS256'], 'display_values_supported': [], 'claim_types_supported': ['normal'], 'claims_supported': [], 'claims_parameter_supported': False, 'request_parameter_supported': True, 'request_uri_parameter_supported': False, 'require_request_uri_registration': False, 'check_session_iframe': '://solidcommunity.net/session', 'end_session_endpoint': '://solidcommunity.net/logout', 'authorization_endpoint': '://solidcommunity.net/authorize', 'token_endpoint': '://solidcommunity.net/token', 'userinfo_endpoint': '://solidcommunity.net/userinfo', 'registration_endpoint': '://solidcommunity.net/register'}
I0208 00:58:41.527720 139765113108288 authlib_solid_main.py:33] Registration response: {'redirect_uris': ['://rai-local-test/oid_callback'], 'client_id': '27b64c4dfdbfc7a8ee6fd54b5ce7a585', 'client_secret': '7187f41e3805e7849c4c9cdfcab236a4', 'response_types': ['code'], 'grant_types': ['authorization_code'], 'application_type': 'web', 'id_token_signed_response_alg': 'RS256', 'token_endpoint_auth_method': 'client_secret_basic', 'registration_access_token': 'eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NvbGlkY29tbXVuaXR5Lm5ldCIsImF1ZCI6IjI3YjY0YzRkZmRiZmM3YThlZTZmZDU0YjVjZTdhNTg1Iiwic3ViIjoiMjdiNjRjNGRmZGJmYzdhOGVlNmZkNTRiNWNlN2E1ODUifQ.JJLZPFD3dMLMUVgniBO4lPlQ7_YkufuQ7vbgHDGRjp0ryCXY8QfoFm0adMsktBe4Ju8Pm9Eh7ff9Plb8IKlUMUY_m2-FQfaFAmFoYRKxbyCxYvss9NPE0nyPoaWuu3a92NhRPvLAVkRp176P5P9O7JS00M8l8PaE5jZ0VwKDnNLg9vujATpPAYtyeGQlKRIFTkpgXWTt-pZxf621jYD4U-qu5W2krGHCIIikOTHS8t7o4m6U0k2IgLre5n6Svi0rK1VIK63nYcDyoug9tMRqDyy0EvctRb4eG9Smc_L8YBgFi-oi99ud4BTSsY2JgT16MdIeMkYFlD94yGt_itDWpA', 'registration_client_uri': '://solidcommunity.net/register/27b64c4dfdbfc7a8ee6fd54b5ce7a585', 'client_id_issued_at': 1612742321, 'client_secret_expires_at': 0}
I0208 00:58:41.574192 139765113108288 authlib_solid_main.py:63] Visit: ://solidcommunity.net/authorize?code_challenge=R3C1vhZMfgr7isf2dJRhtWU69il3Yc0KkKFRqJbr_Iw&state=foobarbaz&response_type=code&redirect_uri=%3A%2F%2Frai-local-test%2Foid_callback&code_challenge_method=S256&client_id=27b64c4dfdbfc7a8ee6fd54b5ce7a585&scope=openid+offline_access
Redirected to: ://rai-local-test/oid_callback?code=7089d1c3b5dee6c9ce8f427d9b1b2ed7&state=foobarbaz
I0208 00:58:47.476675 139765113108288 authlib_solid_main.py:70] Redirect params: {'code': ['7089d1c3b5dee6c9ce8f427d9b1b2ed7'], 'state': ['foobarbaz']}
I0208 00:58:47.758980 139765113108288 authlib_solid_main.py:85] {'access_token': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IkpxS29zX2J0SHBnIn0.eyJpc3MiOiJodHRwczovL3NvbGlkY29tbXVuaXR5Lm5ldCIsImF1ZCI6WyIyN2I2NGM0ZGZkYmZjN2E4ZWU2ZmQ1NGI1Y2U3YTU4NSJdLCJzdWIiOiJodHRwczovL2FnZW50eWRyYWdvbi5zb2xpZGNvbW11bml0eS5uZXQvcHJvZmlsZS9jYXJkI21lIiwiZXhwIjoxNjEzOTUxOTI3LCJpYXQiOjE2MTI3NDIzMjcsImp0aSI6ImRjYWNhOTkxNzMyM2FmODEiLCJzY29wZSI6Im9wZW5pZCBvZmZsaW5lX2FjY2VzcyJ9.sb_kreCbUHwaKcqq8OBKt3LgYofy69NGKCvfG3upZxEX6IcQ86sIK5X04NCnOmSWpmrUNvmv0nfM1nuQrRFhUtQWA5wX1BAjfE8aJBCnCrQkGwIwDa_3dAXnsKykuJMUvYp39UWQZIDRr08CC5Jjby8ud6n9XeOAAxdf9RD3La3kTAMtUiE05GQuErLi79PKCCEidf7ahkxMonnrFOgr_705L77getJkDPku_qOxbOr1tbXG-O9Jt03IYKPYsgz2dE-DEPjsP667nBiNzoXwptmE6oVyR1yp4CBY9AEylldKIiAjJsg1n30--f8baFZ-8_d7tLOJ2kSh_qUoLUXXew', 'token_type': 'Bearer', 'expires_in': 1209600, 'refresh_token': 'b5ca5a86b727be1a1bc6ce58fea2d657', 'id_token': 'eyJhbGciOiJSUzI1NiIsImtpZCI6InhoSFJkSFRqQmZRIn0.eyJpc3MiOiJodHRwczovL3NvbGlkY29tbXVuaXR5Lm5ldCIsImF1ZCI6IjI3YjY0YzRkZmRiZmM3YThlZTZmZDU0YjVjZTdhNTg1IiwiYXpwIjoiMjdiNjRjNGRmZGJmYzdhOGVlNmZkNTRiNWNlN2E1ODUiLCJzdWIiOiJodHRwczovL2FnZW50eWRyYWdvbi5zb2xpZGNvbW11bml0eS5uZXQvcHJvZmlsZS9jYXJkI21lIiwiZXhwIjoxNjEzOTUxOTI3LCJpYXQiOjE2MTI3NDIzMjcsImp0aSI6ImM3NzFiY2JlNDdhNDhiYjkiLCJhdF9oYXNoIjoiTUpBNjFyNHhYelhPcElJWGplNHNzUSJ9.GwE8hWAcqPChMnnJs2tkxfSBmkn2h3pUmBUkq7g4p5Zoq_JDGpCkYtfh42TlSwrAdsr_r_0ZRzm55XmoommOnZD84hxL708mHPInOKyxERTWnU3lxLwepieGb8Ga3gbUhIxpUWju7ZRQR-zvq_89NlFdD6WpDsr-zcLErlM60e-s4F8NQ7g3HfrF5wR-dZORJNAMFKEoNiJL2_EE1l0bc9d2oJBQNnypdp1XVi3qPSDDTkX-3vEg9_me7xtegGLL0LoyRTlSk_sFYmTFITxAtu-De7KpaPQ35QkYS_untRVXizpKpR0arqqhzcDMEZA_tqi02XNcQZo2tmAgmk3cBw'}
I0208 00:58:47.759160 139765113108288 authlib_solid_main.py:98] access token: {'iss': '://solidcommunity.net', 'aud': ['27b64c4dfdbfc7a8ee6fd54b5ce7a585'], 'sub': '://agentydragon.solidcommunity.net/profile/card#me', 'exp': 1613951927, 'iat': 1612742327, 'jti': 'dcaca9917323af81', 'scope': 'openid offline_access'}
I0208 00:58:47.759228 139765113108288 authlib_solid_main.py:99] id token: {'iss': '://solidcommunity.net', 'aud': '27b64c4dfdbfc7a8ee6fd54b5ce7a585', 'azp': '27b64c4dfdbfc7a8ee6fd54b5ce7a585', 'sub': '://agentydragon.solidcommunity.net/profile/card#me', 'exp': 1613951927, 'iat': 1612742327, 'jti': 'c771bcbe47a48bb9', 'at_hash': 'MJA61r4xXzXOpIIXje4ssQ'}
I0208 00:58:48.319766 139765113108288 authlib_solid_main.py:104] {'X-Powered-By': 'solid-server/5.6.3', 'Vary': 'Accept, Authorization, Origin, Access-Control-Request-Headers', 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Methods': 'OPTIONS,HEAD,GET,PATCH,POST,PUT,DELETE', 'Access-Control-Max-Age': '1728000', 'Access-Control-Expose-Headers': 'Authorization, User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Updates-Via, Allow, WAC-Allow, Content-Length, WWW-Authenticate, MS-Author-Via', 'Allow': 'COPY,GET,HEAD,POST,PATCH,PUT,DELETE', 'Link': '<://agentydragon.solidcommunity.net/.well-known/solid>; rel="service", <://solidcommunity.net>; rel="://openid.net/specs/connect/1.0/issuer", <private.acl>; rel="acl", <private.meta>; rel="describedBy", <://www.w3.org/ns/ldp#Resource>; rel="type"', 'Accept-Patch': 'application/sparql-update', 'WWW-Authenticate': 'Bearer realm="://solidcommunity.net", error="access_denied", error_description="Token does not pass the audience allow filter"', 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': '35', 'ETag': '<SNIP>"', 'Date': 'Sun, 07 Feb 2021 23:58:48 GMT', 'Connection': 'keep-alive'}
I0208 00:58:48.739730 139765113108288 authlib_solid_main.py:109] 403 <!doctype html>
...
<title>No permission</title>
...
<p>
You are currently logged in as <code></code>,
but do not have permission to access <code>://agentydragon.solidcommunity.net/private/</code>.
</p>
Earlier, a simpler version of this code that didn’t use the challenge-verifier scheme (and instead, IIRC, passed client_secret
to token endpoint), had the exact same issue.
The app has an entry in “trusted applications” with read/write/append access modes.
Finally, I decided to go back to basics and to check that bearer tokens actually work in the JS client libraries. So I opened the Node demo app (://github.com/inrupt/solid-client-authn-js/tree/master/packages/node/example/demoClientApp), and added tokenType: 'Bearer'
to the session.login
call. And I got this result:
So that makes me believe that it’s less likely a problem with my code, and more likely a breakage of bearer token auth in node-solid-server
.
Is there anything I’m doing obviously wrong in my code?
If what I’m trying to do is supposed to work, and I don’t have any problems in my code, could it be fixed? It’s a little frustrating, because I thought DPoP was an optional extension that I did not need in my client
3 posts - 1 participant