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"
39 applicationJSON = "application/json"
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 {
56 CookieDomains []string
60 CookieExpire time.Duration
61 CookieRefresh time.Duration
63 Validator func(string) bool
69 OAuthCallbackPath 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
80 basicAuthValidator basic.Validator
81 displayHtpasswdForm bool
86 SkipProviderButton bool
88 BasicAuthPassword string
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
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)
114 return nil, fmt.Errorf("error initialising session store: %v", err)
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)
121 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
124 for _, u := range opts.GetCompiledRegex() {
125 logger.Printf("compiled skip-auth-regex => %q", u)
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)
134 redirectURL := opts.GetRedirectURL()
135 if redirectURL.Path == "" {
136 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
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)
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)
152 return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
156 var basicAuthValidator basic.Validator
157 if opts.HtpasswdFile != "" {
158 logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
160 basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
162 return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
166 sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
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,
218 SignInMessage: buildSignInMessage(opts),
219 AllowedGroups: opts.AllowedGroups,
221 basicAuthValidator: basicAuthValidator,
222 displayHtpasswdForm: basicAuthValidator != nil,
223 sessionChain: sessionChain,
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,
239 for _, verifier := range opts.GetJWTBearerVerifiers() {
240 sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
245 chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
248 if validator != nil {
249 chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
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,
262func buildSignInMessage(opts *options.Options) string {
264 if len(opts.Banner) >= 1 {
265 if opts.Banner == "-" {
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])
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()
290 u.Scheme = httpsScheme
292 u.Scheme = httpScheme
299func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (*sessionsapi.SessionState, error) {
301 return nil, errors.New("missing code")
303 redirectURI := p.GetRedirectURI(host)
304 s, err := p.provider.Redeem(ctx, redirectURI, code)
310 s.Email, err = p.provider.GetEmailAddress(ctx, s)
311 if err != nil && err.Error() != "not implemented" {
317 s.User, err = p.provider.GetUserName(ctx, s)
318 if err != nil && err.Error() != "not implemented" {
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 {
339 if !strings.HasSuffix(domain, cookieDomain) {
340 logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
348 Domain: cookieDomain,
349 HttpOnly: p.CookieHTTPOnly,
350 Secure: p.CookieSecure,
351 Expires: now.Add(expiration),
352 SameSite: cookies.ParseSameSite(p.CookieSameSite),
356// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
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: /")
387 logger.Printf("Error writing robots.txt: %v", err)
388 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
391 rw.WriteHeader(http.StatusOK)
394// ErrorPage writes an error response
395func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
402 Title: fmt.Sprintf("%d %s", code, title),
404 ProxyPrefix: p.ProxyPrefix,
406 err := p.templates.ExecuteTemplate(rw, "error.html", t)
408 logger.Printf("Error rendering error.html template: %v", err)
409 http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
413// SignInPage writes the sing in template to the response
414func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
416 err := p.ClearSessionCookie(rw, req)
418 logger.Printf("Error clearing session cookie: %v", err)
419 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
424 redirectURL, err := p.GetRedirect(req)
426 logger.Errorf("Error obtaining redirect: %v", err)
427 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
431 if redirectURL == p.SignInPath {
435 // We allow unescaped template.HTML since it is user configured options
439 SignInMessage template.HTML
446 ProviderName: p.provider.Data().ProviderName,
447 SignInMessage: template.HTML(p.SignInMessage),
448 CustomLogin: p.displayHtpasswdForm,
449 Redirect: redirectURL,
451 ProxyPrefix: p.ProxyPrefix,
452 Footer: template.HTML(p.Footer),
454 if p.providerNameOverride != "" {
455 t.ProviderName = p.providerNameOverride
457 err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
459 logger.Printf("Error rendering sign_in.html template: %v", err)
460 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
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 {
469 user := req.FormValue("username")
470 passwd := req.FormValue("password")
475 if p.basicAuthValidator.Validate(user, passwd) {
476 logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile")
479 logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile")
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()
491 redirect = req.Header.Get("X-Auth-Request-Redirect")
492 if req.Form.Get("rd") != "" {
493 redirect = req.Form.Get("rd")
495 if !p.IsValidRedirect(redirect) {
496 // Use RequestURI to preserve ?query
497 redirect = req.URL.RequestURI()
498 if strings.HasPrefix(redirect, p.ProxyPrefix) {
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) {
513 colon := strings.LastIndexByte(host, ':')
514 if colon != -1 && validOptionalPort(host[colon:]) {
515 host, port = host[:colon], host[colon+1:]
518 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
519 host = host[1 : len(host)-1]
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 == ":*" {
535 for _, b := range port[1:] {
536 if b < '0' || b > '9' {
543// IsValidRedirect checks whether the redirect URL is whitelisted
544func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
547 // The user didn't specify a redirect, should fallback to `/`
549 case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
551 case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
552 redirectURL, err := url.Parse(redirect)
554 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
557 redirectHostname := redirectURL.Hostname()
559 for _, domain := range p.whitelistDomains {
560 domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
561 if domainHostname == "" {
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 == "") {
579 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
582 logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
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) {
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 {
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 {
624 remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
626 logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
627 // Possibly spoofed X-Real-IP header
631 if remoteAddr == nil {
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) {
643 switch path := req.URL.Path; {
644 case path == p.RobotsPath:
646 case p.IsWhitelistedRequest(req):
647 p.SkipAuthProxy(rw, req)
648 case path == p.SignInPath:
650 case path == p.SignOutPath:
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:
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)
669 logger.Errorf("Error obtaining redirect: %v", err)
670 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
674 user, ok := p.ManualSignIn(req)
676 session := &sessionsapi.SessionState{User: user}
677 err = p.SaveSession(rw, req, session)
679 logger.Printf("Error saving session: %v", err)
680 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
683 http.Redirect(rw, req, redirect, http.StatusFound)
685 if p.SkipProviderButton {
686 p.OAuthStart(rw, req)
688 p.SignInPage(rw, req, http.StatusOK)
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)
698 http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
702 Email string `json:"email"`
703 PreferredUsername string `json:"preferredUsername,omitempty"`
705 Email: session.Email,
706 PreferredUsername: session.PreferredUsername,
708 rw.Header().Set("Content-Type", "application/json")
709 rw.WriteHeader(http.StatusOK)
710 err = json.NewEncoder(rw).Encode(userInfo)
712 logger.Printf("Error encoding user info: %v", err)
713 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
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)
721 logger.Errorf("Error obtaining redirect: %v", err)
722 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
725 err = p.ClearSessionCookie(rw, req)
727 logger.Errorf("Error clearing session cookie: %v", err)
728 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
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) {
737 nonce, err := encryption.Nonce()
739 logger.Errorf("Error obtaining nonce: %v", err)
740 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
743 p.SetCSRFCookie(rw, req, nonce)
744 redirect, err := p.GetRedirect(req)
746 logger.Errorf("Error obtaining redirect: %v", err)
747 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
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()
762 logger.Errorf("Error while parsing OAuth2 callback: %v", err)
763 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
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)
773 session, err := p.redeemCode(req.Context(), util.GetRequestHost(req), req.Form.Get("code"))
775 logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
776 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
780 s := strings.SplitN(req.Form.Get("state"), ":", 2)
782 logger.Error("Error while parsing OAuth2 state: invalid length")
783 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
788 c, err := req.Cookie(p.CSRFCookieName)
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())
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")
801 if !p.IsValidRedirect(redirect) {
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)
810 logger.Printf("Error saving session state for %s: %v", remoteAddr, err)
811 p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
814 http.Redirect(rw, req, redirect, http.StatusFound)
816 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
817 p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
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)
825 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
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)
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)
848 // we are authenticated
849 p.addHeadersForProxying(rw, req, session)
850 p.serveMux.ServeHTTP(rw, req)
853 // we need to send the user to a login screen
855 // no point redirecting an AJAX request
856 p.ErrorJSON(rw, http.StatusUnauthorized)
860 if p.SkipProviderButton {
861 p.OAuthStart(rw, req)
863 p.SignInPage(rw, req, http.StatusForbidden)
868 logger.Errorf("Unexpected internal error: %v", err)
869 p.ErrorPage(rw, http.StatusInternalServerError,
870 "Internal Error", "Internal Error")
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
884 getSession.ServeHTTP(rw, req)
887 return nil, ErrNeedsLogin
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)
898 logger.Printf("Error clearing session cookie: %v", err)
900 return nil, ErrNeedsLogin
906// addHeadersForProxying adds the appropriate headers the request / response for proxying
907func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
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")
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}
919 req.Header.Del("X-Forwarded-Email")
922 if session.PreferredUsername != "" {
923 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
925 req.Header.Del("X-Forwarded-Preferred-Username")
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")
934 req.Header["X-Forwarded-User"] = []string{session.User}
935 if session.Email != "" {
936 req.Header["X-Forwarded-Email"] = []string{session.Email}
938 req.Header.Del("X-Forwarded-Email")
942 if session.PreferredUsername != "" {
943 req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
945 req.Header.Del("X-Forwarded-Preferred-Username")
948 if len(session.Groups) > 0 {
949 for _, group := range session.Groups {
950 req.Header.Add("X-Forwarded-Groups", group)
953 req.Header.Del("X-Forwarded-Groups")
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)
962 rw.Header().Del("X-Auth-Request-Email")
964 if session.PreferredUsername != "" {
965 rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
967 rw.Header().Del("X-Auth-Request-Preferred-Username")
970 if p.PassAccessToken {
971 if session.AccessToken != "" {
972 rw.Header().Set("X-Auth-Request-Access-Token", session.AccessToken)
974 rw.Header().Del("X-Auth-Request-Access-Token")
978 if len(session.Groups) > 0 {
979 for _, group := range session.Groups {
980 rw.Header().Add("X-Auth-Request-Groups", group)
983 rw.Header().Del("X-Auth-Request-Groups")
987 if p.PassAccessToken {
988 if session.AccessToken != "" {
989 req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
991 req.Header.Del("X-Forwarded-Access-Token")
995 if p.PassAuthorization {
996 if session.IDToken != "" {
997 req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", session.IDToken)}
999 req.Header.Del("Authorization")
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)
1011 rw.Header().Del("Authorization")
1014 if p.SetAuthorization {
1015 if session.IDToken != "" {
1016 rw.Header().Set("Authorization", fmt.Sprintf("Bearer %s", session.IDToken))
1018 rw.Header().Del("Authorization")
1022 if session.Email == "" {
1023 rw.Header().Set("GAP-Auth", session.User)
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 {
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 {
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 {
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list