-
Notifications
You must be signed in to change notification settings - Fork 195
Description
Preflight Checklist
- I could not find a solution in the documentation, the existing issues or discussions
- I have joined the ZITADEL chat
Version
v3.42.0
Describe the problem caused by this bug
These two methods don't support PKCE for OPs that require PKCE for device authorization:
rp.DeviceAuthorizationrp.DeviceAccessToken
This causes device authorization to fail when used against an OP that requires PKCE for device authorization.
To reproduce
Configure an OP (e.g., KeyCloak) to require PKCE, then attempt to use device authorization via zitadel/oidc.
Screenshots
No response
Expected behavior
The methods used in device authorization flow should support code challenges.
Additional Context
We found some workarounds:
DeviceAuthorization does support a custom authFn, so you can pass a custom http.FormAuthorization function that adds the code_challenge and code_challenge_method:
var authFn zhttp.FormAuthorization = func(form url.Values) {
form.Add("code_challenge", challenge)
form.Add("code_challenge_method", challengeMethod)
}
resp, err := rp.DeviceAuthorization(ctx, p.scopes, p.provider, authFn)
if err != nil {
return nil, err
}For getting the token, rp.DeviceAccessToken doesn't support an auth function. We worked around this limitation by creating a custom http RoundTripper that adds code_verifier to the form data and then rewrites the request body before sending it. Since the relying party supports passing a custom RoundTripper, this works. We place the code_verifier in the request context so the RoundTripper can access it during the request:
func NewPkceClient() *http.Client {
// Create a new HTTP client with a custom RoundTripper that adds the PKCE verifier to the request headers.
return &http.Client{
Transport: &pkceRoundTripper{
base: http.DefaultTransport,
},
Timeout: time.Minute,
}
}
type pkceRoundTripper struct {
base http.RoundTripper
}
func (rt *pkceRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
verifierVal := r.Context().Value(CodeVerifierHeader)
if verifier, ok := verifierVal.(string); ok && verifier != "" {
slog.Debug("Found PKCE code_verifier, including into request body")
// Parse the existing form data from the request body.
// This consumes r.Body but populates r.Form.
if err := r.ParseForm(); err != nil {
return nil, err
}
// Add the code_verifier to the parsed form values.
r.Form.Set("code_verifier", verifier)
// Encode the updated form values back into a string.
newBody := r.Form.Encode() // e.g., "client_id=...&grant_type=...&code_verifier=..."
// Replace the request body with the new data.
r.Body = io.NopCloser(strings.NewReader(newBody))
// Update the Content-Length header to match the new body size.
// Also update the header value, as some servers might check it.
contentLength := int64(len(newBody))
r.ContentLength = contentLength
r.Header.Set("Content-Length", fmt.Sprintf("%d", contentLength))
}
return rt.base.RoundTrip(r)
}Metadata
Metadata
Assignees
Labels
Type
Projects
Status