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