git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #596 from grnhse/extra-jwt-token-session
3686b0bJoel Speed
CHANGELOG.md+1
@@ -55,6 +55,7 @@
55
55
56## Changes since v5.1.1
56## Changes since v5.1.1
57
57
58+- [#596](https://github.com/oauth2-proxy/oauth2-proxy/pull/596) Validate Bearer IDTokens in headers with correct provider/extra JWT Verifier (@NickMeves)
58- [#620](https://github.com/oauth2-proxy/oauth2-proxy/pull/620) Add HealthCheck middleware (@JoelSpeed)
59- [#620](https://github.com/oauth2-proxy/oauth2-proxy/pull/620) Add HealthCheck middleware (@JoelSpeed)
59- [#597](https://github.com/oauth2-proxy/oauth2-proxy/pull/597) Don't log invalid redirect if redirect is empty (@JoelSpeed)
60- [#597](https://github.com/oauth2-proxy/oauth2-proxy/pull/597) Don't log invalid redirect if redirect is empty (@JoelSpeed)
60- [#604](https://github.com/oauth2-proxy/oauth2-proxy/pull/604) Add Keycloak local testing environment (@EvgeniGordeev)
61- [#604](https://github.com/oauth2-proxy/oauth2-proxy/pull/604) Add Keycloak local testing environment (@EvgeniGordeev)
oauthproxy.go+70−59
@@ -88,35 +88,36 @@ type OAuthProxy struct {
88 AuthOnlyPath string
88 AuthOnlyPath string
89 UserInfoPath string
89 UserInfoPath string
90
90
91− redirectURL *url.URL // the url to receive requests at
91+ redirectURL *url.URL // the url to receive requests at
92− whitelistDomains []string
92+ whitelistDomains []string
93− provider providers.Provider
93+ provider providers.Provider
94− providerNameOverride string
94+ providerNameOverride string
95− sessionStore sessionsapi.SessionStore
95+ sessionStore sessionsapi.SessionStore
96− ProxyPrefix string
96+ ProxyPrefix string
97− SignInMessage string
97+ SignInMessage string
98− HtpasswdFile *HtpasswdFile
98+ HtpasswdFile *HtpasswdFile
99− DisplayHtpasswdForm bool
99+ DisplayHtpasswdForm bool
100− serveMux http.Handler
100+ serveMux http.Handler
101− SetXAuthRequest bool
101+ SetXAuthRequest bool
102− PassBasicAuth bool
102+ PassBasicAuth bool
103− SetBasicAuth bool
103+ SetBasicAuth bool
104− SkipProviderButton bool
104+ SkipProviderButton bool
105− PassUserHeaders bool
105+ PassUserHeaders bool
106− BasicAuthPassword string
106+ BasicAuthPassword string
107− PassAccessToken bool
107+ PassAccessToken bool
108− SetAuthorization bool
108+ SetAuthorization bool
109− PassAuthorization bool
109+ PassAuthorization bool
110− PreferEmailToUser bool
110+ PreferEmailToUser bool
111− skipAuthRegex []string
111+ skipAuthRegex []string
112− skipAuthPreflight bool
112+ skipAuthPreflight bool
113− skipJwtBearerTokens bool
113+ skipJwtBearerTokens bool
114+ mainJwtBearerVerifier *oidc.IDTokenVerifier
114− jwtBearerVerifiers []*oidc.IDTokenVerifier
115+ extraJwtBearerVerifiers []*oidc.IDTokenVerifier
115− compiledRegex []*regexp.Regexp
116+ compiledRegex []*regexp.Regexp
116− templates *template.Template
117+ templates *template.Template
117− realClientIPParser ipapi.RealClientIPParser
118+ realClientIPParser ipapi.RealClientIPParser
118− Banner string
119+ Banner string
119− Footer string
120+ Footer string
120}
121}
121
122
122// UpstreamProxy represents an upstream server to proxy to
123// UpstreamProxy represents an upstream server to proxy to
@@ -317,32 +318,33 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) *OAuthPro
317 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
318 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
318 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
319 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
319
320
320− ProxyPrefix: opts.ProxyPrefix,
321+ ProxyPrefix: opts.ProxyPrefix,
321− provider: opts.GetProvider(),
322+ provider: opts.GetProvider(),
322− providerNameOverride: opts.ProviderName,
323+ providerNameOverride: opts.ProviderName,
323− sessionStore: opts.GetSessionStore(),
324+ sessionStore: opts.GetSessionStore(),
324− serveMux: serveMux,
325+ serveMux: serveMux,
325− redirectURL: redirectURL,
326+ redirectURL: redirectURL,
326− whitelistDomains: opts.WhitelistDomains,
327+ whitelistDomains: opts.WhitelistDomains,
327− skipAuthRegex: opts.SkipAuthRegex,
328+ skipAuthRegex: opts.SkipAuthRegex,
328− skipAuthPreflight: opts.SkipAuthPreflight,
329+ skipAuthPreflight: opts.SkipAuthPreflight,
329− skipJwtBearerTokens: opts.SkipJwtBearerTokens,
330+ skipJwtBearerTokens: opts.SkipJwtBearerTokens,
331+ mainJwtBearerVerifier: opts.GetOIDCVerifier(),
330− jwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
332+ extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
331− compiledRegex: opts.GetCompiledRegex(),
333+ compiledRegex: opts.GetCompiledRegex(),
332− realClientIPParser: opts.GetRealClientIPParser(),
334+ realClientIPParser: opts.GetRealClientIPParser(),
333− SetXAuthRequest: opts.SetXAuthRequest,
335+ SetXAuthRequest: opts.SetXAuthRequest,
334− PassBasicAuth: opts.PassBasicAuth,
336+ PassBasicAuth: opts.PassBasicAuth,
335− SetBasicAuth: opts.SetBasicAuth,
337+ SetBasicAuth: opts.SetBasicAuth,
336− PassUserHeaders: opts.PassUserHeaders,
338+ PassUserHeaders: opts.PassUserHeaders,
337− BasicAuthPassword: opts.BasicAuthPassword,
339+ BasicAuthPassword: opts.BasicAuthPassword,
338− PassAccessToken: opts.PassAccessToken,
340+ PassAccessToken: opts.PassAccessToken,
339− SetAuthorization: opts.SetAuthorization,
341+ SetAuthorization: opts.SetAuthorization,
340− PassAuthorization: opts.PassAuthorization,
342+ PassAuthorization: opts.PassAuthorization,
341− PreferEmailToUser: opts.PreferEmailToUser,
343+ PreferEmailToUser: opts.PreferEmailToUser,
342− SkipProviderButton: opts.SkipProviderButton,
344+ SkipProviderButton: opts.SkipProviderButton,
343− templates: loadTemplates(opts.CustomTemplatesDir),
345+ templates: loadTemplates(opts.CustomTemplatesDir),
344− Banner: opts.Banner,
346+ Banner: opts.Banner,
345− Footer: opts.Footer,
347+ Footer: opts.Footer,
346 }
348 }
347}
349}
348
350
@@ -1139,15 +1141,24 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState
1139 return nil, err
1141 return nil, err
1140 }
1142 }
1141
1143
1142− for _, verifier := range p.jwtBearerVerifiers {
1144+ // If we are using an oidc provider, go ahead and try that provider first with its Verifier
1145+ // and Bearer Token -> Session converter
1146+ if p.mainJwtBearerVerifier != nil {
1143− bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1147+ bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
1148+ if err == nil {
1149+ return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1150+ }
1151+ }
1144
1152
1153+ // Otherwise, attempt to verify against the extra JWT issuers and use a more generic
1154+ // Bearer Token -> Session converter
1155+ for _, verifier := range p.extraJwtBearerVerifiers {
1156+ bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1145 if err != nil {
1157 if err != nil {
1146− logger.Printf("failed to verify bearer token: %v", err)
1147 continue
1158 continue
1148 }
1159 }
1149
1160
1150− return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1161+ return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1151 }
1162 }
1152 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1163 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1153}
1164}
oauthproxy_test.go+3−3
@@ -1578,7 +1578,7 @@ func TestGetJwtSession(t *testing.T) {
1578 // Bearer
1578 // Bearer
1579 expires := time.Unix(1912151821, 0)
1579 expires := time.Unix(1912151821, 0)
1580 session, _ := test.proxy.GetJwtSession(test.req)
1580 session, _ := test.proxy.GetJwtSession(test.req)
1581− assert.Equal(t, session.User, "john@example.com")
1581+ assert.Equal(t, session.User, "1234567890")
1582 assert.Equal(t, session.Email, "john@example.com")
1582 assert.Equal(t, session.Email, "john@example.com")
1583 assert.Equal(t, session.ExpiresOn, &expires)
1583 assert.Equal(t, session.ExpiresOn, &expires)
1584 assert.Equal(t, session.IDToken, goodJwt)
1584 assert.Equal(t, session.IDToken, goodJwt)
@@ -1590,12 +1590,12 @@ func TestGetJwtSession(t *testing.T) {
1590
1590
1591 // Check PassAuthorization, should overwrite Basic header
1591 // Check PassAuthorization, should overwrite Basic header
1592 assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
1592 assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
1593− assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "john@example.com")
1593+ assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "1234567890")
1594 assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
1594 assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
1595
1595
1596 // SetAuthorization and SetXAuthRequest
1596 // SetAuthorization and SetXAuthRequest
1597 assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
1597 assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
1598− assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "john@example.com")
1598+ assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "1234567890")
1599 assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
1599 assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
1600}
1600}
1601
1601
pkg/validation/options.go−4
@@ -178,10 +178,6 @@ func Validate(o *options.Options) error {
178 }
178 }
179
179
180 if o.SkipJwtBearerTokens {
180 if o.SkipJwtBearerTokens {
181− // If we are using an oidc provider, go ahead and add that provider to the list
182− if o.GetOIDCVerifier() != nil {
183− o.SetJWTBearerVerifiers(append(o.GetJWTBearerVerifiers(), o.GetOIDCVerifier()))
184− }
185 // Configure extra issuers
181 // Configure extra issuers
186 if len(o.ExtraJwtIssuers) > 0 {
182 if len(o.ExtraJwtIssuers) > 0 {
187 var jwtIssuers []jwtIssuer
183 var jwtIssuers []jwtIssuer
providers/oidc_test.go−1
@@ -124,7 +124,6 @@ func newOIDCServer(body []byte) (*url.URL, *httptest.Server) {
124}
124}
125
125
126func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
126func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
127−
128 key, _ := rsa.GenerateKey(rand.Reader, 2048)
127 key, _ := rsa.GenerateKey(rand.Reader, 2048)
129 standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
128 standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
130 return standardClaims.SignedString(key)
129 return standardClaims.SignedString(key)
providers/provider_default.go+5−6
@@ -164,14 +164,13 @@ func (p *ProviderData) CreateSessionStateFromBearerToken(ctx context.Context, ra
164
164
165 newSession := &sessions.SessionState{
165 newSession := &sessions.SessionState{
166 Email: claims.Email,
166 Email: claims.Email,
167− User: claims.Email,
167+ User: claims.Subject,
168 PreferredUsername: claims.PreferredUsername,
168 PreferredUsername: claims.PreferredUsername,
169+ AccessToken: rawIDToken,
170+ IDToken: rawIDToken,
171+ RefreshToken: "",
172+ ExpiresOn: &idToken.Expiry,
169 }
173 }
170
174
171− newSession.AccessToken = rawIDToken
172− newSession.IDToken = rawIDToken
173− newSession.RefreshToken = ""
174− newSession.ExpiresOn = &idToken.Expiry
175−
176 return newSession, nil
175 return newSession, nil
177}
176}
providers/provider_default_test.go+41
@@ -2,10 +2,15 @@ package providers
2
2
3import (
3import (
4 "context"
4 "context"
5+ "crypto/rand"
6+ "crypto/rsa"
5 "net/url"
7 "net/url"
6 "testing"
8 "testing"
7 "time"
9 "time"
8
10
11+ "github.com/coreos/go-oidc"
12+ "github.com/dgrijalva/jwt-go"
13+
9 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
14 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
10 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/assert"
11)
16)
@@ -47,3 +52,39 @@ func TestAcrValuesConfigured(t *testing.T) {
47 result := p.GetLoginURL("https://my.test.app/oauth", "")
52 result := p.GetLoginURL("https://my.test.app/oauth", "")
48 assert.Contains(t, result, "acr_values=testValue")
53 assert.Contains(t, result, "acr_values=testValue")
49}
54}
55+
56+func TestCreateSessionStateFromBearerToken(t *testing.T) {
57+ minimalIDToken := jwt.StandardClaims{
58+ Audience: "asdf1234",
59+ ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
60+ Id: "id-some-id",
61+ IssuedAt: time.Now().Unix(),
62+ Issuer: "https://issuer.example.com",
63+ NotBefore: 0,
64+ Subject: "123456789",
65+ }
66+ // From oidc_test.go
67+ verifier := oidc.NewVerifier(
68+ "https://issuer.example.com",
69+ fakeKeySetStub{},
70+ &oidc.Config{ClientID: "asdf1234"},
71+ )
72+
73+ key, err := rsa.GenerateKey(rand.Reader, 2048)
74+ assert.NoError(t, err)
75+ rawIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, minimalIDToken).SignedString(key)
76+ assert.NoError(t, err)
77+ // Pass to a dummy Verifier to get an oidc.IDToken from the rawIDToken for our actual test below
78+ idToken, err := verifier.Verify(context.Background(), rawIDToken)
79+ assert.NoError(t, err)
80+
81+ session, err := (*ProviderData)(nil).CreateSessionStateFromBearerToken(context.Background(), rawIDToken, idToken)
82+ assert.NoError(t, err)
83+
84+ assert.Equal(t, rawIDToken, session.AccessToken)
85+ assert.Equal(t, rawIDToken, session.IDToken)
86+ assert.Equal(t, "123456789", session.Email)
87+ assert.Equal(t, "123456789", session.User)
88+ assert.Empty(t, session.RefreshToken)
89+ assert.Empty(t, session.PreferredUsername)
90+}