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  }