38
loading...
This website collects cookies to deliver better user experience
{header}.{payload}.{signature}
Base64URL
encoded before being added to that structure.1 The header section includes information on what algorithm was used to sign the token and, if applicable, the id of the public key needed to validate it. The payload is where the substance of the token goes. For access tokens, this will be information about the user and what they should be allowed to do. The payload can include really any valid JSON, though there are a few standard fields (or “claims” in the JWT parlance) that are frequently used for most tokens like issuer, expiration time, and audience, among others. Finally, there’s the signature, the JWT’s linchpin. It’s created by taking the first two parts and feeding them through the algorithm indicated in the header.https://{your-auth0-domain}/.well-known/jwks.json
: 2{
"keys": [
{
"alg": "RS256",
"kty": "RSA",
"use": "sig",
"n": "mff4bkiJV-ve8IY_...",
"e": "AQAB",
"kid": "NzI4MjFDODk5NDBDQ0U4QTAyQTExREFDMEIyRkYzNzBCQjc3QkM3RQ",
"x5t": "NzI4MjFDODk5NDBDQ0U4QTAyQTExREFDMEIyRkYzNzBCQjc3QkM3RQ",
"x5c": [
"MIIDCTCCA..."
]
},
...
]
}
keys
parameter above is a representation of a public key. There's a lot of information in each but the most important properties for our purposes are the n
and e
parameters, two values derived from the underlying private key. With those two values we can verify that a JWT was signed with the corresponding private key. Also important is the kid
property, a string that uniquely identifies the key. As I mentioned above, a token's header can include a unique identifier of the public key needed to validate its signature. We can then use that id to pick the correct key from this set of keys.kid
property we discussed above).kid
, and uses that public key to validate that the token's signature is valid. If it is, it knows that it must in fact have been issued by Auth0.iss
and aud
claims) expected? Has the token expired (a timestamp encoding its expiration date can be contained with the exp
claim)?scope
or permissions
claims.cryptography
package. Here’s what that looks like:from cryptography.hazmat.primitives.asymmetric import rsa
def generate_public_private_key_pair():
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
return (public_key, private_key)
(public_key, private_key) = generate_public_private_key_pair()
exponent
and key_size
parameters are doing you'll have to delve a bit deeper into RSA keys. For our purposes, suffice it to say that those values are reasonable and commonly-used defaults.read
access, write
access, or both and this level of access is communicated through the permissions
claim.4import jwt
ALGORITHM = "RS256"
PUBLIC_KEY_ID = "sample-key-id"
def encode_token(payload):
return jwt.encode(
payload=payload,
key=private_key, # The private key created in the previous step
algorithm=ALGORITHM,
headers={
"kid": PUBLIC_KEY_ID,
},
)
def get_mock_user_claims(permissions):
return {
"sub": "123|auth0",
"iss": "some-issuer", # Should match the issuer your app expects
"aud": "audience", # Should match the audience your app expects
"iat": 0, # Issued a long time ago: 1/1/1970
"exp": 9999999999, # One long-lasting token, expiring 11/20/2286
"permissions": permissions,
}
def get_mock_token(permissions):
return encode_token(
get_mock_user_claims(permissions)
)
def get_mock_read_only_token():
return get_mock_token(permissions=["read"])
def get_mock_read_write_token():
return get_mock_token(permissions=["read", "write"])
PUBLIC_KEY_ID
and ALGORITHM
values in constants so that we can reuse them later. The specific value for the public key id is arbitrary but the kid
value passed to the JWT header will have to match the kid
value of the JWK we will generate in the next step.iss
and aud
claims) typically need to match those your app expects. You are likely passing these values as configs to to the library your app uses to interact with Auth0, although you may also have implemented this check in your own code.cryptography
and jwt
packages in tandem, we can take care of this fairly handily:import jwt
from jwt.utils import to_base64url_uint
def get_jwk(public_key):
public_numbers = public_key.public_numbers()
return {
"kid": PUBLIC_KEY_ID, # Public key id constant from previous step
"alg": ALGORITHM, # Algorithm constant from previous step
"kty": "RSA",
"use": "sig",
"n": to_base64url_uint(public_numbers.n).decode("ascii"),
"e": to_base64url_uint(public_numbers.e).decode("ascii"),
}
jwk = get_jwk(public_key)
pytest
in a Django app recently.get_auth0_jwks
. My goal then was to patch that method, so I created a pytest
fixture to intercept calls made to it and return my JWK instead.# conftest.py
@pytest.fixture(autouse=True)
def mock_auth0_jwks(mocker):
jwk = get_jwk(public_key)
mocker.patch(
"myapp.authentication.get_auth0_jwks", return_value={"keys": [jwk]}
)
conftest.py
file at the root of my project so that pytest
will make it available to every one of my tests. I also asked pytest
to automatically use that fixture so that I don't have to explicitly call it in each test. This way we won't have mysterious test failures because someone forgot to load and use this fixture before adding a new test.conftest.py
file to define a couple of global fixtures, each of which exposed an API client preloaded with the appropriate permissions:# conftest.py
from rest_framework.test import APIClient
@pytest.fixture()
def api_client():
return APIClient()
@pytest.fixture()
def api_client_read_only(api_client):
api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {get_mock_read_only_token()}")
return api_client
@pytest.fixture()
def api_client_read_write(api_client):
api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {get_mock_read_write_token()}")
return api_client
from rest_framework import status
def test_unauthenticated_user_cant_get_resource(api_client):
response = api_client.get("/resource", format="json")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_read_only_user_can_get_resource(api_client_read_only):
response = api_client_read_only.get("/resource", format="json")
assert response.status_code == status.HTTP_200_OK
def test_read_only_user_cant_post_resource(api_client_read_only):
response = api_client_read_only.post("/resource", format="json")
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_read_write_user_can_post_resource(api_client_read_write):
response = api_client_read_write.post("/resource", format="json")
assert response.status_code == status.HTTP_200_OK
One important thing to re-iterate here is that there’s nothing necessarily secret about the contents of the JWT. The payload and header are just Base64URL
encoded, something which is easily reversed by anyone. So these tokens are not a way to encrypt data, but rather a way to pass data around with a guarantee that the data hasn’t been modified by some shady people on the internet. ↩
Here’s a random example I found for a “sample” app: https://sample.auth0.com/.well-known/jwks.json ↩
Users will typically include the token along with a request by setting the value Authorization
request header to Bearer {token_value}
↩
Note that some apps use the scope
claim instead to community scope of access. You'll want to figure out what claim your app relies on. ↩