Tips, Tricks, and Reasons for JSON Web Tokens (JWTs)

Emin Martinian

JWT: JSON Web Token

Used for authentication/authorization such as:

Why JWTs?

Imagine app with many features + servers + engineers:

  • Load balance, payments, profiles, PII, DB
  • Local/remote/international workers + consultants
  • How to manage security?
    • Can't give everyone access to sensitive info

Authentication vs Validation

JWT: Authentication Request

JWT: Authentication Response

JWT: Application Request

Separate Auth From Validation

Auth Server has secrets; needs security + maintenance

  • App Server(s) needs public keys; low security
  • Easy to deploy App Server(s); e.g., serverless
  • Lower security for App Server(s), logs, debug, etc.

What do JWTs look like?

Base64 encoded header.payload.signature:

HEADER:     { "alg": "EdDSA", "typ": "JWT" }
PAYLOAD:    {"sub": "a", "name": "arbitrary data", "iat": 1 }
SIGNATURE:  SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-L
            L5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw
            
ENCODED JWT:   eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9
               .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9
               .SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-L
                L5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw

Signed using EdDSA with secret key:

MC4CAQAwBQYDK2VwBCIEIC+D6rD2YbXtV0ccR3smoR0ynhVuyyqvplFLbQWDdAtn

Main JWT Fields

  • sub: Subject (username, email, etc.)
  • iat: Issued at (useful for checking freshness)
  • exp: Expiry (useful for managing life-cycle )
  • nbf: Not before (useful for managing life-cycle )

Python/Flask Example

  • Easy to verify/decode using libraries (e.g., pyjwt)
    • can compose checks using decorators:
@app.route('/support/urgent')  # built-in flask decorator
@requires_jwt                  # custom decorator to validate JWT
@jwt_claims(['paid_support'])  # ensures token is for premium user
@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
def support_urgent():
    ... # process ending support request

Python/Flask Example

  • Easy to verify/decode using libraries (e.g., pyjwt)
    • can compose checks using decorators:
@app.route('/support/urgent')  # built-in flask decorator
@requires_jwt                  # custom decorator to validate JWT
@jwt_claims(['paid_support'])  # ensures token is for premium user
@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
def support_urgent():
    ... # process ending support request

Python/Flask Example

  • Easy to verify/decode using libraries (e.g., pyjwt)
    • can compose checks using decorators:
@app.route('/support/urgent')  # built-in flask decorator
@requires_jwt                  # custom decorator to validate JWT
@jwt_claims(['paid_support'])  # ensures token is for premium user
@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
def support_urgent():
    ... # process ending support request

Python/Flask Example

  • Easy to verify/decode using libraries (e.g., pyjwt)
    • can compose checks using decorators:
@app.route('/support/urgent')  # built-in flask decorator
@requires_jwt                  # custom decorator to validate JWT
@jwt_claims(['paid_support'])  # ensures token is for premium user
@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
def support_urgent():
    ... # process ending support request

Python/Flask Example

  • Easy to verify/decode using libraries (e.g., pyjwt)
    • can compose checks using decorators:
@app.route('/support/urgent')  # built-in flask decorator
@requires_jwt                  # custom decorator to validate JWT
@jwt_claims(['paid_support'])  # ensures token is for premium user
@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
def support_urgent():
    ... # process ending support request

Example of @requires_jwt

def requires_jwt(func):
    @wraps(func)
    def decorated(*args, **kwargs):        
        token = request.headers.get("Authorization").split(" ")[1]
        if not token:
            return 'missing token', 401  # if no token return error   
        try:
            g.decoded_jwt = jwt.decode(
                token, algorithms=['EdDSA'],
                key=current_app.config['JWT_KEY'])  # public key
            return func(*args, **kwargs)
        except Exception as problem:
            return f'{problem=}', 401 # return 401 or other error code
    return decorated

Example of @jwt_claims

def jwt_claims(claims_list: typing.Sequence[str]):
    def make_decorator(func):
        @wraps(func)
        def decorated(*args, **kwargs):        
            missing = [c for c in claims_list
                       if not g.decoded_jwt.get(c)]
            if missing:
                return f'Missing claims: {missing}', 401
            return func(*args, **kwargs)
        return decorated
    return make_decorator

Example Use Case: Proxy

  • Auth Server grants JWT letting Alice to act for Bob
  • claims: {"sub": "Alice", "proxy": "Bob"}
  • Alice sends request to act for Bob

Example Use Case: Proxy

  • Auth Server grants JWT letting Alice to act for Bob
  • claims: {"sub": "Alice", "proxy": "Bob"}
  • Alice sends request to act for Bob
@APP.route("/issue")
@requires_jwt
def issue():
    "Example route to create an issue."
    user = g.decoded_jwt.get('proxy', g.decoded_jwt.get('sub'))
    msg = f'Created issue assigned to {user}.'
    # ... Create the actual issue here



    return msg

Example Use Case: Proxy

  • Auth Server grants JWT letting Alice to act for Bob
  • claims: {"sub": "Alice", "proxy": "Bob"}
  • Alice sends request to act for Bob
@APP.route("/issue")
@requires_jwt
def issue():
    "Example route to create an issue."
    user = g.decoded_jwt.get('proxy', g.decoded_jwt.get('sub'))
    msg = f'Created issue assigned to {user}.'
    # ... Create the actual issue here
    real_user = g.decoded_jwt['sub']
    if real_user != user:
        msg += f'\n{real_user} acted on behalf of {user}'
    return msg

Caveats

  • Beware using header fields to check signature
    • don't trust alg field or limit possibilities
      • e.g., algorithms=['EdDSA']
    • be careful with kid, jku, jwk, etc.
  • Don't simulate sessions with JWTs
    • Use access/refresh tokens to solve logout/revocation

Example JKU Header Attack

  • Header can provide URL for key (useful):
    • {alg: "EdDSA", jku: "https://good.com/pk.json"}
  • Attacker can replace JKU with their own key:
    • {alg: "EdDSA", jku: "https://bad.com/pk.json"}
  • Don't trust header (validate against whitelist)

Example ALG Header Attack

  • Header can provide URL for key (useful):
    • {alg: "EdDSA", jku: "https://good.com/pk.json"}
  • Attacker can replace ALG with symmetric version:
    • {alg: "HS256", jku: "https://good.com/pk.json"}
  • Don't trust header (validate against whitelist)

Revocation via Access/Refresh

  • Problem: Can't cancel or logout a JWT
  • Solution: Refresh/Access token
    • "refresh token" with long expiry
    • used to get access token w/o credential check
    • "access token" with short expiry
    • can be used to access services

Get Refresh Token

Get Access Token

Use Access Token

Revocation

Separate validation from parsing

Summary and next steps