mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Merge pull request #63213 from filmil/oidc-dist-claims
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Implements OIDC distributed claims. Next step to enable this feature is to enable claim caching. A distributed claim allows the OIDC provider to delegate a claim to a separate URL. Distributed claims are of the form as seen below, and are defined in the OIDC Connect Core 1.0, section 5.6.2. See: https://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims Example claim: ``` { ... (other normal claims)... "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "endpoint": "https://www.example.com", "access_token": "f005ba11" }, }, } ``` Example response to a followup request to https://www.example.com is a JWT-encoded claim token: ``` { "iss": "https://www.example.com", "aud": "my-client", "groups": ["team1", "team2"], "exp": 9876543210 } ``` Apart from the indirection, the distributed claim behaves exactly the same as a standard claim. For Kubernetes, this means that the token must be verified using the same approach as for the original OIDC token. This requires the presence of "iss", "aud" and "exp" claims in addition to "groups". All existing OIDC options (e.g. groups prefix) apply. Any claim can be made distributed, even though the "groups" claim is the primary use case. Allows groups to be a single string due to https://github.com/kubernetes/kubernetes/issues/33290, even though OIDC defines "groups" claim to be an array of strings. So, this will be parsed correctly: ``` { "iss": "https://www.example.com", "aud": "my-client", "groups": "team1", "exp": 9876543210 } ``` Expects that distributed claims endpoints return JWT, per OIDC specs. In case both a standard and a distributed claim with the same name exist, standard claim wins. The specs seem undecided about the correct approach here. Distributed claims are resolved serially. This could be parallelized for performance if needed. Aggregated claims are silently skipped. Support could be added if needed. **What this PR does / why we need it**: Makes it possible to retrieve many group memberships by offloading to a dedicated backend for groups resolution. **Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: Fixes #62920 **Special notes for your reviewer**: There are a few TODOs that seem better handled in separate commits. **Release note**: ```release-note Lays groundwork for OIDC distributed claims handling in the apiserver authentication token checker. A distributed claim allows the OIDC provider to delegate a claim to a separate URL. Distributed claims are of the form as seen below, and are defined in the OIDC Connect Core 1.0, section 5.6.2. For details, see: http://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims ```
This commit is contained in:
commit
ab9f64afa9
@ -8,11 +8,13 @@ load(
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
size = "small",
|
||||
srcs = ["oidc_test.go"],
|
||||
data = glob(["testdata/**"]),
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//vendor/github.com/coreos/go-oidc:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/gopkg.in/square/go-jose.v2:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
],
|
||||
|
@ -34,9 +34,11 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -48,6 +50,12 @@ import (
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
)
|
||||
|
||||
var (
|
||||
// synchronizeTokenIDVerifierForTest should be set to true to force a
|
||||
// wait until the token ID verifiers are ready.
|
||||
synchronizeTokenIDVerifierForTest = false
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// IssuerURL is the URL the provider signs ID Tokens as. This will be the "iss"
|
||||
// field of all tokens produced by the provider and is used for configuration
|
||||
@ -106,6 +114,70 @@ type Options struct {
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// initVerifier creates a new ID token verifier for the given configuration and issuer URL. On success, calls setVerifier with the
|
||||
// resulting verifier.
|
||||
func initVerifier(ctx context.Context, config *oidc.Config, iss string) (*oidc.IDTokenVerifier, error) {
|
||||
provider, err := oidc.NewProvider(ctx, iss)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init verifier failed: %v", err)
|
||||
}
|
||||
return provider.Verifier(config), nil
|
||||
}
|
||||
|
||||
// asyncIDTokenVerifier is an ID token verifier that allows async initialization
|
||||
// of the issuer check. Must be passed by reference as it wraps sync.Mutex.
|
||||
type asyncIDTokenVerifier struct {
|
||||
m sync.Mutex
|
||||
|
||||
// v is the ID token verifier initialized asynchronously. It remains nil
|
||||
// up until it is eventually initialized.
|
||||
// Guarded by m
|
||||
v *oidc.IDTokenVerifier
|
||||
}
|
||||
|
||||
// newAsyncIDTokenVerifier creates a new asynchronous token verifier. The
|
||||
// verifier is available immediately, but may remain uninitialized for some time
|
||||
// after creation.
|
||||
func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string) *asyncIDTokenVerifier {
|
||||
t := &asyncIDTokenVerifier{}
|
||||
|
||||
sync := make(chan struct{})
|
||||
// Polls indefinitely in an attempt to initialize the distributed claims
|
||||
// verifier, or until context canceled.
|
||||
initFn := func() (done bool, err error) {
|
||||
glog.V(4).Infof("oidc authenticator: attempting init: iss=%v", iss)
|
||||
v, err := initVerifier(ctx, c, iss)
|
||||
if err != nil {
|
||||
glog.Errorf("oidc authenticator: async token verifier for issuer: %q: %v", iss, err)
|
||||
return false, nil
|
||||
}
|
||||
t.m.Lock()
|
||||
defer t.m.Unlock()
|
||||
t.v = v
|
||||
close(sync)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
if done, _ := initFn(); !done {
|
||||
go wait.PollUntil(time.Second*10, initFn, ctx.Done())
|
||||
}
|
||||
}()
|
||||
|
||||
if synchronizeTokenIDVerifierForTest {
|
||||
<-sync
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// verifier returns the underlying ID token verifier, or nil if one is not yet initialized.
|
||||
func (a *asyncIDTokenVerifier) verifier() *oidc.IDTokenVerifier {
|
||||
a.m.Lock()
|
||||
defer a.m.Unlock()
|
||||
return a.v
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
issuerURL string
|
||||
|
||||
@ -120,6 +192,9 @@ type Authenticator struct {
|
||||
verifier atomic.Value
|
||||
|
||||
cancel context.CancelFunc
|
||||
|
||||
// resolver is used to resolve distributed claims.
|
||||
resolver *claimResolver
|
||||
}
|
||||
|
||||
func (a *Authenticator) setVerifier(v *oidc.IDTokenVerifier) {
|
||||
@ -217,16 +292,6 @@ func newAuthenticator(opts Options, initVerifier func(ctx context.Context, a *Au
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx = oidc.ClientContext(ctx, client)
|
||||
|
||||
authenticator := &Authenticator{
|
||||
issuerURL: opts.IssuerURL,
|
||||
usernameClaim: opts.UsernameClaim,
|
||||
usernamePrefix: opts.UsernamePrefix,
|
||||
groupsClaim: opts.GroupsClaim,
|
||||
groupsPrefix: opts.GroupsPrefix,
|
||||
requiredClaims: opts.RequiredClaims,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
now := opts.now
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
@ -238,43 +303,237 @@ func newAuthenticator(opts Options, initVerifier func(ctx context.Context, a *Au
|
||||
Now: now,
|
||||
}
|
||||
|
||||
var resolver *claimResolver
|
||||
if opts.GroupsClaim != "" {
|
||||
resolver = newClaimResolver(opts.GroupsClaim, client, verifierConfig)
|
||||
}
|
||||
|
||||
authenticator := &Authenticator{
|
||||
issuerURL: opts.IssuerURL,
|
||||
usernameClaim: opts.UsernameClaim,
|
||||
usernamePrefix: opts.UsernamePrefix,
|
||||
groupsClaim: opts.GroupsClaim,
|
||||
groupsPrefix: opts.GroupsPrefix,
|
||||
requiredClaims: opts.RequiredClaims,
|
||||
cancel: cancel,
|
||||
resolver: resolver,
|
||||
}
|
||||
|
||||
initVerifier(ctx, authenticator, verifierConfig)
|
||||
return authenticator, nil
|
||||
}
|
||||
|
||||
func hasCorrectIssuer(iss, tokenData string) bool {
|
||||
parts := strings.Split(tokenData, ".")
|
||||
// untrustedIssuer extracts an untrusted "iss" claim from the given JWT token,
|
||||
// or returns an error if the token can not be parsed. Since the JWT is not
|
||||
// verified, the returned issuer should not be trusted.
|
||||
func untrustedIssuer(token string) (string, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return false
|
||||
return "", fmt.Errorf("malformed token")
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return false
|
||||
return "", fmt.Errorf("error decoding token: %v", err)
|
||||
}
|
||||
claims := struct {
|
||||
// WARNING: this JWT is not verified. Do not trust these claims.
|
||||
Issuer string `json:"iss"`
|
||||
}{}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return "", fmt.Errorf("while unmarshaling token: %v", err)
|
||||
}
|
||||
return claims.Issuer, nil
|
||||
}
|
||||
|
||||
func hasCorrectIssuer(iss, tokenData string) bool {
|
||||
uiss, err := untrustedIssuer(tokenData)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if claims.Issuer != iss {
|
||||
if uiss != iss {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// endpoint represents an OIDC distributed claims endpoint.
|
||||
type endpoint struct {
|
||||
// URL to use to request the distributed claim. This URL is expected to be
|
||||
// prefixed by one of the known issuer URLs.
|
||||
URL string `json:"endpoint,omitempty"`
|
||||
// AccessToken is the bearer token to use for access. If empty, it is
|
||||
// not used. Access token is optional per the OIDC distributed claims
|
||||
// specification.
|
||||
// See: http://openid.net/specs/openid-connect-core-1_0.html#DistributedExample
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
// JWT is the container for aggregated claims. Not supported at the moment.
|
||||
// See: http://openid.net/specs/openid-connect-core-1_0.html#AggregatedExample
|
||||
JWT string `json:"JWT,omitempty"`
|
||||
}
|
||||
|
||||
// claimResolver expands distributed claims by calling respective claim source
|
||||
// endpoints.
|
||||
type claimResolver struct {
|
||||
// claim is the distributed claim that may be resolved.
|
||||
claim string
|
||||
|
||||
// client is the to use for resolving distributed claims
|
||||
client *http.Client
|
||||
|
||||
// config is the OIDC configuration used for resolving distributed claims.
|
||||
config *oidc.Config
|
||||
|
||||
// verifierPerIssuer contains, for each issuer, the appropriate verifier to use
|
||||
// for this claim. It is assumed that there will be very few entries in
|
||||
// this map.
|
||||
// Guarded by m.
|
||||
verifierPerIssuer map[string]*asyncIDTokenVerifier
|
||||
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// newClaimResolver creates a new resolver for distributed claims.
|
||||
func newClaimResolver(claim string, client *http.Client, config *oidc.Config) *claimResolver {
|
||||
return &claimResolver{claim: claim, client: client, config: config, verifierPerIssuer: map[string]*asyncIDTokenVerifier{}}
|
||||
}
|
||||
|
||||
// Verifier returns either the verifier for the specified issuer, or error.
|
||||
func (r *claimResolver) Verifier(iss string) (*oidc.IDTokenVerifier, error) {
|
||||
r.m.Lock()
|
||||
av := r.verifierPerIssuer[iss]
|
||||
if av == nil {
|
||||
// This lazy init should normally be very quick.
|
||||
// TODO: Make this context cancelable.
|
||||
ctx := oidc.ClientContext(context.Background(), r.client)
|
||||
av = newAsyncIDTokenVerifier(ctx, r.config, iss)
|
||||
r.verifierPerIssuer[iss] = av
|
||||
}
|
||||
r.m.Unlock()
|
||||
|
||||
v := av.verifier()
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("verifier not initialized for issuer: %q", iss)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// expand extracts the distributed claims from claim names and claim sources.
|
||||
// The extracted claim value is pulled up into the supplied claims.
|
||||
//
|
||||
// Distributed claims are of the form as seen below, and are defined in the
|
||||
// OIDC Connect Core 1.0, section 5.6.2.
|
||||
// See: https://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims
|
||||
//
|
||||
// {
|
||||
// ... (other normal claims)...
|
||||
// "_claim_names": {
|
||||
// "groups": "src1"
|
||||
// },
|
||||
// "_claim_sources": {
|
||||
// "src1": {
|
||||
// "endpoint": "https://www.example.com",
|
||||
// "access_token": "f005ba11"
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
func (r *claimResolver) expand(c claims) error {
|
||||
const (
|
||||
// The claim containing a map of endpoint references per claim.
|
||||
// OIDC Connect Core 1.0, section 5.6.2.
|
||||
claimNamesKey = "_claim_names"
|
||||
// The claim containing endpoint specifications.
|
||||
// OIDC Connect Core 1.0, section 5.6.2.
|
||||
claimSourcesKey = "_claim_sources"
|
||||
)
|
||||
|
||||
_, ok := c[r.claim]
|
||||
if ok {
|
||||
// There already is a normal claim, skip resolving.
|
||||
return nil
|
||||
}
|
||||
names, ok := c[claimNamesKey]
|
||||
if !ok {
|
||||
// No _claim_names, no keys to look up.
|
||||
return nil
|
||||
}
|
||||
|
||||
claimToSource := map[string]string{}
|
||||
if err := json.Unmarshal([]byte(names), &claimToSource); err != nil {
|
||||
return fmt.Errorf("oidc: error parsing distributed claim names: %v", err)
|
||||
}
|
||||
|
||||
rawSources, ok := c[claimSourcesKey]
|
||||
if !ok {
|
||||
// Having _claim_names claim, but no _claim_sources is not an expected
|
||||
// state.
|
||||
return fmt.Errorf("oidc: no claim sources")
|
||||
}
|
||||
|
||||
var sources map[string]endpoint
|
||||
if err := json.Unmarshal([]byte(rawSources), &sources); err != nil {
|
||||
// The claims sources claim is malformed, this is not an expected state.
|
||||
return fmt.Errorf("oidc: could not parse claim sources: %v", err)
|
||||
}
|
||||
|
||||
src, ok := claimToSource[r.claim]
|
||||
if !ok {
|
||||
// No distributed claim present.
|
||||
return nil
|
||||
}
|
||||
ep, ok := sources[src]
|
||||
if !ok {
|
||||
return fmt.Errorf("id token _claim_names contained a source %s missing in _claims_sources", src)
|
||||
}
|
||||
if ep.URL == "" {
|
||||
// This is maybe an aggregated claim (ep.JWT != "").
|
||||
return nil
|
||||
}
|
||||
return r.resolve(ep, c)
|
||||
}
|
||||
|
||||
// resolve requests distributed claims from all endpoints passed in,
|
||||
// and inserts the lookup results into allClaims.
|
||||
func (r *claimResolver) resolve(endpoint endpoint, allClaims claims) error {
|
||||
// TODO: cache resolved claims.
|
||||
jwt, err := getClaimJWT(r.client, endpoint.URL, endpoint.AccessToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("while getting distributed claim %q: %v", r.claim, err)
|
||||
}
|
||||
untrustedIss, err := untrustedIssuer(jwt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting untrusted issuer from endpoint %v failed for claim %q: %v", endpoint.URL, r.claim, err)
|
||||
}
|
||||
v, err := r.Verifier(untrustedIss)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifying untrusted issuer %v failed: %v", untrustedIss, err)
|
||||
}
|
||||
t, err := v.Verify(context.Background(), jwt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify distributed claim token: %v", err)
|
||||
}
|
||||
var distClaims claims
|
||||
if err := t.Claims(&distClaims); err != nil {
|
||||
return fmt.Errorf("could not parse distributed claims for claim %v: %v", r.claim, err)
|
||||
}
|
||||
value, ok := distClaims[r.claim]
|
||||
if !ok {
|
||||
return fmt.Errorf("jwt returned by distributed claim endpoint %s did not contain claim: %v", endpoint, r.claim)
|
||||
}
|
||||
allClaims[r.claim] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) AuthenticateToken(token string) (user.Info, bool, error) {
|
||||
if !hasCorrectIssuer(a.issuerURL, token) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
verifier, ok := a.idTokenVerifier()
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("oidc: authenticator not initialized")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
idToken, err := verifier.Verify(ctx, token)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: verify token: %v", err)
|
||||
@ -284,6 +543,12 @@ func (a *Authenticator) AuthenticateToken(token string) (user.Info, bool, error)
|
||||
if err := idToken.Claims(&c); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse claims: %v", err)
|
||||
}
|
||||
if a.resolver != nil {
|
||||
if err := a.resolver.expand(c); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: could not expand distributed claims: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var username string
|
||||
if err := c.unmarshalClaim(a.usernameClaim, &username); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.usernameClaim, err)
|
||||
@ -349,6 +614,39 @@ func (a *Authenticator) AuthenticateToken(token string) (user.Info, bool, error)
|
||||
return info, true, nil
|
||||
}
|
||||
|
||||
// getClaimJWT gets a distributed claim JWT from url, using the supplied access
|
||||
// token as bearer token. If the access token is "", the authorization header
|
||||
// will not be set.
|
||||
// TODO: Allow passing in JSON hints to the IDP.
|
||||
func getClaimJWT(client *http.Client, url, accessToken string) (string, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// TODO: Allow passing request body with configurable information.
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("while calling %v: %v", url, err)
|
||||
}
|
||||
if accessToken != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", accessToken))
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Report non-OK status code as an error.
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode > http.StatusIMUsed {
|
||||
return "", fmt.Errorf("error while getting distributed claim JWT: %v", response.Status)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
responseBytes, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not decode distributed claim response")
|
||||
}
|
||||
return string(responseBytes), nil
|
||||
}
|
||||
|
||||
type stringOrArray []string
|
||||
|
||||
func (s *stringOrArray) UnmarshalJSON(b []byte) error {
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
@ -25,12 +26,17 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"github.com/golang/glog"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
@ -122,22 +128,148 @@ var (
|
||||
)
|
||||
|
||||
type claimsTest struct {
|
||||
name string
|
||||
options Options
|
||||
now time.Time
|
||||
signingKey *jose.JSONWebKey
|
||||
pubKeys []*jose.JSONWebKey
|
||||
claims string
|
||||
want *user.DefaultInfo
|
||||
wantSkip bool
|
||||
wantErr bool
|
||||
wantInitErr bool
|
||||
name string
|
||||
options Options
|
||||
now time.Time
|
||||
signingKey *jose.JSONWebKey
|
||||
pubKeys []*jose.JSONWebKey
|
||||
claims string
|
||||
want *user.DefaultInfo
|
||||
wantSkip bool
|
||||
wantErr bool
|
||||
wantInitErr bool
|
||||
claimToResponseMap map[string]string
|
||||
openIDConfig string
|
||||
}
|
||||
|
||||
// Replace formats the contents of v into the provided template.
|
||||
func replace(tmpl string, v interface{}) string {
|
||||
t := template.Must(template.New("test").Parse(tmpl))
|
||||
buf := bytes.NewBuffer(nil)
|
||||
t.Execute(buf, &v)
|
||||
ret := buf.String()
|
||||
glog.V(4).Infof("Replaced: %v into: %v", tmpl, ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
// newClaimServer returns a new test HTTPS server, which is rigged to return
|
||||
// OIDC responses to requests that resolve distributed claims. signer is the
|
||||
// signer used for the served JWT tokens. claimToResponseMap is a map of
|
||||
// responses that the server will return for each claim it is given.
|
||||
func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, claimToResponseMap map[string]string, openIDConfig *string) *httptest.Server {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
glog.V(5).Infof("request: %+v", *r)
|
||||
switch r.URL.Path {
|
||||
case "/.testing/keys":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
keyBytes, err := json.Marshal(keys)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error while marshaling keys: %v", err)
|
||||
}
|
||||
glog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes))
|
||||
w.Write(keyBytes)
|
||||
|
||||
case "/.well-known/openid-configuration":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
glog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig)
|
||||
w.Write([]byte(*openIDConfig))
|
||||
// These claims are tested in the unit tests.
|
||||
case "/groups":
|
||||
fallthrough
|
||||
case "/rabbits":
|
||||
if claimToResponseMap == nil {
|
||||
t.Errorf("no claims specified in response")
|
||||
}
|
||||
claim := r.URL.Path[1:] // "/groups" -> "groups"
|
||||
expectedAuth := fmt.Sprintf("Bearer %v_token", claim)
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth != expectedAuth {
|
||||
t.Errorf("bearer token expected: %q, was %q", expectedAuth, auth)
|
||||
}
|
||||
jws, err := signer.Sign([]byte(claimToResponseMap[claim]))
|
||||
if err != nil {
|
||||
t.Errorf("while signing response token: %v", err)
|
||||
}
|
||||
token, err := jws.CompactSerialize()
|
||||
if err != nil {
|
||||
t.Errorf("while serializing response token: %v", err)
|
||||
}
|
||||
w.Write([]byte(token))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "unexpected URL: %v", r.URL)
|
||||
}
|
||||
}))
|
||||
glog.V(4).Infof("Serving OIDC at: %v", ts.URL)
|
||||
return ts
|
||||
}
|
||||
|
||||
// writeTempCert writes out the supplied certificate into a temporary file in
|
||||
// PEM-encoded format. Returns the name of the temporary file used. The caller
|
||||
// is responsible for cleaning the file up.
|
||||
func writeTempCert(t *testing.T, cert []byte) string {
|
||||
tempFile, err := ioutil.TempFile("", "ca.crt")
|
||||
if err != nil {
|
||||
t.Fatalf("could not open temp file: %v", err)
|
||||
}
|
||||
block := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert,
|
||||
}
|
||||
if err := pem.Encode(tempFile, block); err != nil {
|
||||
t.Fatalf("could not write to temp file %v: %v", tempFile.Name(), err)
|
||||
}
|
||||
tempFile.Close()
|
||||
return tempFile.Name()
|
||||
}
|
||||
|
||||
func toKeySet(keys []*jose.JSONWebKey) jose.JSONWebKeySet {
|
||||
ret := jose.JSONWebKeySet{}
|
||||
for _, k := range keys {
|
||||
ret.Keys = append(ret.Keys, *k)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *claimsTest) run(t *testing.T) {
|
||||
var (
|
||||
signer jose.Signer
|
||||
err error
|
||||
)
|
||||
if c.signingKey != nil {
|
||||
// Initialize the signer only in the tests that make use of it. We can
|
||||
// not defer this initialization because the test server uses it too.
|
||||
signer, err = jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(c.signingKey.Algorithm),
|
||||
Key: c.signingKey,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("initialize signer: %v", err)
|
||||
}
|
||||
}
|
||||
// The HTTPS server used for requesting distributed groups claims.
|
||||
ts := newClaimServer(t, toKeySet(c.pubKeys), signer, c.claimToResponseMap, &c.openIDConfig)
|
||||
defer ts.Close()
|
||||
|
||||
// Make the certificate of the helper server available to the authenticator
|
||||
// by writing its root CA certificate into a temporary file.
|
||||
tempFileName := writeTempCert(t, ts.TLS.Certificates[0].Certificate[0])
|
||||
defer os.Remove(tempFileName)
|
||||
c.options.CAFile = tempFileName
|
||||
|
||||
// Allow claims to refer to the serving URL of the test server. For this,
|
||||
// substitute all references to {{.URL}} in appropriate places.
|
||||
v := struct{ URL string }{URL: ts.URL}
|
||||
c.claims = replace(c.claims, &v)
|
||||
c.openIDConfig = replace(c.openIDConfig, &v)
|
||||
c.options.IssuerURL = replace(c.options.IssuerURL, &v)
|
||||
for claim, response := range c.claimToResponseMap {
|
||||
c.claimToResponseMap[claim] = replace(response, &v)
|
||||
}
|
||||
|
||||
// Initialize the authenticator.
|
||||
a, err := newAuthenticator(c.options, func(ctx context.Context, a *Authenticator, config *oidc.Config) {
|
||||
// Set the verifier to use the public key set instead of reading
|
||||
// from a remote.
|
||||
// Set the verifier to use the public key set instead of reading from a remote.
|
||||
a.setVerifier(oidc.NewVerifier(
|
||||
c.options.IssuerURL,
|
||||
&staticKeySet{keys: c.pubKeys},
|
||||
@ -155,13 +287,6 @@ func (c *claimsTest) run(t *testing.T) {
|
||||
}
|
||||
|
||||
// Sign and serialize the claims in a JWT.
|
||||
signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(c.signingKey.Algorithm),
|
||||
Key: c.signingKey,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("initialize signer: %v", err)
|
||||
}
|
||||
jws, err := signer.Sign([]byte(c.claims))
|
||||
if err != nil {
|
||||
t.Fatalf("sign claims: %v", err)
|
||||
@ -200,6 +325,7 @@ func (c *claimsTest) run(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
synchronizeTokenIDVerifierForTest = true
|
||||
tests := []claimsTest{
|
||||
{
|
||||
name: "token",
|
||||
@ -356,6 +482,353 @@ func TestToken(t *testing.T) {
|
||||
Groups: []string{"team1", "team2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groups-distributed",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/groups",
|
||||
"access_token": "groups_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1", "team2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groups-distributed-malformed-claim-names",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "nonexistent-claim-source"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/groups",
|
||||
"access_token": "groups_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "groups-distributed-malformed-names-and-sources",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "groups-distributed-malformed-distributed-claim",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/groups",
|
||||
"access_token": "groups_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
// Doesn't contain the "groups" claim as it promises.
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "groups-distributed-unusual-name",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "rabbits",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"rabbits": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/rabbits",
|
||||
"access_token": "rabbits_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
"rabbits": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"rabbits": ["team1", "team2"],
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1", "team2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groups-distributed-wrong-audience",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/groups",
|
||||
"access_token": "groups_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
// Note mismatching "aud"
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "your-client",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
// "aud" was "your-client", not "my-client"
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "groups-distributed-wrong-audience",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/groups",
|
||||
"access_token": "groups_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
// Note expired timestamp.
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d
|
||||
}`, expired.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
// The distributed token is expired.
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
// Specs are unclear about this behavior. We adopt a behavior where
|
||||
// normal claim wins over a distributed claim by the same name.
|
||||
name: "groups-distributed-normal-claim-wins",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": "team1",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/groups",
|
||||
"access_token": "groups_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"groups": ["team2"],
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
// "team1" is from the normal "groups" claim.
|
||||
Groups: []string{"team1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Groups should be able to be a single string, not just a slice.
|
||||
name: "group-string-claim",
|
||||
@ -382,6 +855,83 @@ func TestToken(t *testing.T) {
|
||||
Groups: []string{"team1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Groups should be able to be a single string, not just a slice.
|
||||
name: "group-string-claim-distributed",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/groups",
|
||||
"access_token": "groups_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"groups": "team1",
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group-string-claim-aggregated-not-supported",
|
||||
options: Options{
|
||||
IssuerURL: "https://auth.example.com",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
GroupsClaim: "groups",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"JWT": "some.jwt.token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
// if the groups claim isn't provided, this shouldn't error out
|
||||
name: "no-groups-claim",
|
||||
@ -661,6 +1211,53 @@ func TestToken(t *testing.T) {
|
||||
Groups: []string{"groups:team1", "groups:team2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groups-prefix-distributed",
|
||||
options: Options{
|
||||
IssuerURL: "{{.URL}}",
|
||||
ClientID: "my-client",
|
||||
UsernameClaim: "username",
|
||||
UsernamePrefix: "oidc:",
|
||||
GroupsClaim: "groups",
|
||||
GroupsPrefix: "groups:",
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"_claim_names": {
|
||||
"groups": "src1"
|
||||
},
|
||||
"_claim_sources": {
|
||||
"src1": {
|
||||
"endpoint": "{{.URL}}/groups",
|
||||
"access_token": "groups_token"
|
||||
}
|
||||
},
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
claimToResponseMap: map[string]string{
|
||||
"groups": fmt.Sprintf(`{
|
||||
"iss": "{{.URL}}",
|
||||
"aud": "my-client",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
},
|
||||
openIDConfig: `{
|
||||
"issuer": "{{.URL}}",
|
||||
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||
}`,
|
||||
want: &user.DefaultInfo{
|
||||
Name: "oidc:jane",
|
||||
Groups: []string{"groups:team1", "groups:team2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid-signing-alg",
|
||||
options: Options{
|
||||
|
Loading…
Reference in New Issue
Block a user