Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Does Zitadel support generating access_token using client_secret_jwt for the grant type of client_credentials #655

Closed
1 of 2 tasks
idavollen opened this issue Sep 21, 2024 · 7 comments
Assignees
Labels
auth docs Improvements or additions to documentation

Comments

@idavollen
Copy link

idavollen commented Sep 21, 2024

Preflight Checklist

  • I could not find a solution in the existing issues, docs, nor discussions
  • I have joined the ZITADEL chat

Describe the docs your are missing or that are wrong

I can find neither documentation nor code example of testing client_credentials with client_secret_jwt to generate access token for machine-to-machine communication without sending the client secret to the authorization server.

I ran https://github.com/zitadel/oidc/blob/3b64e792ed1c01daf6bb3320a8da4ffa346753c2/example/server/main.go as OP and created a Go client that attempts to create access_token with client_secret_jwt.

But in the end, I've got this error message:
Error getting access token: failed to get access token: {"error":"invalid_client"}

I'm looking forward to your confirmation or shed light on how to use Zitadel OIDC for my requested purpose.


package main

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"time"

	"github.com/golang-jwt/jwt/v4"
)

// Function to generate a client_secret_jwt
func generateClientSecretJWT(clientID, clientSecret, tokenURL string) (string, error) {
	// Define the claims for the JWT
	claims := jwt.MapClaims{
		"iss": clientID,
		"sub": clientID,
		"aud": tokenURL,
		"exp": time.Now().Add(time.Minute * 5).Unix(), // 5 minutes expiration
		"iat": time.Now().Unix(),
		"jti": "unique-jwt-id", // You can generate this dynamically
	}

	// Create a new JWT token with HS256 signing method
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// Sign the token using the client secret
	signedToken, err := token.SignedString([]byte(clientSecret))
	if err != nil {
		return "", err
	}

	return signedToken, nil
}

// Function to request an access token from the Authorization Server
func requestAccessToken(clientID, clientSecret, tokenURL string) (string, error) {
	// Generate the client_secret_jwt
	clientAssertion, err := generateClientSecretJWT(clientID, clientSecret, tokenURL)
	if err != nil {
		return "", err
	}
	fmt.Println("Generated jwt payload: ", clientAssertion)

	// Prepare the form data for the token request
	data := url.Values{}
	data.Set("grant_type", "client_credentials")
	data.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
	data.Set("client_assertion", clientAssertion)

	// Create a new POST request
	req, err := http.NewRequest("POST", tokenURL, bytes.NewBufferString(data.Encode()))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	// Send the request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	// Read and parse the response
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to get access token: %s", string(body))
	}

	// Parse the JSON response to extract the access token
	var result map[string]interface{}
	if err := json.Unmarshal(body, &result); err != nil {
		return "", err
	}

	accessToken, ok := result["access_token"].(string)
	if !ok {
		return "", fmt.Errorf("access token not found in response")
	}

	return accessToken, nil
}

func main() {
// I also tried api/secret without luck
	clientID := "sid1"
	clientSecret := "everysecret"
	tokenURL := "https://www.myexample.com/oauth/token"
	// Request the access token
	accessToken, err := requestAccessToken(clientID, clientSecret, tokenURL)
	if err != nil {
		fmt.Println("Error getting access token:", err)
		return
	}

	fmt.Println("Access Token:", accessToken)
}

Additional Context

No response

@idavollen idavollen added the docs Improvements or additions to documentation label Sep 21, 2024
@muhlemmer muhlemmer moved this to 🧐 Investigating in Product Management Sep 22, 2024
@muhlemmer muhlemmer added the auth label Sep 22, 2024
@muhlemmer muhlemmer self-assigned this Sep 25, 2024
@idavollen
Copy link
Author

any progress? Can you confirm that the client_secret_jwt is for the time being not supported? The more complicated private_key_jwt is already supported, why is the easier one, client_secret_jwt is not supported?

@idavollen
Copy link
Author

any feedback? it has been over 3 weeks. can your team either confirm it's bug and give a plan for fixing it, or deny it as a feature?

@muhlemmer
Copy link
Collaborator

We already support JWT Profile grant. You are probably doing something wrong.

  1. For example where do you load the secret from? If you are using the example OP, you need to use the key for the example service.
  2. You use the HS256 algorithm. This is a symetric key algorithm. We only support asymetric keys. The example OP only supports RS256.
  3. We already provide a JWT Profile client package. No need to write all that code manually. Use the package and pass the correct secret and it should work.

any feedback? it has been over 3 weeks. can your team either confirm it's bug and give a plan for fixing it, or deny it as a feature?

Please don't push us for free support. Community support is based on the time we have available.

@muhlemmer muhlemmer closed this as not planned Won't fix, can't repro, duplicate, stale Oct 23, 2024
@github-project-automation github-project-automation bot moved this from 🧐 Investigating to ✅ Done in Product Management Oct 23, 2024
@idavollen
Copy link
Author

@muhlemmer I just browsed through the link mentioned by you to this one : token_endpoint that lists 4 types:

  1. client_secret_basic
  2. client_secret_post
  3. none (PKCE)
  4. private_key_jwt

As you can see, it doesn't list client_secret_jwt based on the shared secret, that is symmetric key, while, does support asymmetric key private_key_jwt, based on PKI RSA key

@idavollen
Copy link
Author

My above observation fits well with your source code : https://github.com/zitadel/oidc/blob/main/pkg/oidc/discovery.go#L152-L156

const (
	AuthMethodBasic         AuthMethod = "client_secret_basic"
	AuthMethodPost          AuthMethod = "client_secret_post"
	AuthMethodNone          AuthMethod = "none"
	AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt"
)

@idavollen
Copy link
Author

both private_key_jwt and client_secret_jwt are introduced by https://www.rfc-editor.org/rfc/rfc8414.html. Zitadel/oidc is declared as OpenID certified OIDC implementation. I'm just wondering why this client_secret_jwt recommended by RFC8414 is left out by your project?

for instance, it's supported by Spring Java web apps, https://docs.spring.io/spring-security/reference/reactive/oauth2/client/client-authentication.html#_authenticate_using_client_secret_jwt, and many Java backends might fail to communicate with your OIDC Provider

So what's the rationale for client_secret_jwt is dropped by your oidc project, especially as OpenID certificated OIDC implementation?

@muhlemmer
Copy link
Collaborator

client_secret_jwt recommended by RFC8414

I don't see the term RECOMMENDED anywhere concerning client_secret_jwt.

for instance, it's supported by Spring Java web apps, https://docs.spring.io/spring-security/reference/reactive/oauth2/client/client-authentication.html#_authenticate_using_client_secret_jwt, and many Java backends might fail to communicate with your OIDC Provider

Spring works fine the the methods we already provide: https://zitadel.com/docs/sdk-examples/java

especially as OpenID certificated OIDC implementation?

We do not need to support all types of auth methods and signing algorithms to become certified. All we need to prove is support for RS256. The only (soft) required authentication method is client_secret_basic as a default if the token_endpoint_auth_methods_supported in provider metadata is empty.

token_endpoint_auth_methods_supported
OPTIONAL. JSON array containing a list of Client Authentication methods supported by this Token Endpoint. The options are client_secret_post, client_secret_basic, client_secret_jwt, and private_key_jwt, as described in Section 9 of OpenID Connect Core 1.0 [OpenID.Core]. Other authentication methods MAY be defined by extensions. If omitted, the default is client_secret_basic -- the HTTP Basic Authentication Scheme specified in Section 2.3.1 of OAuth 2.0 [RFC6749].

token_endpoint_auth_signing_alg_values_supported
OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values) supported by the Token Endpoint for the signature on the JWT [JWT] used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods. Servers SHOULD support RS256. The value none MUST NOT be used.

The above is pretty clear on what SHOULD be supported by all OIDC implementations, server and client alike. Hence, your arguments like "many Java backends might fail" are essentially flawed. If there are implementations that do not support this primitives, please go there and bug them.

We do not plan to support symmetric keys at the moment. Because current methods work fine and are widely supported. So adding symmetric keys is an extra maintenance burden we are not interested in. Please consider this discussion closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auth docs Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

2 participants