Skip to content

Conversation

acroca
Copy link
Member

@acroca acroca commented Oct 6, 2025

Ref: #4003

At the moment the oidc auth type only supports authentication using clientID and clientSecret.

This PR adds authentication using certificates instead. The component will use those certificates to build the client assertion and call the IdP to get a token.

This adds the following new fields in the kafka components:

  • oidcClientAuthMethod to select between the old client_secret mechanism, or the new client_jwt mechanism.
  • oidcClientAssertionCert
  • oidcClientAssertionKey
  • oidcResource
  • oidcAudience

In this PR I also added tests for both authentication mechanisms against a local kafka, using keycloak as an IdP.

@acroca acroca force-pushed the client-assertion-oauth branch from c23e275 to 9cd61ec Compare October 6, 2025 17:02
@acroca acroca marked this pull request as ready for review October 9, 2025 07:40
@acroca acroca requested review from a team as code owners October 9, 2025 07:40
@acroca acroca force-pushed the client-assertion-oauth branch 2 times, most recently from 2e0fe18 to 6cb1166 Compare October 9, 2025 08:06
@acroca acroca force-pushed the client-assertion-oauth branch from 6cb1166 to 742b291 Compare October 9, 2025 08:35
@acroca acroca changed the title Feat: Support client assertion auth method for kafka oidc Feat: Support client JWT auth method for kafka oidc Oct 9, 2025
Comment on lines +243 to 251
if m.OidcClientAuthMethod == "client_secret" && m.OidcClientSecret == "" {
return nil, errors.New("kafka error: missing OIDC Client Secret for authType 'oidc' (client_secret)")
}
if m.OidcClientAuthMethod == "client_jwt" && m.OidcClientAssertionCert == "" {
return nil, errors.New("kafka error: missing OIDC Client Assertion Cert for authType 'oidc' (client_jwt)")
}
if m.OidcClientAuthMethod == "client_jwt" && m.OidcClientAssertionKey == "" {
return nil, errors.New("kafka error: missing OIDC Client Assertion Key for authType 'oidc' (client_jwt)")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can we use pointers to signal presence.

Comment on lines +180 to +196
block, _ := pem.Decode([]byte(ts.ClientAssertionKey))
if block == nil {
return nil, errors.New("invalid PEM private key for client assertion")
}
pk, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// try PKCS1
if rsaKey, err2 := x509.ParsePKCS1PrivateKey(block.Bytes); err2 == nil {
pk = rsaKey
} else {
return nil, fmt.Errorf("unable to parse private key: %w", err)
}
}
rsaKey, ok := pk.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("client_jwt requires RSA private key")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use dapr/kit/crypto/pem.

Subject(ts.ClientID).
Audience([]string{audClaim}).
IssuedAt(now).
Expiration(now.Add(1 * time.Minute)).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 1 minute?

urlValues.Set("scope", strings.Join(ts.Scopes, " "))
}

timeoutCtx, cancel := ctx.WithTimeout(ctx.TODO(), tokenRequestTimeout)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why TODO?

Comment on lines +240 to +263
defer cancel()
ts.configureClient()
req, err := http.NewRequestWithContext(timeoutCtx, http.MethodPost, ts.TokenEndpoint.TokenURL, strings.NewReader(urlValues.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := ts.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("token endpoint returned %d", resp.StatusCode)
}
var tr tokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
return nil, err
}
if tr.AccessToken == "" {
return nil, errors.New("no access_token in response")
}
ts.CachedToken = oauth2.Token{AccessToken: tr.AccessToken, Expiry: time.Now().Add(time.Duration(tr.ExpiresIn) * time.Second)}
return ts.asSaramaToken(), nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there not something that does all this for us in a library?

Comment on lines +21 to +34
-----BEGIN CERTIFICATE-----
MIIEJTCCAg2gAwIBAgIUS0RIkoWlMVd8UfE/I7IEzKD4kmwwDQYJKoZIhvcNAQEL
BQAwGDEWMBQGA1UEAwwNbG9jYWwtb2lkYy1jYTAeFw0yNTEwMDMxMDI3NDlaFw0y
ODAxMDYxMDI3NDlaMBsxGTAXBgNVBAMMEGRhcHItb2lkYy1jbGllbnQwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQIqGyGGlQRqXNDlSuHRDGmnJ4dQDb
jK/KxeC+wXQiDCSfHgP7NOjZOOCiQYKAUs9MRg+tKepxFRORZLAbRlU9kuU4Z7bg
E6wDMztCqTASUZtwZeN36iJDcQ++xnmAi8j0bvs7w00wrgPMXPnT09Sf9sQyEkR2
t26OLcQnZHwrUWHIbZeXqvRF7uYG00GKjFmqI+FjrmDx0hoENew+Jzpk7S/7m5t2
h/weVAr5REtGTIQxbX8nNmoO2JFbyILBcfP9M9R9zxiTEFVgP5sl2QfLFerPpULM
1tAiCO+oPk8CAFiD9TKe+HR/Us4uIxgRmxjgBnzmFSJjDflvK2DslpzPAgMBAAGj
ZDBiMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQU
oCgedGjEdPHOBRq6OwKKGUmpUXIwHwYDVR0jBBgwFoAUImUtm30c0E0Z1Xsep9o/
nXsqyGYwDQYJKoZIhvcNAQELBQADggIBAKafBCz1EUny2pKOI8xTHVkoZjhTvtm2
Ly2DicAEwtESXlBta64bOug4mTq8RLdKxLa8xSBSZxzygTfb+q8IcuuYL0/DAttZ
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use hard coded certificates and keys in tests. They get copied and used in the real world, and they also expire.

Always generate them in the test, and use secret refs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants