git.iko.soy/typeable/oauth2-proxy
on master help sign in
Support JWT Bearer Token and Pass through
commit8083501da68e19c8633726bcab87ea680d9048a0 authorBrian Van Klaveren <bvan@slac.stanford.edu> dateThu 17 Jan 2019, 12:49 (8 years ago) parent0af18d6
4 files changed+212−41
docs/configuration/configuration.md+2 main.go+3 oauthproxy.go+151−34 options.go+56−7
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")
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+151−34
@@ -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
@@ -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)
712+ if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
713+ session, err = p.GetJwtSession(req)
714+ if err != nil {
715+ logger.Printf("Error validating JWT token from Authorization header: %s", err)
716+ }
717+ if session != nil {
718+ saveSession = false
719+ }
702720 }
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
721+
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+ if session != nil && session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
729+ logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
730+ saveSession = true
731+ }
706732 }
707733
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
734+ if session != nil {
735+ var ok bool
736+ if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
737+ logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
738+ clearSession = true
739+ session = nil
740+ } else if ok {
741+ saveSession = true
742+ revalidated = true
743+ }
716744 }
717745
718746 if session != nil && session.IsExpired() {
@@ -854,3 +882,92 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
854882 rw.Header().Set("Content-Type", applicationJSON)
855883 rw.WriteHeader(code)
856884}
885+
886+// GetJwtSession loads a session based on a JWT token in the authorization header.
887+func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
888+ rawBearerToken, err := p.findBearerToken(req)
889+ if err != nil {
890+ return nil, err
891+ }
892+
893+ ctx := context.Background()
894+ var session *sessionsapi.SessionState
895+ for _, verifier := range p.jwtBearerVerifiers {
896+ bearerToken, err := verifier.Verify(ctx, rawBearerToken)
897+
898+ if err != nil {
899+ logger.Printf("failed to verify bearer token: %v", err)
900+ continue
901+ }
902+
903+ var claims struct {
904+ Email string `json:"email"`
905+ Verified *bool `json:"email_verified"`
906+ }
907+
908+ if err := bearerToken.Claims(&claims); err != nil {
909+ return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
910+ }
911+
912+ if claims.Email == "" {
913+ return nil, fmt.Errorf("id_token did not contain an email")
914+ }
915+
916+ if claims.Verified != nil && !*claims.Verified {
917+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
918+ }
919+ user := strings.Split(claims.Email, "@")[0]
920+
921+ session = &sessionsapi.SessionState{
922+ AccessToken: rawBearerToken,
923+ IDToken: rawBearerToken,
924+ RefreshToken: "",
925+ ExpiresOn: bearerToken.Expiry,
926+ Email: claims.Email,
927+ User: user,
928+ }
929+ return session, nil
930+ }
931+ return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
932+}
933+
934+// findBearerToken finds a valid JWT token from the Authorization header of a given request.
935+func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
936+ auth := req.Header.Get("Authorization")
937+ s := strings.SplitN(auth, " ", 2)
938+ if len(s) != 2 {
939+ return "", fmt.Errorf("invalid authorization header %s", auth)
940+ }
941+
942+ var rawBearerToken string
943+ if s[0] == "Bearer" {
944+ rawBearerToken = s[1]
945+ } else if s[0] == "Basic" {
946+ // Check if we have a Bearer token masquerading in Basic
947+ b, err := b64.StdEncoding.DecodeString(s[1])
948+ if err != nil {
949+ return "", err
950+ }
951+ pair := strings.SplitN(string(b), ":", 2)
952+ if len(pair) != 2 {
953+ return "", fmt.Errorf("invalid format %s", b)
954+ }
955+ user, password := pair[0], pair[1]
956+
957+ // check user, user+password, or just password for a token
958+ jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
959+ if jwtRegex.MatchString(user) {
960+ // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
961+ if password == "" || password == "x-oauth-basic" {
962+ rawBearerToken = user
963+ }
964+ } else if jwtRegex.MatchString(password) {
965+ // support passwords and ignore user
966+ rawBearerToken = password
967+ }
968+ } else {
969+ return "", fmt.Errorf("invalid authorization header %s", auth)
970+ }
971+
972+ return rawBearerToken, nil
973+}
options.go+56−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"`
65+ ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"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,15 @@ 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+ jwtIssuers [][]string
123+ jwtBearerVerifiers []*oidc.IDTokenVerifier
120124}
121125
122126// SignatureData holds hmacauth signature hash and key
@@ -244,6 +248,38 @@ func (o *Options) Validate() error {
244248 }
245249 }
246250
251+ if o.SkipJwtBearerTokens {
252+ // If we are using an oidc provider, go ahead and add that provider to the list
253+ if o.oidcVerifier != nil {
254+ o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier)
255+ }
256+ // Configure extra issuers
257+ if len(o.ExtraJwtIssuers) > 0 {
258+ msgs = parseJwtIssuers(o, msgs)
259+ for _, pair := range o.jwtIssuers {
260+ issuer, audience := pair[0], pair[1]
261+ config := &oidc.Config{
262+ ClientID: audience,
263+ }
264+ // Try as an OpenID Connect Provider first
265+ var verifier *oidc.IDTokenVerifier
266+ provider, err := oidc.NewProvider(context.Background(), issuer)
267+ if err != nil {
268+ // Try as JWKS URI
269+ jwksURI := strings.TrimSuffix(issuer, "/") + "/.well-known/jwks.json"
270+ _, err := http.NewRequest("GET", jwksURI, nil)
271+ if err != nil {
272+ return err
273+ }
274+ verifier = oidc.NewVerifier(issuer, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
275+ } else {
276+ verifier = provider.Verifier(config)
277+ }
278+ o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier)
279+ }
280+ }
281+ }
282+
247283 o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
248284
249285 for _, u := range o.Upstreams {
@@ -430,6 +466,19 @@ func parseSignatureKey(o *Options, msgs []string) []string {
430466 return msgs
431467}
432468
469+func parseJwtIssuers(o *Options, msgs []string) []string {
470+ for _, jwtVerifier := range o.ExtraJwtIssuers {
471+ components := strings.Split(jwtVerifier, "=")
472+ if len(components) < 2 {
473+ return append(msgs, "invalid jwt verifier uri=audience spec: "+
474+ jwtVerifier)
475+ }
476+ uri, audience := components[0], strings.Join(components[1:], "=")
477+ o.jwtIssuers = append(o.jwtIssuers, []string{uri, audience})
478+ }
479+ return msgs
480+}
481+
433482func validateCookieName(o *Options, msgs []string) []string {
434483 cookie := &http.Cookie{Name: o.CookieName}
435484 if cookie.String() == "" {