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