Bearer tokens don't work?

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 :frowning:

Only now, couple minutes after posting this, I found and read some of Solid-OIDC spec: SOLID-OIDC.

Section 6 says:

No option other that DPoP-bound access tokens is mentioned, so it sounds to me like they might not be supported either.

(In that case, maybe node-solid-server could give a more precise message, like "Bearer tokens not supported, use DPoP".)

OK, implementing DPoP ended up not being as hard as I assumed.

I managed to put together a simple Flask app that can read private data from a pod: Rai / solid-flask · GitLab

2 Likes