Support JWT Bearer Token and Pass through
This commit is contained in:
parent
0af18d6d7c
commit
8083501da6
@ -41,6 +41,7 @@ Usage of oauth2_proxy:
|
|||||||
-custom-templates-dir string: path to custom html templates
|
-custom-templates-dir string: path to custom html templates
|
||||||
-display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true)
|
-display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true)
|
||||||
-email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
|
-email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
|
||||||
|
-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)
|
||||||
-flush-interval: period between flushing response buffers when streaming responses (default "1s")
|
-flush-interval: period between flushing response buffers when streaming responses (default "1s")
|
||||||
-footer string: custom footer string. Use "-" to disable default footer.
|
-footer string: custom footer string. Use "-" to disable default footer.
|
||||||
-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)
|
-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:
|
|||||||
-signature-key string: GAP-Signature request signature key (algorithm:secretkey)
|
-signature-key string: GAP-Signature request signature key (algorithm:secretkey)
|
||||||
-skip-auth-preflight: will skip authentication for OPTIONS requests
|
-skip-auth-preflight: will skip authentication for OPTIONS requests
|
||||||
-skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)
|
-skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)
|
||||||
|
-skip-jwt-bearer-tokens: will skip requests that have verified JWT bearer tokens
|
||||||
-skip-oidc-discovery: bypass OIDC endpoint discovery. login-url, redeem-url and oidc-jwks-url must be configured in this case
|
-skip-oidc-discovery: bypass OIDC endpoint discovery. login-url, redeem-url and oidc-jwks-url must be configured in this case
|
||||||
-skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start
|
-skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start
|
||||||
-ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS
|
-ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS
|
||||||
|
3
main.go
3
main.go
@ -23,6 +23,7 @@ func main() {
|
|||||||
whitelistDomains := StringArray{}
|
whitelistDomains := StringArray{}
|
||||||
upstreams := StringArray{}
|
upstreams := StringArray{}
|
||||||
skipAuthRegex := StringArray{}
|
skipAuthRegex := StringArray{}
|
||||||
|
jwtIssuers := StringArray{}
|
||||||
googleGroups := StringArray{}
|
googleGroups := StringArray{}
|
||||||
redisSentinelConnectionURLs := StringArray{}
|
redisSentinelConnectionURLs := StringArray{}
|
||||||
|
|
||||||
@ -48,6 +49,8 @@ func main() {
|
|||||||
flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
|
flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
|
||||||
flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS")
|
flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS")
|
||||||
flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
|
flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
|
||||||
|
flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens")
|
||||||
|
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)")
|
||||||
|
|
||||||
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
||||||
flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
|
flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
|
||||||
|
121
oauthproxy.go
121
oauthproxy.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
b64 "encoding/base64"
|
b64 "encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -13,6 +14,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc"
|
||||||
"github.com/mbland/hmacauth"
|
"github.com/mbland/hmacauth"
|
||||||
"github.com/pusher/oauth2_proxy/cookie"
|
"github.com/pusher/oauth2_proxy/cookie"
|
||||||
"github.com/pusher/oauth2_proxy/logger"
|
"github.com/pusher/oauth2_proxy/logger"
|
||||||
@ -92,6 +94,8 @@ type OAuthProxy struct {
|
|||||||
PassAuthorization bool
|
PassAuthorization bool
|
||||||
skipAuthRegex []string
|
skipAuthRegex []string
|
||||||
skipAuthPreflight bool
|
skipAuthPreflight bool
|
||||||
|
skipJwtBearerTokens bool
|
||||||
|
jwtBearerVerifiers []*oidc.IDTokenVerifier
|
||||||
compiledRegex []*regexp.Regexp
|
compiledRegex []*regexp.Regexp
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
Footer string
|
Footer string
|
||||||
@ -206,6 +210,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
|||||||
logger.Printf("compiled skip-auth-regex => %q", u)
|
logger.Printf("compiled skip-auth-regex => %q", u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.SkipJwtBearerTokens {
|
||||||
|
logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
|
||||||
|
for _, issuer := range opts.ExtraJwtIssuers {
|
||||||
|
logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
|
||||||
|
}
|
||||||
|
}
|
||||||
redirectURL := opts.redirectURL
|
redirectURL := opts.redirectURL
|
||||||
if redirectURL.Path == "" {
|
if redirectURL.Path == "" {
|
||||||
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
|
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
|
||||||
@ -247,6 +257,8 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
|||||||
whitelistDomains: opts.WhitelistDomains,
|
whitelistDomains: opts.WhitelistDomains,
|
||||||
skipAuthRegex: opts.SkipAuthRegex,
|
skipAuthRegex: opts.SkipAuthRegex,
|
||||||
skipAuthPreflight: opts.SkipAuthPreflight,
|
skipAuthPreflight: opts.SkipAuthPreflight,
|
||||||
|
skipJwtBearerTokens: opts.SkipJwtBearerTokens,
|
||||||
|
jwtBearerVerifiers: opts.jwtBearerVerifiers,
|
||||||
compiledRegex: opts.CompiledRegex,
|
compiledRegex: opts.CompiledRegex,
|
||||||
SetXAuthRequest: opts.SetXAuthRequest,
|
SetXAuthRequest: opts.SetXAuthRequest,
|
||||||
PassBasicAuth: opts.PassBasicAuth,
|
PassBasicAuth: opts.PassBasicAuth,
|
||||||
@ -693,10 +705,23 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
|
|||||||
// Returns nil, ErrNeedsLogin if user needs to login.
|
// Returns nil, ErrNeedsLogin if user needs to login.
|
||||||
// Set-Cookie headers may be set on the response as a side-effect of calling this method.
|
// Set-Cookie headers may be set on the response as a side-effect of calling this method.
|
||||||
func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
|
func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
|
||||||
|
var session *sessionsapi.SessionState
|
||||||
|
var err error
|
||||||
var saveSession, clearSession, revalidated bool
|
var saveSession, clearSession, revalidated bool
|
||||||
remoteAddr := getRemoteAddr(req)
|
|
||||||
|
|
||||||
session, err := p.LoadCookiedSession(req)
|
if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
|
||||||
|
session, err = p.GetJwtSession(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("Error validating JWT token from Authorization header: %s", err)
|
||||||
|
}
|
||||||
|
if session != nil {
|
||||||
|
saveSession = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteAddr := getRemoteAddr(req)
|
||||||
|
if session == nil {
|
||||||
|
session, err = p.LoadCookiedSession(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Printf("Error loading cookied session: %s", err)
|
logger.Printf("Error loading cookied session: %s", err)
|
||||||
}
|
}
|
||||||
@ -704,7 +729,9 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
|
|||||||
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
|
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
|
||||||
saveSession = true
|
saveSession = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if session != nil {
|
||||||
var ok bool
|
var ok bool
|
||||||
if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
|
if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
|
||||||
logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
|
logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
|
||||||
@ -714,6 +741,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
|
|||||||
saveSession = true
|
saveSession = true
|
||||||
revalidated = true
|
revalidated = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if session != nil && session.IsExpired() {
|
if session != nil && session.IsExpired() {
|
||||||
logger.Printf("Removing session: token expired %s", session)
|
logger.Printf("Removing session: token expired %s", session)
|
||||||
@ -854,3 +882,92 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
|
|||||||
rw.Header().Set("Content-Type", applicationJSON)
|
rw.Header().Set("Content-Type", applicationJSON)
|
||||||
rw.WriteHeader(code)
|
rw.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJwtSession loads a session based on a JWT token in the authorization header.
|
||||||
|
func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
|
||||||
|
rawBearerToken, err := p.findBearerToken(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
var session *sessionsapi.SessionState
|
||||||
|
for _, verifier := range p.jwtBearerVerifiers {
|
||||||
|
bearerToken, err := verifier.Verify(ctx, rawBearerToken)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed to verify bearer token: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Verified *bool `json:"email_verified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bearerToken.Claims(&claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Email == "" {
|
||||||
|
return nil, fmt.Errorf("id_token did not contain an email")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Verified != nil && !*claims.Verified {
|
||||||
|
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
|
||||||
|
}
|
||||||
|
user := strings.Split(claims.Email, "@")[0]
|
||||||
|
|
||||||
|
session = &sessionsapi.SessionState{
|
||||||
|
AccessToken: rawBearerToken,
|
||||||
|
IDToken: rawBearerToken,
|
||||||
|
RefreshToken: "",
|
||||||
|
ExpiresOn: bearerToken.Expiry,
|
||||||
|
Email: claims.Email,
|
||||||
|
User: user,
|
||||||
|
}
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBearerToken finds a valid JWT token from the Authorization header of a given request.
|
||||||
|
func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
|
||||||
|
auth := req.Header.Get("Authorization")
|
||||||
|
s := strings.SplitN(auth, " ", 2)
|
||||||
|
if len(s) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid authorization header %s", auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawBearerToken string
|
||||||
|
if s[0] == "Bearer" {
|
||||||
|
rawBearerToken = s[1]
|
||||||
|
} else if s[0] == "Basic" {
|
||||||
|
// Check if we have a Bearer token masquerading in Basic
|
||||||
|
b, err := b64.StdEncoding.DecodeString(s[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
pair := strings.SplitN(string(b), ":", 2)
|
||||||
|
if len(pair) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid format %s", b)
|
||||||
|
}
|
||||||
|
user, password := pair[0], pair[1]
|
||||||
|
|
||||||
|
// check user, user+password, or just password for a token
|
||||||
|
jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
|
||||||
|
if jwtRegex.MatchString(user) {
|
||||||
|
// Support blank passwords or magic `x-oauth-basic` passwords - nothing else
|
||||||
|
if password == "" || password == "x-oauth-basic" {
|
||||||
|
rawBearerToken = user
|
||||||
|
}
|
||||||
|
} else if jwtRegex.MatchString(password) {
|
||||||
|
// support passwords and ignore user
|
||||||
|
rawBearerToken = password
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("invalid authorization header %s", auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawBearerToken, nil
|
||||||
|
}
|
||||||
|
49
options.go
49
options.go
@ -61,6 +61,8 @@ type Options struct {
|
|||||||
|
|
||||||
Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
|
Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
|
||||||
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
|
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
|
||||||
|
SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
|
||||||
|
ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"`
|
||||||
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
|
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
|
||||||
BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
|
BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
|
||||||
PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
|
PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
|
||||||
@ -117,6 +119,8 @@ type Options struct {
|
|||||||
sessionStore sessionsapi.SessionStore
|
sessionStore sessionsapi.SessionStore
|
||||||
signatureData *SignatureData
|
signatureData *SignatureData
|
||||||
oidcVerifier *oidc.IDTokenVerifier
|
oidcVerifier *oidc.IDTokenVerifier
|
||||||
|
jwtIssuers [][]string
|
||||||
|
jwtBearerVerifiers []*oidc.IDTokenVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignatureData holds hmacauth signature hash and key
|
// SignatureData holds hmacauth signature hash and key
|
||||||
@ -244,6 +248,38 @@ func (o *Options) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.SkipJwtBearerTokens {
|
||||||
|
// If we are using an oidc provider, go ahead and add that provider to the list
|
||||||
|
if o.oidcVerifier != nil {
|
||||||
|
o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier)
|
||||||
|
}
|
||||||
|
// Configure extra issuers
|
||||||
|
if len(o.ExtraJwtIssuers) > 0 {
|
||||||
|
msgs = parseJwtIssuers(o, msgs)
|
||||||
|
for _, pair := range o.jwtIssuers {
|
||||||
|
issuer, audience := pair[0], pair[1]
|
||||||
|
config := &oidc.Config{
|
||||||
|
ClientID: audience,
|
||||||
|
}
|
||||||
|
// Try as an OpenID Connect Provider first
|
||||||
|
var verifier *oidc.IDTokenVerifier
|
||||||
|
provider, err := oidc.NewProvider(context.Background(), issuer)
|
||||||
|
if err != nil {
|
||||||
|
// Try as JWKS URI
|
||||||
|
jwksURI := strings.TrimSuffix(issuer, "/") + "/.well-known/jwks.json"
|
||||||
|
_, err := http.NewRequest("GET", jwksURI, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
verifier = oidc.NewVerifier(issuer, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
|
||||||
|
} else {
|
||||||
|
verifier = provider.Verifier(config)
|
||||||
|
}
|
||||||
|
o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
|
o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
|
||||||
|
|
||||||
for _, u := range o.Upstreams {
|
for _, u := range o.Upstreams {
|
||||||
@ -430,6 +466,19 @@ func parseSignatureKey(o *Options, msgs []string) []string {
|
|||||||
return msgs
|
return msgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseJwtIssuers(o *Options, msgs []string) []string {
|
||||||
|
for _, jwtVerifier := range o.ExtraJwtIssuers {
|
||||||
|
components := strings.Split(jwtVerifier, "=")
|
||||||
|
if len(components) < 2 {
|
||||||
|
return append(msgs, "invalid jwt verifier uri=audience spec: "+
|
||||||
|
jwtVerifier)
|
||||||
|
}
|
||||||
|
uri, audience := components[0], strings.Join(components[1:], "=")
|
||||||
|
o.jwtIssuers = append(o.jwtIssuers, []string{uri, audience})
|
||||||
|
}
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
func validateCookieName(o *Options, msgs []string) []string {
|
func validateCookieName(o *Options, msgs []string) []string {
|
||||||
cookie := &http.Cookie{Name: o.CookieName}
|
cookie := &http.Cookie{Name: o.CookieName}
|
||||||
if cookie.String() == "" {
|
if cookie.String() == "" {
|
||||||
|
Loading…
Reference in New Issue
Block a user