Repository: zitadel/oidc
Pull request #629 (2024-08-01 10:37:45)

feat(op): allow returning of parent errors to client
This change introduces the possibility to return parent errors to clients. This is implemented by an optional flag on the `*oidc.Error`.

Currently it only support setting the parent inside JSON responses, not redirect URLs. As Go errors don't unmarshall well, only the marshaller is implemented for the moment.

Related to https://github.com/zitadel/zitadel/issues/8362

### Definition of Ready

- [x] I am happy with the code
- [x] Short description of the feature/issue is added in the pr description
- [x] PR is linked to the corresponding user story
- [x] Acceptance criteria are met
- [x] All open todos and follow ups are defined in a new ticket and justified
- [x] Deviations from the acceptance criteria and design are agreed with the PO and documented.
- [x] No debug or dead code
- [x] My code has no repetitions
- [x] Critical parts are tested automatically
- [x] Where possible E2E tests are implemented
- [x] Documentation/examples are up-to-date
- [x] All non-functional requirements are met
- [x] Functionality of the acceptance criteria is checked manually on the dev system.




Patch:
diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go
index 2f0572d..1100f73 100644
--- a/pkg/oidc/error.go
+++ b/pkg/oidc/error.go
@@ -1,6 +1,7 @@
 package oidc
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"log/slog"
@@ -133,6 +134,24 @@ type Error struct {
 	Description      string    `json:"error_description,omitempty" schema:"error_description,omitempty"`
 	State            string    `json:"state,omitempty" schema:"state,omitempty"`
 	redirectDisabled bool      `schema:"-"`
+	returnParent     bool      `schema:"-"`
+}
+
+func (e *Error) MarshalJSON() ([]byte, error) {
+	m := struct {
+		Error            errorType `json:"error"`
+		ErrorDescription string    `json:"error_description,omitempty"`
+		State            string    `json:"state,omitempty"`
+		Parent           string    `json:"parent,omitempty"`
+	}{
+		Error:            e.ErrorType,
+		ErrorDescription: e.Description,
+		State:            e.State,
+	}
+	if e.returnParent {
+		m.Parent = e.Parent.Error()
+	}
+	return json.Marshal(m)
 }
 
 func (e *Error) Error() string {
@@ -165,6 +184,18 @@ func (e *Error) WithParent(err error) *Error {
 	return e
 }
 
+// WithReturnParentToClient allows returning the set parent error to the HTTP client.
+// Currently it only supports setting the parent inside JSON responses, not redirect URLs.
+// As Go errors don't unmarshal well, only the marshaller is implemented for the moment.
+//
+// Warning: parent errors may contain sensitive data or unwanted details about the server status.
+// Also, the `parent` field is not a standard error field and might confuse certain clients
+// that require fully compliant responses.
+func (e *Error) WithReturnParentToClient(b bool) *Error {
+	e.returnParent = b
+	return e
+}
+
 func (e *Error) WithDescription(desc string, args ...any) *Error {
 	e.Description = fmt.Sprintf(desc, args...)
 	return e


Test patch:
diff --git a/pkg/oidc/error_test.go b/pkg/oidc/error_test.go
index 2eeb4e6..40d30b1 100644
--- a/pkg/oidc/error_test.go
+++ b/pkg/oidc/error_test.go
@@ -1,11 +1,14 @@
 package oidc
 
 import (
+	"encoding/json"
+	"errors"
 	"io"
 	"log/slog"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestDefaultToServerError(t *testing.T) {
@@ -151,3 +154,39 @@ func TestError_LogValue(t *testing.T) {
 		})
 	}
 }
+
+func TestError_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name string
+		e    *Error
+		want string
+	}{
+		{
+			name: "simple error",
+			e:    ErrAccessDenied(),
+			want: `{"error":"access_denied","error_description":"The authorization request was denied."}`,
+		},
+		{
+			name: "with description",
+			e:    ErrAccessDenied().WithDescription("oops"),
+			want: `{"error":"access_denied","error_description":"oops"}`,
+		},
+		{
+			name: "with parent",
+			e:    ErrServerError().WithParent(errors.New("oops")),
+			want: `{"error":"server_error"}`,
+		},
+		{
+			name: "with return parent",
+			e:    ErrServerError().WithParent(errors.New("oops")).WithReturnParentToClient(true),
+			want: `{"error":"server_error","parent":"oops"}`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := json.Marshal(tt.e)
+			require.NoError(t, err)
+			assert.JSONEq(t, tt.want, string(got))
+		})
+	}
+}