github.com/greenpau/go-authcrunch@v1.1.4/pkg/authn/handle_http_sandbox.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 "fmt" 20 "net/http" 21 "strings" 22 23 "github.com/greenpau/go-authcrunch/pkg/authn/enums/operator" 24 "github.com/greenpau/go-authcrunch/pkg/identity" 25 "github.com/greenpau/go-authcrunch/pkg/identity/qr" 26 "github.com/greenpau/go-authcrunch/pkg/requests" 27 "github.com/greenpau/go-authcrunch/pkg/user" 28 "github.com/greenpau/go-authcrunch/pkg/util" 29 addrutil "github.com/greenpau/go-authcrunch/pkg/util/addr" 30 "go.uber.org/zap" 31 ) 32 33 func (p *Portal) handleHTTPSandbox(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) error { 34 var sandboxID, sandboxPartition, sandboxSecret string 35 p.disableClientCache(w) 36 sandboxEndpoint, err := getEndpoint(r.URL.Path, "/sandbox/") 37 if err != nil { 38 p.logger.Debug( 39 "failed to parse sandbox id from url path", 40 zap.String("session_id", rr.Upstream.SessionID), 41 zap.String("request_id", rr.ID), 42 zap.Error(err), 43 ) 44 rr.Response.RedirectURL = rr.Upstream.BasePath 45 return p.handleHTTPError(ctx, w, r, rr, http.StatusBadRequest) 46 } 47 48 sandboxArr := strings.SplitN(sandboxEndpoint, "/", 2) 49 sandboxID = sandboxArr[0] 50 if len(sandboxArr) == 2 { 51 sandboxPartition = sandboxArr[1] 52 } 53 54 // Parse sandbox cookie and authenticate temporary session ID 55 // and secret. 56 for _, cookie := range r.Cookies() { 57 if cookie.Name != p.cookie.SandboxID { 58 continue 59 } 60 v := strings.TrimSpace(cookie.Value) 61 if v == "" { 62 continue 63 } 64 sandboxSecret = v 65 } 66 67 if sandboxSecret == "" { 68 p.logger.Debug( 69 "failed sandbox request", 70 zap.String("session_id", rr.Upstream.SessionID), 71 zap.String("request_id", rr.ID), 72 zap.String("error", "sandbox secret not found"), 73 ) 74 rr.Response.RedirectURL = rr.Upstream.BasePath 75 return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized) 76 } 77 78 usr, err := p.sandboxes.Get(sandboxID) 79 if err != nil { 80 p.logger.Debug( 81 "failed to extract cached entry from sandbox", 82 zap.String("session_id", rr.Upstream.SessionID), 83 zap.String("request_id", rr.ID), 84 zap.Error(err), 85 ) 86 rr.Response.RedirectURL = rr.Upstream.BasePath 87 return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized) 88 } 89 90 if usr.Authenticator.TempSecret != sandboxSecret { 91 p.logger.Debug( 92 "failed sandbox request", 93 zap.String("session_id", rr.Upstream.SessionID), 94 zap.String("request_id", rr.ID), 95 zap.String("error", "temp secret mismatch"), 96 ) 97 rr.Response.RedirectURL = rr.Upstream.BasePath 98 return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized) 99 } 100 101 if usr.Authenticator.TempSessionID != sandboxID { 102 p.logger.Debug( 103 "failed sandbox request", 104 zap.String("session_id", rr.Upstream.SessionID), 105 zap.String("request_id", rr.ID), 106 zap.String("error", "sandbox id mismatch"), 107 ) 108 rr.Response.RedirectURL = rr.Upstream.BasePath 109 return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized) 110 } 111 112 // Handle auxiliary functions, e.g. QR code and sandbox termination. 113 switch { 114 case strings.HasPrefix(sandboxPartition, "mfa-app-barcode/"): 115 // Handle App Portal barcode. 116 sandboxPartition = strings.TrimPrefix(sandboxPartition, "mfa-app-barcode/") 117 return p.handleHTTPSandboxMfaBarcode(ctx, w, r, sandboxPartition) 118 case sandboxPartition == "terminate": 119 p.sandboxes.Delete(sandboxID) 120 return p.handleHTTPRedirectSeeOther(ctx, w, r, rr, "login") 121 } 122 123 p.logger.Debug( 124 "user authorization sandbox", 125 zap.String("sandbox_id", sandboxID), 126 zap.String("sandbox_secret", sandboxSecret), 127 zap.String("sandbox_partition", sandboxPartition), 128 zap.Any("checkpoints", usr.Checkpoints), 129 ) 130 131 // Populate username (sub) and email address (email) 132 rr.User.Username = usr.Claims.Subject 133 rr.User.Email = usr.Claims.Email 134 135 data, err := p.nextSandboxCheckpoint(r, rr, usr, sandboxPartition) 136 if err != nil { 137 p.logger.Warn( 138 "user authorization checkpoint failed", 139 zap.String("session_id", rr.Upstream.SessionID), 140 zap.String("request_id", rr.ID), 141 zap.Error(err), 142 ) 143 data["error"] = err.Error() 144 } else { 145 p.logger.Debug( 146 "next user authorization checkpoint", 147 zap.String("session_id", rr.Upstream.SessionID), 148 zap.String("request_id", rr.ID), 149 zap.Any("data", data), 150 ) 151 } 152 153 if _, exists := data["view"]; exists { 154 switch data["view"] { 155 case "terminate": 156 p.sandboxes.Delete(sandboxID) 157 case "redirect": 158 return p.handleHTTPRedirectSeeOther(ctx, w, r, rr, "sandbox/"+sandboxID) 159 } 160 } 161 162 if rr.Response.Code == 0 { 163 rr.Response.Code = http.StatusOK 164 } 165 166 if _, exists := data["authorized"]; exists { 167 // The user passed all authorization checkpoints. 168 p.logger.Info( 169 "user passed all authorization checkpoints", 170 zap.String("session_id", rr.Upstream.SessionID), 171 zap.String("request_id", rr.ID), 172 zap.Any("checkpoints", usr.Checkpoints), 173 ) 174 p.grantAccess(ctx, w, r, rr, usr) 175 w.WriteHeader(rr.Response.Code) 176 return nil 177 } 178 179 // Handle the processing of user views, e.g. app or U2F tokens, etc. 180 resp := p.ui.GetArgs() 181 resp.PageTitle = "User Authorization" 182 if _, exists := data["title"]; exists { 183 resp.PageTitle = data["title"].(string) 184 } 185 resp.BaseURL(rr.Upstream.BasePath) 186 resp.Data["id"] = sandboxID 187 for k, v := range data { 188 resp.Data[k] = v 189 } 190 191 content, err := p.ui.Render("sandbox", resp) 192 if err != nil { 193 return p.handleHTTPRenderError(ctx, w, r, rr, err) 194 } 195 return p.handleHTTPRenderHTML(ctx, w, rr.Response.Code, content.Bytes()) 196 } 197 198 func (p *Portal) nextSandboxCheckpoint(r *http.Request, rr *requests.Request, usr *user.User, action string) (map[string]interface{}, error) { 199 var verifiedCount int 200 m := make(map[string]interface{}) 201 backend := p.getIdentityStoreByRealm(usr.Authenticator.Realm) 202 if backend == nil { 203 m["title"] = "Internal Server Error" 204 m["view"] = "terminate" 205 return m, fmt.Errorf("Authentication realm not found") 206 } 207 rr.Upstream.Method = backend.GetKind() 208 rr.Upstream.Realm = backend.GetRealm() 209 210 for _, checkpoint := range usr.Checkpoints { 211 if !checkpoint.Passed { 212 continue 213 } 214 switch checkpoint.Type { 215 case "password", "mfa": 216 verifiedCount++ 217 } 218 } 219 220 for _, checkpoint := range usr.Checkpoints { 221 if checkpoint.Passed { 222 continue 223 } 224 if checkpoint.FailedAttempts > 5 { 225 rr.Response.Code = http.StatusForbidden 226 m["title"] = "Authorization Failed" 227 m["view"] = "terminate" 228 return m, fmt.Errorf("You have failed a number of security challenges. Thus, your session failed to meet authorization requirements") 229 } 230 switch checkpoint.Type { 231 case "password": 232 if r.Method != "POST" { 233 switch action { 234 case "password-recovery": 235 m["title"] = "Password Recovery" 236 m["view"] = "password_recovery" 237 m["action"] = "auth" 238 default: 239 m["title"] = "Authentication" 240 m["view"] = "password_auth" 241 m["action"] = "auth" 242 } 243 return m, nil 244 } 245 switch action { 246 case "password-recovery": 247 rr.Response.Code = http.StatusNotImplemented 248 // User recovers a password 249 m["title"] = "Password Recovery Failed" 250 m["view"] = "terminate" 251 return m, fmt.Errorf("Password recovery failed. Please retry") 252 default: 253 // Handle password authentication. 254 if err := validateSandboxPasswordForm(r, rr); err != nil { 255 checkpoint.FailedAttempts++ 256 rr.Response.Code = http.StatusBadRequest 257 m["title"] = "Authentication Failed" 258 m["view"] = "error" 259 p.logger.Warn( 260 "invalid password for submission", 261 zap.String("session_id", rr.Upstream.SessionID), 262 zap.String("request_id", rr.ID), 263 zap.String("src_ip", addrutil.GetSourceAddress(r)), 264 zap.String("src_conn_ip", addrutil.GetSourceConnAddress(r)), 265 zap.Int("checkpoint_id", checkpoint.ID), 266 zap.String("checkpoint_name", checkpoint.Name), 267 zap.String("checkpoint_type", checkpoint.Type), 268 ) 269 return m, err 270 } 271 rr.Flags.Enabled = true 272 if err := backend.Request(operator.Authenticate, rr); err != nil { 273 rr.Response.Code = http.StatusUnauthorized 274 checkpoint.FailedAttempts++ 275 m["title"] = "Authentication Failed" 276 m["view"] = "error" 277 p.logger.Warn( 278 "password authentication failed", 279 zap.String("session_id", rr.Upstream.SessionID), 280 zap.String("request_id", rr.ID), 281 zap.Int("checkpoint_id", checkpoint.ID), 282 zap.String("src_ip", addrutil.GetSourceAddress(r)), 283 zap.String("src_conn_ip", addrutil.GetSourceConnAddress(r)), 284 zap.String("checkpoint_name", checkpoint.Name), 285 zap.String("checkpoint_type", checkpoint.Type), 286 ) 287 return m, fmt.Errorf("Password authentication failed. Please retry") 288 } 289 p.logger.Info( 290 "user authorization checkpoint passed", 291 zap.String("session_id", rr.Upstream.SessionID), 292 zap.String("request_id", rr.ID), 293 zap.Int("checkpoint_id", checkpoint.ID), 294 zap.String("checkpoint_name", checkpoint.Name), 295 zap.String("checkpoint_type", checkpoint.Type), 296 ) 297 checkpoint.Passed = true 298 checkpoint.FailedAttempts = 0 299 verifiedCount++ 300 m["view"] = "redirect" 301 return m, nil 302 } 303 case "mfa": 304 if err := backend.Request(operator.GetMfaTokens, rr); err != nil { 305 checkpoint.FailedAttempts++ 306 m["title"] = "Authorization Failed" 307 m["view"] = "error" 308 return m, err 309 } 310 var configured, appConfigured, uniConfigured bool 311 bundle := rr.Response.Payload.(*identity.MfaTokenBundle) 312 for _, token := range bundle.Get() { 313 switch token.Type { 314 case "totp": 315 configured = true 316 appConfigured = true 317 case "u2f": 318 configured = true 319 uniConfigured = true 320 } 321 } 322 323 switch { 324 case !configured && (action == ""): 325 m["title"] = "Token Registration" 326 m["view"] = "mfa_mixed_register" 327 m["action"] = "register" 328 case appConfigured && uniConfigured && (action == ""): 329 m["title"] = "Token Selection" 330 m["view"] = "mfa_mixed_auth" 331 m["action"] = "auth" 332 case appConfigured && (action == "mfa-app-auth" || action == ""): 333 m["title"] = "Authenticator App" 334 m["view"] = "mfa_app_auth" 335 m["action"] = "auth" 336 if r.Method != "POST" { 337 break 338 } 339 // Handle authenticator app passcode. 340 if err := validateMfaAuthTokenForm(r, rr); err != nil { 341 m["title"] = "Authorization Failed" 342 m["view"] = "error" 343 return m, err 344 } 345 var tokenErrors []string 346 var tokenValidated bool 347 for _, token := range bundle.Get() { 348 if token.Type != "totp" { 349 continue 350 } 351 if token.Disabled { 352 continue 353 } 354 if err := token.ValidateCode(rr.MfaToken.Passcode); err != nil { 355 tokenErrors = append(tokenErrors, err.Error()) 356 continue 357 } 358 tokenValidated = true 359 break 360 } 361 if tokenValidated { 362 // If validated successfully, continue. 363 p.logger.Info( 364 "user authorization checkpoint passed", 365 zap.String("session_id", rr.Upstream.SessionID), 366 zap.String("request_id", rr.ID), 367 zap.Int("checkpoint_id", checkpoint.ID), 368 zap.String("checkpoint_name", checkpoint.Name), 369 zap.String("checkpoint_type", checkpoint.Type), 370 ) 371 checkpoint.Passed = true 372 checkpoint.FailedAttempts = 0 373 verifiedCount++ 374 m["view"] = "redirect" 375 return m, nil 376 } 377 if len(tokenErrors) == 0 { 378 tokenErrors = append(tokenErrors, "No available application tokens found") 379 } 380 m["view"] = "error" 381 checkpoint.FailedAttempts++ 382 return m, fmt.Errorf(strings.Join(tokenErrors, "\n")) 383 case uniConfigured && (action == "mfa-u2f-auth" || action == ""): 384 m["title"] = "Hardware Token" 385 m["view"] = "mfa_u2f_auth" 386 m["action"] = "auth" 387 if r.Method == "POST" { 388 if err := validateAuthU2FTokenForm(r, rr); err != nil { 389 m["view"] = "error" 390 checkpoint.FailedAttempts++ 391 return m, err 392 } 393 rr.WebAuthn.Challenge = usr.Authenticator.TempChallenge 394 if err := backend.Request(operator.Authenticate, rr); err != nil { 395 m["view"] = "error" 396 checkpoint.FailedAttempts++ 397 return m, fmt.Errorf("Token verification failed. Please retry") 398 } 399 checkpoint.Passed = true 400 checkpoint.FailedAttempts = 0 401 verifiedCount++ 402 m["view"] = "redirect" 403 return m, nil 404 } 405 if err := backend.Request(operator.GetMfaTokens, rr); err != nil { 406 m["view"] = "error" 407 checkpoint.FailedAttempts++ 408 return m, err 409 } 410 bundle := rr.Response.Payload.(*identity.MfaTokenBundle) 411 creds := []map[string]interface{}{} 412 for _, t := range bundle.Get() { 413 if t.Type != "u2f" { 414 continue 415 } 416 cred := make(map[string]interface{}) 417 cred["id"] = t.Parameters["u2f_id"] 418 cred["type"] = t.Parameters["u2f_type"] 419 cred["transports"] = strings.Split(t.Parameters["u2f_transports"], ",") 420 creds = append(creds, cred) 421 } 422 usr.Authenticator.TempChallenge = util.GetRandomString(64) 423 m["webauthn_challenge"] = usr.Authenticator.TempChallenge 424 m["webauthn_rp_name"] = "AUTHP" 425 m["webauthn_timeout"] = "60000" 426 m["webauthn_user_verification"] = "discouraged" 427 m["webauthn_ext_uvm"] = "false" 428 m["webauthn_ext_loc"] = "false" 429 m["webauthn_tx_auth_simple"] = "Could you please verify yourself?" 430 m["webauthn_credentials"] = creds 431 case !appConfigured && (action == "mfa-app-register"): 432 m["title"] = "Authenticator App Registration" 433 m["view"] = "mfa_app_register" 434 m["action"] = "register" 435 if r.Method == "POST" { 436 // Perform the validation of the newly registered token. 437 if err := validateAddMfaTokenForm(r, rr); err != nil { 438 m["view"] = "error" 439 checkpoint.FailedAttempts++ 440 return m, err 441 } 442 if err := backend.Request(operator.AddMfaToken, rr); err != nil { 443 m["view"] = "error" 444 checkpoint.FailedAttempts++ 445 return m, err 446 } 447 checkpoint.Passed = true 448 checkpoint.FailedAttempts = 0 449 verifiedCount++ 450 m["view"] = "redirect" 451 return m, nil 452 } 453 // Display QR code for token registration. 454 qr := qr.NewCode() 455 qr.Secret = util.GetRandomStringFromRange(64, 92) 456 qr.Type = "totp" 457 qr.Label = fmt.Sprintf("AUTHP:%s", usr.Claims.Email) 458 qr.Period = 30 459 qr.Issuer = "AUTHP" 460 qr.Digits = 6 461 if err := qr.Build(); err != nil { 462 return m, fmt.Errorf("Failed creating QR code: %v", err) 463 } 464 m["mfa_label"] = qr.Issuer 465 m["mfa_comment"] = "My Authentication App" 466 m["mfa_email"] = usr.Claims.Email 467 m["mfa_type"] = qr.Type 468 m["mfa_secret"] = qr.Secret 469 m["mfa_period"] = fmt.Sprintf("%d", qr.Period) 470 m["mfa_digits"] = fmt.Sprintf("%d", qr.Digits) 471 m["code_uri"] = qr.Get() 472 m["code_uri_encoded"] = qr.GetEncoded() 473 case !uniConfigured && (action == "mfa-u2f-register"): 474 m["title"] = "Hardware Token Registration" 475 m["view"] = "mfa_u2f_register" 476 m["action"] = "register" 477 if r.Method == "POST" { 478 if err := validateAddU2FTokenForm(r, rr); err != nil { 479 m["view"] = "error" 480 checkpoint.FailedAttempts++ 481 return m, err 482 } 483 if err := backend.Request(operator.AddMfaToken, rr); err != nil { 484 m["view"] = "error" 485 checkpoint.FailedAttempts++ 486 return m, err 487 } 488 checkpoint.Passed = true 489 checkpoint.FailedAttempts = 0 490 verifiedCount++ 491 m["view"] = "redirect" 492 return m, nil 493 } 494 // Display U2F registration. 495 usr.Authenticator.TempChallenge = util.GetRandomStringFromRange(64, 92) 496 m["webauthn_challenge"] = usr.Authenticator.TempChallenge 497 m["webauthn_rp_name"] = "AUTHP" 498 m["webauthn_user_id"] = usr.Claims.ID 499 m["webauthn_user_email"] = usr.Claims.Email 500 m["webauthn_user_verification"] = "discouraged" 501 m["webauthn_attestation"] = "direct" 502 if usr.Claims.Name == "" { 503 m["webauthn_user_display_name"] = usr.Claims.Subject 504 } else { 505 m["webauthn_user_display_name"] = usr.Claims.Name 506 } 507 default: 508 checkpoint.FailedAttempts++ 509 m["title"] = "Bad Request" 510 m["view"] = "error" 511 return m, fmt.Errorf("Detected unsupported MFA authorization type") 512 } 513 if !checkpoint.Passed { 514 return m, nil 515 } 516 default: 517 checkpoint.FailedAttempts++ 518 m["title"] = "Bad Request" 519 m["view"] = "error" 520 return m, fmt.Errorf("Detected unsupported authorization type: %v", checkpoint.Type) 521 } 522 } 523 524 if (verifiedCount > 0) && (len(usr.Checkpoints) == verifiedCount) { 525 m["authorized"] = true 526 } 527 return m, nil 528 }