Just a quick post about Auth0. The point is that authentication, authorization can be tricky, especially when you want to support a bunch of features like 2FA, user management, SSO (Single Sign On), passwordless, etc. We also do not like having to deal with user-password database logic. It seems that Auth0 (and many other competitors) offers an easy and cheap way out.

For web related development, I am more used to JS, but let’s do some Python Flask. (1) Sign up on Auth0 and (2) create an Application on their dashboard and (3) create an API on their dashboard as we want to protect some REST API. From the application, we have Client ID, Client Secret, Auth0 Domain or Auth0 Base URL. From the API, we have the API identifier, aka Auth0 Audience.

During app initialization, just specify all these parameters:

from authlib.flask.client import OAuth

app = Flask(__name__)
app.secret_key = SECRET_KEY

oauth = OAuth(app)
auth0 = oauth.register(
    'auth0',
    client_id=AUTH0_CLIENT_ID,
    client_secret=AUTH0_CLIENT_SECRET,
    api_base_url=AUTH0_BASE_URL,
    access_token_url=AUTH0_BASE_URL + '/oauth/token',
    authorize_url=AUTH0_BASE_URL + '/authorize',
    client_kwargs={
        'scope': 'openid profile',
    },
)

To login and obtain a token, all we need is a few lines of code!

@app.route('/login')
def login():
    return auth0.authorize_redirect(
        redirect_uri=url_for('callback', _external=True),
        audience=AUTH0_AUDIENCE)


@app.route("/callback")
    def callback():
        return jsonify(auth0.authorize_access_token())

Basically, we provided to OAuth a callback URL. After the user signs in, our callback endpoint /callback is hit and the auth0 object can process and return us access_token (not to be confused with id_token). If you did not specify an API / Auth0 Audience, then access_token would not be JWT and is much shorter and is to with used only with Auth0’s endpoints at Auth0 Base URL. However, we did specify the Auth0 Audience, so access_token is a JWT. Here is the sample output:

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlFUQkROalJFUlRZMVFUWTBNMFUxUkVJNE16Y3lOMFV5TnpZMk1UUXpNekU0TVRFMk56WkdNUSJ9.eyJpc3MiOiJodHRwczovL2Rldi1vNG56cjM1cy5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NWM4YTRiYTM3YjdlZWM2NDAyMjI4NjcxIiwiYXVkIjpbIm15dGVzdGFwaSIsImh0dHBzOi8vZGV2LW80bnpyMzVzLmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE1NTI3ODQ4NTgsImV4cCI6MTU1Mjg3MTI1OCwiYXpwIjoiTE44WUR1MWhXckVTbjM5bmUxdTh3SFMwRkw1VVVKRnEiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIn0.gutSVg4YpzFO9b0UcFiUDOy4nR9epRXDX1vapuj4BI9Q8H0EN0eFX1UYBA5O2mIyQWVAwtyLBL5fnJCA6bGORRARxhPiMt7ObKY1U5PHYCUET70V4QRaAo-Qk-iv8_86XKyeKIUPR41r-DJcxBHbifyFr7bFMD6cRotBIMgj9WOn8-sX5blr_zyuON_hDJxTSbqkxjS4ZJetvHdfhBZXvTcfKJ9wptryfYq7QXRDz05-Z4z1v0DYXET9JO1eIGDVFoZODMdiFnHQIu2cuCZ4pGF0mVKReAtetjlGydB9Zsr0XppeWJXEb3MjfDfIEAiHUhKzc96WVVFFKXLwxa8A-w",
  "expires_at": 1552871259,
  "expires_in": 86400,
  "id_token": ".........",
  "token_type": "Bearer"
}

Our user can take access_token and specify in subsequent calls to our API. We will add a typical require_auth decorator to check this header. This is taken from this link and it seems easy to adapt.

from functools import wraps
from flask import Flask, jsonify, redirect, request, url_for, g
from urllib.request import urlopen
import json
from jose import jwt


class AuthError(Exception):
    def __init__(self, error, status_code):
        self.error = error
        self.status_code = status_code


def get_token_auth_header():
    """Obtains the access token from the Authorization Header."""
    auth = request.headers.get("Authorization", None)
    if not auth:
        raise AuthError({
            "code": "authorization_header_missing",
            "description": "Authorization header is expected"
        }, 401)

    parts = auth.split()

    if parts[0].lower() != "bearer":
        raise AuthError({
            "code": "invalid_header",
            "description": "Authorization header must start with Bearer"
        }, 401)
    elif len(parts) == 1:
        raise AuthError({
            "code": "invalid_header",
            "description": "Token not found"
        }, 401)
    elif len(parts) > 2:
        raise AuthError({
            "code": "invalid_header",
            "description": "Authorization header must be Bearer token"
        }, 401)
    token = parts[1]
    return token


def requires_auth(f):
    """Determines if the access token is valid."""
    @wraps(f)
    def decorated(*args, **kwargs):
        token = get_token_auth_header()
        jsonurl = urlopen("https://" + AUTH0_DOMAIN + "/.well-known/jwks.json")
        jwks = json.loads(jsonurl.read())
        try:
            unverified_header = jwt.get_unverified_header(token)
        except jwt.JWTError:
            raise AuthError({"code": "invalid_header",
                             "description":
                             "Invalid header. "
                             "Use an RS256 signed JWT Access Token"}, 401)
        if unverified_header["alg"] == "HS256":
            raise AuthError({"code": "invalid_header",
                             "description":
                             "Invalid header. "
                             "Use an RS256 signed JWT Access Token"}, 401)
        rsa_key = {}
        for key in jwks["keys"]:
            if key["kid"] == unverified_header["kid"]:
                rsa_key = {
                    "kty": key["kty"],
                    "kid": key["kid"],
                    "use": key["use"],
                    "n": key["n"],
                    "e": key["e"]
                }
        if rsa_key:
            try:
                payload = jwt.decode(
                    token,
                    rsa_key,
                    algorithms=ALGORITHMS,
                    audience=AUTH0_AUDIENCE,
                    issuer="https://" + AUTH0_DOMAIN + "/"
                )
            except jwt.ExpiredSignatureError:
                raise AuthError({"code": "token_expired",
                                 "description": "token is expired"}, 401)
            except jwt.JWTClaimsError:
                raise AuthError({"code": "invalid_claims",
                                 "description":
                                 "incorrect claims,"
                                 " please check the audience and issuer"}, 401)
            except Exception:
                raise AuthError({"code": "invalid_header",
                                 "description":
                                 "Unable to parse authentication"
                                 " token."}, 401)
            g.auth = payload
            return f(*args, **kwargs)
        raise AuthError({"code": "invalid_header",
                         "description": "Unable to find appropriate key"}, 401)
    return decorated


@app.route('/validate')
@requires_auth
def validate():
    """Validate token"""
    return jsonify(g.auth)

Observe that when the authorization succeeds, the thread-local request context g will contain payload which looks like this:

{
  "iss": "https://dev-aaaaaaaa.auth0.com/",
  "sub": "auth0|5c8a4ba37b7eec6402228671",
  "aud": ["mytestapi", "https://dev-aaaaaaaa.auth0.com/userinfo"],
  "iat": 1552722298,
  "exp": 1552808698,
  "azp": "LN8YDu1hWrESn39ne1u8wHS0FL5UUJFq",
  "scope": "openid profile"
}

We can also get userinfo by hitting Auth0 Base URL’s /userinfo with our access_token.