From ff2ce906c8118cff07be2ecb5b93e5d47f1c7efe Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 7 Jul 2026 21:31:09 +0000 Subject: [PATCH] fix(auth): accept EC/PS/EdDSA-signed OIDC ID tokens, not just RS256 The OIDC verifier was built with a bare oidc.Config{ClientID: ...}, so go-oidc applied its default of accepting RS256-signed ID tokens only. An identity provider configured with an EC signing key (e.g. Authentik) issues ES256-signed tokens, and the callback failed verification with: failed to verify ID token: oidc: malformed jwt: unexpected signature algorithm "HS256"; expected ["RS256"] surfacing to the user as HTTP 500 "failed to fetch user info" (#10677; the underlying cause became visible after the logging fix in #10679). Set SupportedSigningAlgs to the standard asymmetric algorithms (RS256/384/512, ES256/384/512, PS256/384/512, EdDSA). All are verified against the provider's published JWKS. HS256 is intentionally excluded: it is symmetric and would validate against the client secret, a different and security-sensitive trust model. Tested with a functional spec that signs an ES256 ID token and confirms it verifies with the configured algorithms and is rejected under go-oidc's RS256-only default (using oidc.StaticKeySet, no network). Closes #10677 Signed-off-by: Ettore Di Giacinto Assisted-by: Claude:claude-opus-4-8 [Claude Code] --- core/http/auth/oauth.go | 26 +++++- core/http/auth/oauth_signing_algs_test.go | 101 ++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 core/http/auth/oauth_signing_algs_test.go diff --git a/core/http/auth/oauth.go b/core/http/auth/oauth.go index 457419301..cb57806a3 100644 --- a/core/http/auth/oauth.go +++ b/core/http/auth/oauth.go @@ -23,6 +23,27 @@ import ( "github.com/mudler/LocalAI/pkg/httpclient" ) +// oidcSupportedSigningAlgs is the set of ID-token signature algorithms the OIDC +// verifier accepts. go-oidc defaults to RS256 only, which rejects identity +// providers that sign tokens with EC keys (ES*) or other algorithms — e.g. +// Authentik configured with an EC signing key fails the callback with +// "unexpected signature algorithm ... expected [RS256]" (#10677). All entries +// are asymmetric algorithms verified against the provider's published JWKS; +// HS256 is intentionally excluded (it is symmetric and would validate against +// the client secret, a different and security-sensitive trust model). +var oidcSupportedSigningAlgs = []string{ + oidc.RS256, oidc.RS384, oidc.RS512, + oidc.ES256, oidc.ES384, oidc.ES512, + oidc.PS256, oidc.PS384, oidc.PS512, + oidc.EdDSA, +} + +// OIDCSupportedSigningAlgs returns a copy of the ID-token signature algorithms +// the OIDC verifier accepts. Exposed for tests. +func OIDCSupportedSigningAlgs() []string { + return append([]string(nil), oidcSupportedSigningAlgs...) +} + // providerEntry holds the OAuth2/OIDC config for a single provider. type providerEntry struct { oauth2Config oauth2.Config @@ -85,7 +106,10 @@ func NewOAuthManager(baseURL string, params OAuthParams) (*OAuthManager, error) return nil, fmt.Errorf("OIDC discovery failed for %s: %w", params.OIDCIssuer, err) } - verifier := provider.Verifier(&oidc.Config{ClientID: params.OIDCClientID}) + verifier := provider.Verifier(&oidc.Config{ + ClientID: params.OIDCClientID, + SupportedSigningAlgs: oidcSupportedSigningAlgs, + }) m.providers[ProviderOIDC] = &providerEntry{ name: ProviderOIDC, diff --git a/core/http/auth/oauth_signing_algs_test.go b/core/http/auth/oauth_signing_algs_test.go new file mode 100644 index 000000000..a23f43f11 --- /dev/null +++ b/core/http/auth/oauth_signing_algs_test.go @@ -0,0 +1,101 @@ +//go:build auth + +package auth_test + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/mudler/LocalAI/core/http/auth" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + testOIDCIssuer = "https://issuer.example.test" + testOIDCClientID = "localai-test-client" +) + +// signES256IDToken builds a minimal ES256-signed JWT in JOSE compact form. The +// signature is the raw R||S encoding (32 bytes each for P-256) that OIDC +// verifiers expect, not ASN.1/DER. +func signES256IDToken(key *ecdsa.PrivateKey, claims map[string]any) string { + b64 := func(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } + header := b64([]byte(`{"alg":"ES256","typ":"JWT"}`)) + payloadJSON, err := json.Marshal(claims) + Expect(err).NotTo(HaveOccurred()) + signingInput := header + "." + b64(payloadJSON) + + digest := sha256.Sum256([]byte(signingInput)) + r, s, err := ecdsa.Sign(rand.Reader, key, digest[:]) + Expect(err).NotTo(HaveOccurred()) + sig := make([]byte, 64) + r.FillBytes(sig[:32]) + s.FillBytes(sig[32:]) + + return signingInput + "." + b64(sig) +} + +var _ = Describe("OIDC ID-token signing algorithms", func() { + var ( + ctx context.Context + key *ecdsa.PrivateKey + idToken string + keySet *oidc.StaticKeySet + ) + + BeforeEach(func() { + ctx = context.Background() + var err error + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + Expect(err).NotTo(HaveOccurred()) + idToken = signES256IDToken(key, map[string]any{ + "iss": testOIDCIssuer, + "sub": "user-1", + "aud": testOIDCClientID, + "email": "user@example.test", + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), + }) + keySet = &oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{key.Public()}} + }) + + It("accepts an EC (ES256) signed ID token with the configured algorithms", func() { + verifier := oidc.NewVerifier(testOIDCIssuer, keySet, &oidc.Config{ + ClientID: testOIDCClientID, + SupportedSigningAlgs: auth.OIDCSupportedSigningAlgs(), + }) + tok, err := verifier.Verify(ctx, idToken) + Expect(err).NotTo(HaveOccurred()) + Expect(tok.Subject).To(Equal("user-1")) + }) + + It("rejects the same token under go-oidc's RS256-only default (the pre-fix behavior)", func() { + // This reproduces #10677: without a broadened SupportedSigningAlgs the + // verifier only accepts RS256 and rejects EC-signed tokens. + verifier := oidc.NewVerifier(testOIDCIssuer, keySet, &oidc.Config{ + ClientID: testOIDCClientID, + }) + _, err := verifier.Verify(ctx, idToken) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("ES256")) + }) + + It("advertises the common asymmetric algorithms and excludes the symmetric HS256", func() { + algs := auth.OIDCSupportedSigningAlgs() + Expect(algs).To(ContainElements( + oidc.RS256, oidc.RS384, oidc.RS512, + oidc.ES256, oidc.ES384, oidc.ES512, + oidc.PS256, oidc.EdDSA, + )) + Expect(algs).NotTo(ContainElement("HS256")) + }) +})