Why

In this article we talk about the integration of Auth0 with the help of the programming language Golang. The focus here is clearly on server-side validation and verification of the user token. Why only the backend part? Simple: the rest is well documented on Auth0-docs. Only the topic covered in this article is, in my opinion, poorly or extremely incomprehensibly documented. Therefore this short article.

Authentication flow

This image shows well how the authentication process with Auth0 works on the example of a Single-Page-Application (SPA), e.g. with Angular.js, React.js or Vue.js.

https://auth0.com/docs/architecture-scenarios/web-app-sso/part-1

Our focus here is clearly on the server-side token validation. We assume that the user has successfully authenticated himself. In this example, our Auth0 app is configured as a single page application. Have a look at part 4 on the following image.

https://auth0.com/docs/architecture-scenarios/spa-api/part-1

What is the challenge?

First we need to clarify what the token of Auth0 is. This is a JSON web token (JWT) with claims. The token is signed with RS256.  RS256: JWT will be signed with your private signing key and they can be verified using your public signing key (see Certificates – Signing Certificate section)- from the docs of Auth0. Keep in mind that you can change this settings. But we are going to work with the default single-page-application settings. So, first challenge: verify the token using the public signing key. There are also two more information inside the token: issuer and audience. The audience is simply your Auth0 client-id. The issuer is your auth0 domain. That means we are assume the token to have this information with correct values inside. Let’s write some code.

Server-side implementation

The actual and easy task is to pass the token to the backend. For a classic REST API, a header (Authorization: Bearer ) etc. is of course appropriate. In my example the token is transferred in a protobuffer. But it doesn’t matter, because we only care about the token itself.

The next task is to parse the token into the type *jwt.Token. It comes to us as a type string. And by the way: The type definitions are implemented in the following go lib: github.com/form3tech-oss/jwt-go.

This results in the following steps:

  • Parse the Token
  • During parsing, parse RSA public key
  • Validate audience claim
  • Validate issuer claim
  • Grant access

You can find exactly this procedure in the following code example. Keep in mind that env, _ := utils.ParseEnvironmentVariables() is my own wrapper to access environment variables. Side note on environment variables: https://12factor.net/config

package providers

import (
    "encoding/json"
    "errors"
    "net/http"

    "github.com/form3tech-oss/jwt-go"
    log "github.com/sirupsen/logrus"
    "gitlab.com/queensandbees/mvp/backend/utils"
)

type Auth0ProviderInterface interface {
    Authenticate(token string) error
}

type Jwks struct {
    Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys struct {
    Kty string   `json:"kty"`
    Kid string   `json:"kid"`
    Use string   `json:"use"`
    N   string   `json:"n"`
    E   string   `json:"e"`
    X5c []string `json:"x5c"`
}

type auth0Provider struct {
    audience string
    iss      string
    pemUri   string
}

func NewAuth0Provider() Auth0ProviderInterface {
    env, _ := utils.ParseEnvironmentVariables()
    return &auth0Provider{
        audience: env.AuthZeroAudience,
        iss:      env.AuthZeroIss,
        pemUri:   env.AuthZeroPemURI,
    }
}

func (ap *auth0Provider) Authenticate(token string) error {

    log.Info("start authentication with token")

    claims := jwt.MapClaims{}
    parsedToken, err := jwt.ParseWithClaims(token, claims, func(jwtToken *jwt.Token) (interface{}, error) {
        cert, err := ap.getPemCert(jwtToken)
        if err != nil {
            return errors.New("access denid"), err
        }
        result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
        return result, nil
    })

    if err != nil {
        log.Error(err.Error())
        return errors.New("access denid")
    }

    aud := parsedToken.Claims.(jwt.MapClaims).VerifyAudience(ap.audience, false)

    if !aud {
        return errors.New("access denid")
    }

    checkIss := parsedToken.Claims.(jwt.MapClaims).VerifyIssuer(ap.iss, false)
    if !checkIss {
        return errors.New("access denied")
    }

    return nil
}

func (ap *auth0Provider) getPemCert(token *jwt.Token) (string, error) {
    cert := ""
    env, _ := utils.ParseEnvironmentVariables()
    resp, err := http.Get(env.AuthZeroPemURI)

    if err != nil {
        return cert, err
    }
    defer resp.Body.Close()

    var jwks = Jwks{}
    err = json.NewDecoder(resp.Body).Decode(&jwks)

    if err != nil {
        return cert, err
    }

    for k, _ := range jwks.Keys {
        if token.Header["kid"] == jwks.Keys[k].Kid {
            cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
        }
    }

    if cert == "" {
        err := errors.New("unable to find appropriate key")
        log.Errorf("error in parse public key: %s", err.Error())
        return cert, err
    }

    return cert, nil
}

And for those who are interested, here is the environment variable wrapper:

package utils

import (
    "github.com/kelseyhightower/envconfig"
)

type QueensEnvironment struct {
    AuthZeroAudience string `required:"true"`
    AuthZeroIss      string `required:"true"`
    AuthZeroPemURI   string `required:"true"`
}

func ParseEnvironmentVariables() (*QueensEnvironment, error) {
    env := &QueensEnvironment{}
    if err := envconfig.Process("queens", env); err != nil {
        return env, err
    }

    return env, nil
}

Summary

Auth0 is an absolutely powerful authentication provider that takes a lot of work out of your hands. I hope I could help one or the other who might have stumbled over the same problem as I did.

Categories:

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *