golang-jwt / jwt

Community maintained clone of https://github.com/dgrijalva/jwt-go

Home Page:https://golang-jwt.github.io/jwt/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Still trying to understand 'ParseRSAPublicKeyFromPEM'

lknite opened this issue · comments

Scenario:

  • I'm working on an app and cli.
  • The app runs in kubernetes and is configured with OIDC.
  • My cli interacts with the app, and if the bearer token has expired, pops up a web browser to gather up credentials.

Everything works perfectly, except, in my app I have a hard coded value:

	SecretKey := "-----BEGIN CERTIFICATE-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC..." + "\n-----END CERTIFICATE-----"

	key, err := jwt.ParseRSAPublicKeyFromPEM([]byte(SecretKey))
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	
	// Parse takes the token string and a function for looking up the key. The latter is especially
	// useful if you use multiple keys for your application.  The standard is to use 'kid' in the
	// head of the token to identify which key to use, but the parsed token (head and claims) is provided
	// to the callback, providing flexibility.
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// Don't forget to validate the alg is what you expect:
		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}

		// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
		return key, nil
	})
	if err != nil {
		return nil, err //log.Fatal(err)
	}

I'm using keycloak, and in the past I followed a guide .

The guide had me browse to a page on the keycloak server to get the RSA public key. However, I don't understand why this is required. What if the user was using a different provider other than keycloak, how would they obtain the RSA Public key? I've configured OIDC on various apps such as harbor, and they always want the ProviderUrl, ClientID, and ClientSecret ... other than claims, but no keys. Shouldn't the RSA Public key be something easily obtainable in an automatic way?

Perhaps the guide I followed didn't understand what the RSA Public key was for and mislead me. On another page folks started talking about generating public and private keys.

I'd like my app to work with whatever OIDC provider the user might have. So how can I get the needed value automatically?

Is it just the public https cert used by the keycloak server, or the public ca which was used to sign the keycloak server public cert? (I generate tls certs for my apps automatically using cert-manager.) That's usually what's required when people click 'verify' the issuer. I could mount the ca.crt within the pod, then as long as the app used the system certs it should be able to verify.

I just read this article about security vulnerabilities around JWT. Now I understand that the RSA key above is for verifying the JWT token itself.

Initially, I see a couple options:

  1. somehow it should be possible to get the RSA public key via the oidc protocol? (seems preferred)
  2. is it enough to just verify the TLS public certificate is valid for the fqdn of the keycloak server i'm using, and then trust the jwt as verified?

I see keycloak has this url to get certs, but is this consistent accross all oidc providers?

  • https://<server>/realms/my-realm/protocol/openid-connect/certs
{
    "keys": [
        {
            "kid": "hjQx3z8lcjCqfiaY1Y9m79oBgvGi7_i1mfg-I5unscM",
            "kty": "RSA",
            "alg": "RSA-OAEP",
            "use": "enc",
            "n": "lAqdac7iZFxvtsTsLhU90E2rBYvOZflsPmiqdHOVS0gUXg60T5O_IsGe3AjSybufil17uKTOxfRWiG-4mDvgQb7XdE-AKxxMEGtH7BgN0IP5b3kqYmXe4FQaH2Jt7nmUBCkZi78lk2DkN8MSqr7Vx_RMu2FDNJmyP2X2VTAuraHN_77OZ8c6y-JrFR0MCKt-F7WPjWboD9AGlonJwPKldhvDWH_SKb2D8Re6jMYguM04mU-8nKLLbYpIFC9h2KD5wjc-GcAlJld7gkaz4IxO1iVsIhi0N2gOEhUttAxE0oacViNLcW0-m7THDR_Vf7CsfV0WHeOZP1_QsYAuqTfGBQ",
            "e": "AQAB",
            "x5c": [
                "MIIClzCCAX8CBgGMwVkL9zANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARob21lMB4XDTIzMTIzMTE5MjYyN1oXDTMzMTIzMTE5MjgwN1owDzENMAsGA1UEAwwEaG9tZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJQKnWnO4mRcb7bE7C4VPdBNqwWLzmX5bD5oqnRzlUtIFF4OtE+TvyLBntwI0sm7n4pde7ikzsX0VohvuJg74EG+13RPgCscTBBrR+wYDdCD+W95KmJl3uBUGh9ibe55lAQpGYu/JZNg5DfDEqq+1cf0TLthQzSZsj9l9lUwLq2hzf++zmfHOsviaxUdDAirfhe1j41m6A/QBpaJycDypXYbw1h/0im9g/EXuozGILjNOJlPvJyiy22KSBQvYdig+cI3PhnAJSZXe4JGs+CMTtYlbCIYtDdoDhIVLbQMRNKGnFYjS3FtPpu0xw0f1X+wrH1dFh3jmT9f0LGALqk3xgUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAblrPouTjs3FE6gKW1OKbJCPY8eLxagxWTJlAd3L2MMQwd+8PJrWg9itU1zbRYbAvuswG2vyyXueDG8mKNuyB//Ps2vHc4nhJ+Ftlq4Jtfr2mL7OcGllfbQ2xu9hSJ2rlAbfK2v8aadgihuHs5Eei+vbvwBPaCLk/r+Vr2uvQa9IdmZU4sT/HDp0HzQhS0tbn4JNKMw5uGlBhaStzbCwt+LU77o0hbHCuR8Zd4bpPA8IngsFq2xLrj5uevISzXRTGePopeLmHs+7Ze4PDEF/s/8w77pgZSxIBIoHaXtM+hOwnDTXqvEXf3zQRPCFCROyLZXmKacEk6p3DascoEgT5Sw=="
            ],
            "x5t": "CInAMKTky5oTCoSS-UPd8YrMDeI",
            "x5t#S256": "OP7rnCaYN-F9t1oCtftMIpkF0AlXLM6jwOI4Bb5tlbQ"
        },
        {
            "kid": "-BKYLwH9j-X6I06_clQOTeGhaBdO_V_gyohAgT5nz2U",
            "kty": "RSA",
            "alg": "RS256",
            "use": "sig",
            "n": "35XEG26lDImM9HHl4w1M9kgkAV1CJfpZPbiLRT_-IzZMrIXgpNzyoXe36LdKbB9pSPcPrw6kQu63CndLBtEq2p1H8ShBSxnwODZQx-YaAyi_yX7mO1OEAxvwkdmkWcbk9kp-YUFFDcuE8FLAywwn91vjfv6oh5yuACRshUHsxJfAX-_P46Rz9OxnOdmOMHeigu2pJ6KnIUkz4JpK068k8-ajHJCjtC2DjZ48olkAEdO-MpA9T_dpwb1sjlhO6p3OBBPGnyisC8l5U6dK6TRDRIqhdqELeQZYfZz2dDFWg3UWBGYr2fvlHSDyYtxqO9Qn0y7WHzYsuv8mhYED_-jTgQ",
            "e": "AQAB",
            "x5c": [
                "MIIClzCCAX8CBgGMwVkMjjANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARob21lMB4XDTIzMTIzMTE5MjYyN1oXDTMzMTIzMTE5MjgwN1owDzENMAsGA1UEAwwEaG9tZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+VxBtupQyJjPRx5eMNTPZIJAFdQiX6WT24i0U//iM2TKyF4KTc8qF3t+i3SmwfaUj3D68OpELutwp3SwbRKtqdR/EoQUsZ8Dg2UMfmGgMov8l+5jtThAMb8JHZpFnG5PZKfmFBRQ3LhPBSwMsMJ/db437+qIecrgAkbIVB7MSXwF/vz+Okc/TsZznZjjB3ooLtqSeipyFJM+CaStOvJPPmoxyQo7Qtg42ePKJZABHTvjKQPU/3acG9bI5YTuqdzgQTxp8orAvJeVOnSuk0Q0SKoXahC3kGWH2c9nQxVoN1FgRmK9n75R0g8mLcajvUJ9Mu1h82LLr/JoWBA//o04ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAPdAWFWvgiJtziPEyOy92JHZjq8RR3mPZhaAhdjoSB5oEjhe4k1c4frEjqsN072fUl7daL5Z39qqDDHN50EcRz+FeezgTi4RkOCmKvG7SapxKgv95+eBFo3R0+7Ecf+NpBJ284CQSAtcEAo1o+MEjXMOtGcb5OvHRbkjzGpHOsUCv2xXBBB31HuYKfPH0b+oz4YT8f9qQvwDmWuvoI/bYzywV4KxC/35RIJOHur9NhXTH6WyunTL5rSx5ifUWH1oTXytz+DAZ7Ug/6KlaCL42cnhAX9hVFOUvBr1EP7qvXpW7DJfKlJ+EBLjJ8fOBptZrCiEMWU7r/iMtR1cVCXQutg=="
            ],
            "x5t": "qucT8pa0PBZBvAhclzB3FoECFe8",
            "x5t#S256": "t1d5h_sDof-UO0iWSwnwiYmbJukjMjMDXB6rxgzhbbM"
        }
    ]
}

Update:
I followed the technique at this article to disable JWT verification, however, I'd like to do this the right way with verification setup automatically.

The RSA key (or any other public/private key such as ECDSA) is not the TLS certificate, but the certificate keycloak uses to sign the tokens. Keycloak uses the private part to sign. You need the public part to verify the token.

the easiest way to obtain this public key is using JSON Web Key Set. There is a plugin for JWT called https://github.com/MicahParks/keyfunc, this library can be used to obtain a JWKS from
an URL and can be supplied as the JWK keyfunc.

Most authorization servers such as keycloak present their public keys using JWKS. The URL depends on the keycloak url and the domain: https://www.keycloak.org/docs/latest/securing_apps/index.html#_certificate_endpoint

Ah, and most OIDC providers implement https://datatracker.ietf.org/doc/html/rfc8414. This allows you to discover the JWKS URI from a standardized URL location. I am not sure if keyfunc supports finding the JWKS locations this way. @MicahParks ?

The keyfunc and jwkset projects do not have any features for automatically finding JWK Sets. Specifying the location of a hosted JWK Set is a required feature of OpenID Connect Discovery.

In your case @lknite, it seems like the software you are writing in the context of this GitHub issue is for authenticating (and parsing) JWTs from Keycloak. This piece of software does not need to be aware of what OpenID Connect is or how it works, it only needs to authenticate JWTs.

The way you are currently doing it, with the embedded certificate is sufficient for some use cases, provided engineers know when and how to rotate the cert and redeploy in the event of key compromise, expiration, or other rotation. At the very least, the certificate is likely set to expire, after which point, the public key embedded in it shouldn't be used anymore.

Keycloak hosts a JWK Set for convenience. This JWK Set will automatically be updated with new public keys that will be used to sign JWTs. This allows programs to HTTP GET the JWK Set, cache the contents, then verify JWTs with the public key material found in there. In the case of Keycloak, the JWK Set is hosted at:

/realms/{realm-name}/protocol/openid-connect/certs

Here's the doc page for reference.
https://www.keycloak.org/docs/latest/securing_apps/index.html#_certificate_endpoint

If Keycloak were to change where the JWK Set is hosted, that would be a breaking change in my opinion. Such a change should require these docs to be updated and a new major release with a migration guide.

Given all of this, I would suggest you use the JWK Set Keycloak provides to get your public keys. Here's an example from the keyfunc project specifically for Keycloak. It will do all of the caching and updating for you automatically.
https://github.com/MicahParks/keyfunc/blob/master/examples/keycloak/main.go

Thanks everyone. I fully understand the situation now. Seems the oidc standard could use an update.

I understand that in my implementation I may or may not be able to get the needed certs and algorithm depending on the oidc provider being used. Since I can't be sure which provider is selected, I can implement some "best efforts to get the needed certs and discover the jwt verification algorithms to configure", as well as use the two strategies mentioned above to discover certs, with a fall-back strategy of just not verifying the jwt as long as TLS verification of the oidc server is valid.

Thanks everyone. I fully understand the situation now. Seems the oidc standard could use an update.

I understand that in my implementation I may or may not be able to get the needed certs and algorithm depending on the oidc provider being used. Since I can't be sure which provider is selected, I can implement some "best efforts to get the needed certs and discover the jwt verification algorithms to configure", as well as use the two strategies mentioned above to discover certs, with a fall-back strategy of just not verifying the jwt as long as TLS verification of the oidc server is valid.

I would not recommend that "fall-back" strategy as you have no way to verify the token. The TLS certificate of the OIDC has nothing to do with the token. The only sane choice you have at this point is to fail the authentication.

Upon further review, I see the rfc mentioned above rfc8414, is the familiar /.well-known/openid-configuration, and what we are interested in regarding this issue is the 'jwks_uri' field, which is surprisingly OPTIONAL. Being optional, makes me wonder if there is another way to verify the token. In any case, I suspect requiring that field or flagging the OIDC server as incompatible if its missing may be appropriate.

I understand now that when I think of 'harbor' using OIDC, it is also the client in the exchange, so trusting the OIDC FQDN is all that's required, whereas in my use case I have a separate cli and app, and when the app gets the token, the only verification it has to ensure the token came from the trusted oidc server is the method we've been discussing here. Ok, looking into consuming that 'jwks_uri' using the libraries you've mentioned. I'll report my findings here for folks who happen to come across this issue later with the same questions.

Note:

  • I see in the rfc8414 that the 'jwks_uri' field is OPTIONAL, however, when I look at the url provided by @MicahParks it looks to be required, that's a good sign.

image

  • rfc8414

It’s unfortunately a little bit of a mess, in a way that RFC8414, which is an OAuth 2.0 related RFC says it’s optional, but the use of the OpenID Connect (which builds upon OAuth 2.0) makes it required.

Some code for those who might be interested:

// get verification public certificates via oidc server

func initJwks() {
        /**
         * acquire jwks uri via rfc8414: begin
         */

        // initialize rest client
        client := &http.Client{
                Timeout: time.Second * 10,
        }
        uri := os.Getenv("OIDC_PROVIDER_URI") + "/.well-known/openid-configuration"
        log.Printf("uri: %v", uri)
        req, err := http.NewRequest("GET", uri, nil)
        if err != nil {
                log.Fatal(err)
        }

        // invoke method
        resp, err := client.Do(req)
        if err != nil {
                log.Fatal(err)
        }
        defer resp.Body.Close()

        //
        if resp.StatusCode != 200 {
                log.Fatal("jwks, StatusCode: " + strconv.Itoa(resp.StatusCode))
        }

        // read off body of response
        respBody, _ := io.ReadAll(resp.Body)

        //
        type Rfc8414 struct {
                Jwks_uri string `json:"jwks_uri"`
        }
        var oidc Rfc8414

        // parse json, acquire jwks_uri
        json.Unmarshal(respBody, &oidc)
        log.Printf("oidc.Jwks_uri: %v", oidc.Jwks_uri)

        /**
         * acquire jwks uri via rfc8414: end
         */

        //
        jwks, err = keyfunc.NewDefault([]string{oidc.Jwks_uri})
        if err != nil {
                log.Fatalf("Failed to create client JWK set. Error: %s", err)
        }
}

// verify tokens / get claims

                // parse the JWT
		token, err := jwt.Parse(tokenString, jwks.Keyfunc)
		if err != nil {
			log.Fatalf("Failed to parse the JWT.\nError: %s", err)
		}

                if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
                        return claims, nil
                }

                return nil, errors.New("authentication failed")

@lknite did you take a look at the keyfunc Keycloak example? It does what is in the example and a bit more.

Thanks again, I see now it lets you do the .Parse in a simpler way, thanks for the heads up. I had seen that example but somehow ended up with the code above. I'll see about simplifying things.

token, err := jwt.Parse(jwtB64, jwks.Keyfunc)

code updated with keyfunc