git.iko.soy/typeable/oauth2-proxy
on 8cbf921 help sign in
typeable/oauth2-proxy/oauthproxy.go
1091 lines 35 kB history raw
1package main
3import (
4 "context"
5 b64 "encoding/base64"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "html/template"
10 "net"
11 "net/http"
12 "net/url"
13 "regexp"
14 "strings"
15 "time"
17 "github.com/coreos/go-oidc"
18 "github.com/justinas/alice"
19 ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
20 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
21 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
22 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
23 "github.com/oauth2-proxy/oauth2-proxy/pkg/authentication/basic"
24 "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
25 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
26 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
27 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
28 "github.com/oauth2-proxy/oauth2-proxy/pkg/middleware"
29 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
30 "github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
31 "github.com/oauth2-proxy/oauth2-proxy/pkg/util"
32 "github.com/oauth2-proxy/oauth2-proxy/providers"
35const (
36 httpScheme = "http"
37 httpsScheme = "https"
39 applicationJSON = "application/json"
42var (
43 // ErrNeedsLogin means the user should be redirected to the login page
44 ErrNeedsLogin = errors.New("redirect to login page")
46 // Used to check final redirects are not susceptible to open redirects.
47 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
48 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
51// OAuthProxy is the main authentication proxy
52type OAuthProxy struct {
53 CookieSeed string
54 CookieName string
55 CSRFCookieName string
56 CookieDomains []string
57 CookiePath string
58 CookieSecure bool
59 CookieHTTPOnly bool
60 CookieExpire time.Duration
61 CookieRefresh time.Duration
62 CookieSameSite string
63 Validator func(string) bool
65 RobotsPath string
66 SignInPath string
67 SignOutPath string
68 OAuthStartPath string
69 OAuthCallbackPath string
70 AuthOnlyPath string
71 UserInfoPath string
73 redirectURL *url.URL // the url to receive requests at
74 whitelistDomains []string
75 provider providers.Provider
76 providerNameOverride string
77 sessionStore sessionsapi.SessionStore
78 ProxyPrefix string
79 SignInMessage string
80 basicAuthValidator basic.Validator
81 displayHtpasswdForm bool
82 serveMux http.Handler
83 SetXAuthRequest bool
84 PassBasicAuth bool
85 SetBasicAuth bool
86 SkipProviderButton bool
87 PassUserHeaders bool
88 BasicAuthPassword string
89 PassAccessToken bool
90 SetAuthorization bool
91 PassAuthorization bool
92 PreferEmailToUser bool
93 skipAuthRegex []string
94 skipAuthPreflight bool
95 skipAuthStripHeaders bool
96 skipJwtBearerTokens bool
97 mainJwtBearerVerifier *oidc.IDTokenVerifier
98 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
99 compiledRegex []*regexp.Regexp
100 templates *template.Template
101 realClientIPParser ipapi.RealClientIPParser
102 trustedIPs *ip.NetSet
103 Banner string
104 Footer string
105 AllowedGroups []string
107 sessionChain alice.Chain
110// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
111func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
112 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
113 if err != nil {
114 return nil, fmt.Errorf("error initialising session store: %v", err)
115 }
117 templates := loadTemplates(opts.CustomTemplatesDir)
118 proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
119 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
120 if err != nil {
121 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
122 }
124 for _, u := range opts.GetCompiledRegex() {
125 logger.Printf("compiled skip-auth-regex => %q", u)
126 }
128 if opts.SkipJwtBearerTokens {
129 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
130 for _, issuer := range opts.ExtraJwtIssuers {
131 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
132 }
133 }
134 redirectURL := opts.GetRedirectURL()
135 if redirectURL.Path == "" {
136 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
137 }
139 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
140 refresh := "disabled"
141 if opts.Cookie.Refresh != time.Duration(0) {
142 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
143 }
145 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)
147 trustedIPs := ip.NewNetSet()
148 for _, ipStr := range opts.TrustedIPs {
149 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
150 trustedIPs.AddIPNet(*ipNet)
151 } else {
152 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
153 }
154 }
156 var basicAuthValidator basic.Validator
157 if opts.HtpasswdFile != "" {
158 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
159 var err error
160 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
161 if err != nil {
162 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
163 }
164 }
166 sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
168 return &OAuthProxy{
169 CookieName: opts.Cookie.Name,
170 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
171 CookieSeed: opts.Cookie.Secret,
172 CookieDomains: opts.Cookie.Domains,
173 CookiePath: opts.Cookie.Path,
174 CookieSecure: opts.Cookie.Secure,
175 CookieHTTPOnly: opts.Cookie.HTTPOnly,
176 CookieExpire: opts.Cookie.Expire,
177 CookieRefresh: opts.Cookie.Refresh,
178 CookieSameSite: opts.Cookie.SameSite,
179 Validator: validator,
181 RobotsPath: "/robots.txt",
182 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
183 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
184 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
185 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
186 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
187 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
189 ProxyPrefix: opts.ProxyPrefix,
190 provider: opts.GetProvider(),
191 providerNameOverride: opts.ProviderName,
192 sessionStore: sessionStore,
193 serveMux: upstreamProxy,
194 redirectURL: redirectURL,
195 whitelistDomains: opts.WhitelistDomains,
196 skipAuthRegex: opts.SkipAuthRegex,
197 skipAuthPreflight: opts.SkipAuthPreflight,
198 skipAuthStripHeaders: opts.SkipAuthStripHeaders,
199 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
200 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
201 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
202 compiledRegex: opts.GetCompiledRegex(),
203 realClientIPParser: opts.GetRealClientIPParser(),
204 SetXAuthRequest: opts.SetXAuthRequest,
205 PassBasicAuth: opts.PassBasicAuth,
206 SetBasicAuth: opts.SetBasicAuth,
207 PassUserHeaders: opts.PassUserHeaders,
208 BasicAuthPassword: opts.BasicAuthPassword,
209 PassAccessToken: opts.PassAccessToken,
210 SetAuthorization: opts.SetAuthorization,
211 PassAuthorization: opts.PassAuthorization,
212 PreferEmailToUser: opts.PreferEmailToUser,
213 SkipProviderButton: opts.SkipProviderButton,
214 templates: templates,
215 trustedIPs: trustedIPs,
216 Banner: opts.Banner,
217 Footer: opts.Footer,
218 SignInMessage: buildSignInMessage(opts),
219 AllowedGroups: opts.AllowedGroups,
221 basicAuthValidator: basicAuthValidator,
222 displayHtpasswdForm: basicAuthValidator != nil,
223 sessionChain: sessionChain,
224 }, nil
227func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
228 chain := alice.New(middleware.NewScope())
230 if opts.SkipJwtBearerTokens {
231 sessionLoaders := []middlewareapi.TokenToSessionLoader{}
232 if opts.GetOIDCVerifier() != nil {
233 sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
234 Verifier: opts.GetOIDCVerifier(),
235 TokenToSession: opts.GetProvider().CreateSessionStateFromBearerToken,
236 })
237 }
239 for _, verifier := range opts.GetJWTBearerVerifiers() {
240 sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
241 Verifier: verifier,
242 })
243 }
245 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
246 }
248 if validator != nil {
249 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
250 }
252 chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
253 SessionStore: sessionStore,
254 RefreshPeriod: opts.Cookie.Refresh,
255 RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
256 ValidateSessionState: opts.GetProvider().ValidateSessionState,
257 }))
259 return chain
262func buildSignInMessage(opts *options.Options) string {
263 var msg string
264 if len(opts.Banner) >= 1 {
265 if opts.Banner == "-" {
266 msg = ""
267 } else {
268 msg = opts.Banner
269 }
270 } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
271 if len(opts.EmailDomains) > 1 {
272 msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
273 } else if opts.EmailDomains[0] != "*" {
274 msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
275 }
276 }
277 return msg
280// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
281// redirect clients to once authenticated
282func (p *OAuthProxy) GetRedirectURI(host string) string {
283 // default to the request Host if not set
284 if p.redirectURL.Host != "" {
285 return p.redirectURL.String()
286 }
287 u := *p.redirectURL
288 if u.Scheme == "" {
289 if p.CookieSecure {
290 u.Scheme = httpsScheme
291 } else {
292 u.Scheme = httpScheme
293 }
294 }
295 u.Host = host
296 return u.String()
299func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
300 if code == "" {
301 return nil, errors.New("missing code")
302 }
303 redirectURI := p.GetRedirectURI(host)
304 s, err := p.provider.Redeem(ctx, redirectURI, code)
305 if err != nil {
306 return nil, err
307 }
309 if s.Email == "" {
310 s.Email, err = p.provider.GetEmailAddress(ctx, s)
311 if err != nil && err.Error() != "not implemented" {
312 return nil, err
313 }
314 }
316 if s.User == "" {
317 s.User, err = p.provider.GetUserName(ctx, s)
318 if err != nil && err.Error() != "not implemented" {
319 return nil, err
320 }
321 }
323 return s, nil
326// MakeCSRFCookie creates a cookie for CSRF
327func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
328 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
331func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
332 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
334 if cookieDomain != "" {
335 domain := util.GetRequestHost(req)
336 if h, _, err := net.SplitHostPort(domain); err == nil {
337 domain = h
338 }
339 if !strings.HasSuffix(domain, cookieDomain) {
340 logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
341 }
342 }
344 return &http.Cookie{
345 Name: name,
346 Value: value,
347 Path: p.CookiePath,
348 Domain: cookieDomain,
349 HttpOnly: p.CookieHTTPOnly,
350 Secure: p.CookieSecure,
351 Expires: now.Add(expiration),
352 SameSite: cookies.ParseSameSite(p.CookieSameSite),
353 }
356// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
357// session
358func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
359 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
362// SetCSRFCookie adds a CSRF cookie to the response
363func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
364 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
367// ClearSessionCookie creates a cookie to unset the user's authentication cookie
368// stored in the user's session
369func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
370 return p.sessionStore.Clear(rw, req)
373// LoadCookiedSession reads the user's authentication details from the request
374func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
375 return p.sessionStore.Load(req)
378// SaveSession creates a new session cookie value and sets this on the response
379func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
380 return p.sessionStore.Save(rw, req, s)
383// RobotsTxt disallows scraping pages from the OAuthProxy
384func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
385 _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
386 if err != nil {
387 logger.Printf("Error writing robots.txt: %v", err)
388 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
389 return
390 }
391 rw.WriteHeader(http.StatusOK)
394// ErrorPage writes an error response
395func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
396 rw.WriteHeader(code)
397 t := struct {
398 Title string
399 Message string
400 ProxyPrefix string
401 }{
402 Title: fmt.Sprintf("%d %s", code, title),
403 Message: message,
404 ProxyPrefix: p.ProxyPrefix,
405 }
406 err := p.templates.ExecuteTemplate(rw, "error.html", t)
407 if err != nil {
408 logger.Printf("Error rendering error.html template: %v", err)
409 http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
410 }
413// SignInPage writes the sing in template to the response
414func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
415 prepareNoCache(rw)
416 err := p.ClearSessionCookie(rw, req)
417 if err != nil {
418 logger.Printf("Error clearing session cookie: %v", err)
419 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
420 return
421 }
422 rw.WriteHeader(code)
424 redirectURL, err := p.GetRedirect(req)
425 if err != nil {
426 logger.Errorf("Error obtaining redirect: %v", err)
427 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
428 return
429 }
431 if redirectURL == p.SignInPath {
432 redirectURL = "/"
433 }
435 // We allow unescaped template.HTML since it is user configured options
436 /* #nosec G203 */
437 t := struct {
438 ProviderName string
439 SignInMessage template.HTML
440 CustomLogin bool
441 Redirect string
442 Version string
443 ProxyPrefix string
444 Footer template.HTML
445 }{
446 ProviderName: p.provider.Data().ProviderName,
447 SignInMessage: template.HTML(p.SignInMessage),
448 CustomLogin: p.displayHtpasswdForm,
449 Redirect: redirectURL,
450 Version: VERSION,
451 ProxyPrefix: p.ProxyPrefix,
452 Footer: template.HTML(p.Footer),
453 }
454 if p.providerNameOverride != "" {
455 t.ProviderName = p.providerNameOverride
456 }
457 err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
458 if err != nil {
459 logger.Printf("Error rendering sign_in.html template: %v", err)
460 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
461 }
464// ManualSignIn handles basic auth logins to the proxy
465func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
466 if req.Method != "POST" || p.basicAuthValidator == nil {
467 return "", false
468 }
469 user := req.FormValue("username")
470 passwd := req.FormValue("password")
471 if user == "" {
472 return "", false
473 }
474 // check auth
475 if p.basicAuthValidator.Validate(user, passwd) {
476 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
477 return user, true
478 }
479 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
480 return "", false
483// GetRedirect reads the query parameter to get the URL to redirect clients to
484// once authenticated with the OAuthProxy
485func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
486 err = req.ParseForm()
487 if err != nil {
488 return
489 }
491 redirect = req.Header.Get("X-Auth-Request-Redirect")
492 if req.Form.Get("rd") != "" {
493 redirect = req.Form.Get("rd")
494 }
495 if !p.IsValidRedirect(redirect) {
496 // Use RequestURI to preserve ?query
497 redirect = req.URL.RequestURI()
498 if strings.HasPrefix(redirect, p.ProxyPrefix) {
499 redirect = "/"
500 }
501 }
503 return
506// splitHostPort separates host and port. If the port is not valid, it returns
507// the entire input as host, and it doesn't check the validity of the host.
508// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
509// *** taken from net/url, modified validOptionalPort() to accept ":*"
510func splitHostPort(hostport string) (host, port string) {
511 host = hostport
513 colon := strings.LastIndexByte(host, ':')
514 if colon != -1 && validOptionalPort(host[colon:]) {
515 host, port = host[:colon], host[colon+1:]
516 }
518 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
519 host = host[1 : len(host)-1]
520 }
522 return
525// validOptionalPort reports whether port is either an empty string
526// or matches /^:\d*$/
527// *** taken from net/url, modified to accept ":*"
528func validOptionalPort(port string) bool {
529 if port == "" || port == ":*" {
530 return true
531 }
532 if port[0] != ':' {
533 return false
534 }
535 for _, b := range port[1:] {
536 if b < '0' || b > '9' {
537 return false
538 }
539 }
540 return true
543// IsValidRedirect checks whether the redirect URL is whitelisted
544func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
545 switch {
546 case redirect == "":
547 // The user didn't specify a redirect, should fallback to `/`
548 return false
549 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
550 return true
551 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
552 redirectURL, err := url.Parse(redirect)
553 if err != nil {
554 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
555 return false
556 }
557 redirectHostname := redirectURL.Hostname()
559 for _, domain := range p.whitelistDomains {
560 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
561 if domainHostname == "" {
562 continue
563 }
565 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
566 // the domain names match, now validate the ports
567 // if the whitelisted domain's port is '*', allow all ports
568 // if the whitelisted domain contains a specific port, only allow that port
569 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
570 redirectPort := redirectURL.Port()
571 if (domainPort == "*") ||
572 (domainPort == redirectPort) ||
573 (domainPort == "" && redirectPort == "") {
574 return true
575 }
576 }
577 }
579 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
580 return false
581 default:
582 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
583 return false
584 }
587// IsWhitelistedRequest is used to check if auth should be skipped for this request
588func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
589 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
590 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) || p.IsTrustedIP(req)
593// IsWhitelistedPath is used to check if the request path is allowed without auth
594func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
595 for _, u := range p.compiledRegex {
596 if u.MatchString(path) {
597 return true
598 }
599 }
600 return false
603// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
604var noCacheHeaders = map[string]string{
605 "Expires": time.Unix(0, 0).Format(time.RFC1123),
606 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
607 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
610// prepareNoCache prepares headers for preventing browser caching.
611func prepareNoCache(w http.ResponseWriter) {
612 // Set NoCache headers
613 for k, v := range noCacheHeaders {
614 w.Header().Set(k, v)
615 }
618// IsTrustedIP is used to check if a request comes from a trusted client IP address.
619func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
620 if p.trustedIPs == nil {
621 return false
622 }
624 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
625 if err != nil {
626 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
627 // Possibly spoofed X-Real-IP header
628 return false
629 }
631 if remoteAddr == nil {
632 return false
633 }
635 return p.trustedIPs.Has(remoteAddr)
638func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
639 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
640 prepareNoCache(rw)
641 }
643 switch path := req.URL.Path; {
644 case path == p.RobotsPath:
645 p.RobotsTxt(rw)
646 case p.IsWhitelistedRequest(req):
647 p.SkipAuthProxy(rw, req)
648 case path == p.SignInPath:
649 p.SignIn(rw, req)
650 case path == p.SignOutPath:
651 p.SignOut(rw, req)
652 case path == p.OAuthStartPath:
653 p.OAuthStart(rw, req)
654 case path == p.OAuthCallbackPath:
655 p.OAuthCallback(rw, req)
656 case path == p.AuthOnlyPath:
657 p.AuthenticateOnly(rw, req)
658 case path == p.UserInfoPath:
659 p.UserInfo(rw, req)
660 default:
661 p.Proxy(rw, req)
662 }
665// SignIn serves a page prompting users to sign in
666func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
667 redirect, err := p.GetRedirect(req)
668 if err != nil {
669 logger.Errorf("Error obtaining redirect: %v", err)
670 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
671 return
672 }
674 user, ok := p.ManualSignIn(req)
675 if ok {
676 session := &sessionsapi.SessionState{User: user}
677 err = p.SaveSession(rw, req, session)
678 if err != nil {
679 logger.Printf("Error saving session: %v", err)
680 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
681 return
682 }
683 http.Redirect(rw, req, redirect, http.StatusFound)
684 } else {
685 if p.SkipProviderButton {
686 p.OAuthStart(rw, req)
687 } else {
688 p.SignInPage(rw, req, http.StatusOK)
689 }
690 }
693//UserInfo endpoint outputs session email and preferred username in JSON format
694func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
696 session, err := p.getAuthenticatedSession(rw, req)
697 if err != nil {
698 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
699 return
700 }
701 userInfo := struct {
702 Email string `json:"email"`
703 PreferredUsername string `json:"preferredUsername,omitempty"`
704 }{
705 Email: session.Email,
706 PreferredUsername: session.PreferredUsername,
707 }
708 rw.Header().Set("Content-Type", "application/json")
709 rw.WriteHeader(http.StatusOK)
710 err = json.NewEncoder(rw).Encode(userInfo)
711 if err != nil {
712 logger.Printf("Error encoding user info: %v", err)
713 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
714 }
717// SignOut sends a response to clear the authentication cookie
718func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
719 redirect, err := p.GetRedirect(req)
720 if err != nil {
721 logger.Errorf("Error obtaining redirect: %v", err)
722 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
723 return
724 }
725 err = p.ClearSessionCookie(rw, req)
726 if err != nil {
727 logger.Errorf("Error clearing session cookie: %v", err)
728 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
729 return
730 }
731 http.Redirect(rw, req, redirect, http.StatusFound)
734// OAuthStart starts the OAuth2 authentication flow
735func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
736 prepareNoCache(rw)
737 nonce, err := encryption.Nonce()
738 if err != nil {
739 logger.Errorf("Error obtaining nonce: %v", err)
740 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
741 return
742 }
743 p.SetCSRFCookie(rw, req, nonce)
744 redirect, err := p.GetRedirect(req)
745 if err != nil {
746 logger.Errorf("Error obtaining redirect: %v", err)
747 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
748 return
749 }
750 redirectURI := p.GetRedirectURI(util.GetRequestHost(req))
751 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
754// OAuthCallback is the OAuth2 authentication flow callback that finishes the
755// OAuth2 authentication flow
756func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
757 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
759 // finish the oauth cycle
760 err := req.ParseForm()
761 if err != nil {
762 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
763 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
764 return
765 }
766 errorString := req.Form.Get("error")
767 if errorString != "" {
768 logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
769 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
770 return
771 }
773 session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
774 if err != nil {
775 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
776 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
777 return
778 }
780 s := strings.SplitN(req.Form.Get("state"), ":", 2)
781 if len(s) != 2 {
782 logger.Error("Error while parsing OAuth2 state: invalid length")
783 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
784 return
785 }
786 nonce := s[0]
787 redirect := s[1]
788 c, err := req.Cookie(p.CSRFCookieName)
789 if err != nil {
790 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
791 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
792 return
793 }
794 p.ClearCSRFCookie(rw, req)
795 if c.Value != nonce {
796 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
797 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
798 return
799 }
801 if !p.IsValidRedirect(redirect) {
802 redirect = "/"
803 }
805 // set cookie, or deny
806 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
807 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
808 err := p.SaveSession(rw, req, session)
809 if err != nil {
810 logger.Printf("Error saving session state for %s: %v", remoteAddr, err)
811 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
812 return
813 }
814 http.Redirect(rw, req, redirect, http.StatusFound)
815 } else {
816 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
817 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
818 }
821// AuthenticateOnly checks whether the user is currently logged in
822func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
823 session, err := p.getAuthenticatedSession(rw, req)
824 if err != nil {
825 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
826 return
827 }
829 // we are authenticated
830 p.addHeadersForProxying(rw, req, session)
831 rw.WriteHeader(http.StatusAccepted)
834// SkipAuthProxy proxies whitelisted requests and skips authentication
835func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
836 if p.skipAuthStripHeaders {
837 p.stripAuthHeaders(req)
838 }
839 p.serveMux.ServeHTTP(rw, req)
842// Proxy proxies the user request if the user is authenticated else it prompts
843// them to authenticate
844func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
845 session, err := p.getAuthenticatedSession(rw, req)
846 switch err {
847 case nil:
848 // we are authenticated
849 p.addHeadersForProxying(rw, req, session)
850 p.serveMux.ServeHTTP(rw, req)
852 case ErrNeedsLogin:
853 // we need to send the user to a login screen
854 if isAjax(req) {
855 // no point redirecting an AJAX request
856 p.ErrorJSON(rw, http.StatusUnauthorized)
857 return
858 }
860 if p.SkipProviderButton {
861 p.OAuthStart(rw, req)
862 } else {
863 p.SignInPage(rw, req, http.StatusForbidden)
864 }
866 default:
867 // unknown error
868 logger.Errorf("Unexpected internal error: %v", err)
869 p.ErrorPage(rw, http.StatusInternalServerError,
870 "Internal Error", "Internal Error")
871 }
875// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
876// Returns nil, ErrNeedsLogin if user needs to login.
877// Set-Cookie headers may be set on the response as a side-effect of calling this method.
878func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
879 var session *sessionsapi.SessionState
881 getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
882 session = middleware.GetRequestScope(req).Session
883 }))
884 getSession.ServeHTTP(rw, req)
886 if session == nil {
887 return nil, ErrNeedsLogin
888 }
890 invalidEmail := session != nil && session.Email != "" && !p.Validator(session.Email)
891 invalidGroups := session != nil && !p.validateGroups(session.Groups)
893 if invalidEmail || invalidGroups {
894 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
895 // Invalid session, clear it
896 err := p.ClearSessionCookie(rw, req)
897 if err != nil {
898 logger.Printf("Error clearing session cookie: %v", err)
899 }
900 return nil, ErrNeedsLogin
901 }
903 return session, nil
906// addHeadersForProxying adds the appropriate headers the request / response for proxying
907func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
908 if p.PassBasicAuth {
909 if p.PreferEmailToUser && session.Email != "" {
910 req.SetBasicAuth(session.Email, p.BasicAuthPassword)
911 req.Header["X-Forwarded-User"] = []string{session.Email}
912 req.Header.Del("X-Forwarded-Email")
913 } else {
914 req.SetBasicAuth(session.User, p.BasicAuthPassword)
915 req.Header["X-Forwarded-User"] = []string{session.User}
916 if session.Email != "" {
917 req.Header["X-Forwarded-Email"] = []string{session.Email}
918 } else {
919 req.Header.Del("X-Forwarded-Email")
920 }
921 }
922 if session.PreferredUsername != "" {
923 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
924 } else {
925 req.Header.Del("X-Forwarded-Preferred-Username")
926 }
927 }
929 if p.PassUserHeaders {
930 if p.PreferEmailToUser && session.Email != "" {
931 req.Header["X-Forwarded-User"] = []string{session.Email}
932 req.Header.Del("X-Forwarded-Email")
933 } else {
934 req.Header["X-Forwarded-User"] = []string{session.User}
935 if session.Email != "" {
936 req.Header["X-Forwarded-Email"] = []string{session.Email}
937 } else {
938 req.Header.Del("X-Forwarded-Email")
939 }
940 }
942 if session.PreferredUsername != "" {
943 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
944 } else {
945 req.Header.Del("X-Forwarded-Preferred-Username")
946 }
948 if len(session.Groups) > 0 {
949 for _, group := range session.Groups {
950 req.Header.Add("X-Forwarded-Groups", group)
951 }
952 } else {
953 req.Header.Del("X-Forwarded-Groups")
954 }
955 }
957 if p.SetXAuthRequest {
958 rw.Header().Set("X-Auth-Request-User", session.User)
959 if session.Email != "" {
960 rw.Header().Set("X-Auth-Request-Email", session.Email)
961 } else {
962 rw.Header().Del("X-Auth-Request-Email")
963 }
964 if session.PreferredUsername != "" {
965 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
966 } else {
967 rw.Header().Del("X-Auth-Request-Preferred-Username")
968 }
970 if p.PassAccessToken {
971 if session.AccessToken != "" {
972 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
973 } else {
974 rw.Header().Del("X-Auth-Request-Access-Token")
975 }
976 }
978 if len(session.Groups) > 0 {
979 for _, group := range session.Groups {
980 rw.Header().Add("X-Auth-Request-Groups", group)
981 }
982 } else {
983 rw.Header().Del("X-Auth-Request-Groups")
984 }
985 }
987 if p.PassAccessToken {
988 if session.AccessToken != "" {
989 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
990 } else {
991 req.Header.Del("X-Forwarded-Access-Token")
992 }
993 }
995 if p.PassAuthorization {
996 if session.IDToken != "" {
997 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
998 } else {
999 req.Header.Del("Authorization")
1002 if p.SetBasicAuth {
1003 switch {
1004 case p.PreferEmailToUser && session.Email != "":
1005 authVal := b64.StdEncoding.EncodeToString([]byte(session.Email + ":" + p.BasicAuthPassword))
1006 rw.Header().Set("Authorization", "Basic "+authVal)
1007 case session.User != "":
1008 authVal := b64.StdEncoding.EncodeToString([]byte(session.User + ":" + p.BasicAuthPassword))
1009 rw.Header().Set("Authorization", "Basic "+authVal)
1010 default:
1011 rw.Header().Del("Authorization")
1014 if p.SetAuthorization {
1015 if session.IDToken != "" {
1016 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
1017 } else {
1018 rw.Header().Del("Authorization")
1022 if session.Email == "" {
1023 rw.Header().Set("GAP-Auth", session.User)
1024 } else {
1025 rw.Header().Set("GAP-Auth", session.Email)
1029// stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex
1030func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
1031 if p.PassBasicAuth {
1032 req.Header.Del("X-Forwarded-User")
1033 req.Header.Del("X-Forwarded-Groups")
1034 req.Header.Del("X-Forwarded-Email")
1035 req.Header.Del("X-Forwarded-Preferred-Username")
1036 req.Header.Del("Authorization")
1039 if p.PassUserHeaders {
1040 req.Header.Del("X-Forwarded-User")
1041 req.Header.Del("X-Forwarded-Groups")
1042 req.Header.Del("X-Forwarded-Email")
1043 req.Header.Del("X-Forwarded-Preferred-Username")
1046 if p.PassAccessToken {
1047 req.Header.Del("X-Forwarded-Access-Token")
1050 if p.PassAuthorization {
1051 req.Header.Del("Authorization")
1055// isAjax checks if a request is an ajax request
1056func isAjax(req *http.Request) bool {
1057 acceptValues := req.Header.Values("Accept")
1058 const ajaxReq = applicationJSON
1059 for _, v := range acceptValues {
1060 if v == ajaxReq {
1061 return true
1064 return false
1067// ErrorJSON returns the error code with an application/json mime type
1068func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1069 rw.Header().Set("Content-Type", applicationJSON)
1070 rw.WriteHeader(code)
1073func (p *OAuthProxy) validateGroups(groups []string) bool {
1074 if len(p.AllowedGroups) == 0 {
1075 return true
1078 allowedGroups := map[string]struct{}{}
1080 for _, group := range p.AllowedGroups {
1081 allowedGroups[group] = struct{}{}
1084 for _, group := range groups {
1085 if _, ok := allowedGroups[group]; ok {
1086 return true
1090 return false