mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-07 23:07:34 -04:00
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 <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
This commit is contained in:
@@ -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,
|
||||
|
||||
101
core/http/auth/oauth_signing_algs_test.go
Normal file
101
core/http/auth/oauth_signing_algs_test.go
Normal file
@@ -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"))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user