github.com/greenpau/go-authcrunch@v1.1.4/pkg/authz/authenticate.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 authz
    16  
    17  import (
    18  	"context"
    19  	"github.com/greenpau/go-authcrunch/pkg/authz/bypass"
    20  	"github.com/greenpau/go-authcrunch/pkg/authz/handlers"
    21  	"github.com/greenpau/go-authcrunch/pkg/errors"
    22  	"github.com/greenpau/go-authcrunch/pkg/requests"
    23  	"github.com/greenpau/go-authcrunch/pkg/user"
    24  	"github.com/greenpau/go-authcrunch/pkg/util"
    25  	addrutil "github.com/greenpau/go-authcrunch/pkg/util/addr"
    26  	"github.com/greenpau/go-authcrunch/pkg/util/validate"
    27  	"go.uber.org/zap"
    28  	"net/http"
    29  	"net/url"
    30  	"strings"
    31  )
    32  
    33  var (
    34  	placeholders = []string{
    35  		"http.request.uri", "uri",
    36  		"url",
    37  	}
    38  )
    39  
    40  // Authenticate authorizes HTTP requests.
    41  func (g *Gatekeeper) Authenticate(w http.ResponseWriter, r *http.Request, ar *requests.AuthorizationRequest) error {
    42  	// Perform authorization bypass checks
    43  	if g.bypassEnabled && bypass.Match(r, g.config.BypassConfigs) {
    44  		ar.Response.Authorized = false
    45  		ar.Response.Bypassed = true
    46  		g.logger.Info(
    47  			"authorization bypassed",
    48  			zap.String("session_id", ar.SessionID),
    49  			zap.String("request_id", ar.ID),
    50  			zap.String("src_ip", addrutil.GetSourceAddress(r)),
    51  			zap.String("src_conn_ip", addrutil.GetSourceConnAddress(r)),
    52  			zap.String("url", addrutil.GetTargetURL(r)),
    53  		)
    54  		return nil
    55  	}
    56  
    57  	g.parseSessionID(r, ar)
    58  
    59  	usr, err := g.tokenValidator.Authorize(context.Background(), r, ar)
    60  	if err != nil {
    61  		ar.Response.Error = err
    62  		return g.handleUnauthorizedUser(w, r, ar)
    63  	}
    64  	return g.handleAuthorizedUser(w, r, ar, usr)
    65  }
    66  
    67  // handleAuthorizedUser handles authorized requests.
    68  func (g *Gatekeeper) handleAuthorizedUser(w http.ResponseWriter, r *http.Request, ar *requests.AuthorizationRequest, usr *user.User) error {
    69  	g.injectHeaders(r, usr)
    70  	g.stripAuthToken(r, usr)
    71  
    72  	ar.Response.Authorized = true
    73  
    74  	if usr.Cached {
    75  		ar.Response.User = usr.GetRequestIdentity()
    76  		return nil
    77  	}
    78  
    79  	ar.Response.User = usr.BuildRequestIdentity(g.config.UserIdentityField)
    80  
    81  	if err := g.tokenValidator.CacheUser(usr); err != nil {
    82  		g.logger.Error(
    83  			"token caching error",
    84  			zap.String("session_id", ar.SessionID),
    85  			zap.String("request_id", ar.ID),
    86  			zap.Error(err),
    87  		)
    88  	}
    89  	return nil
    90  }
    91  
    92  // parseSessionID extracts Session ID from HTTP request.
    93  func (g *Gatekeeper) parseSessionID(r *http.Request, ar *requests.AuthorizationRequest) {
    94  	if cookie, err := r.Cookie("AUTHP_SESSION_ID"); err == nil {
    95  		v, err := url.Parse(cookie.Value)
    96  		if err == nil && v.String() != "" {
    97  			ar.SessionID = util.SanitizeSessionID(v.String())
    98  		}
    99  	}
   100  }
   101  
   102  // handleUnauthorizedUser handles failed authorization requests.
   103  func (g *Gatekeeper) handleUnauthorizedUser(w http.ResponseWriter, r *http.Request, ar *requests.AuthorizationRequest) error {
   104  	err := ar.Response.Error
   105  	g.logger.Debug(
   106  		"token validation error",
   107  		zap.String("session_id", ar.SessionID),
   108  		zap.String("request_id", ar.ID),
   109  		zap.Error(err),
   110  	)
   111  
   112  	switch {
   113  	case (err == errors.ErrAccessNotAllowed) || (err == errors.ErrAccessNotAllowedByPathACL):
   114  		return g.handleAuthorizeWithForbidden(w, r, ar)
   115  	case (err == errors.ErrBasicAuthFailed) || (err == errors.ErrAPIKeyAuthFailed):
   116  		return g.handleAuthorizeWithAuthFailed(w, r, ar)
   117  	case err == errors.ErrCryptoKeyStoreTokenData:
   118  		return g.handleAuthorizeWithBadRequest(w, r, ar)
   119  	}
   120  
   121  	g.expireAuthCookies(w, r)
   122  
   123  	if !g.config.AuthRedirectDisabled {
   124  		return g.handleAuthorizeWithRedirect(w, r, ar)
   125  	}
   126  
   127  	return err
   128  }
   129  
   130  // expireAuthCookies sends cookie delete in HTTP response.
   131  func (g *Gatekeeper) expireAuthCookies(w http.ResponseWriter, r *http.Request) {
   132  	cookies := g.tokenValidator.GetAuthCookies()
   133  	if cookies == nil {
   134  		return
   135  	}
   136  
   137  	for _, cookie := range r.Cookies() {
   138  		if _, exists := cookies[cookie.Name]; !exists {
   139  			continue
   140  		}
   141  		w.Header().Add("Set-Cookie", cookie.Name+"=delete; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT")
   142  	}
   143  	return
   144  }
   145  
   146  // handleAuthorizeWithAuthFailed handles failed authorization requests based on
   147  // basic authentication and API keys.
   148  func (g *Gatekeeper) handleAuthorizeWithAuthFailed(w http.ResponseWriter, r *http.Request, ar *requests.AuthorizationRequest) error {
   149  	g.expireAuthCookies(w, r)
   150  	w.WriteHeader(401)
   151  	w.Write([]byte(`401 Unauthorized`))
   152  	return ar.Response.Error
   153  }
   154  
   155  // handleAuthorizeWithBadRequest handles failed authorization requests where
   156  // user data was insufficient to establish a user.
   157  func (g *Gatekeeper) handleAuthorizeWithBadRequest(w http.ResponseWriter, r *http.Request, ar *requests.AuthorizationRequest) error {
   158  	g.expireAuthCookies(w, r)
   159  	w.WriteHeader(400)
   160  	w.Write([]byte(`400 Bad Request`))
   161  	return ar.Response.Error
   162  }
   163  
   164  // handleAuthorizeWithForbidden handles forbidden responses.
   165  func (g *Gatekeeper) handleAuthorizeWithForbidden(w http.ResponseWriter, r *http.Request, ar *requests.AuthorizationRequest) error {
   166  	if g.config.ForbiddenURL == "" {
   167  		w.WriteHeader(403)
   168  		w.Write([]byte(`Forbidden`))
   169  		return ar.Response.Error
   170  	}
   171  
   172  	if strings.Contains(g.config.ForbiddenURL, "{") && strings.Contains(g.config.ForbiddenURL, "}") {
   173  		// Run through placeholder replacer.
   174  		redirectLocation := g.config.ForbiddenURL
   175  		for _, placeholder := range placeholders {
   176  			switch placeholder {
   177  			case "uri", "http.request.uri":
   178  				redirectLocation = strings.ReplaceAll(redirectLocation, "{"+placeholder+"}", r.URL.String())
   179  			case "url":
   180  				redirectLocation = strings.ReplaceAll(redirectLocation, "{"+placeholder+"}", util.GetCurrentURL(r))
   181  			}
   182  		}
   183  		w.Header().Set("Location", redirectLocation)
   184  	} else {
   185  		w.Header().Set("Location", g.config.ForbiddenURL)
   186  	}
   187  	w.WriteHeader(303)
   188  	w.Write([]byte(`Forbidden`))
   189  	return ar.Response.Error
   190  }
   191  
   192  func (g *Gatekeeper) handleAuthorizeWithRedirect(w http.ResponseWriter, r *http.Request, ar *requests.AuthorizationRequest) error {
   193  	if ar.Redirect.AuthURL == "" {
   194  		ar.Redirect.AuthURL = g.config.AuthURLPath
   195  	}
   196  
   197  	ar.Redirect.QueryDisabled = g.config.AuthRedirectQueryDisabled
   198  	ar.Redirect.QueryParameter = g.config.AuthRedirectQueryParameter
   199  	if g.config.AuthRedirectStatusCode > 0 {
   200  		ar.Redirect.StatusCode = g.config.AuthRedirectStatusCode
   201  	}
   202  
   203  	if len(g.config.LoginHintValidators) > 0 {
   204  		g.handleLoginHint(r, ar)
   205  	}
   206  
   207  	if g.config.AdditionalScopes {
   208  		g.handleAdditionalScopes(r, ar)
   209  	}
   210  
   211  	if g.config.RedirectWithJavascript {
   212  		g.logger.Debug(
   213  			"redirecting unauthorized user",
   214  			zap.String("session_id", ar.SessionID),
   215  			zap.String("request_id", ar.ID),
   216  			zap.String("method", "js"),
   217  		)
   218  		handlers.HandleJavascriptRedirect(w, r, ar)
   219  	} else {
   220  		g.logger.Debug(
   221  			"redirecting unauthorized user",
   222  			zap.String("session_id", ar.SessionID),
   223  			zap.String("request_id", ar.ID),
   224  			zap.String("method", "location"),
   225  		)
   226  		handlers.HandleLocationHeaderRedirect(w, r, ar)
   227  	}
   228  	return ar.Response.Error
   229  }
   230  
   231  func (g *Gatekeeper) stripAuthToken(r *http.Request, usr *user.User) {
   232  	if !g.config.StripTokenEnabled {
   233  		return
   234  	}
   235  	switch usr.TokenSource {
   236  	case "cookie":
   237  		if usr.TokenName == "" {
   238  			return
   239  		}
   240  
   241  		if _, exists := r.Header["Cookie"]; !exists {
   242  			return
   243  		}
   244  
   245  		for i, entry := range r.Header["Cookie"] {
   246  			var updatedEntry []string
   247  			var updateCookie bool
   248  			for _, cookie := range strings.Split(entry, ";") {
   249  				s := strings.TrimSpace(cookie)
   250  				if strings.HasPrefix(s, usr.TokenName+"=") {
   251  					// Skip the cookie matching the token name.
   252  					updateCookie = true
   253  					continue
   254  				}
   255  				if strings.Contains(s, usr.Token) {
   256  					// Skip the cookie with the value matching user token.
   257  					updateCookie = true
   258  					continue
   259  				}
   260  				updatedEntry = append(updatedEntry, cookie)
   261  			}
   262  			if !updateCookie {
   263  				continue
   264  			}
   265  			r.Header["Cookie"][i] = strings.Join(updatedEntry, ";")
   266  		}
   267  	}
   268  }
   269  
   270  func (g *Gatekeeper) injectHeaders(r *http.Request, usr *user.User) {
   271  	if g.config.PassClaimsWithHeaders {
   272  		// Inject default X-Token headers.
   273  		headers := usr.GetRequestHeaders()
   274  		if headers == nil {
   275  			headers = make(map[string]string)
   276  			if usr.Claims.Name != "" {
   277  				headers["X-Token-User-Name"] = usr.Claims.Name
   278  			}
   279  			if usr.Claims.Email != "" {
   280  				headers["X-Token-User-Email"] = usr.Claims.Email
   281  			}
   282  			if len(usr.Claims.Roles) > 0 {
   283  				headers["X-Token-User-Roles"] = strings.Join(usr.Claims.Roles, " ")
   284  			}
   285  			if usr.Claims.Subject != "" {
   286  				headers["X-Token-Subject"] = usr.Claims.Subject
   287  			}
   288  			usr.SetRequestHeaders(headers)
   289  		}
   290  
   291  		for k, v := range headers {
   292  			if g.injectedHeaders != nil {
   293  				if _, exists := g.injectedHeaders[k]; exists {
   294  					continue
   295  				}
   296  			}
   297  			r.Header.Set(k, v)
   298  		}
   299  	}
   300  
   301  	// Inject custom headers.
   302  	for _, entry := range g.config.HeaderInjectionConfigs {
   303  		if v := usr.GetClaimValueByField(entry.Field); v != "" {
   304  			r.Header.Set(entry.Header, v)
   305  		}
   306  	}
   307  }
   308  
   309  func (g *Gatekeeper) handleLoginHint(r *http.Request, ar *requests.AuthorizationRequest) {
   310  	if loginHint := r.URL.Query().Get("login_hint"); loginHint != "" {
   311  		if err := validate.LoginHint(loginHint, g.config.LoginHintValidators); err != nil {
   312  			g.logger.Warn(err.Error())
   313  		} else {
   314  			ar.Redirect.LoginHint = loginHint
   315  		}
   316  	}
   317  }
   318  
   319  func (g *Gatekeeper) handleAdditionalScopes(r *http.Request, ar *requests.AuthorizationRequest) {
   320  	if additionalScopes := r.URL.Query().Get("additional_scopes"); additionalScopes != "" {
   321  		if err := validate.AdditionalScopes(additionalScopes); err != nil {
   322  			g.logger.Warn("Provide a valid set of additional scopes in the query parameter (ex.: scope_A scopeB)",
   323  				zap.String("additional_scopes", additionalScopes),
   324  				zap.Error(err),
   325  			)
   326  		} else {
   327  			ar.Redirect.AdditionalScopes = additionalScopes
   328  		}
   329  	}
   330  
   331  }