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