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 }