github.com/greenpau/go-authcrunch@v1.0.50/pkg/authn/portal.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 "os" 20 "sort" 21 22 "github.com/greenpau/go-authcrunch/pkg/acl" 23 "github.com/greenpau/go-authcrunch/pkg/authn/cache" 24 "github.com/greenpau/go-authcrunch/pkg/authn/cookie" 25 "github.com/greenpau/go-authcrunch/pkg/authn/icons" 26 "github.com/greenpau/go-authcrunch/pkg/authn/transformer" 27 "github.com/greenpau/go-authcrunch/pkg/authn/ui" 28 "github.com/greenpau/go-authcrunch/pkg/authz/options" 29 "github.com/greenpau/go-authcrunch/pkg/authz/validator" 30 "github.com/greenpau/go-authcrunch/pkg/errors" 31 "github.com/greenpau/go-authcrunch/pkg/idp" 32 "github.com/greenpau/go-authcrunch/pkg/ids" 33 "github.com/greenpau/go-authcrunch/pkg/kms" 34 "github.com/greenpau/go-authcrunch/pkg/registry" 35 "github.com/greenpau/go-authcrunch/pkg/sso" 36 cfgutil "github.com/greenpau/go-authcrunch/pkg/util/cfg" 37 38 "fmt" 39 "path" 40 "strings" 41 "time" 42 43 "github.com/google/uuid" 44 "go.uber.org/zap" 45 ) 46 47 const ( 48 defaultPortalACLCondition = "match roles authp/admin authp/user authp/guest superuser superadmin" 49 defaultPortalACLAction = "allow stop" 50 ) 51 52 // Portal is an authentication portal. 53 type Portal struct { 54 id string 55 config *PortalConfig 56 userRegistry registry.UserRegistry 57 validator *validator.TokenValidator 58 keystore *kms.CryptoKeyStore 59 identityStores []ids.IdentityStore 60 identityProviders []idp.IdentityProvider 61 ssoProviders []sso.SingleSignOnProvider 62 cookie *cookie.Factory 63 transformer *transformer.Factory 64 ui *ui.Factory 65 startedAt time.Time 66 sessions *cache.SessionCache 67 sandboxes *cache.SandboxCache 68 loginOptions map[string]interface{} 69 logger *zap.Logger 70 } 71 72 // PortalParameters are input parameters for NewPortal. 73 type PortalParameters struct { 74 Config *PortalConfig `json:"config,omitempty" xml:"config,omitempty" yaml:"config,omitempty"` 75 Logger *zap.Logger `json:"logger,omitempty" xml:"logger,omitempty" yaml:"logger,omitempty"` 76 IdentityStores []ids.IdentityStore `json:"identity_stores,omitempty" xml:"identity_stores,omitempty" yaml:"identity_stores,omitempty"` 77 IdentityProviders []idp.IdentityProvider `json:"identity_providers,omitempty" xml:"identity_providers,omitempty" yaml:"identity_providers,omitempty"` 78 SingleSignOnProviders []sso.SingleSignOnProvider `json:"sso_providers,omitempty" xml:"sso_providers,omitempty" yaml:"sso_providers,omitempty"` 79 } 80 81 // NewPortal returns an instance of Portal. 82 func NewPortal(params PortalParameters) (*Portal, error) { 83 if params.Logger == nil { 84 return nil, errors.ErrNewPortalLoggerNil 85 } 86 if params.Config == nil { 87 return nil, errors.ErrNewPortalConfigNil 88 } 89 90 if err := params.Config.Validate(); err != nil { 91 return nil, errors.ErrNewPortal.WithArgs(err) 92 } 93 p := &Portal{ 94 id: uuid.New().String(), 95 config: params.Config, 96 logger: params.Logger, 97 } 98 99 for _, storeName := range params.Config.IdentityStores { 100 var storeFound bool 101 for _, store := range params.IdentityStores { 102 if store.GetName() == storeName { 103 if !store.Configured() { 104 return nil, errors.ErrNewPortal.WithArgs( 105 fmt.Errorf("identity store %q not configured", storeName), 106 ) 107 } 108 p.identityStores = append(p.identityStores, store) 109 storeFound = true 110 break 111 } 112 } 113 if !storeFound { 114 return nil, errors.ErrNewPortal.WithArgs( 115 fmt.Errorf("identity store %q not found", storeName), 116 ) 117 } 118 } 119 120 for _, providerName := range params.Config.IdentityProviders { 121 var providerFound bool 122 for _, provider := range params.IdentityProviders { 123 if provider.GetName() == providerName { 124 if !provider.Configured() { 125 return nil, errors.ErrNewPortal.WithArgs( 126 fmt.Errorf("identity provider %q not configured", providerName), 127 ) 128 } 129 p.identityProviders = append(p.identityProviders, provider) 130 providerFound = true 131 break 132 } 133 } 134 if !providerFound { 135 return nil, errors.ErrNewPortal.WithArgs( 136 fmt.Errorf("identity provider %q not found", providerName), 137 ) 138 } 139 } 140 141 for _, providerName := range params.Config.SingleSignOnProviders { 142 var providerFound bool 143 for _, provider := range params.SingleSignOnProviders { 144 if provider.GetName() == providerName { 145 if !provider.Configured() { 146 return nil, errors.ErrNewPortal.WithArgs( 147 fmt.Errorf("sso provider %q not configured", providerName), 148 ) 149 } 150 p.ssoProviders = append(p.ssoProviders, provider) 151 providerFound = true 152 break 153 } 154 } 155 if !providerFound { 156 return nil, errors.ErrNewPortal.WithArgs( 157 fmt.Errorf("sso provider %q not found", providerName), 158 ) 159 } 160 } 161 162 if len(p.identityStores) < 1 && len(p.identityProviders) < 1 { 163 return nil, errors.ErrNewPortal.WithArgs(errors.ErrPortalConfigBackendsNotFound) 164 } 165 166 if err := p.configure(); err != nil { 167 return nil, err 168 } 169 return p, nil 170 } 171 172 // GetName returns the configuration name of the Portal. 173 func (p *Portal) GetName() string { 174 return p.config.Name 175 } 176 177 func (p *Portal) configure() error { 178 if err := p.configureEssentials(); err != nil { 179 return err 180 } 181 if err := p.configureCryptoKeyStore(); err != nil { 182 return err 183 } 184 if err := p.configureLoginOptions(); err != nil { 185 return err 186 } 187 if err := p.configureUserInterface(); err != nil { 188 return err 189 } 190 if err := p.configureUserTransformer(); err != nil { 191 return err 192 } 193 194 if len(p.config.TrustedLogoutRedirectURIConfigs) > 0 { 195 p.logger.Debug( 196 "Logout redirect URI configuration", 197 zap.Any("trusted_logout_redirect_uri_configs", p.config.TrustedLogoutRedirectURIConfigs), 198 ) 199 } else { 200 p.logger.Debug("Logout redirect URI configuration not present") 201 } 202 203 return nil 204 } 205 206 func (p *Portal) configureEssentials() error { 207 p.logger.Debug( 208 "Configuring caching", 209 zap.String("portal_name", p.config.Name), 210 zap.String("portal_id", p.id), 211 ) 212 213 p.sessions = cache.NewSessionCache() 214 p.sessions.Run() 215 p.sandboxes = cache.NewSandboxCache() 216 p.sandboxes.Run() 217 218 p.logger.Debug( 219 "Configuring cookie parameters", 220 zap.String("portal_name", p.config.Name), 221 ) 222 223 c, err := cookie.NewFactory(p.config.CookieConfig) 224 if err != nil { 225 return err 226 } 227 p.cookie = c 228 return nil 229 } 230 231 func (p *Portal) configureCryptoKeyStore() error { 232 if len(p.config.AccessListConfigs) == 0 { 233 p.config.AccessListConfigs = []*acl.RuleConfiguration{ 234 { 235 // Admin users can access everything. 236 Conditions: []string{defaultPortalACLCondition}, 237 Action: defaultPortalACLAction, 238 }, 239 } 240 } 241 242 p.logger.Debug( 243 "Configuring authentication ACL", 244 zap.String("portal_name", p.config.Name), 245 zap.String("portal_id", p.id), 246 zap.Any("access_list_configs", p.config.AccessListConfigs), 247 ) 248 249 if p.config.TokenValidatorOptions == nil { 250 p.config.TokenValidatorOptions = options.NewTokenValidatorOptions() 251 } 252 p.config.TokenValidatorOptions.ValidateBearerHeader = true 253 254 // The below line is disabled because path match is not part of the ACL. 255 // p.config.TokenValidatorOptions.ValidateMethodPath = true 256 257 accessList := acl.NewAccessList() 258 accessList.SetLogger(p.logger) 259 ctx := context.Background() 260 if err := accessList.AddRules(ctx, p.config.AccessListConfigs); err != nil { 261 return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err) 262 } 263 264 p.keystore = kms.NewCryptoKeyStore() 265 p.keystore.SetLogger(p.logger) 266 267 // Load token configuration into key managers, extract token verification 268 // keys and add them to token validator. 269 if p.config.CryptoKeyStoreConfig != nil { 270 // Add default token name, lifetime, etc. 271 if err := p.keystore.AddDefaults(p.config.CryptoKeyStoreConfig); err != nil { 272 return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err) 273 } 274 } 275 276 if len(p.config.CryptoKeyConfigs) == 0 { 277 if err := p.keystore.AutoGenerate("default", "ES512"); err != nil { 278 return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err) 279 } 280 } else { 281 if err := p.keystore.AddKeysWithConfigs(p.config.CryptoKeyConfigs); err != nil { 282 return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err) 283 } 284 } 285 286 if err := p.keystore.HasVerifyKeys(); err != nil { 287 return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err) 288 } 289 290 p.validator = validator.NewTokenValidator() 291 if err := p.validator.Configure(ctx, p.keystore.GetVerifyKeys(), accessList, p.config.TokenValidatorOptions); err != nil { 292 return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err) 293 } 294 295 p.logger.Debug( 296 "Configured validator ACL", 297 zap.String("portal_name", p.config.Name), 298 zap.String("portal_id", p.id), 299 zap.Any("token_validator_options", p.config.TokenValidatorOptions), 300 zap.Any("token_grantor_options", p.config.TokenGrantorOptions), 301 ) 302 return nil 303 } 304 305 func (p *Portal) configureLoginOptions() error { 306 p.loginOptions = make(map[string]interface{}) 307 p.loginOptions["form_required"] = "no" 308 p.loginOptions["realm_dropdown_required"] = "no" 309 p.loginOptions["authenticators_required"] = "no" 310 p.loginOptions["identity_required"] = "no" 311 312 if err := p.configureIdentityStoreLogin(); err != nil { 313 return err 314 } 315 316 if err := p.configureIdentityProviderLogin(); err != nil { 317 return err 318 } 319 320 if err := p.configureLoginIcons(); err != nil { 321 return err 322 } 323 324 p.logger.Debug( 325 "Provisioned login options", 326 zap.String("portal_name", p.config.Name), 327 zap.String("portal_id", p.id), 328 zap.Any("options", p.loginOptions), 329 zap.Int("identity_store_count", len(p.config.IdentityStores)), 330 zap.Int("identity_provider_count", len(p.config.IdentityProviders)), 331 ) 332 333 return nil 334 } 335 336 func (p *Portal) configureLoginIcons() error { 337 var entries []*icons.LoginIcon 338 339 for _, store := range p.identityStores { 340 icon := store.GetLoginIcon() 341 entries = append(entries, icon) 342 } 343 344 for _, provider := range p.identityProviders { 345 icon := provider.GetLoginIcon() 346 entries = append(entries, icon) 347 } 348 349 sort.Slice(entries[:], func(i, j int) bool { 350 return entries[i].Priority > entries[j].Priority 351 }) 352 353 var iconConfigs []map[string]string 354 355 for i, icon := range entries { 356 iconConfig := icon.GetConfig() 357 iconConfigs = append(iconConfigs, iconConfig) 358 if i == 0 { 359 p.loginOptions["default_realm"] = iconConfig["realm"] 360 } 361 } 362 363 p.loginOptions["authenticators"] = iconConfigs 364 365 if len(iconConfigs) == 1 { 366 p.loginOptions["hide_contact_support_link"] = "yes" 367 p.loginOptions["hide_forgot_username_link"] = "yes" 368 p.loginOptions["hide_register_link"] = "yes" 369 p.loginOptions["hide_links"] = "yes" 370 for _, iconConfig := range iconConfigs { 371 if v, exists := iconConfig["contact_support_enabled"]; exists && v == "yes" { 372 p.loginOptions["hide_contact_support_link"] = "no" 373 p.loginOptions["hide_links"] = "no" 374 } 375 if v, exists := iconConfig["registration_enabled"]; exists && v == "yes" { 376 p.loginOptions["hide_register_link"] = "no" 377 p.loginOptions["hide_links"] = "no" 378 } 379 if v, exists := iconConfig["username_recovery_enabled"]; exists && v == "yes" { 380 p.loginOptions["hide_forgot_username_link"] = "no" 381 p.loginOptions["hide_links"] = "no" 382 } 383 } 384 } 385 386 return nil 387 } 388 389 func (p *Portal) configureIdentityStoreLogin() error { 390 if len(p.config.IdentityStores) < 1 { 391 return nil 392 } 393 394 p.logger.Debug( 395 "Configuring identity store login options", 396 zap.String("portal_name", p.config.Name), 397 zap.String("portal_id", p.id), 398 zap.Int("identity_store_count", len(p.config.IdentityStores)), 399 ) 400 401 var stores []map[string]string 402 403 for _, store := range p.identityStores { 404 cfg := make(map[string]string) 405 cfg["realm"] = store.GetRealm() 406 cfg["default"] = "no" 407 switch store.GetKind() { 408 case "local": 409 cfg["label"] = strings.ToTitle(store.GetRealm()) 410 cfg["default"] = "yes" 411 case "ldap": 412 cfg["label"] = strings.ToUpper(store.GetRealm()) 413 default: 414 cfg["label"] = strings.ToTitle(store.GetRealm()) 415 } 416 stores = append(stores, cfg) 417 } 418 419 if len(stores) > 0 { 420 p.loginOptions["form_required"] = "yes" 421 p.loginOptions["identity_required"] = "yes" 422 p.loginOptions["realms"] = stores 423 } 424 425 if len(stores) > 1 { 426 p.loginOptions["realm_dropdown_required"] = "yes" 427 p.loginOptions["authenticators_required"] = "yes" 428 } 429 430 for _, store := range p.identityStores { 431 icon := store.GetLoginIcon() 432 icon.SetRealm(store.GetRealm()) 433 switch store.GetKind() { 434 case "local": 435 icon.RegistrationEnabled = false 436 icon.UsernameRecoveryEnabled = false 437 case "ldap": 438 icon.RegistrationEnabled = false 439 icon.UsernameRecoveryEnabled = false 440 } 441 } 442 443 return nil 444 } 445 446 func (p *Portal) configureIdentityProviderLogin() error { 447 if len(p.config.IdentityProviders) < 1 { 448 return nil 449 } 450 451 p.logger.Debug( 452 "Configuring identity provider login options", 453 zap.String("portal_name", p.config.Name), 454 zap.String("portal_id", p.id), 455 zap.Int("identity_provider_count", len(p.config.IdentityProviders)), 456 ) 457 458 for _, provider := range p.identityProviders { 459 icon := provider.GetLoginIcon() 460 icon.SetRealm(provider.GetRealm()) 461 switch provider.GetKind() { 462 case "oauth": 463 icon.SetEndpoint(path.Join(provider.GetKind()+"2", provider.GetRealm())) 464 default: 465 icon.SetEndpoint(path.Join(provider.GetKind(), provider.GetRealm())) 466 } 467 } 468 469 p.loginOptions["authenticators_required"] = "yes" 470 471 return nil 472 } 473 474 func (p *Portal) configureUserInterface() error { 475 p.logger.Debug( 476 "Configuring user interface", 477 zap.String("portal_name", p.config.Name), 478 zap.String("portal_id", p.id), 479 ) 480 481 p.ui = ui.NewFactory() 482 if p.config.UI.Title == "" { 483 p.ui.Title = "Sign In" 484 } else { 485 p.ui.Title = p.config.UI.Title 486 } 487 488 if p.config.UI.CustomCSSPath != "" { 489 p.ui.CustomCSSPath = p.config.UI.CustomCSSPath 490 if err := ui.StaticAssets.AddAsset("assets/css/custom.css", "text/css", p.config.UI.CustomCSSPath); err != nil { 491 return errors.ErrStaticAssetAddFailed.WithArgs("assets/css/custom.css", "text/css", p.config.UI.CustomCSSPath, p.config.Name, err) 492 } 493 } 494 495 if p.config.UI.CustomJsPath != "" { 496 p.ui.CustomJsPath = p.config.UI.CustomJsPath 497 if err := ui.StaticAssets.AddAsset("assets/js/custom.js", "application/javascript", p.config.UI.CustomJsPath); err != nil { 498 return errors.ErrStaticAssetAddFailed.WithArgs("assets/js/custom.js", "application/javascript", p.config.UI.CustomJsPath, p.config.Name, err) 499 } 500 } 501 502 if p.config.UI.CustomHTMLHeaderPath != "" { 503 b, err := os.ReadFile(p.config.UI.CustomHTMLHeaderPath) 504 if err != nil { 505 return errors.ErrCustomHTMLHeaderNotReadable.WithArgs(p.config.UI.CustomHTMLHeaderPath, p.config.Name, err) 506 } 507 for k, v := range ui.PageTemplates { 508 headIndex := strings.Index(v, "<meta name=\"description\"") 509 if headIndex < 1 { 510 continue 511 } 512 v = v[:headIndex] + string(b) + v[headIndex:] 513 ui.PageTemplates[k] = v 514 } 515 } 516 517 for _, staticAsset := range p.config.UI.StaticAssets { 518 if err := ui.StaticAssets.AddAsset(staticAsset.Path, staticAsset.ContentType, staticAsset.FsPath); err != nil { 519 return errors.ErrStaticAssetAddFailed.WithArgs(staticAsset.Path, staticAsset.ContentType, staticAsset.FsPath, p.config.Name, err) 520 } 521 } 522 523 if p.config.UI.LogoURL != "" { 524 p.ui.LogoURL = p.config.UI.LogoURL 525 p.ui.LogoDescription = p.config.UI.LogoDescription 526 } else { 527 p.ui.LogoURL = path.Join(p.ui.LogoURL) 528 } 529 530 if p.config.UI.MetaTitle != "" { 531 p.ui.MetaTitle = p.config.UI.MetaTitle 532 } else { 533 p.ui.MetaTitle = "Authentication Portal" 534 } 535 536 if p.config.UI.MetaAuthor != "" { 537 p.ui.MetaAuthor = p.config.UI.MetaAuthor 538 } else { 539 p.ui.MetaAuthor = "Paul Greenberg github.com/greenpau" 540 } 541 542 if p.config.UI.MetaDescription != "" { 543 p.ui.MetaDescription = p.config.UI.MetaDescription 544 } else { 545 p.ui.MetaDescription = "Performs user authentication." 546 } 547 548 if len(p.config.UI.PrivateLinks) > 0 { 549 p.ui.PrivateLinks = p.config.UI.PrivateLinks 550 } 551 552 if len(p.config.UI.Realms) > 0 { 553 p.ui.Realms = p.config.UI.Realms 554 } 555 556 if p.config.UI.Theme == "" { 557 p.config.UI.Theme = "basic" 558 } 559 if _, exists := ui.Themes[p.config.UI.Theme]; !exists { 560 return errors.ErrUserInterfaceThemeNotFound.WithArgs(p.config.Name, p.config.UI.Theme) 561 } 562 563 // User Interface Templates 564 for k := range ui.PageTemplates { 565 tmplNameParts := strings.SplitN(k, "/", 2) 566 tmplTheme := tmplNameParts[0] 567 tmplName := tmplNameParts[1] 568 if tmplTheme != p.config.UI.Theme { 569 continue 570 } 571 if _, exists := p.config.UI.Templates[tmplName]; !exists { 572 p.logger.Debug( 573 "Configuring default authentication user interface templates", 574 zap.String("portal_name", p.config.Name), 575 zap.String("template_theme", tmplTheme), 576 zap.String("template_name", tmplName), 577 ) 578 if err := p.ui.AddBuiltinTemplate(k); err != nil { 579 return errors.ErrUserInterfaceBuiltinTemplateAddFailed.WithArgs(p.config.Name, tmplName, tmplTheme, err) 580 } 581 p.ui.Templates[tmplName] = p.ui.Templates[k] 582 } 583 } 584 585 for tmplName, tmplPath := range p.config.UI.Templates { 586 p.logger.Debug( 587 "Configuring non-default authentication user interface templates", 588 zap.String("portal_name", p.config.Name), 589 zap.String("portal_id", p.id), 590 zap.String("template_name", tmplName), 591 zap.String("template_path", tmplPath), 592 ) 593 if err := p.ui.AddTemplate(tmplName, tmplPath); err != nil { 594 return errors.ErrUserInterfaceCustomTemplateAddFailed.WithArgs(p.config.Name, tmplName, tmplPath, err) 595 } 596 } 597 598 p.logger.Debug( 599 "Configured user interface", 600 zap.String("portal_name", p.config.Name), 601 zap.String("portal_id", p.id), 602 zap.String("title", p.ui.Title), 603 zap.String("logo_url", p.ui.LogoURL), 604 zap.String("logo_description", p.ui.LogoDescription), 605 zap.Any("action_endpoint", p.ui.ActionEndpoint), 606 zap.Any("private_links", p.ui.PrivateLinks), 607 zap.Any("realms", p.ui.Realms), 608 zap.String("theme", p.config.UI.Theme), 609 ) 610 611 return nil 612 } 613 614 func (p *Portal) configureUserTransformer() error { 615 if len(p.config.UserTransformerConfigs) == 0 { 616 return nil 617 } 618 619 p.logger.Debug( 620 "Configuring user transforms", 621 zap.String("portal_name", p.config.Name), 622 zap.String("portal_id", p.id), 623 ) 624 625 tr, err := transformer.NewFactory(p.config.UserTransformerConfigs) 626 if err != nil { 627 return err 628 } 629 p.transformer = tr 630 631 p.logger.Debug( 632 "Configured user transforms", 633 zap.String("portal_name", p.config.Name), 634 zap.String("portal_id", p.id), 635 zap.Any("transforms", p.config.UserTransformerConfigs), 636 ) 637 return nil 638 } 639 640 // AddUserRegistry adds registry.UserRegistry instance to Portal. 641 func (p *Portal) AddUserRegistry(userRegistry registry.UserRegistry) error { 642 p.config.UserRegistries = cfgutil.DedupStrArr(p.config.UserRegistries) 643 644 if len(p.config.UserRegistries) < 1 { 645 return fmt.Errorf("auth portal has no user registries configured") 646 } 647 if len(p.config.UserRegistries) > 1 { 648 return fmt.Errorf("auth portal does not support multiple user registries: %v", p.config.UserRegistries) 649 } 650 651 p.userRegistry = userRegistry 652 653 p.logger.Debug( 654 "Configured user registration", 655 zap.String("portal_name", p.config.Name), 656 zap.String("portal_id", p.id), 657 zap.Any("user_registry", p.userRegistry.GetConfig()), 658 ) 659 660 return nil 661 } 662 663 // GetIdentityStoreNames returns a list of existing identity stores. 664 func (p *Portal) GetIdentityStoreNames() map[string]string { 665 var m map[string]string 666 for _, store := range p.identityStores { 667 if m == nil { 668 m = make(map[string]string) 669 } 670 m[store.GetName()] = store.GetRealm() 671 } 672 return m 673 }