github.com/greenpau/go-authcrunch@v1.1.4/pkg/authn/handle_http_login.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 "net/url" 22 "path" 23 "strings" 24 "time" 25 26 "github.com/greenpau/go-authcrunch/pkg/authn/enums/operator" 27 "github.com/greenpau/go-authcrunch/pkg/idp" 28 "github.com/greenpau/go-authcrunch/pkg/ids" 29 "github.com/greenpau/go-authcrunch/pkg/requests" 30 "github.com/greenpau/go-authcrunch/pkg/user" 31 "github.com/greenpau/go-authcrunch/pkg/util" 32 addrutil "github.com/greenpau/go-authcrunch/pkg/util/addr" 33 "go.uber.org/zap" 34 ) 35 36 func (p *Portal) handleHTTPLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, usr *user.User) error { 37 p.injectRedirectURL(ctx, w, r, rr) 38 if usr != nil { 39 return p.handleHTTPRedirect(ctx, w, r, rr, "/portal") 40 } 41 if r.Method != "POST" { 42 return p.handleHTTPLoginScreen(ctx, w, r, rr) 43 } 44 45 return p.handleHTTPLoginRequest(ctx, w, r, rr) 46 } 47 48 func (p *Portal) handleHTTPLoginScreen(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) error { 49 resp := p.ui.GetArgs() 50 resp.BaseURL(rr.Upstream.BasePath) 51 if p.config.UI.Title == "" { 52 resp.PageTitle = "Sign In" 53 } else { 54 resp.PageTitle = p.config.UI.Title 55 } 56 resp.Data["authenticated"] = rr.Response.Authenticated 57 resp.Data["login_options"] = p.loginOptions 58 59 content, err := p.ui.Render("login", resp) 60 if err != nil { 61 return p.handleHTTPRenderError(ctx, w, r, rr, err) 62 } 63 return p.handleHTTPRenderHTML(ctx, w, http.StatusOK, content.Bytes()) 64 } 65 66 func (p *Portal) getIdentityProviderByRealm(realm string) idp.IdentityProvider { 67 for _, provider := range p.identityProviders { 68 if provider.GetRealm() == realm { 69 return provider 70 } 71 } 72 return nil 73 } 74 75 func (p *Portal) getIdentityStoreByRealm(realm string) ids.IdentityStore { 76 for _, store := range p.identityStores { 77 if store.GetRealm() == realm { 78 return store 79 } 80 } 81 return nil 82 } 83 84 func (p *Portal) getAuthenticatorByRealm(realm string) map[string]string { 85 if store := p.getIdentityStoreByRealm(realm); store != nil { 86 return map[string]string{ 87 "name": store.GetName(), 88 "realm": store.GetRealm(), 89 "kind": store.GetKind(), 90 } 91 } 92 if provider := p.getIdentityProviderByRealm(realm); provider != nil { 93 return map[string]string{ 94 "name": provider.GetName(), 95 "realm": provider.GetRealm(), 96 "kind": provider.GetKind(), 97 } 98 } 99 100 return nil 101 } 102 103 // handleHTTPLoginRequest handles the processing of user id/email and optional 104 // authentication realm. The requester gets redirected to sandbox for 105 // authentication. 106 func (p *Portal) handleHTTPLoginRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) error { 107 p.disableClientCache(w) 108 if r.Method != "POST" { 109 return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized) 110 } 111 r.Body = http.MaxBytesReader(w, r.Body, 1024) 112 113 identity, err := util.ParseIdentity(r) 114 if err != nil { 115 return p.handleHTTPErrorWithLog(ctx, w, r, rr, http.StatusUnauthorized, err.Error()) 116 } 117 118 // Identify the backend associated with the user and determine challenges. 119 if err := p.identifyUserRequest(rr, identity); err != nil { 120 rr.Response.Code = http.StatusBadRequest 121 return p.handleHTTPErrorWithLog(ctx, w, r, rr, rr.Response.Code, err.Error()) 122 } 123 124 // Create a temporary user. 125 m := make(map[string]interface{}) 126 m["sub"] = rr.User.Username 127 m["email"] = rr.User.Email 128 if rr.User.FullName != "" { 129 m["name"] = rr.User.FullName 130 } 131 if len(rr.User.Roles) > 0 { 132 m["roles"] = rr.User.Roles 133 } 134 m["jti"] = rr.Upstream.SessionID 135 m["exp"] = time.Now().Add(time.Duration(5) * time.Second).UTC().Unix() 136 m["iat"] = time.Now().UTC().Unix() 137 m["nbf"] = time.Now().Add(time.Duration(60) * time.Second * -1).UTC().Unix() 138 if _, exists := m["origin"]; !exists { 139 m["origin"] = rr.Upstream.Realm 140 } 141 m["iss"] = util.GetIssuerURL(r) 142 m["addr"] = addrutil.GetSourceAddress(r) 143 144 combineGroupRoles(m) 145 146 // Perform user claim transformation if necessary. 147 if err := p.transformUser(ctx, rr, m); err != nil { 148 return err 149 } 150 151 // Inject portal-specific roles. 152 injectPortalRoles(m, p.config) 153 usr, err := user.NewUser(m) 154 if err != nil { 155 rr.Response.Code = http.StatusBadRequest 156 return p.handleHTTPErrorWithLog(ctx, w, r, rr, http.StatusBadRequest, err.Error()) 157 } 158 159 // Build a list of additional verification/acceptance challenges. 160 if err := p.injectUserChallenges(usr, m, rr.User.Challenges); err != nil { 161 p.logger.Warn( 162 "user checkpoint injection failed", 163 zap.String("session_id", rr.Upstream.SessionID), 164 zap.String("request_id", rr.ID), 165 zap.Any("user", m), 166 zap.Any("challenges", rr.User.Challenges), 167 zap.Error(err), 168 ) 169 rr.Response.Code = http.StatusInternalServerError 170 return err 171 } 172 173 // Build a list of additional user-specific UI links. 174 if v, exists := m["frontend_links"]; exists { 175 if err := usr.AddFrontendLinks(v); err != nil { 176 p.logger.Warn( 177 "frontend link creation failed", 178 zap.String("session_id", rr.Upstream.SessionID), 179 zap.String("request_id", rr.ID), 180 zap.Any("user", m), 181 zap.Error(err), 182 ) 183 rr.Response.Code = http.StatusInternalServerError 184 return err 185 } 186 } 187 188 usr.Authenticator.Name = rr.Upstream.Name 189 usr.Authenticator.Realm = rr.Upstream.Realm 190 usr.Authenticator.Method = rr.Upstream.Method 191 192 // Grant temporary cookie and redirect to sandbox URL for authentication. 193 usr.Authenticator.TempSessionID = util.GetRandomStringFromRange(36, 48) 194 usr.Authenticator.TempSecret = util.GetRandomStringFromRange(36, 48) 195 if err := p.sandboxes.Add(usr.Authenticator.TempSessionID, usr); err != nil { 196 rr.Response.Code = http.StatusInternalServerError 197 return p.handleHTTPErrorWithLog(ctx, w, r, rr, http.StatusInternalServerError, err.Error()) 198 } 199 redirectLocation := fmt.Sprintf("%s%s/%s", 200 rr.Upstream.BaseURL, 201 path.Join(rr.Upstream.BasePath, "/sandbox/"), 202 usr.Authenticator.TempSessionID, 203 ) 204 205 w.Header().Set("Set-Cookie", p.cookie.GetCookie(addrutil.GetSourceHost(r), p.cookie.SandboxID, usr.Authenticator.TempSecret)) 206 w.Header().Set("Location", redirectLocation) 207 w.WriteHeader(http.StatusSeeOther) 208 return nil 209 } 210 211 func (p *Portal) injectUserChallenges(usr *user.User, data map[string]interface{}, chals []string) error { 212 var entries []string 213 entries = append(entries, chals...) 214 entryMap := make(map[string]bool) 215 for _, chal := range chals { 216 entryMap[chal] = true 217 } 218 219 if v, exists := data["challenges"]; exists { 220 switch challenges := v.(type) { 221 case []string: 222 for _, chal := range challenges { 223 if _, exists := entryMap[chal]; !exists { 224 entries = append(entries, chal) 225 entryMap[chal] = true 226 } 227 } 228 default: 229 return fmt.Errorf("malformed challenges") 230 } 231 } 232 233 checkpoints, err := user.NewCheckpoints(entries) 234 if err != nil { 235 return err 236 } 237 if len(checkpoints) < 1 { 238 return fmt.Errorf("no checkpoints") 239 } 240 usr.Checkpoints = checkpoints 241 return nil 242 } 243 244 func (p *Portal) identifyUserRequest(rr *requests.Request, identity map[string]string) error { 245 // Identify the backend associated with the user. 246 backend := p.getIdentityStoreByRealm(identity["realm"]) 247 if backend == nil { 248 return fmt.Errorf("no matching realm found") 249 } 250 rr.Upstream.Name = backend.GetName() 251 rr.Upstream.Method = backend.GetKind() 252 rr.Upstream.Realm = backend.GetRealm() 253 rr.Flags.Enabled = true 254 rr.User.Username = identity["user"] 255 return backend.Request(operator.IdentifyUser, rr) 256 } 257 258 func (p *Portal) authenticateLoginRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, credentials map[string]string) error { 259 rr.User.Username = credentials["username"] 260 rr.User.Password = credentials["password"] 261 backend := p.getIdentityStoreByRealm(credentials["realm"]) 262 if backend == nil { 263 rr.Response.Code = http.StatusBadRequest 264 return fmt.Errorf("no matching realm found") 265 } 266 rr.Upstream.Method = backend.GetKind() 267 rr.Upstream.Realm = backend.GetRealm() 268 rr.Flags.Enabled = true 269 270 if err := backend.Request(operator.IdentifyUser, rr); err != nil { 271 rr.Response.Code = http.StatusUnauthorized 272 return err 273 } 274 275 if len(rr.User.Challenges) != 1 { 276 return fmt.Errorf("detected too many auth challenges") 277 } 278 if rr.User.Challenges[0] != "password" { 279 return fmt.Errorf("detected unsupported auth challenges") 280 } 281 if err := backend.Request(operator.Authenticate, rr); err != nil { 282 rr.Response.Code = http.StatusUnauthorized 283 return err 284 } 285 rr.Response.Code = http.StatusOK 286 return nil 287 } 288 289 func (p *Portal) authorizeLoginRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) error { 290 backend := p.getAuthenticatorByRealm(rr.Upstream.Realm) 291 if backend == nil { 292 rr.Response.Code = http.StatusBadRequest 293 return fmt.Errorf("no matching realm found") 294 } 295 296 m := make(map[string]interface{}) 297 298 switch rr.Upstream.Method { 299 case "oauth2", "saml": 300 switch pm := rr.Response.Payload.(type) { 301 case map[string]interface{}: 302 m = pm 303 // Process groups, group, role, roles. 304 default: 305 return fmt.Errorf("response payload not a map") 306 } 307 combineGroupRoles(m) 308 default: 309 m["sub"] = rr.User.Username 310 m["email"] = rr.User.Email 311 if rr.User.FullName != "" { 312 m["name"] = rr.User.FullName 313 } 314 if len(rr.User.Roles) > 0 { 315 m["roles"] = rr.User.Roles 316 } 317 } 318 319 m["jti"] = rr.Upstream.SessionID 320 m["exp"] = time.Now().Add(time.Duration(p.keystore.GetTokenLifetime(nil, nil)) * time.Second).UTC().Unix() 321 m["iat"] = time.Now().UTC().Unix() 322 m["nbf"] = time.Now().Add(time.Duration(60)*time.Second*-1).UTC().Unix() * 1000 323 if _, exists := m["origin"]; !exists { 324 m["origin"] = rr.Upstream.Realm 325 } 326 m["iss"] = util.GetIssuerURL(r) 327 m["addr"] = addrutil.GetSourceAddress(r) 328 329 // Perform user claim transformation if necessary. 330 if err := p.transformUser(ctx, rr, m); err != nil { 331 return err 332 } 333 injectPortalRoles(m, p.config) 334 usr, err := user.NewUser(m) 335 if err != nil { 336 rr.Response.Code = http.StatusUnauthorized 337 return err 338 } 339 if err := p.keystore.SignToken(nil, nil, usr); err != nil { 340 p.logger.Warn( 341 "user token signing failed", 342 zap.String("session_id", rr.Upstream.SessionID), 343 zap.String("request_id", rr.ID), 344 zap.Any("user", m), 345 zap.Error(err), 346 ) 347 rr.Response.Code = http.StatusInternalServerError 348 return err 349 } 350 usr.Authenticator.Name = backend["name"] 351 usr.Authenticator.Realm = backend["realm"] 352 usr.Authenticator.Method = backend["kind"] 353 354 // Build a list of additional user-specific UI links. 355 if rr.Response.Workflow != "json-api" { 356 if v, exists := m["frontend_links"]; exists { 357 if err := usr.AddFrontendLinks(v); err != nil { 358 p.logger.Warn( 359 "frontend link creation failed", 360 zap.String("session_id", rr.Upstream.SessionID), 361 zap.String("request_id", rr.ID), 362 zap.Any("user", m), 363 zap.Error(err), 364 ) 365 rr.Response.Code = http.StatusInternalServerError 366 return err 367 } 368 } 369 } 370 371 p.logger.Info( 372 "Successful login", 373 zap.String("session_id", rr.Upstream.SessionID), 374 zap.String("request_id", rr.ID), 375 zap.Any("backend", usr.Authenticator), 376 zap.Any("user", m), 377 ) 378 p.grantAccess(ctx, w, r, rr, usr) 379 return nil 380 } 381 382 func (p *Portal) grantAccess(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, usr *user.User) { 383 var redirectLocation string 384 385 usr.SetExpiresAtClaim(time.Now().Add(time.Duration(p.keystore.GetTokenLifetime(nil, nil)) * time.Second).UTC().Unix()) 386 usr.SetIssuedAtClaim(time.Now().UTC().Unix()) 387 usr.SetNotBeforeClaim(time.Now().Add(time.Duration(60) * time.Second * -1).UTC().Unix()) 388 389 if err := p.keystore.SignToken(nil, nil, usr); err != nil { 390 p.logger.Warn( 391 "user token signing failed", 392 zap.String("session_id", rr.Upstream.SessionID), 393 zap.String("request_id", rr.ID), 394 zap.Error(err), 395 ) 396 rr.Response.Code = http.StatusInternalServerError 397 return 398 } 399 400 h := addrutil.GetSourceHost(r) 401 402 rr.Response.Authenticated = true 403 usr.Authorized = true 404 p.sessions.Add(rr.Upstream.SessionID, usr) 405 406 w.Header().Set("Authorization", "Bearer "+usr.Token) 407 w.Header().Set("Set-Cookie", p.cookie.GetCookie(h, usr.TokenName, usr.Token)) 408 409 // Add a cookie with identity token, if id_token is available. 410 if rr.Response.IdentityTokenCookie.Enabled { 411 w.Header().Add("Set-Cookie", p.cookie.GetIdentityTokenCookie(rr.Response.IdentityTokenCookie.Name, rr.Response.IdentityTokenCookie.Payload)) 412 } 413 414 if rr.Response.Workflow == "json-api" { 415 // Do not perform redirects to API logins. 416 rr.Response.Code = http.StatusOK 417 return 418 } 419 420 // Delete sandbox cookie, if present. 421 w.Header().Add("Set-Cookie", p.cookie.GetDeleteCookie(h, p.cookie.SandboxID)) 422 423 // Determine whether redirect cookie is present and reditect to the page that 424 // forwarded a user to the authentication portal. 425 if cookie, err := r.Cookie(p.cookie.Referer); err == nil { 426 if redirectURL, err := url.Parse(cookie.Value); err == nil { 427 redirectLocation = redirectURL.String() 428 p.logger.Debug( 429 "Detected cookie-based redirect", 430 zap.String("session_id", rr.Upstream.SessionID), 431 zap.String("request_id", rr.ID), 432 zap.String("redirect_url", redirectLocation), 433 ) 434 w.Header().Add("Set-Cookie", p.cookie.GetDeleteCookie(h, p.cookie.Referer)) 435 } 436 } 437 if redirectLocation == "" { 438 // Redirect authenticated user to portal page when no redirect cookie found. 439 redirectLocation = rr.Upstream.BaseURL + path.Join(rr.Upstream.BasePath, "/portal") 440 } 441 w.Header().Set("Location", redirectLocation) 442 rr.Response.Code = http.StatusSeeOther 443 return 444 } 445 446 func combineGroupRoles(m map[string]interface{}) { 447 var roles []string 448 roleMap := make(map[string]interface{}) 449 450 for _, k := range []string{"roles", "role", "group", "groups"} { 451 if v, exists := m[k]; exists { 452 switch val := v.(type) { 453 case string: 454 if _, found := roleMap[val]; !found { 455 roleMap[val] = true 456 roles = append(roles, val) 457 } 458 case []string: 459 for _, va := range val { 460 if _, found := roleMap[va]; !found { 461 roleMap[va] = true 462 roles = append(roles, va) 463 } 464 } 465 case []interface{}: 466 for _, entry := range val { 467 switch e := entry.(type) { 468 case string: 469 if _, found := roleMap[e]; !found { 470 roleMap[e] = true 471 roles = append(roles, e) 472 } 473 } 474 } 475 } 476 delete(m, k) 477 } 478 } 479 if len(roles) > 0 { 480 m["roles"] = roles 481 } 482 } 483 484 func injectPortalRoles(m map[string]interface{}, cfg *PortalConfig) { 485 var roles, updatedRoles []string 486 var reservedRoleFound bool 487 roleMap := make(map[string]bool) 488 reservedRoles := cfg.GetReservedPortalRoles() 489 490 v, exists := m["roles"] 491 if !exists { 492 guestRoles := []string{} 493 for _, roleName := range cfg.GetGuestPortalRoles() { 494 guestRoles = append(guestRoles, roleName) 495 } 496 m["roles"] = guestRoles 497 return 498 } 499 switch val := v.(type) { 500 case string: 501 roles = strings.Split(val, " ") 502 case []string: 503 roles = val 504 case []interface{}: 505 for _, entry := range val { 506 switch e := entry.(type) { 507 case string: 508 roles = append(roles, e) 509 } 510 } 511 } 512 for _, roleName := range roles { 513 roleName = strings.TrimSpace(roleName) 514 if roleName == "" { 515 continue 516 } 517 if _, exists := roleMap[roleName]; exists { 518 continue 519 } 520 if _, exists := reservedRoles[roleName]; exists { 521 reservedRoles[roleName] = true 522 reservedRoleFound = true 523 } 524 roleMap[roleName] = true 525 updatedRoles = append(updatedRoles, roleName) 526 } 527 if !reservedRoleFound { 528 updatedRoles = append(updatedRoles, defaultGuestRoleName) 529 } 530 m["roles"] = updatedRoles 531 return 532 } 533 534 func (p *Portal) transformUser(ctx context.Context, rr *requests.Request, m map[string]interface{}) error { 535 if p.transformer == nil { 536 return nil 537 } 538 if rr.Upstream.Realm != "" { 539 m["realm"] = rr.Upstream.Realm 540 } 541 if err := p.transformer.Transform(m); err != nil { 542 p.logger.Warn( 543 "user transformation failed", 544 zap.String("session_id", rr.Upstream.SessionID), 545 zap.String("request_id", rr.ID), 546 zap.Any("user", m), 547 zap.Error(err), 548 ) 549 if strings.HasSuffix(err.Error(), "block/deny") { 550 rr.Response.Code = http.StatusForbidden 551 } else { 552 rr.Response.Code = http.StatusInternalServerError 553 } 554 return err 555 } 556 p.logger.Debug( 557 "user transformation ended", 558 zap.String("session_id", rr.Upstream.SessionID), 559 zap.String("request_id", rr.ID), 560 zap.Any("user", m), 561 ) 562 return nil 563 }