github.com/greenpau/go-authcrunch@v1.1.4/pkg/authn/handle_http_sandbox.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  	"strings"
    22  
    23  	"github.com/greenpau/go-authcrunch/pkg/authn/enums/operator"
    24  	"github.com/greenpau/go-authcrunch/pkg/identity"
    25  	"github.com/greenpau/go-authcrunch/pkg/identity/qr"
    26  	"github.com/greenpau/go-authcrunch/pkg/requests"
    27  	"github.com/greenpau/go-authcrunch/pkg/user"
    28  	"github.com/greenpau/go-authcrunch/pkg/util"
    29  	addrutil "github.com/greenpau/go-authcrunch/pkg/util/addr"
    30  	"go.uber.org/zap"
    31  )
    32  
    33  func (p *Portal) handleHTTPSandbox(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request) error {
    34  	var sandboxID, sandboxPartition, sandboxSecret string
    35  	p.disableClientCache(w)
    36  	sandboxEndpoint, err := getEndpoint(r.URL.Path, "/sandbox/")
    37  	if err != nil {
    38  		p.logger.Debug(
    39  			"failed to parse sandbox id from url path",
    40  			zap.String("session_id", rr.Upstream.SessionID),
    41  			zap.String("request_id", rr.ID),
    42  			zap.Error(err),
    43  		)
    44  		rr.Response.RedirectURL = rr.Upstream.BasePath
    45  		return p.handleHTTPError(ctx, w, r, rr, http.StatusBadRequest)
    46  	}
    47  
    48  	sandboxArr := strings.SplitN(sandboxEndpoint, "/", 2)
    49  	sandboxID = sandboxArr[0]
    50  	if len(sandboxArr) == 2 {
    51  		sandboxPartition = sandboxArr[1]
    52  	}
    53  
    54  	// Parse sandbox cookie and authenticate temporary session ID
    55  	// and secret.
    56  	for _, cookie := range r.Cookies() {
    57  		if cookie.Name != p.cookie.SandboxID {
    58  			continue
    59  		}
    60  		v := strings.TrimSpace(cookie.Value)
    61  		if v == "" {
    62  			continue
    63  		}
    64  		sandboxSecret = v
    65  	}
    66  
    67  	if sandboxSecret == "" {
    68  		p.logger.Debug(
    69  			"failed sandbox request",
    70  			zap.String("session_id", rr.Upstream.SessionID),
    71  			zap.String("request_id", rr.ID),
    72  			zap.String("error", "sandbox secret not found"),
    73  		)
    74  		rr.Response.RedirectURL = rr.Upstream.BasePath
    75  		return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized)
    76  	}
    77  
    78  	usr, err := p.sandboxes.Get(sandboxID)
    79  	if err != nil {
    80  		p.logger.Debug(
    81  			"failed to extract cached entry from sandbox",
    82  			zap.String("session_id", rr.Upstream.SessionID),
    83  			zap.String("request_id", rr.ID),
    84  			zap.Error(err),
    85  		)
    86  		rr.Response.RedirectURL = rr.Upstream.BasePath
    87  		return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized)
    88  	}
    89  
    90  	if usr.Authenticator.TempSecret != sandboxSecret {
    91  		p.logger.Debug(
    92  			"failed sandbox request",
    93  			zap.String("session_id", rr.Upstream.SessionID),
    94  			zap.String("request_id", rr.ID),
    95  			zap.String("error", "temp secret mismatch"),
    96  		)
    97  		rr.Response.RedirectURL = rr.Upstream.BasePath
    98  		return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized)
    99  	}
   100  
   101  	if usr.Authenticator.TempSessionID != sandboxID {
   102  		p.logger.Debug(
   103  			"failed sandbox request",
   104  			zap.String("session_id", rr.Upstream.SessionID),
   105  			zap.String("request_id", rr.ID),
   106  			zap.String("error", "sandbox id mismatch"),
   107  		)
   108  		rr.Response.RedirectURL = rr.Upstream.BasePath
   109  		return p.handleHTTPError(ctx, w, r, rr, http.StatusUnauthorized)
   110  	}
   111  
   112  	// Handle auxiliary functions, e.g. QR code and sandbox termination.
   113  	switch {
   114  	case strings.HasPrefix(sandboxPartition, "mfa-app-barcode/"):
   115  		// Handle App Portal barcode.
   116  		sandboxPartition = strings.TrimPrefix(sandboxPartition, "mfa-app-barcode/")
   117  		return p.handleHTTPSandboxMfaBarcode(ctx, w, r, sandboxPartition)
   118  	case sandboxPartition == "terminate":
   119  		p.sandboxes.Delete(sandboxID)
   120  		return p.handleHTTPRedirectSeeOther(ctx, w, r, rr, "login")
   121  	}
   122  
   123  	p.logger.Debug(
   124  		"user authorization sandbox",
   125  		zap.String("sandbox_id", sandboxID),
   126  		zap.String("sandbox_secret", sandboxSecret),
   127  		zap.String("sandbox_partition", sandboxPartition),
   128  		zap.Any("checkpoints", usr.Checkpoints),
   129  	)
   130  
   131  	// Populate username (sub) and email address (email)
   132  	rr.User.Username = usr.Claims.Subject
   133  	rr.User.Email = usr.Claims.Email
   134  
   135  	data, err := p.nextSandboxCheckpoint(r, rr, usr, sandboxPartition)
   136  	if err != nil {
   137  		p.logger.Warn(
   138  			"user authorization checkpoint failed",
   139  			zap.String("session_id", rr.Upstream.SessionID),
   140  			zap.String("request_id", rr.ID),
   141  			zap.Error(err),
   142  		)
   143  		data["error"] = err.Error()
   144  	} else {
   145  		p.logger.Debug(
   146  			"next user authorization checkpoint",
   147  			zap.String("session_id", rr.Upstream.SessionID),
   148  			zap.String("request_id", rr.ID),
   149  			zap.Any("data", data),
   150  		)
   151  	}
   152  
   153  	if _, exists := data["view"]; exists {
   154  		switch data["view"] {
   155  		case "terminate":
   156  			p.sandboxes.Delete(sandboxID)
   157  		case "redirect":
   158  			return p.handleHTTPRedirectSeeOther(ctx, w, r, rr, "sandbox/"+sandboxID)
   159  		}
   160  	}
   161  
   162  	if rr.Response.Code == 0 {
   163  		rr.Response.Code = http.StatusOK
   164  	}
   165  
   166  	if _, exists := data["authorized"]; exists {
   167  		// The user passed all authorization checkpoints.
   168  		p.logger.Info(
   169  			"user passed all authorization checkpoints",
   170  			zap.String("session_id", rr.Upstream.SessionID),
   171  			zap.String("request_id", rr.ID),
   172  			zap.Any("checkpoints", usr.Checkpoints),
   173  		)
   174  		p.grantAccess(ctx, w, r, rr, usr)
   175  		w.WriteHeader(rr.Response.Code)
   176  		return nil
   177  	}
   178  
   179  	// Handle the processing of user views, e.g. app or U2F tokens, etc.
   180  	resp := p.ui.GetArgs()
   181  	resp.PageTitle = "User Authorization"
   182  	if _, exists := data["title"]; exists {
   183  		resp.PageTitle = data["title"].(string)
   184  	}
   185  	resp.BaseURL(rr.Upstream.BasePath)
   186  	resp.Data["id"] = sandboxID
   187  	for k, v := range data {
   188  		resp.Data[k] = v
   189  	}
   190  
   191  	content, err := p.ui.Render("sandbox", resp)
   192  	if err != nil {
   193  		return p.handleHTTPRenderError(ctx, w, r, rr, err)
   194  	}
   195  	return p.handleHTTPRenderHTML(ctx, w, rr.Response.Code, content.Bytes())
   196  }
   197  
   198  func (p *Portal) nextSandboxCheckpoint(r *http.Request, rr *requests.Request, usr *user.User, action string) (map[string]interface{}, error) {
   199  	var verifiedCount int
   200  	m := make(map[string]interface{})
   201  	backend := p.getIdentityStoreByRealm(usr.Authenticator.Realm)
   202  	if backend == nil {
   203  		m["title"] = "Internal Server Error"
   204  		m["view"] = "terminate"
   205  		return m, fmt.Errorf("Authentication realm not found")
   206  	}
   207  	rr.Upstream.Method = backend.GetKind()
   208  	rr.Upstream.Realm = backend.GetRealm()
   209  
   210  	for _, checkpoint := range usr.Checkpoints {
   211  		if !checkpoint.Passed {
   212  			continue
   213  		}
   214  		switch checkpoint.Type {
   215  		case "password", "mfa":
   216  			verifiedCount++
   217  		}
   218  	}
   219  
   220  	for _, checkpoint := range usr.Checkpoints {
   221  		if checkpoint.Passed {
   222  			continue
   223  		}
   224  		if checkpoint.FailedAttempts > 5 {
   225  			rr.Response.Code = http.StatusForbidden
   226  			m["title"] = "Authorization Failed"
   227  			m["view"] = "terminate"
   228  			return m, fmt.Errorf("You have failed a number of security challenges. Thus, your session failed to meet authorization requirements")
   229  		}
   230  		switch checkpoint.Type {
   231  		case "password":
   232  			if r.Method != "POST" {
   233  				switch action {
   234  				case "password-recovery":
   235  					m["title"] = "Password Recovery"
   236  					m["view"] = "password_recovery"
   237  					m["action"] = "auth"
   238  				default:
   239  					m["title"] = "Authentication"
   240  					m["view"] = "password_auth"
   241  					m["action"] = "auth"
   242  				}
   243  				return m, nil
   244  			}
   245  			switch action {
   246  			case "password-recovery":
   247  				rr.Response.Code = http.StatusNotImplemented
   248  				// User recovers a password
   249  				m["title"] = "Password Recovery Failed"
   250  				m["view"] = "terminate"
   251  				return m, fmt.Errorf("Password recovery failed. Please retry")
   252  			default:
   253  				// Handle password authentication.
   254  				if err := validateSandboxPasswordForm(r, rr); err != nil {
   255  					checkpoint.FailedAttempts++
   256  					rr.Response.Code = http.StatusBadRequest
   257  					m["title"] = "Authentication Failed"
   258  					m["view"] = "error"
   259  					p.logger.Warn(
   260  						"invalid password for submission",
   261  						zap.String("session_id", rr.Upstream.SessionID),
   262  						zap.String("request_id", rr.ID),
   263  						zap.String("src_ip", addrutil.GetSourceAddress(r)),
   264  						zap.String("src_conn_ip", addrutil.GetSourceConnAddress(r)),
   265  						zap.Int("checkpoint_id", checkpoint.ID),
   266  						zap.String("checkpoint_name", checkpoint.Name),
   267  						zap.String("checkpoint_type", checkpoint.Type),
   268  					)
   269  					return m, err
   270  				}
   271  				rr.Flags.Enabled = true
   272  				if err := backend.Request(operator.Authenticate, rr); err != nil {
   273  					rr.Response.Code = http.StatusUnauthorized
   274  					checkpoint.FailedAttempts++
   275  					m["title"] = "Authentication Failed"
   276  					m["view"] = "error"
   277  					p.logger.Warn(
   278  						"password authentication failed",
   279  						zap.String("session_id", rr.Upstream.SessionID),
   280  						zap.String("request_id", rr.ID),
   281  						zap.Int("checkpoint_id", checkpoint.ID),
   282  						zap.String("src_ip", addrutil.GetSourceAddress(r)),
   283  						zap.String("src_conn_ip", addrutil.GetSourceConnAddress(r)),
   284  						zap.String("checkpoint_name", checkpoint.Name),
   285  						zap.String("checkpoint_type", checkpoint.Type),
   286  					)
   287  					return m, fmt.Errorf("Password authentication failed. Please retry")
   288  				}
   289  				p.logger.Info(
   290  					"user authorization checkpoint passed",
   291  					zap.String("session_id", rr.Upstream.SessionID),
   292  					zap.String("request_id", rr.ID),
   293  					zap.Int("checkpoint_id", checkpoint.ID),
   294  					zap.String("checkpoint_name", checkpoint.Name),
   295  					zap.String("checkpoint_type", checkpoint.Type),
   296  				)
   297  				checkpoint.Passed = true
   298  				checkpoint.FailedAttempts = 0
   299  				verifiedCount++
   300  				m["view"] = "redirect"
   301  				return m, nil
   302  			}
   303  		case "mfa":
   304  			if err := backend.Request(operator.GetMfaTokens, rr); err != nil {
   305  				checkpoint.FailedAttempts++
   306  				m["title"] = "Authorization Failed"
   307  				m["view"] = "error"
   308  				return m, err
   309  			}
   310  			var configured, appConfigured, uniConfigured bool
   311  			bundle := rr.Response.Payload.(*identity.MfaTokenBundle)
   312  			for _, token := range bundle.Get() {
   313  				switch token.Type {
   314  				case "totp":
   315  					configured = true
   316  					appConfigured = true
   317  				case "u2f":
   318  					configured = true
   319  					uniConfigured = true
   320  				}
   321  			}
   322  
   323  			switch {
   324  			case !configured && (action == ""):
   325  				m["title"] = "Token Registration"
   326  				m["view"] = "mfa_mixed_register"
   327  				m["action"] = "register"
   328  			case appConfigured && uniConfigured && (action == ""):
   329  				m["title"] = "Token Selection"
   330  				m["view"] = "mfa_mixed_auth"
   331  				m["action"] = "auth"
   332  			case appConfigured && (action == "mfa-app-auth" || action == ""):
   333  				m["title"] = "Authenticator App"
   334  				m["view"] = "mfa_app_auth"
   335  				m["action"] = "auth"
   336  				if r.Method != "POST" {
   337  					break
   338  				}
   339  				// Handle authenticator app passcode.
   340  				if err := validateMfaAuthTokenForm(r, rr); err != nil {
   341  					m["title"] = "Authorization Failed"
   342  					m["view"] = "error"
   343  					return m, err
   344  				}
   345  				var tokenErrors []string
   346  				var tokenValidated bool
   347  				for _, token := range bundle.Get() {
   348  					if token.Type != "totp" {
   349  						continue
   350  					}
   351  					if token.Disabled {
   352  						continue
   353  					}
   354  					if err := token.ValidateCode(rr.MfaToken.Passcode); err != nil {
   355  						tokenErrors = append(tokenErrors, err.Error())
   356  						continue
   357  					}
   358  					tokenValidated = true
   359  					break
   360  				}
   361  				if tokenValidated {
   362  					// If validated successfully, continue.
   363  					p.logger.Info(
   364  						"user authorization checkpoint passed",
   365  						zap.String("session_id", rr.Upstream.SessionID),
   366  						zap.String("request_id", rr.ID),
   367  						zap.Int("checkpoint_id", checkpoint.ID),
   368  						zap.String("checkpoint_name", checkpoint.Name),
   369  						zap.String("checkpoint_type", checkpoint.Type),
   370  					)
   371  					checkpoint.Passed = true
   372  					checkpoint.FailedAttempts = 0
   373  					verifiedCount++
   374  					m["view"] = "redirect"
   375  					return m, nil
   376  				}
   377  				if len(tokenErrors) == 0 {
   378  					tokenErrors = append(tokenErrors, "No available application tokens found")
   379  				}
   380  				m["view"] = "error"
   381  				checkpoint.FailedAttempts++
   382  				return m, fmt.Errorf(strings.Join(tokenErrors, "\n"))
   383  			case uniConfigured && (action == "mfa-u2f-auth" || action == ""):
   384  				m["title"] = "Hardware Token"
   385  				m["view"] = "mfa_u2f_auth"
   386  				m["action"] = "auth"
   387  				if r.Method == "POST" {
   388  					if err := validateAuthU2FTokenForm(r, rr); err != nil {
   389  						m["view"] = "error"
   390  						checkpoint.FailedAttempts++
   391  						return m, err
   392  					}
   393  					rr.WebAuthn.Challenge = usr.Authenticator.TempChallenge
   394  					if err := backend.Request(operator.Authenticate, rr); err != nil {
   395  						m["view"] = "error"
   396  						checkpoint.FailedAttempts++
   397  						return m, fmt.Errorf("Token verification failed. Please retry")
   398  					}
   399  					checkpoint.Passed = true
   400  					checkpoint.FailedAttempts = 0
   401  					verifiedCount++
   402  					m["view"] = "redirect"
   403  					return m, nil
   404  				}
   405  				if err := backend.Request(operator.GetMfaTokens, rr); err != nil {
   406  					m["view"] = "error"
   407  					checkpoint.FailedAttempts++
   408  					return m, err
   409  				}
   410  				bundle := rr.Response.Payload.(*identity.MfaTokenBundle)
   411  				creds := []map[string]interface{}{}
   412  				for _, t := range bundle.Get() {
   413  					if t.Type != "u2f" {
   414  						continue
   415  					}
   416  					cred := make(map[string]interface{})
   417  					cred["id"] = t.Parameters["u2f_id"]
   418  					cred["type"] = t.Parameters["u2f_type"]
   419  					cred["transports"] = strings.Split(t.Parameters["u2f_transports"], ",")
   420  					creds = append(creds, cred)
   421  				}
   422  				usr.Authenticator.TempChallenge = util.GetRandomString(64)
   423  				m["webauthn_challenge"] = usr.Authenticator.TempChallenge
   424  				m["webauthn_rp_name"] = "AUTHP"
   425  				m["webauthn_timeout"] = "60000"
   426  				m["webauthn_user_verification"] = "discouraged"
   427  				m["webauthn_ext_uvm"] = "false"
   428  				m["webauthn_ext_loc"] = "false"
   429  				m["webauthn_tx_auth_simple"] = "Could you please verify yourself?"
   430  				m["webauthn_credentials"] = creds
   431  			case !appConfigured && (action == "mfa-app-register"):
   432  				m["title"] = "Authenticator App Registration"
   433  				m["view"] = "mfa_app_register"
   434  				m["action"] = "register"
   435  				if r.Method == "POST" {
   436  					// Perform the validation of the newly registered token.
   437  					if err := validateAddMfaTokenForm(r, rr); err != nil {
   438  						m["view"] = "error"
   439  						checkpoint.FailedAttempts++
   440  						return m, err
   441  					}
   442  					if err := backend.Request(operator.AddMfaToken, rr); err != nil {
   443  						m["view"] = "error"
   444  						checkpoint.FailedAttempts++
   445  						return m, err
   446  					}
   447  					checkpoint.Passed = true
   448  					checkpoint.FailedAttempts = 0
   449  					verifiedCount++
   450  					m["view"] = "redirect"
   451  					return m, nil
   452  				}
   453  				// Display QR code for token registration.
   454  				qr := qr.NewCode()
   455  				qr.Secret = util.GetRandomStringFromRange(64, 92)
   456  				qr.Type = "totp"
   457  				qr.Label = fmt.Sprintf("AUTHP:%s", usr.Claims.Email)
   458  				qr.Period = 30
   459  				qr.Issuer = "AUTHP"
   460  				qr.Digits = 6
   461  				if err := qr.Build(); err != nil {
   462  					return m, fmt.Errorf("Failed creating QR code: %v", err)
   463  				}
   464  				m["mfa_label"] = qr.Issuer
   465  				m["mfa_comment"] = "My Authentication App"
   466  				m["mfa_email"] = usr.Claims.Email
   467  				m["mfa_type"] = qr.Type
   468  				m["mfa_secret"] = qr.Secret
   469  				m["mfa_period"] = fmt.Sprintf("%d", qr.Period)
   470  				m["mfa_digits"] = fmt.Sprintf("%d", qr.Digits)
   471  				m["code_uri"] = qr.Get()
   472  				m["code_uri_encoded"] = qr.GetEncoded()
   473  			case !uniConfigured && (action == "mfa-u2f-register"):
   474  				m["title"] = "Hardware Token Registration"
   475  				m["view"] = "mfa_u2f_register"
   476  				m["action"] = "register"
   477  				if r.Method == "POST" {
   478  					if err := validateAddU2FTokenForm(r, rr); err != nil {
   479  						m["view"] = "error"
   480  						checkpoint.FailedAttempts++
   481  						return m, err
   482  					}
   483  					if err := backend.Request(operator.AddMfaToken, rr); err != nil {
   484  						m["view"] = "error"
   485  						checkpoint.FailedAttempts++
   486  						return m, err
   487  					}
   488  					checkpoint.Passed = true
   489  					checkpoint.FailedAttempts = 0
   490  					verifiedCount++
   491  					m["view"] = "redirect"
   492  					return m, nil
   493  				}
   494  				// Display U2F registration.
   495  				usr.Authenticator.TempChallenge = util.GetRandomStringFromRange(64, 92)
   496  				m["webauthn_challenge"] = usr.Authenticator.TempChallenge
   497  				m["webauthn_rp_name"] = "AUTHP"
   498  				m["webauthn_user_id"] = usr.Claims.ID
   499  				m["webauthn_user_email"] = usr.Claims.Email
   500  				m["webauthn_user_verification"] = "discouraged"
   501  				m["webauthn_attestation"] = "direct"
   502  				if usr.Claims.Name == "" {
   503  					m["webauthn_user_display_name"] = usr.Claims.Subject
   504  				} else {
   505  					m["webauthn_user_display_name"] = usr.Claims.Name
   506  				}
   507  			default:
   508  				checkpoint.FailedAttempts++
   509  				m["title"] = "Bad Request"
   510  				m["view"] = "error"
   511  				return m, fmt.Errorf("Detected unsupported MFA authorization type")
   512  			}
   513  			if !checkpoint.Passed {
   514  				return m, nil
   515  			}
   516  		default:
   517  			checkpoint.FailedAttempts++
   518  			m["title"] = "Bad Request"
   519  			m["view"] = "error"
   520  			return m, fmt.Errorf("Detected unsupported authorization type: %v", checkpoint.Type)
   521  		}
   522  	}
   523  
   524  	if (verifiedCount > 0) && (len(usr.Checkpoints) == verifiedCount) {
   525  		m["authorized"] = true
   526  	}
   527  	return m, nil
   528  }