Mint and verify short-lived RS256 user tokens.

ExApps sign after AppAPI auth; Microservices check a static public key or an OIDC issuer and forward the same bearer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-28 14:03:52 +02:00
co-authored by Cursor
parent 92feaa6915
commit f082561cc6
13 changed files with 1091 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
package usertoken
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"math/big"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
func TestOIDCVerifier(t *testing.T) {
key := testKey(t)
var srv *httptest.Server
mux := http.NewServeMux()
srv = httptest.NewServer(mux)
t.Cleanup(srv.Close)
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"issuer": srv.URL,
"jwks_uri": srv.URL + "/jwks",
"id_token_signing_alg_values_supported": []string{"RS256"},
})
})
mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(publicJWKS(&key.PublicKey))
})
auth, err := New(t.Context(), Config{
OIDCIssuer: srv.URL,
OIDCAudience: "mgmnt",
GroupsClaim: "identity_groups",
})
if err != nil {
t.Fatalf("New: %v", err)
}
now := time.Now()
groups := []string{"family"}
raw, err := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iss": srv.URL,
"sub": "konrad",
"preferred_username": "konrad",
"aud": "mgmnt",
"iat": now.Unix(),
"exp": now.Add(time.Minute).Unix(),
"identity_groups": groups,
}).SignedString(key)
if err != nil {
t.Fatal(err)
}
caller, err := auth.Verify(t.Context(), raw)
if err != nil {
t.Fatalf("Verify: %v", err)
}
if caller.Subject != "konrad" || len(caller.Groups) != 1 || caller.Groups[0] != "family" {
t.Fatalf("caller = %+v", caller)
}
if _, err := New(t.Context(), Config{
PublicKey: &key.PublicKey,
Issuer: "manager",
Audience: "mgmnt",
OIDCIssuer: srv.URL,
}); err == nil {
t.Fatal("both modes accepted")
}
if _, err := auth.Verify(t.Context(), "not-a-token"); !errors.Is(err, ErrUnauthorized) {
t.Fatalf("bad token err = %v", err)
}
}
func publicJWKS(pub *rsa.PublicKey) map[string]any {
return map[string]any{
"keys": []map[string]string{{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": base64.RawURLEncoding.EncodeToString(pub.N.Bytes()),
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pub.E)).Bytes()),
}},
}
}