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 }