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 }