1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2024-11-28 09:08:44 +02:00
oauth2-proxy/oauthproxy.go

1148 lines
36 KiB
Go
Raw Normal View History

2012-12-11 04:59:23 +03:00
package main
import (
"context"
2016-06-20 13:17:39 +02:00
b64 "encoding/base64"
"encoding/json"
2012-12-11 04:59:23 +03:00
"errors"
"fmt"
2015-03-18 00:06:06 +02:00
"html/template"
2015-03-19 21:59:48 +02:00
"net"
2012-12-11 04:59:23 +03:00
"net/http"
"net/url"
2015-01-19 18:10:37 +02:00
"regexp"
2012-12-11 04:59:23 +03:00
"strings"
"time"
2014-08-07 23:16:39 +03:00
"github.com/coreos/go-oidc"
2020-07-18 01:42:51 +02:00
"github.com/justinas/alice"
ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
"github.com/oauth2-proxy/oauth2-proxy/v7/providers"
2012-12-11 04:59:23 +03:00
)
2018-11-29 16:26:41 +02:00
const (
httpScheme = "http"
httpsScheme = "https"
2018-01-28 00:48:52 +02:00
applicationJSON = "application/json"
2018-11-29 16:26:41 +02:00
)
var (
// ErrNeedsLogin means the user should be redirected to the login page
ErrNeedsLogin = errors.New("redirect to login page")
// Used to check final redirects are not susceptible to open redirects.
// Matches //, /\ and both of these with whitespace in between (eg / / or / \).
invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
)
2020-09-23 03:54:32 +02:00
// allowedRoute manages method + path based allowlists
type allowedRoute struct {
method string
pathRegex *regexp.Regexp
}
// OAuthProxy is the main authentication proxy
type OAuthProxy struct {
2015-03-18 05:13:45 +02:00
CookieSeed string
CookieName string
2017-03-28 03:14:38 +02:00
CSRFCookieName string
CookieDomains []string
CookiePath string
2015-03-18 05:13:45 +02:00
CookieSecure bool
2018-11-29 16:26:41 +02:00
CookieHTTPOnly bool
2015-03-18 05:13:45 +02:00
CookieExpire time.Duration
CookieRefresh time.Duration
CookieSameSite string
2015-03-18 05:13:45 +02:00
Validator func(string) bool
2012-12-11 04:59:23 +03:00
RobotsPath string
SignInPath string
2017-03-21 18:39:26 +02:00
SignOutPath string
OAuthStartPath string
OAuthCallbackPath string
AuthOnlyPath string
UserInfoPath string
allowedRoutes []allowedRoute
redirectURL *url.URL // the url to receive requests at
whitelistDomains []string
provider providers.Provider
providerNameOverride string
sessionStore sessionsapi.SessionStore
ProxyPrefix string
SignInMessage string
basicAuthValidator basic.Validator
displayHtpasswdForm bool
serveMux http.Handler
SetXAuthRequest bool
PassBasicAuth bool
SetBasicAuth bool
SkipProviderButton bool
PassUserHeaders bool
BasicAuthPassword string
PassAccessToken bool
SetAuthorization bool
PassAuthorization bool
PreferEmailToUser bool
skipAuthPreflight bool
skipAuthStripHeaders bool
skipJwtBearerTokens bool
mainJwtBearerVerifier *oidc.IDTokenVerifier
extraJwtBearerVerifiers []*oidc.IDTokenVerifier
templates *template.Template
realClientIPParser ipapi.RealClientIPParser
Implements --trusted-ip option (#552) * Implements --ip-whitelist option * Included IPWhitelist option to allow one-or-more selected CIDR ranges to bypass OAuth2 authentication. * Adds IPWhitelist, a fast lookup table for multiple CIDR ranges. * Renamed IPWhitelist ipCIDRSet * Fixed unessesary pointer usage in ipCIDRSet * Update CHANGELOG.md * Update CHANGELOG.md * Updated to not use err.Error() in printf statements * Imrpoved language for --ip-whitelist descriptions. * Improve IP whitelist options error messages * Clarify options single-host normalization * Wrote a book about ipCIDRSet * Added comment to IsWhitelistedIP in oauthproxy.go * Rewrite oauthproxy test case as table driven * oops * Support whitelisting by low-level remote address * Added more test-cases, improved descriptions * Move ip_cidr_set.go to pkg/ip/net_set.go * Add more whitelist test use cases. * Oops * Use subtests for TestIPWhitelist * Add minimal tests for ip.NetSet * Use switch statment * Renamed ip-whitelist to whitelist-ip * Update documentation with a warning. * Update pkg/apis/options/options.go * Update CHANGELOG.md Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Apply suggestions from code review Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix fmt * Move ParseIPNet into abstraction * Add warning in case of --reverse-proxy * Update pkg/validation/options_test.go * Rename --whitelist-ip to --trusted-ip * Update oauthproxy.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
2020-07-11 12:10:58 +02:00
trustedIPs *ip.NetSet
Banner string
Footer string
AllowedGroups []string
2020-07-18 01:42:51 +02:00
sessionChain alice.Chain
2012-12-11 04:59:23 +03:00
}
2019-12-20 16:44:59 +02:00
// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
if err != nil {
return nil, fmt.Errorf("error initialising session store: %v", err)
}
templates := loadTemplates(opts.CustomTemplatesDir)
proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
if err != nil {
return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
}
2019-10-04 15:07:31 +02:00
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)
}
}
2020-04-13 14:50:34 +02:00
redirectURL := opts.GetRedirectURL()
if redirectURL.Path == "" {
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
}
2012-12-11 04:59:23 +03:00
2020-04-13 14:50:34 +02:00
logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
refresh := "disabled"
if opts.Cookie.Refresh != time.Duration(0) {
refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
}
2015-03-18 05:13:45 +02:00
logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh)
2015-03-18 05:13:45 +02:00
Implements --trusted-ip option (#552) * Implements --ip-whitelist option * Included IPWhitelist option to allow one-or-more selected CIDR ranges to bypass OAuth2 authentication. * Adds IPWhitelist, a fast lookup table for multiple CIDR ranges. * Renamed IPWhitelist ipCIDRSet * Fixed unessesary pointer usage in ipCIDRSet * Update CHANGELOG.md * Update CHANGELOG.md * Updated to not use err.Error() in printf statements * Imrpoved language for --ip-whitelist descriptions. * Improve IP whitelist options error messages * Clarify options single-host normalization * Wrote a book about ipCIDRSet * Added comment to IsWhitelistedIP in oauthproxy.go * Rewrite oauthproxy test case as table driven * oops * Support whitelisting by low-level remote address * Added more test-cases, improved descriptions * Move ip_cidr_set.go to pkg/ip/net_set.go * Add more whitelist test use cases. * Oops * Use subtests for TestIPWhitelist * Add minimal tests for ip.NetSet * Use switch statment * Renamed ip-whitelist to whitelist-ip * Update documentation with a warning. * Update pkg/apis/options/options.go * Update CHANGELOG.md Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Apply suggestions from code review Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix fmt * Move ParseIPNet into abstraction * Add warning in case of --reverse-proxy * Update pkg/validation/options_test.go * Rename --whitelist-ip to --trusted-ip * Update oauthproxy.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
2020-07-11 12:10:58 +02:00
trustedIPs := ip.NewNetSet()
for _, ipStr := range opts.TrustedIPs {
if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
trustedIPs.AddIPNet(*ipNet)
} else {
return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
}
}
var basicAuthValidator basic.Validator
if opts.HtpasswdFile != "" {
logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
var err error
basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
if err != nil {
return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
}
}
2020-09-23 03:54:32 +02:00
allowedRoutes, err := buildRoutesAllowlist(opts)
if err != nil {
return nil, err
}
2020-07-18 01:42:51 +02:00
sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
return &OAuthProxy{
CookieName: opts.Cookie.Name,
CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
CookieSeed: opts.Cookie.Secret,
CookieDomains: opts.Cookie.Domains,
CookiePath: opts.Cookie.Path,
CookieSecure: opts.Cookie.Secure,
CookieHTTPOnly: opts.Cookie.HTTPOnly,
CookieExpire: opts.Cookie.Expire,
CookieRefresh: opts.Cookie.Refresh,
CookieSameSite: opts.Cookie.SameSite,
2015-03-18 05:13:45 +02:00
Validator: validator,
2014-11-09 21:51:10 +02:00
RobotsPath: "/robots.txt",
SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
2017-03-21 18:39:26 +02:00
SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
2019-03-12 18:46:37 +02:00
OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
ProxyPrefix: opts.ProxyPrefix,
provider: opts.GetProvider(),
providerNameOverride: opts.ProviderName,
sessionStore: sessionStore,
serveMux: upstreamProxy,
redirectURL: redirectURL,
2020-09-23 03:54:32 +02:00
allowedRoutes: allowedRoutes,
whitelistDomains: opts.WhitelistDomains,
skipAuthPreflight: opts.SkipAuthPreflight,
skipAuthStripHeaders: opts.SkipAuthStripHeaders,
skipJwtBearerTokens: opts.SkipJwtBearerTokens,
mainJwtBearerVerifier: opts.GetOIDCVerifier(),
extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
realClientIPParser: opts.GetRealClientIPParser(),
SetXAuthRequest: opts.SetXAuthRequest,
PassBasicAuth: opts.PassBasicAuth,
SetBasicAuth: opts.SetBasicAuth,
PassUserHeaders: opts.PassUserHeaders,
BasicAuthPassword: opts.BasicAuthPassword,
PassAccessToken: opts.PassAccessToken,
SetAuthorization: opts.SetAuthorization,
PassAuthorization: opts.PassAuthorization,
PreferEmailToUser: opts.PreferEmailToUser,
SkipProviderButton: opts.SkipProviderButton,
templates: templates,
Implements --trusted-ip option (#552) * Implements --ip-whitelist option * Included IPWhitelist option to allow one-or-more selected CIDR ranges to bypass OAuth2 authentication. * Adds IPWhitelist, a fast lookup table for multiple CIDR ranges. * Renamed IPWhitelist ipCIDRSet * Fixed unessesary pointer usage in ipCIDRSet * Update CHANGELOG.md * Update CHANGELOG.md * Updated to not use err.Error() in printf statements * Imrpoved language for --ip-whitelist descriptions. * Improve IP whitelist options error messages * Clarify options single-host normalization * Wrote a book about ipCIDRSet * Added comment to IsWhitelistedIP in oauthproxy.go * Rewrite oauthproxy test case as table driven * oops * Support whitelisting by low-level remote address * Added more test-cases, improved descriptions * Move ip_cidr_set.go to pkg/ip/net_set.go * Add more whitelist test use cases. * Oops * Use subtests for TestIPWhitelist * Add minimal tests for ip.NetSet * Use switch statment * Renamed ip-whitelist to whitelist-ip * Update documentation with a warning. * Update pkg/apis/options/options.go * Update CHANGELOG.md Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Apply suggestions from code review Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix fmt * Move ParseIPNet into abstraction * Add warning in case of --reverse-proxy * Update pkg/validation/options_test.go * Rename --whitelist-ip to --trusted-ip * Update oauthproxy.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
2020-07-11 12:10:58 +02:00
trustedIPs: trustedIPs,
Banner: opts.Banner,
Footer: opts.Footer,
2020-07-21 06:01:54 +02:00
SignInMessage: buildSignInMessage(opts),
AllowedGroups: opts.AllowedGroups,
basicAuthValidator: basicAuthValidator,
displayHtpasswdForm: basicAuthValidator != nil,
2020-07-18 01:42:51 +02:00
sessionChain: sessionChain,
}, nil
2012-12-11 04:59:23 +03:00
}
2020-07-18 01:42:51 +02:00
func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
chain := alice.New(middleware.NewScope())
if opts.SkipJwtBearerTokens {
sessionLoaders := []middlewareapi.TokenToSessionLoader{}
if opts.GetOIDCVerifier() != nil {
sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
Verifier: opts.GetOIDCVerifier(),
TokenToSession: opts.GetProvider().CreateSessionStateFromBearerToken,
})
}
for _, verifier := range opts.GetJWTBearerVerifiers() {
sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
Verifier: verifier,
})
}
chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
}
if validator != nil {
chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
}
chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
SessionStore: sessionStore,
RefreshPeriod: opts.Cookie.Refresh,
RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
ValidateSessionState: opts.GetProvider().ValidateSessionState,
}))
return chain
}
2020-07-21 06:01:54 +02:00
func buildSignInMessage(opts *options.Options) string {
var msg string
if len(opts.Banner) >= 1 {
if opts.Banner == "-" {
msg = ""
} else {
msg = opts.Banner
}
} else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
if len(opts.EmailDomains) > 1 {
msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
} else if opts.EmailDomains[0] != "*" {
msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
}
}
return msg
}
2020-09-23 03:54:32 +02:00
// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
// (method=path support)
func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes))
2020-09-23 03:54:32 +02:00
for _, path := range opts.SkipAuthRegex {
compiledRegex, err := regexp.Compile(path)
if err != nil {
return nil, err
}
logger.Printf("Skipping auth - Method: ALL | Path: %s", path)
routes = append(routes, allowedRoute{
2020-09-23 03:54:32 +02:00
method: "",
pathRegex: compiledRegex,
})
}
for _, methodPath := range opts.SkipAuthRoutes {
var (
method string
path string
)
parts := strings.SplitN(methodPath, "=", 2)
2020-09-23 03:54:32 +02:00
if len(parts) == 1 {
method = ""
path = parts[0]
} else {
method = strings.ToUpper(parts[0])
path = parts[1]
2020-09-23 03:54:32 +02:00
}
compiledRegex, err := regexp.Compile(path)
if err != nil {
return nil, err
}
logger.Printf("Skipping auth - Method: %s | Path: %s", method, path)
routes = append(routes, allowedRoute{
2020-09-23 03:54:32 +02:00
method: method,
pathRegex: compiledRegex,
})
}
return routes, nil
}
// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
// redirect clients to once authenticated
func (p *OAuthProxy) GetRedirectURI(host string) string {
2015-03-17 22:25:19 +02:00
// default to the request Host if not set
if p.redirectURL.Host != "" {
return p.redirectURL.String()
2015-03-17 22:25:19 +02:00
}
u := *p.redirectURL
2015-03-17 22:25:19 +02:00
if u.Scheme == "" {
2015-03-18 05:13:45 +02:00
if p.CookieSecure {
2018-11-29 16:26:41 +02:00
u.Scheme = httpsScheme
2015-03-17 22:25:19 +02:00
} else {
2018-11-29 16:26:41 +02:00
u.Scheme = httpScheme
2015-03-17 22:25:19 +02:00
}
}
u.Host = host
return u.String()
}
func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
if code == "" {
return nil, errors.New("missing code")
}
redirectURI := p.GetRedirectURI(host)
s, err := p.provider.Redeem(ctx, redirectURI, code)
2012-12-11 04:59:23 +03:00
if err != nil {
return nil, err
2012-12-11 04:59:23 +03:00
}
return s, nil
}
2012-12-17 21:15:23 +03:00
func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error {
var err error
if s.Email == "" {
s.Email, err = p.provider.GetEmailAddress(ctx, s)
if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
return err
}
2014-08-07 23:16:39 +03:00
}
return p.provider.EnrichSessionState(ctx, s)
2014-08-07 23:16:39 +03:00
}
// MakeCSRFCookie creates a cookie for CSRF
2017-03-28 03:14:38 +02:00
func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
}
func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
if cookieDomain != "" {
2020-08-22 04:50:32 +02:00
domain := util.GetRequestHost(req)
if h, _, err := net.SplitHostPort(domain); err == nil {
domain = h
}
if !strings.HasSuffix(domain, cookieDomain) {
logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
2015-03-19 21:59:48 +02:00
}
2012-12-11 04:59:23 +03:00
}
2015-05-08 17:51:11 +02:00
return &http.Cookie{
2017-03-28 03:14:38 +02:00
Name: name,
2015-05-08 17:51:11 +02:00
Value: value,
Path: p.CookiePath,
Domain: cookieDomain,
2018-11-29 16:26:41 +02:00
HttpOnly: p.CookieHTTPOnly,
2015-03-19 21:59:48 +02:00
Secure: p.CookieSecure,
Expires: now.Add(expiration),
SameSite: cookies.ParseSameSite(p.CookieSameSite),
2012-12-11 04:59:23 +03:00
}
}
// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
// session
2017-03-28 03:14:38 +02:00
func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
}
// SetCSRFCookie adds a CSRF cookie to the response
2017-03-28 03:14:38 +02:00
func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
2012-12-11 04:59:23 +03:00
}
// ClearSessionCookie creates a cookie to unset the user's authentication cookie
// stored in the user's session
2019-05-07 17:13:55 +02:00
func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
return p.sessionStore.Clear(rw, req)
2017-03-28 03:14:38 +02:00
}
// LoadCookiedSession reads the user's authentication details from the request
2019-05-07 17:13:55 +02:00
func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
return p.sessionStore.Load(req)
}
// SaveSession creates a new session cookie value and sets this on the response
2019-05-07 15:27:09 +02:00
func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
2019-05-07 17:13:55 +02:00
return p.sessionStore.Save(rw, req, s)
2012-12-26 18:35:02 +03:00
}
// RobotsTxt disallows scraping pages from the OAuthProxy
func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
_, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
if err != nil {
2020-07-21 03:34:37 +02:00
logger.Printf("Error writing robots.txt: %v", err)
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
2020-07-21 03:34:37 +02:00
rw.WriteHeader(http.StatusOK)
}
// ErrorPage writes an error response
func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
2012-12-11 04:59:23 +03:00
rw.WriteHeader(code)
2012-12-17 21:15:23 +03:00
t := struct {
Title string
Message string
ProxyPrefix string
2012-12-11 04:59:23 +03:00
}{
Title: fmt.Sprintf("%d %s", code, title),
Message: message,
ProxyPrefix: p.ProxyPrefix,
2012-12-11 04:59:23 +03:00
}
err := p.templates.ExecuteTemplate(rw, "error.html", t)
if err != nil {
2020-07-21 03:34:37 +02:00
logger.Printf("Error rendering error.html template: %v", err)
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
}
2012-12-17 21:15:23 +03:00
}
// SignInPage writes the sing in template to the response
func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
prepareNoCache(rw)
err := p.ClearSessionCookie(rw, req)
if err != nil {
2020-07-21 03:34:37 +02:00
logger.Printf("Error clearing session cookie: %v", err)
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
2012-12-17 21:15:23 +03:00
rw.WriteHeader(code)
redirectURL, err := p.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining redirect: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
if redirectURL == p.SignInPath {
redirectURL = "/"
}
2020-07-21 03:49:45 +02:00
// We allow unescaped template.HTML since it is user configured options
/* #nosec G203 */
t := struct {
ProviderName string
SignInMessage template.HTML
CustomLogin bool
Redirect string
2014-11-10 05:01:50 +02:00
Version string
ProxyPrefix string
Footer template.HTML
}{
ProviderName: p.provider.Data().ProviderName,
SignInMessage: template.HTML(p.SignInMessage),
CustomLogin: p.displayHtpasswdForm,
Redirect: redirectURL,
2014-11-10 05:01:50 +02:00
Version: VERSION,
ProxyPrefix: p.ProxyPrefix,
Footer: template.HTML(p.Footer),
}
if p.providerNameOverride != "" {
t.ProviderName = p.providerNameOverride
}
err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
if err != nil {
2020-07-21 03:34:37 +02:00
logger.Printf("Error rendering sign_in.html template: %v", err)
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
}
2012-12-11 04:59:23 +03:00
}
// ManualSignIn handles basic auth logins to the proxy
func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
if req.Method != "POST" || p.basicAuthValidator == nil {
return "", false
}
user := req.FormValue("username")
passwd := req.FormValue("password")
if user == "" {
return "", false
}
// check auth
if p.basicAuthValidator.Validate(user, passwd) {
2019-02-10 19:01:13 +02:00
logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
return user, true
}
2019-02-10 19:01:13 +02:00
logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
return "", false
}
// GetRedirect reads the query parameter to get the URL to redirect clients to
// once authenticated with the OAuthProxy
2017-03-28 03:14:38 +02:00
func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
err = req.ParseForm()
if err != nil {
2017-03-28 03:14:38 +02:00
return
}
redirect = req.Header.Get("X-Auth-Request-Redirect")
if req.Form.Get("rd") != "" {
redirect = req.Form.Get("rd")
}
2017-09-29 17:55:50 +02:00
if !p.IsValidRedirect(redirect) {
// Use RequestURI to preserve ?query
redirect = req.URL.RequestURI()
if strings.HasPrefix(redirect, p.ProxyPrefix) {
redirect = "/"
}
}
2017-03-28 03:14:38 +02:00
return
}
// splitHostPort separates host and port. If the port is not valid, it returns
// the entire input as host, and it doesn't check the validity of the host.
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
// *** taken from net/url, modified validOptionalPort() to accept ":*"
func splitHostPort(hostport string) (host, port string) {
host = hostport
colon := strings.LastIndexByte(host, ':')
if colon != -1 && validOptionalPort(host[colon:]) {
host, port = host[:colon], host[colon+1:]
}
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
host = host[1 : len(host)-1]
}
return
}
// validOptionalPort reports whether port is either an empty string
// or matches /^:\d*$/
// *** taken from net/url, modified to accept ":*"
func validOptionalPort(port string) bool {
if port == "" || port == ":*" {
return true
}
if port[0] != ':' {
return false
}
for _, b := range port[1:] {
if b < '0' || b > '9' {
return false
}
}
return true
}
2017-09-29 17:55:50 +02:00
// IsValidRedirect checks whether the redirect URL is whitelisted
func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
switch {
case redirect == "":
// The user didn't specify a redirect, should fallback to `/`
return false
case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
2017-09-29 17:55:50 +02:00
return true
2017-12-11 11:24:52 +02:00
case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
redirectURL, err := url.Parse(redirect)
if err != nil {
logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
2017-12-11 11:24:52 +02:00
return false
2017-09-29 17:55:50 +02:00
}
redirectHostname := redirectURL.Hostname()
2017-09-29 17:55:50 +02:00
for _, domain := range p.whitelistDomains {
domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
2019-11-14 17:17:12 +02:00
if domainHostname == "" {
continue
}
if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
// the domain names match, now validate the ports
// if the whitelisted domain's port is '*', allow all ports
// if the whitelisted domain contains a specific port, only allow that port
// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
redirectPort := redirectURL.Port()
if (domainPort == "*") ||
(domainPort == redirectPort) ||
(domainPort == "" && redirectPort == "") {
return true
}
2017-09-29 17:55:50 +02:00
}
}
logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
2017-09-29 17:55:50 +02:00
return false
default:
logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
2017-09-29 17:55:50 +02:00
return false
}
}
2020-09-23 03:54:32 +02:00
// IsAllowedRequest is used to check if auth should be skipped for this request
func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool {
isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
2020-09-23 03:54:32 +02:00
return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req)
}
2020-09-23 03:54:32 +02:00
// IsAllowedRoute is used to check if the request method & path is allowed without auth
func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool {
for _, route := range p.allowedRoutes {
if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) {
return true
}
}
return false
}
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
var noCacheHeaders = map[string]string{
"Expires": time.Unix(0, 0).Format(time.RFC1123),
"Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
"X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
}
// prepareNoCache prepares headers for preventing browser caching.
func prepareNoCache(w http.ResponseWriter) {
// Set NoCache headers
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
}
Implements --trusted-ip option (#552) * Implements --ip-whitelist option * Included IPWhitelist option to allow one-or-more selected CIDR ranges to bypass OAuth2 authentication. * Adds IPWhitelist, a fast lookup table for multiple CIDR ranges. * Renamed IPWhitelist ipCIDRSet * Fixed unessesary pointer usage in ipCIDRSet * Update CHANGELOG.md * Update CHANGELOG.md * Updated to not use err.Error() in printf statements * Imrpoved language for --ip-whitelist descriptions. * Improve IP whitelist options error messages * Clarify options single-host normalization * Wrote a book about ipCIDRSet * Added comment to IsWhitelistedIP in oauthproxy.go * Rewrite oauthproxy test case as table driven * oops * Support whitelisting by low-level remote address * Added more test-cases, improved descriptions * Move ip_cidr_set.go to pkg/ip/net_set.go * Add more whitelist test use cases. * Oops * Use subtests for TestIPWhitelist * Add minimal tests for ip.NetSet * Use switch statment * Renamed ip-whitelist to whitelist-ip * Update documentation with a warning. * Update pkg/apis/options/options.go * Update CHANGELOG.md Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Apply suggestions from code review Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix fmt * Move ParseIPNet into abstraction * Add warning in case of --reverse-proxy * Update pkg/validation/options_test.go * Rename --whitelist-ip to --trusted-ip * Update oauthproxy.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
2020-07-11 12:10:58 +02:00
// IsTrustedIP is used to check if a request comes from a trusted client IP address.
func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
if p.trustedIPs == nil {
return false
}
remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
if err != nil {
logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
Implements --trusted-ip option (#552) * Implements --ip-whitelist option * Included IPWhitelist option to allow one-or-more selected CIDR ranges to bypass OAuth2 authentication. * Adds IPWhitelist, a fast lookup table for multiple CIDR ranges. * Renamed IPWhitelist ipCIDRSet * Fixed unessesary pointer usage in ipCIDRSet * Update CHANGELOG.md * Update CHANGELOG.md * Updated to not use err.Error() in printf statements * Imrpoved language for --ip-whitelist descriptions. * Improve IP whitelist options error messages * Clarify options single-host normalization * Wrote a book about ipCIDRSet * Added comment to IsWhitelistedIP in oauthproxy.go * Rewrite oauthproxy test case as table driven * oops * Support whitelisting by low-level remote address * Added more test-cases, improved descriptions * Move ip_cidr_set.go to pkg/ip/net_set.go * Add more whitelist test use cases. * Oops * Use subtests for TestIPWhitelist * Add minimal tests for ip.NetSet * Use switch statment * Renamed ip-whitelist to whitelist-ip * Update documentation with a warning. * Update pkg/apis/options/options.go * Update CHANGELOG.md Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Apply suggestions from code review Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix fmt * Move ParseIPNet into abstraction * Add warning in case of --reverse-proxy * Update pkg/validation/options_test.go * Rename --whitelist-ip to --trusted-ip * Update oauthproxy.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
2020-07-11 12:10:58 +02:00
// Possibly spoofed X-Real-IP header
return false
}
if remoteAddr == nil {
return false
}
return p.trustedIPs.Has(remoteAddr)
}
func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
prepareNoCache(rw)
}
switch path := req.URL.Path; {
case path == p.RobotsPath:
p.RobotsTxt(rw)
2020-09-23 03:54:32 +02:00
case p.IsAllowedRequest(req):
p.SkipAuthProxy(rw, req)
case path == p.SignInPath:
p.SignIn(rw, req)
2017-03-21 18:39:26 +02:00
case path == p.SignOutPath:
p.SignOut(rw, req)
case path == p.OAuthStartPath:
p.OAuthStart(rw, req)
case path == p.OAuthCallbackPath:
p.OAuthCallback(rw, req)
case path == p.AuthOnlyPath:
p.AuthenticateOnly(rw, req)
case path == p.UserInfoPath:
p.UserInfo(rw, req)
default:
p.Proxy(rw, req)
}
}
// SignIn serves a page prompting users to sign in
func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
redirect, err := p.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining redirect: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
2014-10-14 23:22:38 +03:00
return
}
user, ok := p.ManualSignIn(req)
if ok {
2019-05-07 15:27:09 +02:00
session := &sessionsapi.SessionState{User: user}
err = p.SaveSession(rw, req, session)
if err != nil {
logger.Printf("Error saving session: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
http.Redirect(rw, req, redirect, http.StatusFound)
} else {
if p.SkipProviderButton {
p.OAuthStart(rw, req)
} else {
p.SignInPage(rw, req, http.StatusOK)
}
}
}
//UserInfo endpoint outputs session email and preferred username in JSON format
func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
session, err := p.getAuthenticatedSession(rw, req)
if err != nil {
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
userInfo := struct {
Email string `json:"email"`
PreferredUsername string `json:"preferredUsername,omitempty"`
}{
Email: session.Email,
PreferredUsername: session.PreferredUsername,
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
err = json.NewEncoder(rw).Encode(userInfo)
if err != nil {
2020-07-21 03:34:37 +02:00
logger.Printf("Error encoding user info: %v", err)
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
}
}
// SignOut sends a response to clear the authentication cookie
2017-03-21 18:39:26 +02:00
func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
redirect, err := p.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining redirect: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
err = p.ClearSessionCookie(rw, req)
if err != nil {
logger.Errorf("Error clearing session cookie: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
http.Redirect(rw, req, redirect, http.StatusFound)
2017-03-21 18:39:26 +02:00
}
// OAuthStart starts the OAuth2 authentication flow
func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
prepareNoCache(rw)
2019-05-24 18:06:48 +02:00
nonce, err := encryption.Nonce()
2017-03-28 03:14:38 +02:00
if err != nil {
logger.Errorf("Error obtaining nonce: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
2017-03-28 03:14:38 +02:00
return
}
p.SetCSRFCookie(rw, req, nonce)
redirect, err := p.GetRedirect(req)
if err != nil {
logger.Errorf("Error obtaining redirect: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
2020-08-22 04:50:32 +02:00
redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
}
// OAuthCallback is the OAuth2 authentication flow callback that finishes the
// OAuth2 authentication flow
func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
2020-05-23 16:17:41 +02:00
remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
// finish the oauth cycle
err := req.ParseForm()
if err != nil {
logger.Errorf("Error while parsing OAuth2 callback: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
2012-12-11 04:59:23 +03:00
return
}
errorString := req.Form.Get("error")
if errorString != "" {
logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
2012-12-11 04:59:23 +03:00
return
}
2020-08-22 04:50:32 +02:00
session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
if err != nil {
logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
return
}
err = p.enrichSessionState(req.Context(), session)
if err != nil {
logger.Errorf("Error creating session during OAuth2 callback: %v", err)
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
return
}
state := strings.SplitN(req.Form.Get("state"), ":", 2)
if len(state) != 2 {
logger.Error("Error while parsing OAuth2 state: invalid length")
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
2017-03-28 03:14:38 +02:00
return
}
nonce := state[0]
redirect := state[1]
2017-03-28 03:14:38 +02:00
c, err := req.Cookie(p.CSRFCookieName)
if err != nil {
2020-07-21 03:34:37 +02:00
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
2017-03-28 03:14:38 +02:00
return
}
p.ClearCSRFCookie(rw, req)
if c.Value != nonce {
2020-07-21 03:34:37 +02:00
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
2017-03-28 03:14:38 +02:00
return
}
2017-09-29 17:55:50 +02:00
if !p.IsValidRedirect(redirect) {
redirect = "/"
}
// set cookie, or deny
if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
2019-04-23 18:36:18 +02:00
logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
err := p.SaveSession(rw, req, session)
2012-12-11 04:59:23 +03:00
if err != nil {
2020-07-21 03:34:37 +02:00
logger.Printf("Error saving session state for %s: %v", remoteAddr, err)
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
2012-12-11 04:59:23 +03:00
return
}
http.Redirect(rw, req, redirect, http.StatusFound)
} else {
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
2020-07-21 03:34:37 +02:00
p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
}
}
2012-12-11 04:59:23 +03:00
// AuthenticateOnly checks whether the user is currently logged in
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
session, err := p.getAuthenticatedSession(rw, req)
if err != nil {
http.Error(rw, "unauthorized request", http.StatusUnauthorized)
return
}
// we are authenticated
p.addHeadersForProxying(rw, req, session)
rw.WriteHeader(http.StatusAccepted)
}
2020-09-23 03:54:32 +02:00
// SkipAuthProxy proxies allowlisted requests and skips authentication
func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
if p.skipAuthStripHeaders {
p.stripAuthHeaders(req)
}
p.serveMux.ServeHTTP(rw, req)
}
// Proxy proxies the user request if the user is authenticated else it prompts
// them to authenticate
func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
session, err := p.getAuthenticatedSession(rw, req)
switch err {
case nil:
// we are authenticated
p.addHeadersForProxying(rw, req, session)
p.serveMux.ServeHTTP(rw, req)
case ErrNeedsLogin:
// we need to send the user to a login screen
if isAjax(req) {
// no point redirecting an AJAX request
p.ErrorJSON(rw, http.StatusUnauthorized)
return
}
if p.SkipProviderButton {
p.OAuthStart(rw, req)
} else {
p.SignInPage(rw, req, http.StatusForbidden)
}
default:
// unknown error
logger.Errorf("Unexpected internal error: %v", err)
p.ErrorPage(rw, http.StatusInternalServerError,
"Internal Error", "Internal Error")
}
}
// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
// 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.
func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
var session *sessionsapi.SessionState
2020-07-18 01:42:51 +02:00
getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
session = middleware.GetRequestScope(req).Session
}))
getSession.ServeHTTP(rw, req)
if session == nil {
2020-07-18 01:42:51 +02:00
return nil, ErrNeedsLogin
}
invalidEmail := session != nil && session.Email != "" && !p.Validator(session.Email)
invalidGroups := session != nil && !p.validateGroups(session.Groups)
if invalidEmail || invalidGroups {
logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
2020-07-18 01:42:51 +02:00
// Invalid session, clear it
err := p.ClearSessionCookie(rw, req)
if err != nil {
2020-07-21 03:34:37 +02:00
logger.Printf("Error clearing session cookie: %v", err)
}
return nil, ErrNeedsLogin
2012-12-11 04:59:23 +03:00
}
return session, nil
}
// addHeadersForProxying adds the appropriate headers the request / response for proxying
func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
2014-11-09 21:51:10 +02:00
if p.PassBasicAuth {
if p.PreferEmailToUser && session.Email != "" {
req.SetBasicAuth(session.Email, p.BasicAuthPassword)
req.Header["X-Forwarded-User"] = []string{session.Email}
req.Header.Del("X-Forwarded-Email")
} else {
req.SetBasicAuth(session.User, p.BasicAuthPassword)
req.Header["X-Forwarded-User"] = []string{session.User}
if session.Email != "" {
req.Header["X-Forwarded-Email"] = []string{session.Email}
} else {
req.Header.Del("X-Forwarded-Email")
}
}
if session.PreferredUsername != "" {
req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
} else {
req.Header.Del("X-Forwarded-Preferred-Username")
}
2012-12-11 04:59:23 +03:00
}
if p.PassUserHeaders {
if p.PreferEmailToUser && session.Email != "" {
req.Header["X-Forwarded-User"] = []string{session.Email}
req.Header.Del("X-Forwarded-Email")
} else {
req.Header["X-Forwarded-User"] = []string{session.User}
if session.Email != "" {
req.Header["X-Forwarded-Email"] = []string{session.Email}
} else {
req.Header.Del("X-Forwarded-Email")
}
}
if session.PreferredUsername != "" {
req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
} else {
req.Header.Del("X-Forwarded-Preferred-Username")
}
if len(session.Groups) > 0 {
for _, group := range session.Groups {
req.Header.Add("X-Forwarded-Groups", group)
}
} else {
req.Header.Del("X-Forwarded-Groups")
}
}
if p.SetXAuthRequest {
rw.Header().Set("X-Auth-Request-User", session.User)
if session.Email != "" {
rw.Header().Set("X-Auth-Request-Email", session.Email)
} else {
rw.Header().Del("X-Auth-Request-Email")
}
if session.PreferredUsername != "" {
rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
} else {
rw.Header().Del("X-Auth-Request-Preferred-Username")
}
if p.PassAccessToken {
if session.AccessToken != "" {
rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
} else {
rw.Header().Del("X-Auth-Request-Access-Token")
}
}
if len(session.Groups) > 0 {
for _, group := range session.Groups {
rw.Header().Add("X-Auth-Request-Groups", group)
}
} else {
rw.Header().Del("X-Auth-Request-Groups")
}
}
if p.PassAccessToken {
if session.AccessToken != "" {
req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
} else {
req.Header.Del("X-Forwarded-Access-Token")
}
}
if p.PassAuthorization {
if session.IDToken != "" {
req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
} else {
req.Header.Del("Authorization")
}
2018-01-27 12:14:19 +02:00
}
if p.SetBasicAuth {
switch {
case p.PreferEmailToUser && session.Email != "":
authVal := b64.StdEncoding.EncodeToString([]byte(session.Email + ":" + p.BasicAuthPassword))
rw.Header().Set("Authorization", "Basic "+authVal)
case session.User != "":
authVal := b64.StdEncoding.EncodeToString([]byte(session.User + ":" + p.BasicAuthPassword))
rw.Header().Set("Authorization", "Basic "+authVal)
default:
rw.Header().Del("Authorization")
}
}
if p.SetAuthorization {
if session.IDToken != "" {
rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
} else {
rw.Header().Del("Authorization")
}
2018-01-27 12:14:19 +02:00
}
if session.Email == "" {
rw.Header().Set("GAP-Auth", session.User)
} else {
rw.Header().Set("GAP-Auth", session.Email)
}
2012-12-11 04:59:23 +03:00
}
2020-09-23 03:54:32 +02:00
// stripAuthHeaders removes Auth headers for allowlisted routes from skipAuthRegex
func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
if p.PassBasicAuth {
req.Header.Del("X-Forwarded-User")
req.Header.Del("X-Forwarded-Groups")
req.Header.Del("X-Forwarded-Email")
req.Header.Del("X-Forwarded-Preferred-Username")
req.Header.Del("Authorization")
}
if p.PassUserHeaders {
req.Header.Del("X-Forwarded-User")
req.Header.Del("X-Forwarded-Groups")
req.Header.Del("X-Forwarded-Email")
req.Header.Del("X-Forwarded-Preferred-Username")
}
if p.PassAccessToken {
req.Header.Del("X-Forwarded-Access-Token")
}
if p.PassAuthorization {
req.Header.Del("Authorization")
}
}
// isAjax checks if a request is an ajax request
func isAjax(req *http.Request) bool {
acceptValues := req.Header.Values("Accept")
const ajaxReq = applicationJSON
for _, v := range acceptValues {
if v == ajaxReq {
return true
}
}
return false
}
2019-08-13 12:42:23 +02:00
// ErrorJSON returns the error code with an application/json mime type
func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
rw.Header().Set("Content-Type", applicationJSON)
rw.WriteHeader(code)
}
func (p *OAuthProxy) validateGroups(groups []string) bool {
if len(p.AllowedGroups) == 0 {
return true
}
allowedGroups := map[string]struct{}{}
for _, group := range p.AllowedGroups {
allowedGroups[group] = struct{}{}
}
for _, group := range groups {
if _, ok := allowedGroups[group]; ok {
return true
}
}
return false
}