Konubinix' opinionated web of thoughts

Playing With Auth0 for the First Time

Fleeting

Auth0

I want a simple SPA. So IIUC, I only need an application, no API.

Once created, I get a domain, a client id and a client secret.

I can request the domain to find out the well-known OAuth 2.0 Authorization Server Metadata urls.

curl -fsSL https://${domain}/.well-known/oauth-authorization-server 2>&1
curl: (22) The requested URL returned error: 404
curl -fsSL https://${domain}/.well-known/openid-configuration | jq | sed "s|${domain}|\${domain}|"
{
  "issuer": "https://${domain}/",
  "authorization_endpoint": "https://${domain}/authorize",
  "token_endpoint": "https://${domain}/oauth/token",
  "device_authorization_endpoint": "https://${domain}/oauth/device/code",
  "userinfo_endpoint": "https://${domain}/userinfo",
  "mfa_challenge_endpoint": "https://${domain}/mfa/challenge",
  "jwks_uri": "https://${domain}/.well-known/jwks.json",
  "registration_endpoint": "https://${domain}/oidc/register",
  "revocation_endpoint": "https://${domain}/oauth/revoke",
  "scopes_supported": [
    "openid",
    "profile",
    "offline_access",
    "name",
    "given_name",
    "family_name",
    "nickname",
    "email",
    "email_verified",
    "picture",
    "created_at",
    "identities",
    "phone",
    "address"
  ],
  "response_types_supported": [
    "code",
    "token",
    "id_token",
    "code token",
    "code id_token",
    "token id_token",
    "code token id_token"
  ],
  "code_challenge_methods_supported": [
    "S256",
    "plain"
  ],
  "response_modes_supported": [
    "query",
    "fragment",
    "form_post"
  ],
  "subject_types_supported": [
    "public"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post",
    "private_key_jwt"
  ],
  "claims_supported": [
    "aud",
    "auth_time",
    "created_at",
    "email",
    "email_verified",
    "exp",
    "family_name",
    "given_name",
    "iat",
    "identities",
    "iss",
    "name",
    "nickname",
    "phone_number",
    "picture",
    "sub"
  ],
  "request_uri_parameter_supported": false,
  "request_parameter_supported": false,
  "id_token_signing_alg_values_supported": [
    "HS256",
    "RS256",
    "PS256"
  ],
  "token_endpoint_auth_signing_alg_values_supported": [
    "RS256",
    "RS384",
    "PS256"
  ],
  "backchannel_logout_supported": true,
  "backchannel_logout_session_supported": true,
  "end_session_endpoint": "https://${domain}/oidc/logout"
}

That’s better.

Let’s try the implicit grant.

According to the documentation, I should point a browser to the url

https://${domain}/authorize?client_id=$%7Bclient_id%7D&response_type=token

and get a token.

By doing this (in a private session), I indeed get the login page, the consent screen, but eventually, it raises a 500 error.

Now, trying adding a redirect_uri

qutebrowser_dump_cookies.sh
curl --cookie $XDG_RUNTIME_DIR/cookies.txt "https://${domain}/authorize?client_id=${client_id}&response_type=token&redirect_uri=http://localhost:3000"
Found. Redirecting to http://localhost:3000/#access_token=ey...&expires_in=7200&token_type=Bearer

Note that I first ran the flow i qutebrowser, then used its cookies to document the flow in here.

It seems that the optional redirect_uri is required after all.

Analysing the access_token (putting it in jwt.io for instance), this is the content I find.

header:

{
  "alg": "dir",
  "enc": "A256GCM",
  "iss": "https://dev-idf8pew4o81663vw.us.auth0.com/"
}

payload: ""

The payload makes sense, as we did not specify a scope. Yet, it uses a JWE with a symmetric key.

I remember that using the audience claim, we could get a JWS token. By digging into the API section, I can see that auth0 created an API for my application, and that this API is associated to an audience -> https://${domain}/api/v2/

Let’s try with it.

qutebrowser_dump_cookies.sh
curl --cookie $XDG_RUNTIME_DIR/cookies.txt "https://${domain}/authorize?client_id=${client_id}&response_type=token&redirect_uri=http://localhost:3000&audience=https://${domain}/api/v2/"
Found. Redirecting to http://localhost:3000/#access_token=ey...&expires_in=7200&token_type=Bearer

Analyzing this token I get

header

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "...."
}

payload

{
  "iss": "https://${domain}/",
  "sub": "google-oauth2|...",
  "aud": "https://${domain}/api/v2/",
  "iat": 1730886611,
  "exp": 1730893811,
  "scope": "",
  "azp": "${client_id}"
}

I can see the minimal piece of information: my subject, as logged in using google.

So far so good. Now, let’s try to add a custom claim, as I want to use auth0 as an authorization server, to authorized access to scoped resources.

Looking at this default API, it cannot be edited and provide scopes for manipulating auth0 resources.

Therefore, I suppose I need to create another API.

I created an API using “RFC 9068” instead of auth0 for the JWT format and provided the audience “test”

This automatically created an application, called “test (Test Application)” of type “machine to machine”. By digging a little bit, I understand that it is meant to run a client credential grant, therefore no something I want to use for now. So I just removed the created application.

Then, in the “permission” tab of the API settings, I can add custom ones. Is seems like “permission” is the term auth0 uses for scope.

I added a test scope. Let’s see if I can use it.

I suppose I only need to provide the audience=test and scope=test

qutebrowser_dump_cookies.sh
curl --cookie $XDG_RUNTIME_DIR/cookies.txt "https://${domain}/authorize?client_id=${client_id}&response_type=token&redirect_uri=http://localhost:3000&audience=test&scope=test"
Found. Redirecting to http://localhost:3000/#access_token=...&scope=test&expires_in=7200&token_type=Bearer

I can see in the consent screen the description of my scope.

Let’s decompose the received access token.

header

{
  "alg": "RS256",
  "typ": "at+jwt",
  "kid": "..."
}

Interestingly, it now provides the application/at+jwt typ, letting me know that it understood its status as an authorization server.

payload

{
  "iss": "https://${domain}/",
  "sub": "google-oauth2|...",
  "aud": "test",
  "iat": 1730888197,
  "exp": 1730895397,
  "scope": "test",
  "jti": "1HcdiAATYin7RFT2ch5e6Q",
  "client_id": "${client_id}"
}

It provided not only the scope, but also a jti. That’s a good sign.

Now, let’s validate this token.

As per the well-known uri, we know that the keys are in https://${domain}/.well-known/jwks.json.

curl http https://${domain}/.well-known/jwks.json | jq
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "n": "...",
      "e": "AQAB",
      "kid": "...",
      "x5t": "...",
      "x5c": [
        "..."
      ],
      "alg": "RS256"
    },
  ...
  ]
}

Without surprise, we can find the appropriate key, let’s use it to validate the signature.

JWKS_URI="https://${domain}/.well-known/jwks.json"
jwt decode --ignore-exp --secret "$(curl "${JWKS_URI}")" "${token}"|sed -r -e "s|${client_id}|\${client_id}|" -e "s|${domain}|\${domain}|" -e 's/google-oauth2\|[0-9]+/google-oauth2|.../' -e 's/"kid": ".+"/"kid": "..."/'

Token header
------------
{
  "typ": "at+jwt",
  "alg": "RS256",
  "kid": "..."
}

Token claims
------------
{
  "aud": "test",
  "client_id": "${client_id}",
  "exp": 1730917957,
  "iat": 1730910757,
  "iss": "https://${domain}/",
  "jti": "6dYrkLiagV6yYEQWbddjw4",
  "scope": "test",
  "sub": "google-oauth2|..."
}

Although not very explicit, using jwt-cli with --secret= implies a signature verification.

We went through the whole process of creating an auth0 application, an API and then issue an access token and validate it. Now, we are ready to do real life stuff.