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  }