Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit 871a5a5

Browse files
committed
Adds token passthrough using the review token endpoint
Signed-off-by: JoshVanL <[email protected]>
1 parent c5ca429 commit 871a5a5

File tree

5 files changed

+206
-14
lines changed

5 files changed

+206
-14
lines changed

cmd/options/options.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ import (
99
cliflag "k8s.io/component-base/cli/flag"
1010
)
1111

12+
type TokenPassthroughOptions struct {
13+
Audiences []string
14+
Enabled bool
15+
}
16+
17+
func (t *TokenPassthroughOptions) AddFlags(fs *pflag.FlagSet) {
18+
fs.StringSliceVar(&t.Audiences, "token-audiences", t.Audiences, ""+
19+
"List of the identifiers that the resource server presented with the token "+
20+
"identifies as. Audience-aware token authenticators will verify that the token "+
21+
"was intended for at least one of the audiences in this list. If no audiences "+
22+
"are provided, the audience will default to the audience of the Kubernetes "+
23+
"apiserver.")
24+
25+
fs.BoolVar(&t.Enabled, "token-passthrough", t.Enabled, ""+
26+
"Requests with Bearer tokens that fail OIDC validation are tried against "+
27+
"the API server using the Token Review endpoint. If successful, the request "+
28+
"is sent on as is, with no impersonation.")
29+
}
30+
1231
type OIDCAuthenticationOptions struct {
1332
APIAudiences []string
1433
CAFile string
@@ -32,26 +51,22 @@ func (o *OIDCAuthenticationOptions) Validate() error {
3251

3352
func (o *OIDCAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
3453
fs.StringSliceVar(&o.APIAudiences, "api-audiences", o.APIAudiences, ""+
35-
"Identifiers of the API. The service account token authenticator will validate that "+
36-
"tokens used against the API are bound to at least one of these audiences. If the "+
37-
"--service-account-issuer flag is configured and this flag is not, this field "+
38-
"defaults to a single element list containing the issuer URL .")
54+
"Identifiers of the API. This can be used as an additional list of "+
55+
"identifiers that exist in the target audiences of requests.")
3956

4057
fs.StringVar(&o.IssuerURL, "oidc-issuer-url", o.IssuerURL, ""+
41-
"The URL of the OpenID issuer, only HTTPS scheme will be accepted. "+
42-
"If set, it will be used to verify the OIDC JSON Web Token (JWT).")
58+
"The URL of the OpenID issuer, only HTTPS scheme will be accepted.")
4359

4460
fs.StringVar(&o.ClientID, "oidc-client-id", o.ClientID,
45-
"The client ID for the OpenID Connect client, must be set if oidc-issuer-url is set.")
61+
"The client ID for the OpenID Connect client.")
4662

4763
fs.StringVar(&o.CAFile, "oidc-ca-file", o.CAFile, ""+
48-
"If set, the OpenID server's certificate will be verified by one of the authorities "+
49-
"in the oidc-ca-file, otherwise the host's root CA set will be used.")
64+
"The OpenID server's certificate will be verified by one of the authorities "+
65+
"in the oidc-ca-file, otherwise the host's root CA set will be used")
5066

5167
fs.StringVar(&o.UsernameClaim, "oidc-username-claim", "sub", ""+
52-
"The OpenID claim to use as the user name. Note that claims other than the default ('sub') "+
53-
"is not guaranteed to be unique and immutable. This flag is experimental, please see "+
54-
"the authentication documentation for further details.")
68+
"The OpenID claim to use as the username. Note that claims other than the default ('sub') "+
69+
"is not guaranteed to be unique and immutable")
5570

5671
fs.StringVar(&o.UsernamePrefix, "oidc-username-prefix", "", ""+
5772
"If provided, all usernames will be prefixed with this value. If not provided, "+
@@ -60,8 +75,7 @@ func (o *OIDCAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
6075

6176
fs.StringVar(&o.GroupsClaim, "oidc-groups-claim", "", ""+
6277
"If provided, the name of a custom OpenID Connect claim for specifying user groups. "+
63-
"The claim value is expected to be a string or array of strings. This flag is experimental, "+
64-
"please see the authentication documentation for further details.")
78+
"The claim value is expected to be a string or array of strings.")
6579

6680
fs.StringVar(&o.GroupsPrefix, "oidc-groups-prefix", "", ""+
6781
"If provided, all groups will be prefixed with this value to prevent conflicts with "+

cmd/run.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ func NewRunCommand(stopCh <-chan struct{}) *cobra.Command {
4444
}
4545
ssoptionsWithLB := ssoptions.WithLoopback()
4646

47+
tpOptions := new(options.TokenPassthroughOptions)
48+
4749
clientConfigFlags := genericclioptions.NewConfigFlags(true)
4850

4951
healthCheck := probe.New(strconv.Itoa(readinessProbePort))
@@ -124,6 +126,9 @@ func NewRunCommand(stopCh <-chan struct{}) *cobra.Command {
124126
oidcfs := namedFlagSets.FlagSet("OIDC")
125127
oidcOptions.AddFlags(oidcfs)
126128

129+
tpfs := namedFlagSets.FlagSet("Token Passthrough (Alpha)")
130+
tpOptions.AddFlags(tpfs)
131+
127132
ssoptionsWithLB.AddFlags(namedFlagSets.FlagSet("secure serving"))
128133

129134
clientConfigFlags.CacheDir = nil
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright Jetstack Ltd. See LICENSE for details.
2+
package fake
3+
4+
import (
5+
authv1 "k8s.io/api/authentication/v1"
6+
)
7+
8+
type FakeReviewer struct {
9+
CreateFn func(*authv1.TokenReview) (*authv1.TokenReview, error)
10+
}
11+
12+
func New() *FakeReviewer {
13+
return &FakeReviewer{
14+
CreateFn: func(*authv1.TokenReview) (*authv1.TokenReview, error) {
15+
return nil, nil
16+
},
17+
}
18+
}
19+
20+
func (f *FakeReviewer) Create(req *authv1.TokenReview) (*authv1.TokenReview, error) {
21+
return f.CreateFn(req)
22+
}
23+
24+
func (f *FakeReviewer) WithCreate(req *authv1.TokenReview, err error) *FakeReviewer {
25+
f.CreateFn = func(*authv1.TokenReview) (*authv1.TokenReview, error) {
26+
return req, err
27+
}
28+
29+
return f
30+
}

pkg/proxy/tokenreview/tokenreview.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright Jetstack Ltd. See LICENSE for details.
2+
package tokenreview
3+
4+
import (
5+
"fmt"
6+
7+
authv1 "k8s.io/api/authentication/v1"
8+
"k8s.io/client-go/kubernetes"
9+
clientauthv1 "k8s.io/client-go/kubernetes/typed/authentication/v1"
10+
"k8s.io/client-go/rest"
11+
)
12+
13+
type TokenReview struct {
14+
reviewRequester clientauthv1.TokenReviewInterface
15+
audiences []string
16+
}
17+
18+
func New(restConfig *rest.Config, audiences []string) (*TokenReview, error) {
19+
kubeclient, err := kubernetes.NewForConfig(restConfig)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
return &TokenReview{
25+
reviewRequester: kubeclient.AuthenticationV1().TokenReviews(),
26+
audiences: audiences,
27+
}, nil
28+
}
29+
30+
func (t *TokenReview) Review(token string) (bool, error) {
31+
review := t.buildReview(token)
32+
33+
resp, err := t.reviewRequester.Create(review)
34+
if err != nil {
35+
return false, err
36+
}
37+
38+
if len(resp.Status.Error) > 0 {
39+
return false, fmt.Errorf("error authenticating using token review: %s",
40+
resp.Status.Error)
41+
}
42+
43+
return resp.Status.Authenticated, nil
44+
}
45+
46+
func (t *TokenReview) buildReview(token string) *authv1.TokenReview {
47+
return &authv1.TokenReview{
48+
Spec: authv1.TokenReviewSpec{
49+
Token: token,
50+
Audiences: t.audiences,
51+
},
52+
}
53+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright Jetstack Ltd. See LICENSE for details.
2+
package tokenreview
3+
4+
import (
5+
"errors"
6+
"reflect"
7+
"testing"
8+
9+
authv1 "k8s.io/api/authentication/v1"
10+
11+
"github.com/jetstack/kube-oidc-proxy/pkg/proxy/tokenreview/fake"
12+
)
13+
14+
type testT struct {
15+
reviewResp *authv1.TokenReview
16+
errResp error
17+
18+
expAuth bool
19+
expErr error
20+
}
21+
22+
func TestReview(t *testing.T) {
23+
24+
tests := map[string]testT{
25+
"if a create fails then this error is returned": {
26+
reviewResp: nil,
27+
errResp: errors.New("create error response"),
28+
expAuth: false,
29+
expErr: errors.New("create error response"),
30+
},
31+
32+
"if an error exists in the status of the response pass error back": {
33+
reviewResp: &authv1.TokenReview{
34+
Status: authv1.TokenReviewStatus{
35+
Error: "status error",
36+
},
37+
},
38+
errResp: nil,
39+
expAuth: false,
40+
expErr: errors.New("error authenticating using token review: status error"),
41+
},
42+
43+
"if the response returns unauthenticated, return false": {
44+
reviewResp: &authv1.TokenReview{
45+
Status: authv1.TokenReviewStatus{
46+
Authenticated: false,
47+
},
48+
},
49+
errResp: nil,
50+
expAuth: false,
51+
expErr: nil,
52+
},
53+
54+
"if the response returns authenticated, return true": {
55+
reviewResp: &authv1.TokenReview{
56+
Status: authv1.TokenReviewStatus{
57+
Authenticated: true,
58+
},
59+
},
60+
errResp: nil,
61+
expAuth: true,
62+
expErr: nil,
63+
},
64+
}
65+
66+
for name, test := range tests {
67+
t.Run(name, func(t *testing.T) {
68+
runTest(t, test)
69+
})
70+
}
71+
}
72+
73+
func runTest(t *testing.T, test testT) {
74+
tReviewer := &TokenReview{
75+
audiences: nil,
76+
reviewRequester: fake.New().WithCreate(test.reviewResp, test.errResp),
77+
}
78+
79+
authed, err := tReviewer.Review("test-token")
80+
81+
if !reflect.DeepEqual(test.expErr, err) {
82+
t.Errorf("got unexpected error, exp=%v got=%v",
83+
test.expErr, err)
84+
}
85+
86+
if test.expAuth != authed {
87+
t.Errorf("got unexpected authed, exp=%t got=%t",
88+
test.expAuth, authed)
89+
}
90+
}

0 commit comments

Comments
 (0)