jwx: Cannot verify Azure oauth2 token signature of a valid token.

Describe the bug Cannot verify Azure oauth2 token signature, while token is definitely good and valid - allows me to access Microsoft Graph.

The error that shows up while I want to validate the token:

failed to parse payload: failed to verify jws signature: failed to verify message: crypto/rsa: verification error

Pretty sure I must be doing something wrong, but I cannot identify what it is.

go version:

go version go1.16.4 darwin/amd64

To Reproduce / Expected behavior I’m attaching a full code that uses an existing app registration, assigns a device code, created a cached refresh token, and requests a new token:

main.go

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
	"github.com/lestrrat-go/jwx/jwk"
	"github.com/lestrrat-go/jwx/jwt"
)

var cacheAccessor = &TokenCache{"serialized_cache.json"}

func acquireTokenDeviceCode() string {
	config := CreateConfig("config.json")

	app, err := public.New(config.ClientID, public.WithCache(cacheAccessor), public.WithAuthority(config.Authority))
	if err != nil {
		panic(err)
	}

	// look in the cache to see if the account to use has been cached
	var userAccount public.Account
	accounts := app.Accounts()
	for _, account := range accounts {
		if strings.EqualFold(account.PreferredUsername, config.Username) {
			userAccount = account
		}
	}
	// found a cached account, now see if an applicable token has been cached
	// NOTE: this API conflates error states, i.e. err is non-nil if an applicable token isn't
	//       cached or if something goes wrong (making the HTTP request, unmarshalling, etc).
	authResult, err := app.AcquireTokenSilent(context.Background(), config.Scopes, public.WithSilentAccount(userAccount))
	if err != nil {
		// either there was no cached account/token or the call to AcquireTokenSilent() failed
		// make a new request to AAD
		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
		defer cancel()
		devCode, err := app.AcquireTokenByDeviceCode(ctx, config.Scopes)
		if err != nil {
			panic(err)
		}
		fmt.Printf("Device Code is: %s\n", devCode.Result.Message)
		result, err := devCode.AuthenticationResult(ctx)
		if err != nil {
			panic(fmt.Sprintf("got error while waiting for user to input the device code: %s", err))
		}
		fmt.Println("Access token is " + result.AccessToken)
		fmt.Printf("Granted scopes are %v\n", authResult.GrantedScopes)
		return authResult.AccessToken
	}
	fmt.Println("Access token is " + authResult.AccessToken)
	fmt.Printf("Granted scopes are %v\n", authResult.GrantedScopes)
	return authResult.AccessToken
}

func callMicrosoftGraph(token string) {
	url := "https://graph.microsoft.com/v1.0/me"

	// Create a Bearer string by appending string access token
	var bearer = "Bearer " + token

	// Create a new request using http
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		panic(err)
	}

	// add authorization header to the req
	req.Header.Add("Authorization", bearer)

	// Send req using http Client
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		log.Println("Error on response.\n[ERROR] -", err)
	}
	defer resp.Body.Close()

	// read into string
	bytes, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println("Error while reading the response bytes:", err)
	}
	fmt.Println(string(bytes))
}

func getJWTkeyset() (jwk.Set, error) {
	// TODO get this url from https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
	const jwksURL = `https://login.microsoftonline.com/common/discovery/v2.0/keys`

	ctx := context.TODO()
	ar := jwk.NewAutoRefresh(ctx)

	// Tell *jwk.AutoRefresh that we only want to refresh this JWKS
	// when it needs to (based on Cache-Control or Expires header from
	// the HTTP response). If the calculated minimum refresh interval is less
	// than 15 minutes, don't go refreshing any earlier than 15 minutes.
	ar.Configure(jwksURL, jwk.WithMinRefreshInterval(15*time.Minute))

	// Refresh the JWKS once before getting into the main loop.
	// This allows you to check if the JWKS is available before we start
	// a long-running program
	_, err := ar.Refresh(ctx, jwksURL)
	if err != nil {
		fmt.Printf("failed to refresh JWKS: %s\n", err)
		return nil, err
	}

	keyset, err := ar.Fetch(ctx, jwksURL)
	if err != nil {
		fmt.Printf("failed to fetch JWKS: %s\n", err)
		return nil, err
	}
	return keyset, nil
}

func validateToken(tokenString string) bool {
	log.Println("Validating token")

	keyset, err := getJWTkeyset()
	if err != nil {
		panic(err)
	}

	// Actual verification:
	// FINALLY. This is how you Parse and verify the payload.
	// Key IDs are automatically matched.
	// There was a lot of code above, but as a consumer, below is really all you need
	// to write in your code
	token, err := jwt.ParseString(
		tokenString,
		// Tell the parser that you want to use this keyset
		// this will validate signature
		// TODO this does not work! why?
		jwt.WithKeySet(keyset),
		// validate expiration etc.
		jwt.WithValidate(true),
	)
	_ = keyset
	if err != nil {
		fmt.Printf("failed to parse payload: %s\n", err)
		return false
	}
	fmt.Printf("parsed token %v\n", token)
	tid, _ := token.Get("tid")
	fmt.Printf("parsed tid %v\n", tid)
	return true
}

func main() {
	token := acquireTokenDeviceCode()
	valid := validateToken(token)
	log.Printf("valid token: %t", valid)
	callMicrosoftGraph(token)
}

cache_accessor.go:

package main

import (
	"io/ioutil"
	"log"
	"os"

	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
)

type TokenCache struct {
	file string
}

func (t *TokenCache) Replace(cache cache.Unmarshaler, key string) {
	jsonFile, err := os.Open(t.file)
	if err != nil {
		log.Println(err)
	}
	defer jsonFile.Close()
	data, err := ioutil.ReadAll(jsonFile)
	if err != nil {
		log.Println(err)
	}
	err = cache.Unmarshal(data)
	if err != nil {
		log.Println(err)
	}
}

func (t *TokenCache) Export(cache cache.Marshaler, key string) {
	data, err := cache.Marshal()
	if err != nil {
		log.Println(err)
	}
	err = ioutil.WriteFile(t.file, data, 0600)
	if err != nil {
		log.Println(err)
	}
}

utils.go

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package main

import (
	"encoding/json"
	"io/ioutil"
	"log"
	"os"
)

// Config represents the config.json required to run the samples
type Config struct {
	ClientID  string   `json:"client_id"`
	Authority string   `json:"authority"`
	Scopes    []string `json:"scopes"`
	Username  string   `json:"username"`
}

// CreateConfig creates the Config struct from a json file.
func CreateConfig(fileName string) *Config {
	jsonFile, err := os.Open(fileName)
	if err != nil {
		log.Fatal(err)
	}
	defer jsonFile.Close()
	data, err := ioutil.ReadAll(jsonFile)
	if err != nil {
		log.Fatal(err)
	}

	config := &Config{}
	err = json.Unmarshal(data, config)
	if err != nil {
		log.Fatal(err)
	}
	return config
}

config.json (anonymized):

{
    "authority": "https://login.microsoftonline.com/<TENANTID>",
    "client_id": "...",
    "scopes": [
        "user.read"
    ],
    "username": "..."
}

Additional context I’m using version 1.1.7 of the library because of #380 (my last comment)

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 20

Most upvoted comments

Reg. ParseHeader - in this example I did not use it, true, but I did in other tests in the meantime.

However the puzzle is solved already, and of course nothing is wrong with the library 😉

TLDR; Azure provided 2 different kinds of tokens. One is AccessToken (the one I used), the other one is IDToken. The former one is not exactly (although VERY similar) an JWT token. Signature is different, and requires special handling during verification. The latter one is a standard JWT token and can be successfully verified using standard methods. So, in the code above, the solution was to change authResult.AccessToken to authResult.IDToken.RawToken and all works flawlessly. Note that the above-mentioned workaround for the key algorithms is required, nothing changes in that regard.

More info here:

Hmm, bummer. Maybe we “fixed” WithVerify where we shouldn’t have. Will take a look

https://github.com/lestrrat-go/jwx/issues/380#issuecomment-860754799

Instead of answering in multiple places, I’m just going to answer here. I believe this API is okay, because

  1. While WithKeySet forces you to use a key with the alg header, WithVerify (…a misnomer, but anyway) allows you to pass whatever key you want as long as you pass the algorithm name with it.
  2. If you must use WithKeySet, you can always fetch the key, and modify it before passing to WithKeySet

The rationale for requiring alg is security as you mentioned. But the important distinction from the library’s PoV is that we don’t provide automatic ways to shoot yourself in the foot: i.e. we’re not going to take a key set and automatically assume that both the JWS payload and JWK are well-intentioned players who will not sneak alg: "none" or open up the possibility of cheating against potentially weak keys. So WithKeySet, which automatically selects a key to use, is not going to assume that checking alg can be skipped.

On the other hand, WithVerify which takes a single key, is something that the user manually specifies, and therefore we allow the users to shoot themselves in the foot. If you want to fabricate the alg value or whatever, you are free to do as you wish.

Microsoft does not include alg in the key’s header. See here: https://login.microsoftonline.com/common/discovery/v2.0/keys

And it has all the rights to do so, as this key is optional as per RFC: https://datatracker.ietf.org/doc/html/rfc7517#section-4.4

On this note I think that a) The RFC is flawed for allowing this sort of implementation specific loophole/discrepancy, and b) Microsoft should know better to include alg – I mean, it’s not going to cause a problem for anybody if the alg header is present, but causes problems like this if it isn’t. They should just add it 😃


So, what can you do on your end?

First, let me state that I haven’t worked with Azure much, so there may be limitation and workarounds that I’m not aware of. Following comes from many assumptions.

So, assuming that Microsoft will not be adding alg anytime soon, and that while the keys may be rotated frequently but that their algorithm will stay the same, I would opt to find out the the algorithm used by each of their keys, and hardcode them somewhere either:

for iter := set.Iterate(ctx); iter.Next(ctx); {
  pair := iter.Pair()
  key := pair.Value.(jwk.Key)
  ...
  tokn, err := jwt.Parse(rawdata, jwt.WithVerify(jwa.RS256, key)
  if err == nil {
     break // do whatever with token
  }
}

or

set, _ := af.Fetch(microsoftJwksURL)
// assume you know what algs they were using for each key
key1, _ := set.Get(0)
key1.Set(jwk.AlgorithmKey, jwa.RS256)

key2, _ := set.Get(1)
key2.Set(jwk.AlgorithmKey, jws.HS256)

...
jwt.Parse(rawdata, jwt.WithKeySet(set))