dgrijalva / jwt-go

ARCHIVE - Golang implementation of JSON Web Tokens (JWT). This project is now maintained at:
https://github.com/golang-jwt/jwt
MIT License
10.78k stars 997 forks source link

Validation error by RSA, despite public key is correct #465

Closed d3v1l401 closed 3 years ago

d3v1l401 commented 3 years ago

Greetings,

I'm implementing the JWT verification process for an application in Azure AD: once the JWT access token acquired by the client is sent to the backend, the backend needs to verify the token signature first.

To do so, I use the key rotations endpoint by Microsoft over here, of course the "common" path is replaced by the application's Tenant ID.

Every time I verify the JWT it fails the verification from jwt-go's Verify(...) function call, I also debugged and stepped in the code to check exactly where the error happens and it appears to be happening from RSA.

I'm unsure if I'm doing something wrong or if I misunderstood something entirely, anyone who would be kind to help me would be greatly appreciated <3

Here the code:

func getKey(token *jwt.Token) (interface{}, error) {

    // TODO: cache response so we don't have to make a request every time
    // we want to verify a JWT
    set, err := jwk.Fetch(context.Background(), "https://login.microsoftonline.com/common/discovery/v2.0/keys") // <--- "common" has replaced the tenant id in this example, for safety reasons :)
    if err != nil {
        return nil, err
    }

    keyID, ok := token.Header["kid"].(string)
    if !ok {
        return nil, errors.New("expecting JWT header to have string kid")
    }

    if key, exists := set.LookupKeyID(keyID); exists {
        var pubkey interface{}
        key.Raw(&pubkey)

        return pubkey, nil
    }

    return nil, fmt.Errorf("unable to find key %q", keyID)
}

func Validate(jwtToken string) error {

    token, err := jwt.Parse(jwtToken, getKey)
    if err != nil {
        fmt.Println(err.Error()) // <-- Error here
        return nil
    }

    fmt.Println(token)

    return nil
}

The output of this, with the correct JWT token acquired manually by calling the authentication endpoint by MS, is: crypto/rsa: verification error

This is what I've been doing so far that failed:

  1. I tried every single RSA public key in the directory given by MS, regardless of KID.
  2. I tried to modify the code itself of jwt-go, because it looked weird to me the Verify() method also tries to verify the signature field itself:
    // Perform validation
    76  token.Signature = parts[2]
    77  if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {
    78      vErr.Inner = err
    79      vErr.Errors |= ValidationErrorSignatureInvalid
    80  }
    }

to this one

// Perform validation
76  token.Signature = parts[2]
77  if err = token.Method.Verify(strings.Join(parts[0:1], "."), token.Signature, key); err != nil {
78      vErr.Inner = err
79      vErr.Errors |= ValidationErrorSignatureInvalid
80  }
}

Of course without success, it was a desperate attempt.

  1. I tried to switch versions of the MS APIs between v1 and v2, cross-checking key validation as well (v1 JWT <--> v2 Public Key)
  2. I tried to change method between Parse and ParseWithClaims.
  3. I checked that the signing method specified by the token is the same as the one we expect (RS256 + SHA256), it is correct in fact.
d3v1l401 commented 3 years ago

Alright, I solved the problem and I see this is a problem that SO MANY developers are complaining about because there's a lack of documentation by Microsoft themselves.

It appears the discovery/keys endpoint exposes public keys produced by a different set for each application, or at least so I suspect, even if the KID is the same the public key will be somehow different from the one our application expects.

So how do I actually fetch god damn public key I need?

You're gonna cry now: get into your AAD application panel > Expose an API On the top part of your section panel you'll see the "Application URI" field, it's smaller than you'd expect, this is used to create an application ID URI path that will be needed to override the scope with the application's default ones (API permissions).

One you've created the App ID URI, copy it and put it into the scope parameter during the authentication phase and append the /.default path: image

That's it, from this moment the KID will match your Keys indexing page by MS AND will also succeed the authentication using the RSA 256 key.

That's it, it will work now. Feel free to reply to this issue if it is unclear, I'm in no way an AAD expert but f**k MS for wasting my 5 working days on this.

blueJayWays commented 2 years ago

Hey! Thanks for share. I still have a question: Where I can find the value for pubkey variable? Is it in azure? Thank you

d3v1l401 commented 2 years ago

Hey! Thanks for share. I still have a question: Where I can find the value for pubkey variable? Is it in azure? Thank you

Yes it is in Azure. Please check the second comment where I posted the solution.

Remember the public key is actually 'x5c' parameter in the rotated keys.