Move the caller verifier onto internal/jose
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 33s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 33s
Completes KEY-WP-0019. Client.Verify now delegates JWK-set parsing and RS256 signature checking to internal/jose, so the caller path and upstream provider verification share one implementation rather than two copies that drift. Its claim policy stays put: audience and nonce bindings belong to the caller. The existing authclient tests pass unchanged, which is the evidence the migration preserved behaviour. One deliberate strictness increase: a key set containing any malformed RSA signing key is refused outright rather than used alongside a good key. KeyCape's /jwks publishes a single key, so no current deployment is affected. Adds direct tests for internal/jose. It is now the single verifier behind both paths, and testing it only through its callers would leave duplicate key ids, crit headers, even exponents and undersized moduli covered by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P Assistant: claude-code Assistant-Model: opus Assistant-Process: 713576@bnt-lap001 Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
This commit is contained in:
parent
8c1a3e052c
commit
3ec8404c87
4 changed files with 197 additions and 59 deletions
|
|
@ -4,20 +4,18 @@ package authclient
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"keycape/internal/jose"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
|
@ -95,66 +93,23 @@ func (c *Client) Discover(ctx context.Context) (Discovery, error) {
|
|||
// Verify checks RS256 using the discovered issuer's JWKS and exact claim bindings.
|
||||
func (c *Client) Verify(ctx context.Context, d Discovery, token, audience, nonce string) (map[string]any, error) {
|
||||
fail := errors.New("token signature or claim validation failed")
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fail
|
||||
}
|
||||
decode := base64.RawURLEncoding.DecodeString
|
||||
header, err := decode(parts[0])
|
||||
if err != nil {
|
||||
return nil, fail
|
||||
}
|
||||
var h struct {
|
||||
Alg string `json:"alg"`
|
||||
Kid string `json:"kid"`
|
||||
Crit []string `json:"crit"`
|
||||
}
|
||||
if json.Unmarshal(header, &h) != nil || h.Alg != "RS256" || h.Kid == "" || len(h.Crit) != 0 {
|
||||
return nil, fail
|
||||
}
|
||||
var jwks struct {
|
||||
Keys []struct{ Kty, Use, Alg, Kid, N, E string } `json:"keys"`
|
||||
}
|
||||
if err := c.request(ctx, http.MethodGet, d.JWKS, nil, "", "", &jwks); err != nil {
|
||||
|
||||
// Signature checking lives in internal/jose so this path and the upstream
|
||||
// provider verification in the authelia adapter cannot drift apart
|
||||
// (KEY-WP-0019-T05). The claim policy below stays here: audience and nonce
|
||||
// bindings are this caller's, not something a shared verifier can know.
|
||||
var rawJWKS json.RawMessage
|
||||
if err := c.request(ctx, http.MethodGet, d.JWKS, nil, "", "", &rawJWKS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pub *rsa.PublicKey
|
||||
for _, k := range jwks.Keys {
|
||||
if k.Kid != h.Kid {
|
||||
continue
|
||||
}
|
||||
if pub != nil || k.Kty != "RSA" || (k.Alg != "" && k.Alg != "RS256") || (k.Use != "" && k.Use != "sig") {
|
||||
return nil, fail
|
||||
}
|
||||
n, ne := decode(k.N)
|
||||
e, ee := decode(k.E)
|
||||
if ne != nil || ee != nil || len(e) == 0 || len(e) > 4 {
|
||||
return nil, fail
|
||||
}
|
||||
pub = &rsa.PublicKey{N: new(big.Int).SetBytes(n), E: int(new(big.Int).SetBytes(e).Int64())}
|
||||
if pub.N.BitLen() < 2048 || pub.E < 3 || pub.E%2 == 0 {
|
||||
return nil, fail
|
||||
}
|
||||
}
|
||||
if pub == nil {
|
||||
return nil, fail
|
||||
}
|
||||
sig, err := decode(parts[2])
|
||||
keys, err := jose.ParseJWKS(rawJWKS)
|
||||
if err != nil {
|
||||
return nil, fail
|
||||
}
|
||||
hash := sha256.Sum256([]byte(parts[0] + "." + parts[1]))
|
||||
if rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], sig) != nil {
|
||||
return nil, fail
|
||||
}
|
||||
payload, err := decode(parts[1])
|
||||
claims, err := jose.Verify(token, keys)
|
||||
if err != nil {
|
||||
return nil, fail
|
||||
}
|
||||
claims := map[string]any{}
|
||||
if json.Unmarshal(payload, &claims) != nil {
|
||||
return nil, fail
|
||||
}
|
||||
exp, okExp := claims["exp"].(float64)
|
||||
iat, okIat := claims["iat"].(float64)
|
||||
now := float64(time.Now().Unix())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue