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

[management] improve zitadel idp error response detail by decoding errors #2634

Merged
merged 3 commits into from
Sep 27, 2024

Conversation

adasauce
Copy link
Contributor

Describe your changes

Adds a zitadelErrorResponse struct to decode errors returned from zitadel rather than just returning a status code, so more information can be learned about potential configuration issues. Helped to diagnose #2616

I have some other small changes and improvements to compatibility later, but this was a pretty clear cut and standalone improvement.

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)
  • Extended the README / documentation, if necessary

@CLAassistant
Copy link

CLAassistant commented Sep 23, 2024

CLA assistant check
All committers have signed the CLA.

@adasauce adasauce force-pushed the zitadel-improvements branch from c29dac6 to 1176f2a Compare September 23, 2024 19:00
@mlsmaycon mlsmaycon requested a review from bcmmbaga September 26, 2024 20:45
Copy link
Contributor

@bcmmbaga bcmmbaga left a comment

Choose a reason for hiding this comment

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

Could you also include handling for the Zitadel error when requesting JWT token?

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unable to get zitadel token, statusCode %d", resp.StatusCode)
}

management/server/idp/zitadel.go Outdated Show resolved Hide resolved
management/server/idp/zitadel.go Outdated Show resolved Hide resolved
management/server/idp/zitadel.go Outdated Show resolved Hide resolved
@adasauce
Copy link
Contributor Author

Could you also include handling for the Zitadel error when requesting JWT token?

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unable to get zitadel token, statusCode %d", resp.StatusCode)
}

yeah, i think since the only thing the method references internally is the helper, and they're two different structs it makes more sense to pull the parser out as a standalone utility function within the package and pass the bound helper of zm/zc + the io.readcloser of resp.Body.

I have the changes laid out like that locally here, but lmk if you'd like to see it go in that direction vs. something else.

@bcmmbaga
Copy link
Contributor

Could you also include handling for the Zitadel error when requesting JWT token?

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unable to get zitadel token, statusCode %d", resp.StatusCode)
}

yeah, i think since the only thing the method references internally is the helper, and they're two different structs it makes more sense to pull the parser out as a standalone utility function within the package and pass the bound helper of zm/zc + the io.readcloser of resp.Body.

I have the changes laid out like that locally here, but lmk if you'd like to see it go in that direction vs. something else.

Yes, you can extract the parser as a standalone function and initialize the helper within that function, as it's an instance of JsonParser

@adasauce adasauce force-pushed the zitadel-improvements branch from cb3815c to a54e173 Compare September 27, 2024 16:46
@adasauce
Copy link
Contributor Author

ah, have some tests to clean up to expect a zitadel message in the output. sorry didnt check first.

@bcmmbaga
Copy link
Contributor

I ran the tests and found an issue when the JWT request fails. It seems they're using a different error response format

2024-09-27T20:01:23+03:00 WARN [context: SYSTEM] management/server/account.go:1017: failed warming up cache due to error: unable to get zitadel token, statusCode 400, zitadel: error code: 0 message:

Error format:{"error":"invalid_request","error_description":"Errors.User.Machine.Secret.Invalid"}

To make it generic lets return the string(bodyBytes) as the new error. This way, we don't need to worry about the error structure.

// readZitadelError parses errors returned by the zitadel APIs from a response.
func readZitadelError(body io.ReadCloser) error {
	bodyBytes, err := io.ReadAll(body)
	if err != nil {
		return fmt.Errorf("failed to read response body: %w", err)
	}

	return errors.New(string(bodyBytes))
}

Also, the Zitadel tests are failing. Could you take a look, or would you like some help with that?

@adasauce
Copy link
Contributor Author

yeah, I was just going to follow up to mention that it's a different error format with error and error_description fields with an actual failure. we could parse it, or just dump it as is.

I'm on board with just stringifying the body and returning that. the only downside I think is that with the other error response types you get extra data that isn't really important to the error, like encoded type info about the message field being a string.

@bcmmbaga
Copy link
Contributor

bcmmbaga commented Sep 27, 2024

I'm on board with just stringifying the body and returning that. the only downside I think is that with the other error response types you get extra data that isn't really important to the error, like encoded type info about the message field being a string.

If there's a possibility of extra fields in the error response (which I couldn't reproduce), we can extend the zitadelErrorResponse to include these new fields. When constructing the error message, we should ensure to omit any fields with empty values

@adasauce
Copy link
Contributor Author

I'm on board with just stringifying the body and returning that. the only downside I think is that with the other error response types you get extra data that isn't really important to the error, like encoded type info about the message field being a string.

If there's a possibility of extra fields in the error response (which I couldn't reproduce), we can extend the zitadelErrorResponse to include these new fields. When constructing the error message, we should ensure to omit any fields with empty values

before pusing it up, how about this:

// readZitadelError parses errors returned by the zitadel APIs from a response.
func readZitadelError(body io.ReadCloser) error {
	bodyBytes, err := io.ReadAll(body)
	if err != nil {
		return fmt.Errorf("failed to read response body: %w", err)
	}

	helper := JsonParser{}
	var target map[string]interface{}
	err = helper.Unmarshal(bodyBytes, &target)
	if err != nil {
		return fmt.Errorf("error unparsable body: %s", string(bodyBytes))
	}

	// ensure keys are ordered for consistent logging behaviour.
	errorKeys := make([]string, 0, len(target))
	for k := range target {
		errorKeys = append(errorKeys, k)
	}
	slices.Sort(errorKeys)

	var errsOut []string
	for _, k := range errorKeys {
		if _, isEmbedded := target[k].(map[string]interface{}); isEmbedded {
			continue
		}
		errsOut = append(errsOut, fmt.Sprintf("[%v: %v]", k, target[k]))
	}

	if len(errsOut) == 0 {
		return fmt.Errorf("[data missing]")
	}

	return fmt.Errorf(strings.Join(errsOut, " "))
}

omits any extra embedded fields, gets rid of the extra data structures and returns the top level elements that we care about.

I updated the test cases like this to demonstrate:

	requestJWTTokenTestCase2 := requestJWTTokenTest{
		name:                    "Request Bad Status Code",
		inputCode:               400,
		inputRespBody:           "{\"error\": \"invalid_scope\", \"error_description\":\"openid missing\"}",
		helper:                  JsonParser{},
		expectedFuncExitErrDiff: fmt.Errorf("unable to get zitadel token, statusCode 400, zitadel: [error: invalid_scope] [error_description: openid missing]"),
		expectedToken:           "",
	}

@bcmmbaga
Copy link
Contributor

This looks good. Just a minor change, remove the brackets around the error message and we’ll be good to go.

more generically parse the error returned by zitadel.
@adasauce adasauce force-pushed the zitadel-improvements branch from a54e173 to e3cf807 Compare September 27, 2024 18:37
@bcmmbaga bcmmbaga force-pushed the zitadel-improvements branch from f13adf6 to 9f697e8 Compare September 27, 2024 18:57
Copy link

@bcmmbaga bcmmbaga merged commit 58ff7ab into netbirdio:main Sep 27, 2024
21 checks passed
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.

3 participants