git.iko.soy/typeable/oauth2-proxy
on 71dc702 help sign in
typeable/oauth2-proxy/oauthproxy.go
1117 lines 36 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 ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
19 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
20 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
21 "github.com/oauth2-proxy/oauth2-proxy/pkg/authentication/basic"
22 "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
23 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
24 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
25 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
26 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
27 "github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
28 "github.com/oauth2-proxy/oauth2-proxy/providers"
31const (
32 httpScheme = "http"
33 httpsScheme = "https"
35 applicationJSON = "application/json"
38var (
39 // ErrNeedsLogin means the user should be redirected to the login page
40 ErrNeedsLogin = errors.New("redirect to login page")
42 // Used to check final redirects are not susceptible to open redirects.
43 // Matches //, /\ and both of these with whitespace in between (eg / / or / \).
44 invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
47// OAuthProxy is the main authentication proxy
48type OAuthProxy struct {
49 CookieSeed string
50 CookieName string
51 CSRFCookieName string
52 CookieDomains []string
53 CookiePath string
54 CookieSecure bool
55 CookieHTTPOnly bool
56 CookieExpire time.Duration
57 CookieRefresh time.Duration
58 CookieSameSite string
59 Validator func(string) bool
61 RobotsPath string
62 SignInPath string
63 SignOutPath string
64 OAuthStartPath string
65 OAuthCallbackPath string
66 AuthOnlyPath string
67 UserInfoPath string
69 redirectURL *url.URL // the url to receive requests at
70 whitelistDomains []string
71 provider providers.Provider
72 providerNameOverride string
73 sessionStore sessionsapi.SessionStore
74 ProxyPrefix string
75 SignInMessage string
76 basicAuthValidator basic.Validator
77 displayHtpasswdForm bool
78 serveMux http.Handler
79 SetXAuthRequest bool
80 PassBasicAuth bool
81 SetBasicAuth bool
82 SkipProviderButton bool
83 PassUserHeaders bool
84 BasicAuthPassword string
85 PassAccessToken bool
86 SetAuthorization bool
87 PassAuthorization bool
88 PreferEmailToUser bool
89 skipAuthRegex []string
90 skipAuthPreflight bool
91 skipAuthStripHeaders bool
92 skipJwtBearerTokens bool
93 mainJwtBearerVerifier *oidc.IDTokenVerifier
94 extraJwtBearerVerifiers []*oidc.IDTokenVerifier
95 compiledRegex []*regexp.Regexp
96 templates *template.Template
97 realClientIPParser ipapi.RealClientIPParser
98 trustedIPs *ip.NetSet
99 Banner string
100 Footer string
103// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
104func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) {
105 sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
106 if err != nil {
107 return nil, fmt.Errorf("error initialising session store: %v", err)
108 }
110 templates := loadTemplates(opts.CustomTemplatesDir)
111 proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
112 upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
113 if err != nil {
114 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
115 }
117 for _, u := range opts.GetCompiledRegex() {
118 logger.Printf("compiled skip-auth-regex => %q", u)
119 }
121 if opts.SkipJwtBearerTokens {
122 logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
123 for _, issuer := range opts.ExtraJwtIssuers {
124 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
125 }
126 }
127 redirectURL := opts.GetRedirectURL()
128 if redirectURL.Path == "" {
129 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
130 }
132 logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
133 refresh := "disabled"
134 if opts.Cookie.Refresh != time.Duration(0) {
135 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
136 }
138 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)
140 trustedIPs := ip.NewNetSet()
141 for _, ipStr := range opts.TrustedIPs {
142 if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
143 trustedIPs.AddIPNet(*ipNet)
144 } else {
145 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
146 }
147 }
149 var basicAuthValidator basic.Validator
150 if opts.HtpasswdFile != "" {
151 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
152 var err error
153 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
154 if err != nil {
155 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
156 }
157 }
159 return &OAuthProxy{
160 CookieName: opts.Cookie.Name,
161 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
162 CookieSeed: opts.Cookie.Secret,
163 CookieDomains: opts.Cookie.Domains,
164 CookiePath: opts.Cookie.Path,
165 CookieSecure: opts.Cookie.Secure,
166 CookieHTTPOnly: opts.Cookie.HTTPOnly,
167 CookieExpire: opts.Cookie.Expire,
168 CookieRefresh: opts.Cookie.Refresh,
169 CookieSameSite: opts.Cookie.SameSite,
170 Validator: validator,
172 RobotsPath: "/robots.txt",
173 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
174 SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
175 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
176 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
177 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
178 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
180 ProxyPrefix: opts.ProxyPrefix,
181 provider: opts.GetProvider(),
182 providerNameOverride: opts.ProviderName,
183 sessionStore: sessionStore,
184 serveMux: upstreamProxy,
185 redirectURL: redirectURL,
186 whitelistDomains: opts.WhitelistDomains,
187 skipAuthRegex: opts.SkipAuthRegex,
188 skipAuthPreflight: opts.SkipAuthPreflight,
189 skipAuthStripHeaders: opts.SkipAuthStripHeaders,
190 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
191 mainJwtBearerVerifier: opts.GetOIDCVerifier(),
192 extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
193 compiledRegex: opts.GetCompiledRegex(),
194 realClientIPParser: opts.GetRealClientIPParser(),
195 SetXAuthRequest: opts.SetXAuthRequest,
196 PassBasicAuth: opts.PassBasicAuth,
197 SetBasicAuth: opts.SetBasicAuth,
198 PassUserHeaders: opts.PassUserHeaders,
199 BasicAuthPassword: opts.BasicAuthPassword,
200 PassAccessToken: opts.PassAccessToken,
201 SetAuthorization: opts.SetAuthorization,
202 PassAuthorization: opts.PassAuthorization,
203 PreferEmailToUser: opts.PreferEmailToUser,
204 SkipProviderButton: opts.SkipProviderButton,
205 templates: templates,
206 trustedIPs: trustedIPs,
207 Banner: opts.Banner,
208 Footer: opts.Footer,
210 basicAuthValidator: basicAuthValidator,
211 displayHtpasswdForm: basicAuthValidator != nil,
212 }, nil
215// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
216// redirect clients to once authenticated
217func (p *OAuthProxy) GetRedirectURI(host string) string {
218 // default to the request Host if not set
219 if p.redirectURL.Host != "" {
220 return p.redirectURL.String()
221 }
222 u := *p.redirectURL
223 if u.Scheme == "" {
224 if p.CookieSecure {
225 u.Scheme = httpsScheme
226 } else {
227 u.Scheme = httpScheme
228 }
229 }
230 u.Host = host
231 return u.String()
234func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) {
235 if code == "" {
236 return nil, errors.New("missing code")
237 }
238 redirectURI := p.GetRedirectURI(host)
239 s, err = p.provider.Redeem(ctx, redirectURI, code)
240 if err != nil {
241 return
242 }
244 if s.Email == "" {
245 s.Email, err = p.provider.GetEmailAddress(ctx, s)
246 }
248 if s.PreferredUsername == "" {
249 s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s)
250 if err != nil && err.Error() == "not implemented" {
251 err = nil
252 }
253 }
255 if s.User == "" {
256 s.User, err = p.provider.GetUserName(ctx, s)
257 if err != nil && err.Error() == "not implemented" {
258 err = nil
259 }
260 }
261 return
264// MakeCSRFCookie creates a cookie for CSRF
265func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
266 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
269func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
270 cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
272 if cookieDomain != "" {
273 domain := cookies.GetRequestHost(req)
274 if h, _, err := net.SplitHostPort(domain); err == nil {
275 domain = h
276 }
277 if !strings.HasSuffix(domain, cookieDomain) {
278 logger.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
279 }
280 }
282 return &http.Cookie{
283 Name: name,
284 Value: value,
285 Path: p.CookiePath,
286 Domain: cookieDomain,
287 HttpOnly: p.CookieHTTPOnly,
288 Secure: p.CookieSecure,
289 Expires: now.Add(expiration),
290 SameSite: cookies.ParseSameSite(p.CookieSameSite),
291 }
294// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
295// session
296func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
297 http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
300// SetCSRFCookie adds a CSRF cookie to the response
301func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
302 http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
305// ClearSessionCookie creates a cookie to unset the user's authentication cookie
306// stored in the user's session
307func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
308 return p.sessionStore.Clear(rw, req)
311// LoadCookiedSession reads the user's authentication details from the request
312func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
313 return p.sessionStore.Load(req)
316// SaveSession creates a new session cookie value and sets this on the response
317func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
318 return p.sessionStore.Save(rw, req, s)
321// RobotsTxt disallows scraping pages from the OAuthProxy
322func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
323 rw.WriteHeader(http.StatusOK)
324 fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
327// ErrorPage writes an error response
328func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
329 rw.WriteHeader(code)
330 t := struct {
331 Title string
332 Message string
333 ProxyPrefix string
334 }{
335 Title: fmt.Sprintf("%d %s", code, title),
336 Message: message,
337 ProxyPrefix: p.ProxyPrefix,
338 }
339 p.templates.ExecuteTemplate(rw, "error.html", t)
342// SignInPage writes the sing in template to the response
343func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
344 prepareNoCache(rw)
345 p.ClearSessionCookie(rw, req)
346 rw.WriteHeader(code)
348 redirectURL, err := p.GetRedirect(req)
349 if err != nil {
350 logger.Printf("Error obtaining redirect: %s", err.Error())
351 p.ErrorPage(rw, 500, "Internal Error", err.Error())
352 return
353 }
355 if redirectURL == p.SignInPath {
356 redirectURL = "/"
357 }
359 t := struct {
360 ProviderName string
361 SignInMessage template.HTML
362 CustomLogin bool
363 Redirect string
364 Version string
365 ProxyPrefix string
366 Footer template.HTML
367 }{
368 ProviderName: p.provider.Data().ProviderName,
369 SignInMessage: template.HTML(p.SignInMessage),
370 CustomLogin: p.displayHtpasswdForm,
371 Redirect: redirectURL,
372 Version: VERSION,
373 ProxyPrefix: p.ProxyPrefix,
374 Footer: template.HTML(p.Footer),
375 }
376 if p.providerNameOverride != "" {
377 t.ProviderName = p.providerNameOverride
378 }
379 p.templates.ExecuteTemplate(rw, "sign_in.html", t)
382// ManualSignIn handles basic auth logins to the proxy
383func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
384 if req.Method != "POST" || p.basicAuthValidator == nil {
385 return "", false
386 }
387 user := req.FormValue("username")
388 passwd := req.FormValue("password")
389 if user == "" {
390 return "", false
391 }
392 // check auth
393 if p.basicAuthValidator.Validate(user, passwd) {
394 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
395 return user, true
396 }
397 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
398 return "", false
401// GetRedirect reads the query parameter to get the URL to redirect clients to
402// once authenticated with the OAuthProxy
403func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
404 err = req.ParseForm()
405 if err != nil {
406 return
407 }
409 redirect = req.Header.Get("X-Auth-Request-Redirect")
410 if req.Form.Get("rd") != "" {
411 redirect = req.Form.Get("rd")
412 }
413 if !p.IsValidRedirect(redirect) {
414 redirect = req.URL.Path
415 if strings.HasPrefix(redirect, p.ProxyPrefix) {
416 redirect = "/"
417 }
418 }
420 return
423// splitHostPort separates host and port. If the port is not valid, it returns
424// the entire input as host, and it doesn't check the validity of the host.
425// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
426// *** taken from net/url, modified validOptionalPort() to accept ":*"
427func splitHostPort(hostport string) (host, port string) {
428 host = hostport
430 colon := strings.LastIndexByte(host, ':')
431 if colon != -1 && validOptionalPort(host[colon:]) {
432 host, port = host[:colon], host[colon+1:]
433 }
435 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
436 host = host[1 : len(host)-1]
437 }
439 return
442// validOptionalPort reports whether port is either an empty string
443// or matches /^:\d*$/
444// *** taken from net/url, modified to accept ":*"
445func validOptionalPort(port string) bool {
446 if port == "" || port == ":*" {
447 return true
448 }
449 if port[0] != ':' {
450 return false
451 }
452 for _, b := range port[1:] {
453 if b < '0' || b > '9' {
454 return false
455 }
456 }
457 return true
460// IsValidRedirect checks whether the redirect URL is whitelisted
461func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
462 switch {
463 case redirect == "":
464 // The user didn't specify a redirect, should fallback to `/`
465 return false
466 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
467 return true
468 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
469 redirectURL, err := url.Parse(redirect)
470 if err != nil {
471 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
472 return false
473 }
474 redirectHostname := redirectURL.Hostname()
476 for _, domain := range p.whitelistDomains {
477 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
478 if domainHostname == "" {
479 continue
480 }
482 if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
483 // the domain names match, now validate the ports
484 // if the whitelisted domain's port is '*', allow all ports
485 // if the whitelisted domain contains a specific port, only allow that port
486 // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
487 redirectPort := redirectURL.Port()
488 if (domainPort == "*") ||
489 (domainPort == redirectPort) ||
490 (domainPort == "" && redirectPort == "") {
491 return true
492 }
493 }
494 }
496 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
497 return false
498 default:
499 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
500 return false
501 }
504// IsWhitelistedRequest is used to check if auth should be skipped for this request
505func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
506 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
507 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) || p.IsTrustedIP(req)
510// IsWhitelistedPath is used to check if the request path is allowed without auth
511func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
512 for _, u := range p.compiledRegex {
513 if u.MatchString(path) {
514 return true
515 }
516 }
517 return false
520// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
521var noCacheHeaders = map[string]string{
522 "Expires": time.Unix(0, 0).Format(time.RFC1123),
523 "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
524 "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
527// prepareNoCache prepares headers for preventing browser caching.
528func prepareNoCache(w http.ResponseWriter) {
529 // Set NoCache headers
530 for k, v := range noCacheHeaders {
531 w.Header().Set(k, v)
532 }
535// IsTrustedIP is used to check if a request comes from a trusted client IP address.
536func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
537 if p.trustedIPs == nil {
538 return false
539 }
541 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
542 if err != nil {
543 logger.Printf("Error obtaining real IP for trusted IP list: %v", err)
544 // Possibly spoofed X-Real-IP header
545 return false
546 }
548 if remoteAddr == nil {
549 return false
550 }
552 return p.trustedIPs.Has(remoteAddr)
555func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
556 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
557 prepareNoCache(rw)
558 }
560 switch path := req.URL.Path; {
561 case path == p.RobotsPath:
562 p.RobotsTxt(rw)
563 case p.IsWhitelistedRequest(req):
564 p.SkipAuthProxy(rw, req)
565 case path == p.SignInPath:
566 p.SignIn(rw, req)
567 case path == p.SignOutPath:
568 p.SignOut(rw, req)
569 case path == p.OAuthStartPath:
570 p.OAuthStart(rw, req)
571 case path == p.OAuthCallbackPath:
572 p.OAuthCallback(rw, req)
573 case path == p.AuthOnlyPath:
574 p.AuthenticateOnly(rw, req)
575 case path == p.UserInfoPath:
576 p.UserInfo(rw, req)
577 default:
578 p.Proxy(rw, req)
579 }
582// SignIn serves a page prompting users to sign in
583func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
584 redirect, err := p.GetRedirect(req)
585 if err != nil {
586 logger.Printf("Error obtaining redirect: %s", err.Error())
587 p.ErrorPage(rw, 500, "Internal Error", err.Error())
588 return
589 }
591 user, ok := p.ManualSignIn(rw, req)
592 if ok {
593 session := &sessionsapi.SessionState{User: user}
594 p.SaveSession(rw, req, session)
595 http.Redirect(rw, req, redirect, http.StatusFound)
596 } else {
597 if p.SkipProviderButton {
598 p.OAuthStart(rw, req)
599 } else {
600 p.SignInPage(rw, req, http.StatusOK)
601 }
602 }
605//UserInfo endpoint outputs session email and preferred username in JSON format
606func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
608 session, err := p.getAuthenticatedSession(rw, req)
609 if err != nil {
610 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
611 return
612 }
613 userInfo := struct {
614 Email string `json:"email"`
615 PreferredUsername string `json:"preferredUsername,omitempty"`
616 }{
617 Email: session.Email,
618 PreferredUsername: session.PreferredUsername,
619 }
620 rw.Header().Set("Content-Type", "application/json")
621 rw.WriteHeader(http.StatusOK)
622 json.NewEncoder(rw).Encode(userInfo)
625// SignOut sends a response to clear the authentication cookie
626func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
627 redirect, err := p.GetRedirect(req)
628 if err != nil {
629 logger.Printf("Error obtaining redirect: %s", err.Error())
630 p.ErrorPage(rw, 500, "Internal Error", err.Error())
631 return
632 }
633 p.ClearSessionCookie(rw, req)
634 http.Redirect(rw, req, redirect, http.StatusFound)
637// OAuthStart starts the OAuth2 authentication flow
638func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
639 prepareNoCache(rw)
640 nonce, err := encryption.Nonce()
641 if err != nil {
642 logger.Printf("Error obtaining nonce: %s", err.Error())
643 p.ErrorPage(rw, 500, "Internal Error", err.Error())
644 return
645 }
646 p.SetCSRFCookie(rw, req, nonce)
647 redirect, err := p.GetRedirect(req)
648 if err != nil {
649 logger.Printf("Error obtaining redirect: %s", err.Error())
650 p.ErrorPage(rw, 500, "Internal Error", err.Error())
651 return
652 }
653 redirectURI := p.GetRedirectURI(req.Host)
654 http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
657// OAuthCallback is the OAuth2 authentication flow callback that finishes the
658// OAuth2 authentication flow
659func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
660 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
662 // finish the oauth cycle
663 err := req.ParseForm()
664 if err != nil {
665 logger.Printf("Error while parsing OAuth2 callback: %s" + err.Error())
666 p.ErrorPage(rw, 500, "Internal Error", err.Error())
667 return
668 }
669 errorString := req.Form.Get("error")
670 if errorString != "" {
671 logger.Printf("Error while parsing OAuth2 callback: %s ", errorString)
672 p.ErrorPage(rw, 403, "Permission Denied", errorString)
673 return
674 }
676 session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code"))
677 if err != nil {
678 logger.Printf("Error redeeming code during OAuth2 callback: %s ", err.Error())
679 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
680 return
681 }
683 s := strings.SplitN(req.Form.Get("state"), ":", 2)
684 if len(s) != 2 {
685 logger.Printf("Error while parsing OAuth2 state: invalid length")
686 p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
687 return
688 }
689 nonce := s[0]
690 redirect := s[1]
691 c, err := req.Cookie(p.CSRFCookieName)
692 if err != nil {
693 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable too obtain CSRF cookie")
694 p.ErrorPage(rw, 403, "Permission Denied", err.Error())
695 return
696 }
697 p.ClearCSRFCookie(rw, req)
698 if c.Value != nonce {
699 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: csrf token mismatch, potential attack")
700 p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
701 return
702 }
704 if !p.IsValidRedirect(redirect) {
705 redirect = "/"
706 }
708 // set cookie, or deny
709 if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
710 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
711 err := p.SaveSession(rw, req, session)
712 if err != nil {
713 logger.Printf("%s %s", remoteAddr, err)
714 p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
715 return
716 }
717 http.Redirect(rw, req, redirect, http.StatusFound)
718 } else {
719 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
720 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
721 }
724// AuthenticateOnly checks whether the user is currently logged in
725func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
726 session, err := p.getAuthenticatedSession(rw, req)
727 if err != nil {
728 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
729 return
730 }
732 // we are authenticated
733 p.addHeadersForProxying(rw, req, session)
734 rw.WriteHeader(http.StatusAccepted)
737// SkipAuthProxy proxies whitelisted requests and skips authentication
738func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
739 if p.skipAuthStripHeaders {
740 p.stripAuthHeaders(req)
741 }
742 p.serveMux.ServeHTTP(rw, req)
745// Proxy proxies the user request if the user is authenticated else it prompts
746// them to authenticate
747func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
748 session, err := p.getAuthenticatedSession(rw, req)
749 switch err {
750 case nil:
751 // we are authenticated
752 p.addHeadersForProxying(rw, req, session)
753 p.serveMux.ServeHTTP(rw, req)
755 case ErrNeedsLogin:
756 // we need to send the user to a login screen
757 if isAjax(req) {
758 // no point redirecting an AJAX request
759 p.ErrorJSON(rw, http.StatusUnauthorized)
760 return
761 }
763 if p.SkipProviderButton {
764 p.OAuthStart(rw, req)
765 } else {
766 p.SignInPage(rw, req, http.StatusForbidden)
767 }
769 default:
770 // unknown error
771 logger.Printf("Unexpected internal error: %s", err)
772 p.ErrorPage(rw, http.StatusInternalServerError,
773 "Internal Error", "Internal Error")
774 }
778// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
779// Returns nil, ErrNeedsLogin if user needs to login.
780// Set-Cookie headers may be set on the response as a side-effect of calling this method.
781func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
782 var session *sessionsapi.SessionState
783 var err error
784 var saveSession, clearSession, revalidated bool
786 if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
787 session, err = p.GetJwtSession(req)
788 if err != nil {
789 logger.Printf("Error retrieving session from token in Authorization header: %s", err)
790 }
791 if session != nil {
792 saveSession = false
793 }
794 }
796 remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
797 if session == nil {
798 session, err = p.LoadCookiedSession(req)
799 if err != nil {
800 logger.Printf("Error loading cookied session: %s", err)
801 }
803 if session != nil {
804 if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
805 logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
806 saveSession = true
807 }
809 if ok, err := p.provider.RefreshSessionIfNeeded(req.Context(), session); err != nil {
810 logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
811 clearSession = true
812 session = nil
813 } else if ok {
814 saveSession = true
815 revalidated = true
816 }
817 }
818 }
820 if session != nil && session.IsExpired() {
821 logger.Printf("Removing session: token expired %s", session)
822 session = nil
823 saveSession = false
824 clearSession = true
825 }
827 if saveSession && !revalidated && session != nil && session.AccessToken != "" {
828 if !p.provider.ValidateSessionState(req.Context(), session) {
829 logger.Printf("Removing session: error validating %s", session)
830 saveSession = false
831 session = nil
832 clearSession = true
833 }
834 }
836 if session != nil && session.Email != "" && !p.Validator(session.Email) {
837 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
838 session = nil
839 saveSession = false
840 clearSession = true
841 }
843 if saveSession && session != nil {
844 err = p.SaveSession(rw, req, session)
845 if err != nil {
846 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
847 return nil, err
848 }
849 }
851 if clearSession {
852 p.ClearSessionCookie(rw, req)
853 }
855 if session == nil {
856 session, err = p.CheckBasicAuth(req)
857 if err != nil {
858 logger.Printf("Error during basic auth validation: %s", err)
859 }
860 }
862 if session == nil {
863 return nil, ErrNeedsLogin
864 }
866 return session, nil
869// addHeadersForProxying adds the appropriate headers the request / response for proxying
870func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
871 if p.PassBasicAuth {
872 if p.PreferEmailToUser && session.Email != "" {
873 req.SetBasicAuth(session.Email, p.BasicAuthPassword)
874 req.Header["X-Forwarded-User"] = []string{session.Email}
875 req.Header.Del("X-Forwarded-Email")
876 } else {
877 req.SetBasicAuth(session.User, p.BasicAuthPassword)
878 req.Header["X-Forwarded-User"] = []string{session.User}
879 if session.Email != "" {
880 req.Header["X-Forwarded-Email"] = []string{session.Email}
881 } else {
882 req.Header.Del("X-Forwarded-Email")
883 }
884 }
885 if session.PreferredUsername != "" {
886 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
887 } else {
888 req.Header.Del("X-Forwarded-Preferred-Username")
889 }
890 }
892 if p.PassUserHeaders {
893 if p.PreferEmailToUser && session.Email != "" {
894 req.Header["X-Forwarded-User"] = []string{session.Email}
895 req.Header.Del("X-Forwarded-Email")
896 } else {
897 req.Header["X-Forwarded-User"] = []string{session.User}
898 if session.Email != "" {
899 req.Header["X-Forwarded-Email"] = []string{session.Email}
900 } else {
901 req.Header.Del("X-Forwarded-Email")
902 }
903 }
905 if session.PreferredUsername != "" {
906 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
907 } else {
908 req.Header.Del("X-Forwarded-Preferred-Username")
909 }
910 }
912 if p.SetXAuthRequest {
913 rw.Header().Set("X-Auth-Request-User", session.User)
914 if session.Email != "" {
915 rw.Header().Set("X-Auth-Request-Email", session.Email)
916 } else {
917 rw.Header().Del("X-Auth-Request-Email")
918 }
919 if session.PreferredUsername != "" {
920 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
921 } else {
922 rw.Header().Del("X-Auth-Request-Preferred-Username")
923 }
925 if p.PassAccessToken {
926 if session.AccessToken != "" {
927 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
928 } else {
929 rw.Header().Del("X-Auth-Request-Access-Token")
930 }
931 }
932 }
934 if p.PassAccessToken {
935 if session.AccessToken != "" {
936 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
937 } else {
938 req.Header.Del("X-Forwarded-Access-Token")
939 }
940 }
942 if p.PassAuthorization {
943 if session.IDToken != "" {
944 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
945 } else {
946 req.Header.Del("Authorization")
947 }
948 }
949 if p.SetBasicAuth {
950 switch {
951 case p.PreferEmailToUser && session.Email != "":
952 authVal := b64.StdEncoding.EncodeToString([]byte(session.Email + ":" + p.BasicAuthPassword))
953 rw.Header().Set("Authorization", "Basic "+authVal)
954 case session.User != "":
955 authVal := b64.StdEncoding.EncodeToString([]byte(session.User + ":" + p.BasicAuthPassword))
956 rw.Header().Set("Authorization", "Basic "+authVal)
957 default:
958 rw.Header().Del("Authorization")
959 }
960 }
961 if p.SetAuthorization {
962 if session.IDToken != "" {
963 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
964 } else {
965 rw.Header().Del("Authorization")
966 }
967 }
969 if session.Email == "" {
970 rw.Header().Set("GAP-Auth", session.User)
971 } else {
972 rw.Header().Set("GAP-Auth", session.Email)
973 }
976// stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex
977func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
978 if p.PassBasicAuth {
979 req.Header.Del("X-Forwarded-User")
980 req.Header.Del("X-Forwarded-Email")
981 req.Header.Del("X-Forwarded-Preferred-Username")
982 req.Header.Del("Authorization")
983 }
985 if p.PassUserHeaders {
986 req.Header.Del("X-Forwarded-User")
987 req.Header.Del("X-Forwarded-Email")
988 req.Header.Del("X-Forwarded-Preferred-Username")
989 }
991 if p.PassAccessToken {
992 req.Header.Del("X-Forwarded-Access-Token")
993 }
995 if p.PassAuthorization {
996 req.Header.Del("Authorization")
997 }
1000// CheckBasicAuth checks the requests Authorization header for basic auth
1001// credentials and authenticates these against the proxies HtpasswdFile
1002func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
1003 if p.basicAuthValidator == nil {
1004 return nil, nil
1006 auth := req.Header.Get("Authorization")
1007 if auth == "" {
1008 return nil, nil
1010 s := strings.SplitN(auth, " ", 2)
1011 if len(s) != 2 || s[0] != "Basic" {
1012 return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1014 b, err := b64.StdEncoding.DecodeString(s[1])
1015 if err != nil {
1016 return nil, err
1018 pair := strings.SplitN(string(b), ":", 2)
1019 if len(pair) != 2 {
1020 return nil, fmt.Errorf("invalid format %s", b)
1022 if p.basicAuthValidator.Validate(pair[0], pair[1]) {
1023 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1024 return &sessionsapi.SessionState{User: pair[0]}, nil
1026 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1027 return nil, nil
1030// isAjax checks if a request is an ajax request
1031func isAjax(req *http.Request) bool {
1032 acceptValues := req.Header.Values("Accept")
1033 const ajaxReq = applicationJSON
1034 for _, v := range acceptValues {
1035 if v == ajaxReq {
1036 return true
1039 return false
1042// ErrorJSON returns the error code with an application/json mime type
1043func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1044 rw.Header().Set("Content-Type", applicationJSON)
1045 rw.WriteHeader(code)
1048// GetJwtSession loads a session based on a JWT token in the authorization header.
1049// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
1050func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1051 rawBearerToken, err := p.findBearerToken(req)
1052 if err != nil {
1053 return nil, err
1056 // If we are using an oidc provider, go ahead and try that provider first with its Verifier
1057 // and Bearer Token -> Session converter
1058 if p.mainJwtBearerVerifier != nil {
1059 bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
1060 if err == nil {
1061 return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1065 // Otherwise, attempt to verify against the extra JWT issuers and use a more generic
1066 // Bearer Token -> Session converter
1067 for _, verifier := range p.extraJwtBearerVerifiers {
1068 bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1069 if err != nil {
1070 continue
1073 return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1075 return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1078// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1079func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1080 auth := req.Header.Get("Authorization")
1081 s := strings.SplitN(auth, " ", 2)
1082 if len(s) != 2 {
1083 return "", fmt.Errorf("invalid authorization header %s", auth)
1085 jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1086 var rawBearerToken string
1087 if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1088 rawBearerToken = s[1]
1089 } else if s[0] == "Basic" {
1090 // Check if we have a Bearer token masquerading in Basic
1091 b, err := b64.StdEncoding.DecodeString(s[1])
1092 if err != nil {
1093 return "", err
1095 pair := strings.SplitN(string(b), ":", 2)
1096 if len(pair) != 2 {
1097 return "", fmt.Errorf("invalid format %s", b)
1099 user, password := pair[0], pair[1]
1101 // check user, user+password, or just password for a token
1102 if jwtRegex.MatchString(user) {
1103 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1104 if password == "" || password == "x-oauth-basic" {
1105 rawBearerToken = user
1107 } else if jwtRegex.MatchString(password) {
1108 // support passwords and ignore user
1109 rawBearerToken = password
1112 if rawBearerToken == "" {
1113 return "", fmt.Errorf("no valid bearer token found in authorization header")
1116 return rawBearerToken, nil