git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #65 from lsst/jwt_bearer_passthrough

JWT bearer passthrough

commit317f09f41e48c4a0b07d79e18fb54311e6a4ecb4 authorJoel Speed <Joel.speed@hotmail.co.uk> dateFri 21 Jun 2019, 15:40 (7 years ago) parent0af18d63881955
7 files changed+449−52
CHANGELOG.md+4−1 docs/configuration/configuration.md+2 main.go+3 oauthproxy.go+160−41 oauthproxy_test.go+205−2 options.go+74−7 providers/oidc.go+1−1
CHANGELOG.md+4−1
@@ -14,6 +14,10 @@
1414
1515## Changes since v3.2.0
1616
17+- [#65](https://github.com/pusher/oauth2_proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
18+ the `-skip-jwt-bearer-token` options.
19+ - Additional verifiers can be configured via the `-extra-jwt-issuers` flag if the JWT issuers is either an OpenID provider or has a JWKS URL
20+ (e.g. `https://example.com/.well-known/jwks.json`).
1721- [#180](https://github.com/pusher/outh2_proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
1822- [#175](https://github.com/pusher/outh2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
1923 - Includes fix for potential signature checking issue when OIDC discovery is skipped.
@@ -56,7 +60,6 @@
5660- [#111](https://github.com/pusher/oauth2_proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer)
5761- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Restore binary tarball contents to be compatible with bitlys original tarballs (@zeha)
5862- [#185](https://github.com/pusher/oauth2_proxy/pull/185) Fix an unsupported protocol scheme error during token validation when using the Azure provider (@jonas)
59−
6063- [#141](https://github.com/pusher/oauth2_proxy/pull/141) Check google group membership based on email address (@bchess)
6164 - Google Group membership is additionally checked via email address, allowing users outside a GSuite domain to be authorized.
6265
docs/configuration/configuration.md+2
@@ -41,6 +41,7 @@ Usage of oauth2_proxy:
4141 -custom-templates-dir string: path to custom html templates
4242 -display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true)
4343 -email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
44+ -extra-jwt-issuers: if -skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)
4445 -flush-interval: period between flushing response buffers when streaming responses (default "1s")
4546 -footer string: custom footer string. Use "-" to disable default footer.
4647 -gcp-healthchecks: will enable /liveness_check, /readiness_check, and / (with the proper user-agent) endpoints that will make it work well with GCP App Engine and GKE Ingresses (default false)
@@ -89,6 +90,7 @@ Usage of oauth2_proxy:
8990 -signature-key string: GAP-Signature request signature key (algorithm:secretkey)
9091 -skip-auth-preflight: will skip authentication for OPTIONS requests
9192 -skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)
93+ -skip-jwt-bearer-tokens: will skip requests that have verified JWT bearer tokens
9294 -skip-oidc-discovery: bypass OIDC endpoint discovery. login-url, redeem-url and oidc-jwks-url must be configured in this case
9395 -skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start
9496 -ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS
main.go+3
@@ -23,6 +23,7 @@ func main() {
2323 whitelistDomains := StringArray{}
2424 upstreams := StringArray{}
2525 skipAuthRegex := StringArray{}
26+ jwtIssuers := StringArray{}
2627 googleGroups := StringArray{}
2728 redisSentinelConnectionURLs := StringArray{}
2829
@@ -48,6 +49,8 @@ func main() {
4849 flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
4950 flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS")
5051 flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
52+ flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
53+ flagSet.Var(&jwtIssuers, "extra-jwt-issuers", "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
5154
5255 flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
5356 flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
oauthproxy.go+160−41
@@ -1,6 +1,7 @@
11package main
22
33import (
4+ "context"
45 b64 "encoding/base64"
56 "errors"
67 "fmt"
@@ -13,6 +14,7 @@ import (
1314 "strings"
1415 "time"
1516
17+ "github.com/coreos/go-oidc"
1618 "github.com/mbland/hmacauth"
1719 "github.com/pusher/oauth2_proxy/cookie"
1820 "github.com/pusher/oauth2_proxy/logger"
@@ -92,6 +94,8 @@ type OAuthProxy struct {
9294 PassAuthorization bool
9395 skipAuthRegex []string
9496 skipAuthPreflight bool
97+ skipJwtBearerTokens bool
98+ jwtBearerVerifiers []*oidc.IDTokenVerifier
9599 compiledRegex []*regexp.Regexp
96100 templates *template.Template
97101 Footer string
@@ -206,6 +210,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
206210 logger.Printf("compiled skip-auth-regex => %q", u)
207211 }
208212
213+ if opts.SkipJwtBearerTokens {
214+ logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
215+ for _, issuer := range opts.ExtraJwtIssuers {
216+ logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
217+ }
218+ }
209219 redirectURL := opts.redirectURL
210220 if redirectURL.Path == "" {
211221 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
@@ -239,25 +249,27 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
239249 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
240250 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
241251
242− ProxyPrefix: opts.ProxyPrefix,
243− provider: opts.provider,
244− sessionStore: opts.sessionStore,
245− serveMux: serveMux,
246− redirectURL: redirectURL,
247− whitelistDomains: opts.WhitelistDomains,
248− skipAuthRegex: opts.SkipAuthRegex,
249− skipAuthPreflight: opts.SkipAuthPreflight,
250− compiledRegex: opts.CompiledRegex,
251− SetXAuthRequest: opts.SetXAuthRequest,
252− PassBasicAuth: opts.PassBasicAuth,
253− PassUserHeaders: opts.PassUserHeaders,
254− BasicAuthPassword: opts.BasicAuthPassword,
255− PassAccessToken: opts.PassAccessToken,
256− SetAuthorization: opts.SetAuthorization,
257− PassAuthorization: opts.PassAuthorization,
258− SkipProviderButton: opts.SkipProviderButton,
259− templates: loadTemplates(opts.CustomTemplatesDir),
260− Footer: opts.Footer,
252+ ProxyPrefix: opts.ProxyPrefix,
253+ provider: opts.provider,
254+ sessionStore: opts.sessionStore,
255+ serveMux: serveMux,
256+ redirectURL: redirectURL,
257+ whitelistDomains: opts.WhitelistDomains,
258+ skipAuthRegex: opts.SkipAuthRegex,
259+ skipAuthPreflight: opts.SkipAuthPreflight,
260+ skipJwtBearerTokens: opts.SkipJwtBearerTokens,
261+ jwtBearerVerifiers: opts.jwtBearerVerifiers,
262+ compiledRegex: opts.CompiledRegex,
263+ SetXAuthRequest: opts.SetXAuthRequest,
264+ PassBasicAuth: opts.PassBasicAuth,
265+ PassUserHeaders: opts.PassUserHeaders,
266+ BasicAuthPassword: opts.BasicAuthPassword,
267+ PassAccessToken: opts.PassAccessToken,
268+ SetAuthorization: opts.SetAuthorization,
269+ PassAuthorization: opts.PassAuthorization,
270+ SkipProviderButton: opts.SkipProviderButton,
271+ templates: loadTemplates(opts.CustomTemplatesDir),
272+ Footer: opts.Footer,
261273 }
262274}
263275
@@ -638,7 +650,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
638650 }
639651 http.Redirect(rw, req, redirect, 302)
640652 } else {
641− logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Invalid authentication via OAuth2: unauthorized")
653+ logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
642654 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
643655 }
644656}
@@ -693,26 +705,42 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
693705// Returns nil, ErrNeedsLogin if user needs to login.
694706// Set-Cookie headers may be set on the response as a side-effect of calling this method.
695707func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
708+ var session *sessionsapi.SessionState
709+ var err error
696710 var saveSession, clearSession, revalidated bool
697− remoteAddr := getRemoteAddr(req)
698711
699− session, err := p.LoadCookiedSession(req)
700− if err != nil {
701− logger.Printf("Error loading cookied session: %s", err)
702− }
703− if session != nil && session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
704− logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
705− saveSession = true
712+ if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
713+ session, err = p.GetJwtSession(req)
714+ if err != nil {
715+ logger.Printf("Error retrieving session from token in Authorization header: %s", err)
716+ }
717+ if session != nil {
718+ saveSession = false
719+ }
706720 }
707721
708− var ok bool
709− if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
710− logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
711− clearSession = true
712− session = nil
713− } else if ok {
714− saveSession = true
715− revalidated = true
722+ remoteAddr := getRemoteAddr(req)
723+ if session == nil {
724+ session, err = p.LoadCookiedSession(req)
725+ if err != nil {
726+ logger.Printf("Error loading cookied session: %s", err)
727+ }
728+
729+ if session != nil {
730+ if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
731+ logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
732+ saveSession = true
733+ }
734+
735+ if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
736+ logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
737+ clearSession = true
738+ session = nil
739+ } else if ok {
740+ saveSession = true
741+ revalidated = true
742+ }
743+ }
716744 }
717745
718746 if session != nil && session.IsExpired() {
@@ -731,11 +759,13 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
731759 }
732760 }
733761
734− if session != nil && session.Email != "" && !p.Validator(session.Email) {
735− logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
736− session = nil
737− saveSession = false
738− clearSession = true
762+ if session != nil && session.Email != "" {
763+ if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
764+ logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
765+ session = nil
766+ saveSession = false
767+ clearSession = true
768+ }
739769 }
740770
741771 if saveSession && session != nil {
@@ -854,3 +884,92 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
854884 rw.Header().Set("Content-Type", applicationJSON)
855885 rw.WriteHeader(code)
856886}
887+
888+// GetJwtSession loads a session based on a JWT token in the authorization header.
889+func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
890+ rawBearerToken, err := p.findBearerToken(req)
891+ if err != nil {
892+ return nil, err
893+ }
894+
895+ ctx := context.Background()
896+ var session *sessionsapi.SessionState
897+ for _, verifier := range p.jwtBearerVerifiers {
898+ bearerToken, err := verifier.Verify(ctx, rawBearerToken)
899+
900+ if err != nil {
901+ logger.Printf("failed to verify bearer token: %v", err)
902+ continue
903+ }
904+
905+ var claims struct {
906+ Subject string `json:"sub"`
907+ Email string `json:"email"`
908+ Verified *bool `json:"email_verified"`
909+ }
910+
911+ if err := bearerToken.Claims(&claims); err != nil {
912+ return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
913+ }
914+
915+ if claims.Email == "" {
916+ claims.Email = claims.Subject
917+ }
918+
919+ if claims.Verified != nil && !*claims.Verified {
920+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
921+ }
922+
923+ session = &sessionsapi.SessionState{
924+ AccessToken: rawBearerToken,
925+ IDToken: rawBearerToken,
926+ RefreshToken: "",
927+ ExpiresOn: bearerToken.Expiry,
928+ Email: claims.Email,
929+ User: claims.Email,
930+ }
931+ return session, nil
932+ }
933+ return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
934+}
935+
936+// findBearerToken finds a valid JWT token from the Authorization header of a given request.
937+func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
938+ auth := req.Header.Get("Authorization")
939+ s := strings.SplitN(auth, " ", 2)
940+ if len(s) != 2 {
941+ return "", fmt.Errorf("invalid authorization header %s", auth)
942+ }
943+ jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
944+ var rawBearerToken string
945+ if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
946+ rawBearerToken = s[1]
947+ } else if s[0] == "Basic" {
948+ // Check if we have a Bearer token masquerading in Basic
949+ b, err := b64.StdEncoding.DecodeString(s[1])
950+ if err != nil {
951+ return "", err
952+ }
953+ pair := strings.SplitN(string(b), ":", 2)
954+ if len(pair) != 2 {
955+ return "", fmt.Errorf("invalid format %s", b)
956+ }
957+ user, password := pair[0], pair[1]
958+
959+ // check user, user+password, or just password for a token
960+ if jwtRegex.MatchString(user) {
961+ // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
962+ if password == "" || password == "x-oauth-basic" {
963+ rawBearerToken = user
964+ }
965+ } else if jwtRegex.MatchString(password) {
966+ // support passwords and ignore user
967+ rawBearerToken = password
968+ }
969+ }
970+ if rawBearerToken == "" {
971+ return "", fmt.Errorf("no valid bearer token found in authorization header")
972+ }
973+
974+ return rawBearerToken, nil
975+}
oauthproxy_test.go+205−2
@@ -1,8 +1,10 @@
11package main
22
33import (
4+ "context"
45 "crypto"
56 "encoding/base64"
7+ "fmt"
68 "io"
79 "io/ioutil"
810 "net"
@@ -14,6 +16,7 @@ import (
1416 "testing"
1517 "time"
1618
19+ "github.com/coreos/go-oidc"
1720 "github.com/mbland/hmacauth"
1821 "github.com/pusher/oauth2_proxy/logger"
1922 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
@@ -226,8 +229,9 @@ func TestIsValidRedirect(t *testing.T) {
226229
227230type TestProvider struct {
228231 *providers.ProviderData
229− EmailAddress string
230− ValidToken bool
232+ EmailAddress string
233+ ValidToken bool
234+ GroupValidator func(string) bool
231235}
232236
233237func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
@@ -252,6 +256,9 @@ func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
252256 Scope: "profile.email",
253257 },
254258 EmailAddress: emailAddress,
259+ GroupValidator: func(s string) bool {
260+ return true
261+ },
255262 }
256263}
257264
@@ -263,6 +270,13 @@ func (tp *TestProvider) ValidateSessionState(session *sessions.SessionState) boo
263270 return tp.ValidToken
264271}
265272
273+func (tp *TestProvider) ValidateGroup(email string) bool {
274+ if tp.GroupValidator != nil {
275+ return tp.GroupValidator(email)
276+ }
277+ return true
278+}
279+
266280func TestBasicAuthPassword(t *testing.T) {
267281 providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
268282 logger.Printf("%#v", r)
@@ -788,6 +802,25 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
788802 assert.Equal(t, "unauthorized request\n", string(bodyBytes))
789803}
790804
805+func TestAuthOnlyEndpointUnauthorizedOnProviderGroupValidationFailure(t *testing.T) {
806+ test := NewAuthOnlyEndpointTest()
807+ startSession := &sessions.SessionState{
808+ Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
809+ test.SaveSession(startSession)
810+ provider := &TestProvider{
811+ ValidToken: true,
812+ GroupValidator: func(s string) bool {
813+ return false
814+ },
815+ }
816+
817+ test.proxy.provider = provider
818+ test.proxy.ServeHTTP(test.rw, test.req)
819+ assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
820+ bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
821+ assert.Equal(t, "unauthorized request\n", string(bodyBytes))
822+}
823+
791824func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
792825 var pcTest ProcessCookieTest
793826
@@ -1132,3 +1165,173 @@ func TestClearSingleCookie(t *testing.T) {
11321165
11331166 assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries")
11341167}
1168+
1169+type NoOpKeySet struct {
1170+}
1171+
1172+func (NoOpKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) {
1173+ splitStrings := strings.Split(jwt, ".")
1174+ payloadString := splitStrings[1]
1175+ jsonString, err := base64.RawURLEncoding.DecodeString(payloadString)
1176+ return []byte(jsonString), err
1177+}
1178+
1179+func TestGetJwtSession(t *testing.T) {
1180+ /* token payload:
1181+ {
1182+ "sub": "1234567890",
1183+ "aud": "https://test.myapp.com",
1184+ "name": "John Doe",
1185+ "email": "john@example.com",
1186+ "iss": "https://issuer.example.com",
1187+ "iat": 1553691215,
1188+ "exp": 1912151821
1189+ }
1190+ */
1191+ goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
1192+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
1193+ "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
1194+ "E1LCJleHAiOjE5MTIxNTE4MjF9." +
1195+ "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
1196+ "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
1197+
1198+ keyset := NoOpKeySet{}
1199+ verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
1200+ &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
1201+
1202+ test := NewAuthOnlyEndpointTest(func(opts *Options) {
1203+ opts.PassAuthorization = true
1204+ opts.SetAuthorization = true
1205+ opts.SetXAuthRequest = true
1206+ opts.SkipJwtBearerTokens = true
1207+ opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier)
1208+ })
1209+ tp, _ := test.proxy.provider.(*TestProvider)
1210+ tp.GroupValidator = func(s string) bool {
1211+ return true
1212+ }
1213+
1214+ authHeader := fmt.Sprintf("Bearer %s", goodJwt)
1215+ test.req.Header = map[string][]string{
1216+ "Authorization": {authHeader},
1217+ }
1218+
1219+ // Bearer
1220+ session, _ := test.proxy.GetJwtSession(test.req)
1221+ assert.Equal(t, session.User, "john@example.com")
1222+ assert.Equal(t, session.Email, "john@example.com")
1223+ assert.Equal(t, session.ExpiresOn, time.Unix(1912151821, 0))
1224+ assert.Equal(t, session.IDToken, goodJwt)
1225+
1226+ test.proxy.ServeHTTP(test.rw, test.req)
1227+ if test.rw.Code >= 400 {
1228+ t.Fatalf("expected 3xx got %d", test.rw.Code)
1229+ }
1230+
1231+ // Check PassAuthorization, should overwrite Basic header
1232+ assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
1233+ assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "john@example.com")
1234+ assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
1235+
1236+ // SetAuthorization and SetXAuthRequest
1237+ assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
1238+ assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "john@example.com")
1239+ assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
1240+}
1241+
1242+func TestJwtUnauthorizedOnGroupValidationFailure(t *testing.T) {
1243+ goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
1244+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
1245+ "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
1246+ "E1LCJleHAiOjE5MTIxNTE4MjF9." +
1247+ "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
1248+ "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
1249+
1250+ keyset := NoOpKeySet{}
1251+ verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
1252+ &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
1253+
1254+ test := NewAuthOnlyEndpointTest(func(opts *Options) {
1255+ opts.PassAuthorization = true
1256+ opts.SetAuthorization = true
1257+ opts.SetXAuthRequest = true
1258+ opts.SkipJwtBearerTokens = true
1259+ opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier)
1260+ })
1261+ tp, _ := test.proxy.provider.(*TestProvider)
1262+ // Verify ValidateGroup fails JWT authorization
1263+ tp.GroupValidator = func(s string) bool {
1264+ return false
1265+ }
1266+
1267+ authHeader := fmt.Sprintf("Bearer %s", goodJwt)
1268+ test.req.Header = map[string][]string{
1269+ "Authorization": {authHeader},
1270+ }
1271+ test.proxy.ServeHTTP(test.rw, test.req)
1272+ if test.rw.Code != http.StatusUnauthorized {
1273+ t.Fatalf("expected 401 got %d", test.rw.Code)
1274+ }
1275+}
1276+
1277+func TestFindJwtBearerToken(t *testing.T) {
1278+ p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"}
1279+ getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}}
1280+
1281+ validToken := "eyJfoobar.eyJfoobar.12345asdf"
1282+ var token string
1283+
1284+ // Bearer
1285+ getReq.Header = map[string][]string{
1286+ "Authorization": {fmt.Sprintf("Bearer %s", validToken)},
1287+ }
1288+
1289+ token, _ = p.findBearerToken(getReq)
1290+ assert.Equal(t, validToken, token)
1291+
1292+ // Basic - no password
1293+ getReq.SetBasicAuth(token, "")
1294+ token, _ = p.findBearerToken(getReq)
1295+ assert.Equal(t, validToken, token)
1296+
1297+ // Basic - sentinel password
1298+ getReq.SetBasicAuth(token, "x-oauth-basic")
1299+ token, _ = p.findBearerToken(getReq)
1300+ assert.Equal(t, validToken, token)
1301+
1302+ // Basic - any username, password matching jwt pattern
1303+ getReq.SetBasicAuth("any-username-you-could-wish-for", token)
1304+ token, _ = p.findBearerToken(getReq)
1305+ assert.Equal(t, validToken, token)
1306+
1307+ failures := []string{
1308+ // Too many parts
1309+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1310+ // Not enough parts
1311+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA",
1312+ // Invalid encrypted key
1313+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.//////.dGVzdA.dGVzdA.dGVzdA",
1314+ // Invalid IV
1315+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.//////.dGVzdA.dGVzdA",
1316+ // Invalid ciphertext
1317+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.//////.dGVzdA",
1318+ // Invalid tag
1319+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.//////",
1320+ // Invalid header
1321+ "W10.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1322+ // Invalid header
1323+ "######.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1324+ // Missing alc/enc params
1325+ "e30.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1326+ }
1327+
1328+ for _, failure := range failures {
1329+ getReq.Header = map[string][]string{
1330+ "Authorization": {fmt.Sprintf("Bearer %s", failure)},
1331+ }
1332+ _, err := p.findBearerToken(getReq)
1333+ assert.Error(t, err)
1334+ }
1335+
1336+ fmt.Printf("%s", token)
1337+}
options.go+74−7
@@ -61,6 +61,8 @@ type Options struct {
6161
6262 Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
6363 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
64+ SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"`
65+ ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"`
6466 PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
6567 BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
6668 PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
@@ -110,13 +112,14 @@ type Options struct {
110112 GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"`
111113
112114 // internal values that are set after config validation
113− redirectURL *url.URL
114− proxyURLs []*url.URL
115− CompiledRegex []*regexp.Regexp
116− provider providers.Provider
117− sessionStore sessionsapi.SessionStore
118− signatureData *SignatureData
119− oidcVerifier *oidc.IDTokenVerifier
115+ redirectURL *url.URL
116+ proxyURLs []*url.URL
117+ CompiledRegex []*regexp.Regexp
118+ provider providers.Provider
119+ sessionStore sessionsapi.SessionStore
120+ signatureData *SignatureData
121+ oidcVerifier *oidc.IDTokenVerifier
122+ jwtBearerVerifiers []*oidc.IDTokenVerifier
120123}
121124
122125// SignatureData holds hmacauth signature hash and key
@@ -168,6 +171,12 @@ func NewOptions() *Options {
168171 }
169172}
170173
174+// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier.
175+type jwtIssuer struct {
176+ issuerURI string
177+ audience string
178+}
179+
171180func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
172181 parsed, err := url.Parse(toParse)
173182 if err != nil {
@@ -244,6 +253,25 @@ func (o *Options) Validate() error {
244253 }
245254 }
246255
256+ if o.SkipJwtBearerTokens {
257+ // If we are using an oidc provider, go ahead and add that provider to the list
258+ if o.oidcVerifier != nil {
259+ o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier)
260+ }
261+ // Configure extra issuers
262+ if len(o.ExtraJwtIssuers) > 0 {
263+ var jwtIssuers []jwtIssuer
264+ jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs)
265+ for _, jwtIssuer := range jwtIssuers {
266+ verifier, err := newVerifierFromJwtIssuer(jwtIssuer)
267+ if err != nil {
268+ msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err))
269+ }
270+ o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier)
271+ }
272+ }
273+ }
274+
247275 o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
248276
249277 for _, u := range o.Upstreams {
@@ -430,6 +458,45 @@ func parseSignatureKey(o *Options, msgs []string) []string {
430458 return msgs
431459}
432460
461+// parseJwtIssuers takes in an array of strings in the form of issuer=audience
462+// and parses to an array of jwtIssuer structs.
463+func parseJwtIssuers(issuers []string, msgs []string) ([]jwtIssuer, []string) {
464+ var parsedIssuers []jwtIssuer
465+ for _, jwtVerifier := range issuers {
466+ components := strings.Split(jwtVerifier, "=")
467+ if len(components) < 2 {
468+ msgs = append(msgs, fmt.Sprintf("invalid jwt verifier uri=audience spec: %s", jwtVerifier))
469+ continue
470+ }
471+ uri, audience := components[0], strings.Join(components[1:], "=")
472+ parsedIssuers = append(parsedIssuers, jwtIssuer{issuerURI: uri, audience: audience})
473+ }
474+ return parsedIssuers, msgs
475+}
476+
477+// newVerifierFromJwtIssuer takes in issuer information in jwtIssuer info and returns
478+// a verifier for that issuer.
479+func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error) {
480+ config := &oidc.Config{
481+ ClientID: jwtIssuer.audience,
482+ }
483+ // Try as an OpenID Connect Provider first
484+ var verifier *oidc.IDTokenVerifier
485+ provider, err := oidc.NewProvider(context.Background(), jwtIssuer.issuerURI)
486+ if err != nil {
487+ // Try as JWKS URI
488+ jwksURI := strings.TrimSuffix(jwtIssuer.issuerURI, "/") + "/.well-known/jwks.json"
489+ _, err := http.NewRequest("GET", jwksURI, nil)
490+ if err != nil {
491+ return nil, err
492+ }
493+ verifier = oidc.NewVerifier(jwtIssuer.issuerURI, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
494+ } else {
495+ verifier = provider.Verifier(config)
496+ }
497+ return verifier, nil
498+}
499+
433500func validateCookieName(o *Options, msgs []string) []string {
434501 cookie := &http.Cookie{Name: o.CookieName}
435502 if cookie.String() == "" {
providers/oidc.go+1−1
@@ -128,7 +128,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
128128 IDToken: rawIDToken,
129129 RefreshToken: token.RefreshToken,
130130 CreatedAt: time.Now(),
131− ExpiresOn: token.Expiry,
131+ ExpiresOn: idToken.Expiry,
132132 Email: claims.Email,
133133 User: claims.Subject,
134134 }, nil