Verifying Azure B2C token with Go from OpenID Connect (OIDC)

Verifying a token via OpenID Connect is a good start to establishing credentials. But only a start. None of the below is secure, it’s demo code to get started with.

Azure AD B2C Prerequisite

I’m starting with an Azure AD B2C tenant. I won’t go over setting that up.

A reasonable test of whether you’ve set it up for OIDC is to go to the following link when you’ve placed your own values for “yourDomainName” and “yourAzureTenantId” and “yourUserFlow”: https://yourDomainName.b2clogin.com/tfp/yourAzureTenantId/yourUserFlow/v2.0/.well-known/openid-configuration

You’d see something like this JSON detailing a long list of configuration.

{
  "issuer": "https://yourDomainName.b2clogin.com/tfp/yourAzureTenantId/yourUserFlow/v2.0/",
  "authorization_endpoint": "https://yourDomainName.b2clogin.com/yourAzureTenantId/yourUserFlow/oauth2/v2.0/authorize",
  "token_endpoint": "https://yourDomainName.b2clogin.com/yourAzureTenantId/yourUserFlow/oauth2/v2.0/token",
  "end_session_endpoint": "https://yourDomainName.b2clogin.com/yourAzureTenantId/yourUserFlow/oauth2/v2.0/logout",
  "jwks_uri": "https://yourDomainName.b2clogin.com/yourAzureTenantId/yourUserFlow/discovery/v2.0/keys",
  "response_modes_supported": [
    "query",
    "fragment",
    "form_post"
  ],
  "response_types_supported": [
    "code",
    "code id_token",
    "code token",
    "code id_token token",
    "id_token",
    "id_token token",
    "token",
    "token id_token"
  ],
  "scopes_supported": [
    "openid"
  ],
  "subject_types_supported": [
    "pairwise"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "client_secret_basic"
  ],
  "claims_supported": [
    "idp",
    "emails",
    "name",
    "sub",
    "tfp",
    "iss",
    "iat",
    "exp",
    "aud",
    "acr",
    "nonce",
    "auth_time"
  ]
}

Verification

Once you have a setup Azure AD B2C world, you can add clients on. I just happen to have a client running on http://localhost:8080

Client 1

The point of this client is that the red box there hides the Application ID which is used to help verify that token.

  1. Create a provider which uses that very long json from above.

    provider, err := oidc.NewProvider(context.Background(), "https://yourname.b2clogin.com/tfp/yourtenantid/yourUserFlow/v2.0/") //REPLACE THIS WITH YOUR VALUE
  2. Use that oidc provider and the client ID (application ID) to create a verifier

    var verifier = amw.Provider.Verifier(&oidc.Config{ClientID: amw.ClientID})
  3. Get the bearer token out of the Authorization http header and trim toff that “Bearer” word.

  4. Verify the token and use information in the token.

    idToken, err := verifier.Verify(r.Context(), reqToken)
    fmt.Printf("%+v\n", idToken)

    &{Issuer:https://yourname.b2clogin.com/tfp/yourtenantid/yourUserFlow/v2.0/ Audience:[yourtenantid] Subject: Expiry:2019-12-21 18:31:58 -0500 EST IssuedAt:2019-12-21 17:31:58 -0500 EST Nonce: AccessTokenHash: sigAlgorithm:RS256 claims:[123 34 105 115 56 125] distributedClaims:map[]}

  5. If you care about the claims, such as email address if available, you can then parse out that idToken

    // Extract custom claims
    var claims struct {
    	Emails []string `json:"emails"`
    }
    if err := idToken.Claims(&claims); err != nil {
    	fmt.Println(err)
    	http.Error(w, "Unable to retrieve claims", http.StatusUnauthorized)
    	return
    }
    fmt.Printf("%+v\n", claims)

    {Emails:[myemail@mydomain.com]}

Complete code

package main

import (
	"context"
	"fmt"
	"github.com/coreos/go-oidc"
	"log"
	"net/http"
	"strings"

	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
)

type authenticationMiddleware struct {
	ClientID string
	Provider *oidc.Provider
}

func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
	var verifier = amw.Provider.Verifier(&oidc.Config{ClientID: amw.ClientID})

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		reqToken := r.Header.Get("Authorization") //Authorization: Bearer somecrazylongtokenthatsfartoolongtoread
		fmt.Printf("%+v\n", reqToken)
		splitToken := strings.Split(reqToken, "Bearer")
		if len(splitToken) != 2 {
			http.Error(w, "Token doesn't seem right", http.StatusUnauthorized)
			return
		}

		reqToken = strings.TrimSpace(splitToken[1]) //I don't want the word Bearer.

		idToken, err := verifier.Verify(r.Context(), reqToken)
		if err != nil {
			http.Error(w, "Unable to verify token", http.StatusUnauthorized)
			return
		}
		fmt.Printf("%+v\n", idToken)

		var claims struct {
			Emails []string `json:"emails"`
		}
		if err := idToken.Claims(&claims); err != nil {
			fmt.Println(err)
			http.Error(w, "Unable to retrieve claims", http.StatusUnauthorized)
			return
		}
		fmt.Printf("%+v\n", claims)

		// Call the next handler, which can be another middleware in the chain, or the final handler.
		next.ServeHTTP(w, r)
	})
}

func httpHomePage(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello Home Page!")
	fmt.Println("hit home page")
}

func main() {

	provider, err := oidc.NewProvider(context.Background(), "https://yourname.b2clogin.com/tfp/yourtenantid/yourUserFlow/v2.0/") //REPLACE THIS WITH YOUR VALUE
	if err != nil {
		log.Fatal(err)
	}
	amw := authenticationMiddleware{
		Provider: provider,
		ClientID: "<client id guid>", //REPLACE THIS WITH YOUR VALUE
	}

	r := mux.NewRouter()
	r.HandleFunc("/", httpHomePage)

	cors := handlers.CORS(
		handlers.AllowedHeaders([]string{"Authorization"}),
		handlers.AllowedMethods([]string{"GET"}),
		handlers.AllowedOrigins([]string{"http://localhost:4200"}),
	)

	// Apply the CORS middleware to our top-level router, with the defaults.
	log.Fatal(http.ListenAndServe(":8080", cors(amw.Middleware(r))))
}

Summary

My sample code is here.