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
The point of this client is that the red box there hides the Application ID which is used to help verify that token.
-
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
-
Use that oidc provider and the client ID (application ID) to create a verifier
var verifier = amw.Provider.Verifier(&oidc.Config{ClientID: amw.ClientID})
-
Get the bearer token out of the Authorization http header and trim toff that “Bearer” word.
-
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[]} -
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))))
}