Repository: zitadel/oidc (go)

Issue:
Set IDTokenSigningAlgValuesSupported of DiscoveryConfiguration obtained by Discover when initializing RelyingParty.
### Preflight Checklist

- [X] I could not find a solution in the existing issues, docs, nor discussions
- [ ] I have joined the [ZITADEL chat](https://zitadel.com/chat)

### Describe your problem

When [IDTokenVerifier](https://github.com/zitadel/oidc/blob/v3.18.0/pkg/client/rp/relying_party.go#L68) is executed on a RelyingParty created with [NewRelyingPartyOIDC](https://github.com/zitadel/oidc/blob/v3.18.0/pkg/client/rp/relying_party.go#L217), only RS256 is supported.

[CheckSignature](https://github.com/zitadel/oidc/blob/main/pkg/oidc/verifier.go#L150) is performed on [VerifyIDToken](https://github.com/zitadel/oidc/blob/v3.18.0/pkg/client/rp/verifier.go#L33), but only RS256 is supported because supportedSigAlgs is always set to 0.

To solve this, specify [WithSupportedSigningAlgorithms](https://github.com/zitadel/oidc/blob/v3.18.0/pkg/client/rp/verifier.go#L168) in the options argument of NewRelyingPartyOIDC.

To determine the value of WithSupportedSigningAlgorithms, create a [DiscoveryConfiguration](https://github.com/zitadel/oidc/blob/v3.18.0/pkg/oidc/discovery.go#L7) using the [Discover](https://github.com/zitadel/oidc/blob/v3.18.0/pkg/client/client.go#L30) method and specify IDTokenSigningAlgValuesSupported.

It is inefficient to use the Discover method twice with NewRelyingPartyOIDC and WithSupportedSigningAlgorithms.

### Describe your ideal solution

Add the following processing in [NewRelyingPartyOIDC](https://github.com/zitadel/oidc/blob/v3.18.0/pkg/client/rp/relying_party.go#L217).


```go
	rp.verifierOpts = append(rp.verifierOpts, WithSupportedSigningAlgorithms(discoveryConfiguration.IDTokenSigningAlgValuesSupported...))

```


### Version

v3.18.0

### Additional Context

_No response_

Pull request description:
feat: Added the ability to verify ID tokens using the value of id_token_signing_alg_values_supported retrieved from DiscoveryEndpoint
### Definition of Ready

fixes #574

Fixed `discoveryConfiguration.IDTokenSigningAlgValuesSupported` to be set to `WithSupportedSigningAlgorithms()`.

I thought about adding and setting `discoveryConfiguration` to the `relyingParty` struct field and doing `for _, optFunc := range options{}` after getting `discoveryConfiguration`, `WithCustomDiscoveryUrl()` had to be processed before `client.Discover()`.

- [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
- [ ] Critical parts are tested automatically
- [x] Where possible E2E tests are implemented
- [ ] Documentation/examples are up-to-date
- [ ] All non-functional requirements are met
- [ ] Functionality of the acceptance criteria is checked manually on the dev system.

Patch:
diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go
index bd2041b..ac7f466 100644
--- a/pkg/client/rp/relying_party.go
+++ b/pkg/client/rp/relying_party.go
@@ -90,12 +90,13 @@ var DefaultUnauthorizedHandler UnauthorizedHandler = func(w http.ResponseWriter,
 }
 
 type relyingParty struct {
-	issuer            string
-	DiscoveryEndpoint string
-	endpoints         Endpoints
-	oauthConfig       *oauth2.Config
-	oauth2Only        bool
-	pkce              bool
+	issuer                      string
+	DiscoveryEndpoint           string
+	endpoints                   Endpoints
+	oauthConfig                 *oauth2.Config
+	oauth2Only                  bool
+	pkce                        bool
+	useSigningAlgsFromDiscovery bool
 
 	httpClient    *http.Client
 	cookieHandler *httphelper.CookieHandler
@@ -238,6 +239,9 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
 	if err != nil {
 		return nil, err
 	}
+	if rp.useSigningAlgsFromDiscovery {
+		rp.verifierOpts = append(rp.verifierOpts, WithSupportedSigningAlgorithms(discoveryConfiguration.IDTokenSigningAlgValuesSupported...))
+	}
 	endpoints := GetEndpoints(discoveryConfiguration)
 	rp.oauthConfig.Endpoint = endpoints.Endpoint
 	rp.endpoints = endpoints
@@ -348,6 +352,15 @@ func WithLogger(logger *slog.Logger) Option {
 	}
 }
 
+// WithSigningAlgsFromDiscovery appends the [WithSupportedSigningAlgorithms] option to the Verifier Options.
+// The algorithms returned in the `id_token_signing_alg_values_supported` from the discovery response will be set.
+func WithSigningAlgsFromDiscovery() Option {
+	return func(rp *relyingParty) error {
+		rp.useSigningAlgsFromDiscovery = true
+		return nil
+	}
+}
+
 type SignerFromKey func() (jose.Signer, error)
 
 func SignerFromKeyPath(path string) SignerFromKey {


Test patch:
diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go
index 9145c1e..98a9d3a 100644
--- a/pkg/client/integration_test.go
+++ b/pkg/client/integration_test.go
@@ -111,6 +111,92 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) {
 	}
 }
 
+func TestRelyingPartyWithSigningAlgsFromDiscovery(t *testing.T) {
+	targetURL := "http://local-site"
+	localURL, err := url.Parse(targetURL + "/login?requestID=1234")
+	require.NoError(t, err, "local url")
+
+	t.Log("------- start example OP ------")
+	seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
+	clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
+	clientSecret := "secret"
+	client := storage.WebClient(clientID, clientSecret, targetURL)
+	storage.RegisterClients(client)
+	exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
+	var dh deferredHandler
+	opServer := httptest.NewServer(&dh)
+	defer opServer.Close()
+	dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true)
+
+	t.Log("------- create RP ------")
+	provider, err := rp.NewRelyingPartyOIDC(
+		CTX,
+		opServer.URL,
+		clientID,
+		clientSecret,
+		targetURL,
+		[]string{"openid"},
+		rp.WithSigningAlgsFromDiscovery(),
+	)
+	require.NoError(t, err, "new rp")
+
+	t.Log("------- run authorization code flow ------")
+	jar, err := cookiejar.New(nil)
+	require.NoError(t, err, "create cookie jar")
+	httpClient := &http.Client{
+		Timeout: time.Second * 5,
+		CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+		Jar: jar,
+	}
+	state := "state-" + strconv.FormatInt(seed.Int63(), 25)
+	capturedW := httptest.NewRecorder()
+	get := httptest.NewRequest("GET", localURL.String(), nil)
+	rp.AuthURLHandler(func() string { return state }, provider,
+		rp.WithPromptURLParam("Hello, World!", "Goodbye, World!"),
+		rp.WithURLParam("custom", "param"),
+	)(capturedW, get)
+	defer func() {
+		if t.Failed() {
+			t.Log("response body (redirect from RP to OP)", capturedW.Body.String())
+		}
+	}()
+	resp := capturedW.Result()
+	startAuthURL, err := resp.Location()
+	require.NoError(t, err, "get redirect")
+	loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL)
+	form := getForm(t, "get login form", httpClient, loginPageURL)
+	defer func() {
+		if t.Failed() {
+			t.Logf("login form (unfilled): %s", string(form))
+		}
+	}()
+	postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL,
+		gosubmit.Set("username", "test-user@local-site"),
+		gosubmit.Set("password", "verysecure"),
+	)
+	codeBearingURL := getRedirect(t, "get redirect with code", httpClient, postLoginRedirectURL)
+	capturedW = httptest.NewRecorder()
+	get = httptest.NewRequest("GET", codeBearingURL.String(), nil)
+	var idToken string
+	redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
+		idToken = newTokens.IDToken
+		http.Redirect(w, r, targetURL, http.StatusFound)
+	}
+	rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get)
+	defer func() {
+		if t.Failed() {
+			t.Log("token exchange response body", capturedW.Body.String())
+			require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code")
+		}
+	}()
+
+	t.Log("------- verify id token ------")
+	_, err = rp.VerifyIDToken[*oidc.IDTokenClaims](CTX, idToken, provider.IDTokenVerifier())
+	require.NoError(t, err, "verify id token")
+}
+
 func TestResourceServerTokenExchange(t *testing.T) {
 	for _, wrapServer := range []bool{false, true} {
 		t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {