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