github.com/greenpau/go-authcrunch@v1.0.50/pkg/authn/respond_http.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package authn
    16  
    17  import (
    18  	"context"
    19  	"github.com/greenpau/go-authcrunch/pkg/requests"
    20  	"github.com/greenpau/go-authcrunch/pkg/user"
    21  	"github.com/greenpau/go-authcrunch/pkg/util"
    22  	addrutil "github.com/greenpau/go-authcrunch/pkg/util/addr"
    23  	"go.uber.org/zap"
    24  	"net/http"
    25  	"net/url"
    26  	"path"
    27  	"strings"
    28  )
    29  
    30  func (p *Portal) handleHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) error {
    31  	p.injectSessionID(ctx, w, r, rr)
    32  	usr, _ := p.authorizeRequest(ctx, w, r, rr)
    33  	switch {
    34  	case r.URL.Path == "/" || r.URL.Path == "/auth" || r.URL.Path == "/auth/":
    35  		p.injectRedirectURL(ctx, w, r, rr)
    36  		return p.handleHTTPRedirect(ctx, w, r, rr, "/login")
    37  	case strings.Contains(r.URL.Path, "/assets/") || strings.Contains(r.URL.Path, "/favicon"):
    38  		return p.handleHTTPStaticAssets(ctx, w, r, rr)
    39  	case strings.Contains(r.URL.Path, "/portal"):
    40  		return p.handleHTTPPortal(ctx, w, r, rr, usr)
    41  	case strings.HasSuffix(r.URL.Path, "/recover"), strings.HasSuffix(r.URL.Path, "/forgot"):
    42  		// TODO(greenpau): implement password recovery.
    43  		return p.handleHTTPRecover(ctx, w, r, rr)
    44  	case strings.Contains(r.URL.Path, "/settings"):
    45  		return p.handleHTTPSettings(ctx, w, r, rr, usr)
    46  	case strings.HasSuffix(r.URL.Path, "/register"), strings.Contains(r.URL.Path, "/register/"):
    47  		return p.handleHTTPRegister(ctx, w, r, rr)
    48  	case strings.HasSuffix(r.URL.Path, "/whoami"):
    49  		return p.handleHTTPWhoami(ctx, w, r, rr, usr)
    50  	case strings.Contains(r.URL.Path, "/apps/sso"):
    51  		return p.handleHTTPAppsSingleSignOn(ctx, w, r, rr, usr)
    52  	case strings.Contains(r.URL.Path, "/apps/mobile-access"):
    53  		return p.handleHTTPAppsMobileAccess(ctx, w, r, rr, usr)
    54  	case strings.Contains(r.URL.Path, "/oauth2/") && strings.HasSuffix(r.URL.Path, "/logout"):
    55  		return p.handleHTTPExternalLogout(ctx, w, r, rr, "oauth2")
    56  	case strings.Contains(r.URL.Path, "/saml/"):
    57  		return p.handleHTTPExternalLogin(ctx, w, r, rr, "saml")
    58  	case strings.Contains(r.URL.Path, "/oauth2/"):
    59  		return p.handleHTTPExternalLogin(ctx, w, r, rr, "oauth2")
    60  	case strings.Contains(r.URL.Path, "/basic/login/"):
    61  		return p.handleHTTPBasicLogin(ctx, w, r, rr)
    62  	case strings.HasSuffix(r.URL.Path, "/logout"):
    63  		return p.handleHTTPLogout(ctx, w, r, rr, usr)
    64  	case strings.Contains(r.URL.Path, "/sandbox/"):
    65  		return p.handleHTTPSandbox(ctx, w, r, rr)
    66  	case strings.HasSuffix(r.URL.Path, "/login"):
    67  		return p.handleHTTPLogin(ctx, w, r, rr, usr)
    68  	}
    69  	p.injectRedirectURL(ctx, w, r, rr)
    70  	if usr != nil {
    71  		p.logger.Debug(
    72  			"no route",
    73  			zap.String("session_id", rr.Upstream.SessionID),
    74  			zap.String("request_id", rr.ID),
    75  			zap.Any("request_path", r.URL.Path),
    76  			zap.Any("user", usr.Claims),
    77  		)
    78  		return p.handleHTTPError(ctx, w, r, rr, http.StatusNotFound)
    79  	}
    80  	return p.handleHTTPErrorWithLog(ctx, w, r, rr, http.StatusNotFound, "no route")
    81  }
    82  
    83  func (p *Portal) disableClientCache(w http.ResponseWriter) {
    84  	w.Header().Set("Cache-Control", "no-store")
    85  	w.Header().Set("Pragma", "no-cache")
    86  }
    87  
    88  func (p *Portal) handleHTTPErrorWithLog(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, code int, msg string) error {
    89  	p.logger.Warn(
    90  		http.StatusText(code),
    91  		zap.String("session_id", rr.Upstream.SessionID),
    92  		zap.String("request_id", rr.ID),
    93  		zap.Any("error", msg),
    94  		zap.String("source_address", addrutil.GetSourceAddress(r)),
    95  	)
    96  	return p.handleHTTPError(ctx, w, r, rr, code)
    97  }
    98  
    99  func (p *Portal) handleHTTPError(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, code int) error {
   100  	p.disableClientCache(w)
   101  	resp := p.ui.GetArgs()
   102  	resp.BaseURL(rr.Upstream.BasePath)
   103  	resp.PageTitle = http.StatusText(code)
   104  
   105  	switch code {
   106  	case http.StatusForbidden:
   107  		resp.PageTitle = "Access Denied"
   108  		resp.Data["message"] = "Please contact support if you believe this is an error."
   109  	case http.StatusNotFound:
   110  		resp.PageTitle = "Page Not Found"
   111  		resp.Data["message"] = "The page you are looking for could not be found."
   112  	default:
   113  		resp.PageTitle = http.StatusText(code)
   114  	}
   115  
   116  	resp.Data["authenticated"] = rr.Response.Authenticated
   117  
   118  	if rr.Response.RedirectURL != "" {
   119  		resp.Data["go_back_url"] = rr.Response.RedirectURL
   120  	} else {
   121  		if r.Referer() != "" {
   122  			resp.Data["go_back_url"] = util.SanitizeURL(r.Referer())
   123  		} else {
   124  			resp.Data["go_back_url"] = "/"
   125  		}
   126  	}
   127  	content, err := p.ui.Render("generic", resp)
   128  	if err != nil {
   129  		return p.handleHTTPRenderError(ctx, w, r, rr, err)
   130  	}
   131  	return p.handleHTTPRenderHTML(ctx, w, code, content.Bytes())
   132  }
   133  
   134  func (p *Portal) handleHTTPGeneric(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, code int, msg string) error {
   135  	p.disableClientCache(w)
   136  	resp := p.ui.GetArgs()
   137  	resp.BaseURL(rr.Upstream.BasePath)
   138  	resp.PageTitle = msg
   139  	switch code {
   140  	case http.StatusServiceUnavailable:
   141  		resp.Data["message"] = "This service is not available to you."
   142  	}
   143  
   144  	resp.Data["authenticated"] = rr.Response.Authenticated
   145  	resp.Data["go_back_url"] = rr.Upstream.BasePath
   146  	content, err := p.ui.Render("generic", resp)
   147  	if err != nil {
   148  		return p.handleHTTPRenderError(ctx, w, r, rr, err)
   149  	}
   150  	return p.handleHTTPRenderHTML(ctx, w, code, content.Bytes())
   151  }
   152  
   153  func (p *Portal) handleHTTPRedirect(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, location string) error {
   154  	p.disableClientCache(w)
   155  	location = rr.Upstream.BaseURL + path.Join(rr.Upstream.BasePath, location)
   156  	w.Header().Set("Location", location)
   157  	p.logger.Debug(
   158  		"Redirect served",
   159  		zap.String("session_id", rr.Upstream.SessionID),
   160  		zap.String("request_id", rr.ID),
   161  		zap.String("redirect_url", location),
   162  		zap.Int("status_code", http.StatusFound),
   163  	)
   164  	w.WriteHeader(http.StatusFound)
   165  	return nil
   166  }
   167  
   168  func (p *Portal) handleHTTPRedirectSeeOther(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, location string) error {
   169  	p.disableClientCache(w)
   170  	location = rr.Upstream.BaseURL + path.Join(rr.Upstream.BasePath, location)
   171  	w.Header().Set("Location", location)
   172  	p.logger.Debug(
   173  		"Redirect served",
   174  		zap.String("session_id", rr.Upstream.SessionID),
   175  		zap.String("request_id", rr.ID),
   176  		zap.String("redirect_url", location),
   177  		zap.Int("status_code", http.StatusSeeOther),
   178  	)
   179  	w.WriteHeader(http.StatusSeeOther)
   180  	return nil
   181  }
   182  
   183  func (p *Portal) handleHTTPRedirectExternal(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, location string) error {
   184  	w.Header().Set("Location", location)
   185  	p.logger.Debug(
   186  		"External redirect served",
   187  		zap.String("session_id", rr.Upstream.SessionID),
   188  		zap.String("request_id", rr.ID),
   189  		zap.String("redirect_url", location),
   190  		zap.Int("status_code", http.StatusFound),
   191  	)
   192  	w.WriteHeader(http.StatusFound)
   193  	return nil
   194  }
   195  
   196  func (p *Portal) logRequest(msg string, r *http.Request, rr *requests.Request) {
   197  	p.logger.Debug(
   198  		msg,
   199  		zap.String("session_id", rr.Upstream.SessionID),
   200  		zap.String("request_id", rr.ID),
   201  		zap.String("url_path", r.URL.Path),
   202  		zap.Any("request", rr.Upstream),
   203  		zap.String("source_address", addrutil.GetSourceAddress(r)),
   204  	)
   205  }
   206  
   207  func (p *Portal) handleHTTPRenderError(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, err error) error {
   208  	p.logger.Error(
   209  		"Failed HTML response rendering",
   210  		zap.String("session_id", rr.Upstream.SessionID),
   211  		zap.String("request_id", rr.ID),
   212  		zap.Error(err),
   213  	)
   214  
   215  	resp := p.ui.GetArgs()
   216  	resp.BaseURL(rr.Upstream.BasePath)
   217  	resp.PageTitle = http.StatusText(http.StatusInternalServerError)
   218  	resp.Data["go_back_url"] = "/"
   219  	resp.Data["message"] = "Unexpected server error occurred. If persists, please contact support."
   220  
   221  	content, err := p.ui.Render("generic", resp)
   222  	if err != nil {
   223  		w.Header().Set("Content-Type", "text/plain")
   224  		w.WriteHeader(http.StatusInternalServerError)
   225  		w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
   226  		return nil
   227  	}
   228  	return p.handleHTTPRenderHTML(ctx, w, http.StatusInternalServerError, content.Bytes())
   229  }
   230  
   231  func (p *Portal) handleHTTPRenderHTML(ctx context.Context, w http.ResponseWriter, code int, body []byte) error {
   232  	w.Header().Set("Content-Type", "text/html")
   233  	w.WriteHeader(code)
   234  	w.Write(body)
   235  	return nil
   236  }
   237  
   238  func (p *Portal) handleHTTPRenderPlainText(ctx context.Context, w http.ResponseWriter, code int) error {
   239  	w.Header().Set("Content-Type", "text/plain")
   240  	w.WriteHeader(code)
   241  	w.Write([]byte(http.StatusText(code)))
   242  	return nil
   243  }
   244  
   245  func (p *Portal) injectSessionID(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) {
   246  	if cookie, err := r.Cookie(p.cookie.SessionID); err == nil {
   247  		v, err := url.Parse(cookie.Value)
   248  		if err == nil && v.String() != "" {
   249  			rr.Upstream.SessionID = util.SanitizeSessionID(v.String())
   250  			return
   251  		}
   252  	}
   253  	rr.Upstream.SessionID = util.GetRandomStringFromRange(36, 46)
   254  	w.Header().Add("Set-Cookie", p.cookie.GetSessionCookie(addrutil.GetSourceHost(r), rr.Upstream.SessionID))
   255  	return
   256  }
   257  
   258  func (p *Portal) injectRedirectURL(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) {
   259  	if r.Method == "GET" {
   260  		q := r.URL.Query()
   261  		if redirectURL, exists := q["redirect_url"]; exists {
   262  			c := p.cookie.GetCookie(addrutil.GetSourceHost(r), p.cookie.Referer, util.StripQueryParam(redirectURL[0], "login_hint"))
   263  			p.logger.Debug(
   264  				"redirect recorded",
   265  				zap.String("session_id", rr.Upstream.SessionID),
   266  				zap.String("request_id", rr.ID),
   267  				zap.String("redirect_url", c),
   268  			)
   269  			w.Header().Add("Set-Cookie", c)
   270  			rr.Response.RedirectURL = c
   271  		}
   272  	}
   273  }
   274  
   275  func (p *Portal) authorizeRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) (*user.User, error) {
   276  	extractBasePath(ctx, r, rr)
   277  	ar := requests.NewAuthorizationRequest()
   278  	ar.ID = rr.ID
   279  	ar.SessionID = rr.Upstream.SessionID
   280  	usr, err := p.validator.Authorize(ctx, r, ar)
   281  	if err != nil {
   282  		switch err.Error() {
   283  		case "no token found":
   284  			return nil, nil
   285  		default:
   286  			for tokenName := range p.validator.GetAuthCookies() {
   287  				w.Header().Add("Set-Cookie", p.cookie.GetDeleteCookie(addrutil.GetSourceHost(r), tokenName))
   288  			}
   289  			if strings.Contains(r.URL.Path, "/assets/") || strings.Contains(r.URL.Path, "/favicon") {
   290  				return nil, nil
   291  			}
   292  			p.logger.Debug(
   293  				"token error",
   294  				zap.String("session_id", rr.Upstream.SessionID),
   295  				zap.String("request_id", rr.ID),
   296  				zap.Error(err),
   297  			)
   298  			return nil, err
   299  		}
   300  	}
   301  	if usr != nil {
   302  		rr.Response.Authenticated = true
   303  	}
   304  	return usr, nil
   305  }
   306  
   307  func extractBaseURLPath(ctx context.Context, r *http.Request, rr *requests.Request, s string) {
   308  	baseURL, basePath := util.GetBaseURL(r, s)
   309  	rr.Upstream.BaseURL = baseURL
   310  	if basePath == "/" {
   311  		rr.Upstream.BasePath = basePath
   312  		return
   313  	}
   314  	if strings.HasSuffix(basePath, "/") {
   315  		rr.Upstream.BasePath = basePath
   316  		return
   317  	}
   318  	rr.Upstream.BasePath = basePath + "/"
   319  }
   320  
   321  func extractBasePath(ctx context.Context, r *http.Request, rr *requests.Request) {
   322  	switch {
   323  	case r.URL.Path == "/":
   324  		rr.Upstream.BaseURL = util.GetCurrentBaseURL(r)
   325  		rr.Upstream.BasePath = "/"
   326  	case r.URL.Path == "/auth":
   327  		rr.Upstream.BaseURL = util.GetCurrentBaseURL(r)
   328  		rr.Upstream.BasePath = "/auth/"
   329  	case strings.HasSuffix(r.URL.Path, "/portal"):
   330  		extractBaseURLPath(ctx, r, rr, "/portal")
   331  	case strings.Contains(r.URL.Path, "/sandbox/"):
   332  		extractBaseURLPath(ctx, r, rr, "/sandbox/")
   333  	case strings.Contains(r.URL.Path, "/settings"):
   334  		extractBaseURLPath(ctx, r, rr, "/settings")
   335  	case strings.HasSuffix(r.URL.Path, "/recover"), strings.HasSuffix(r.URL.Path, "/forgot"):
   336  		extractBaseURLPath(ctx, r, rr, "/recover,/forgot")
   337  	case strings.HasSuffix(r.URL.Path, "/register"):
   338  		extractBaseURLPath(ctx, r, rr, "/register")
   339  	case strings.HasSuffix(r.URL.Path, "/whoami"):
   340  		extractBaseURLPath(ctx, r, rr, "/whoami")
   341  	case strings.Contains(r.URL.Path, "/saml/"):
   342  		extractBaseURLPath(ctx, r, rr, "/saml/")
   343  	case strings.Contains(r.URL.Path, "/oauth2/"):
   344  		extractBaseURLPath(ctx, r, rr, "/oauth2/")
   345  	case strings.HasSuffix(r.URL.Path, "/basic/login"):
   346  		extractBaseURLPath(ctx, r, rr, "/basic/login")
   347  	case strings.HasSuffix(r.URL.Path, "/logout"):
   348  		extractBaseURLPath(ctx, r, rr, "/logout")
   349  	case strings.Contains(r.URL.Path, "/assets/") || strings.Contains(r.URL.Path, "/favicon"):
   350  		extractBaseURLPath(ctx, r, rr, "/assets/")
   351  	case strings.HasSuffix(r.URL.Path, "/login"):
   352  		extractBaseURLPath(ctx, r, rr, "/login")
   353  	case strings.HasPrefix(r.URL.Path, "/auth"):
   354  		rr.Upstream.BaseURL = util.GetCurrentBaseURL(r)
   355  		rr.Upstream.BasePath = "/auth/"
   356  	default:
   357  		rr.Upstream.BaseURL = util.GetCurrentBaseURL(r)
   358  		rr.Upstream.BasePath = "/"
   359  	}
   360  }