github.com/greenpau/go-authcrunch@v1.0.50/pkg/authn/handle_http_settings_mfa.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  	"encoding/base64"
    20  	"fmt"
    21  	"github.com/greenpau/go-authcrunch/pkg/authn/enums/operator"
    22  	"github.com/greenpau/go-authcrunch/pkg/identity"
    23  	"github.com/greenpau/go-authcrunch/pkg/identity/qr"
    24  	"github.com/greenpau/go-authcrunch/pkg/ids"
    25  	"github.com/greenpau/go-authcrunch/pkg/requests"
    26  	"github.com/greenpau/go-authcrunch/pkg/user"
    27  	"github.com/greenpau/go-authcrunch/pkg/util"
    28  	"github.com/skip2/go-qrcode"
    29  	"go.uber.org/zap"
    30  	"net/http"
    31  	"strings"
    32  )
    33  
    34  func (p *Portal) handleHTTPMfaBarcode(ctx context.Context, w http.ResponseWriter, r *http.Request, endpoint string) error {
    35  	qrCodeEncoded := strings.TrimPrefix(endpoint, "/mfa/barcode/")
    36  	qrCodeEncoded = strings.TrimSuffix(qrCodeEncoded, ".png")
    37  	codeURI, err := base64.StdEncoding.DecodeString(qrCodeEncoded)
    38  	if err != nil {
    39  		return p.handleHTTPRenderPlainText(ctx, w, http.StatusBadRequest)
    40  	}
    41  	png, err := qrcode.Encode(string(codeURI), qrcode.Medium, 256)
    42  	if err != nil {
    43  		return p.handleHTTPRenderPlainText(ctx, w, http.StatusInternalServerError)
    44  	}
    45  	w.Header().Set("Content-Type", "image/png")
    46  	w.Write(png)
    47  	return nil
    48  }
    49  
    50  func (p *Portal) handleHTTPMfaSettings(
    51  	ctx context.Context, r *http.Request, rr *requests.Request,
    52  	usr *user.User, store ids.IdentityStore, data map[string]interface{},
    53  ) error {
    54  	var action string
    55  	var status bool
    56  	entrypoint := "mfa"
    57  	data["view"] = entrypoint
    58  	endpoint, err := getEndpoint(r.URL.Path, "/"+entrypoint)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	switch {
    64  	case strings.HasPrefix(endpoint, "/add/u2f") && r.Method == "POST":
    65  		// Add U2F token.
    66  		action = "add-u2f"
    67  		status = true
    68  		if err := validateAddU2FTokenForm(r, rr); err != nil {
    69  			attachFailStatus(data, fmt.Sprintf("Bad Request: %s", err))
    70  			break
    71  		}
    72  		if err = store.Request(operator.AddMfaToken, rr); err != nil {
    73  			attachFailStatus(data, fmt.Sprintf("%v", err))
    74  			break
    75  		}
    76  		attachSuccessStatus(data, "U2F token has been added")
    77  	case strings.HasPrefix(endpoint, "/add/u2f"):
    78  		// Add U2F token.
    79  		action = "add-u2f"
    80  		data["webauthn_challenge"] = util.GetRandomStringFromRange(64, 92)
    81  		data["webauthn_rp_name"] = "AUTHP"
    82  		data["webauthn_user_id"] = usr.Claims.ID
    83  		data["webauthn_user_email"] = usr.Claims.Email
    84  		data["webauthn_user_verification"] = "discouraged"
    85  		data["webauthn_attestation"] = "direct"
    86  		if usr.Claims.Name == "" {
    87  			data["webauthn_user_display_name"] = usr.Claims.Subject
    88  		} else {
    89  			data["webauthn_user_display_name"] = usr.Claims.Name
    90  		}
    91  	case strings.HasPrefix(endpoint, "/add/app") && r.Method == "POST":
    92  		// Add Application MFA token.
    93  		action = "add-app"
    94  		status = true
    95  		if err := validateAddMfaTokenForm(r, rr); err != nil {
    96  			attachFailStatus(data, fmt.Sprintf("Bad Request: %s", err))
    97  			break
    98  		}
    99  		if err = store.Request(operator.AddMfaToken, rr); err != nil {
   100  			attachFailStatus(data, fmt.Sprintf("%v", err))
   101  			break
   102  		}
   103  		attachSuccessStatus(data, "MFA token has been added")
   104  	case strings.HasPrefix(endpoint, "/add/app"):
   105  		action = "add-app"
   106  		qr := qr.NewCode()
   107  		qr.Secret = util.GetRandomStringFromRange(64, 92)
   108  		qr.Type = "totp"
   109  		qr.Label = fmt.Sprintf("AUTHP:%s", usr.Claims.Email)
   110  		qr.Period = 30
   111  		qr.Issuer = "AUTHP"
   112  		qr.Digits = 6
   113  		if err := qr.Build(); err != nil {
   114  			attachFailStatus(data, fmt.Sprintf("Failed creating QR code: %v", err))
   115  			break
   116  		}
   117  		data["mfa_label"] = qr.Issuer
   118  		data["mfa_comment"] = "My Authentication App"
   119  		data["mfa_email"] = usr.Claims.Email
   120  		data["mfa_type"] = qr.Type
   121  		data["mfa_secret"] = qr.Secret
   122  		data["mfa_period"] = fmt.Sprintf("%d", qr.Period)
   123  		data["mfa_digits"] = fmt.Sprintf("%d", qr.Digits)
   124  		data["code_uri"] = qr.Get()
   125  		data["code_uri_encoded"] = qr.GetEncoded()
   126  	case strings.HasPrefix(endpoint, "/test/app"):
   127  		// Test Application MFA token.
   128  		action = "test-app"
   129  		if r.Method == "POST" {
   130  			status = true
   131  		}
   132  		tokenID, digitCount, err := validateTestMfaTokenURL(endpoint)
   133  		data["mfa_token_id"] = tokenID
   134  		data["mfa_digits"] = digitCount
   135  		if err != nil {
   136  			attachFailStatus(data, fmt.Sprintf("Bad Request: %v", err))
   137  			break
   138  		}
   139  		if r.Method != "POST" {
   140  			break
   141  		}
   142  		// Validate the posted MFA token.
   143  		if err := validateMfaAuthTokenForm(r, rr); err != nil {
   144  			attachFailStatus(data, fmt.Sprintf("Bad Request: %v", err))
   145  			break
   146  		}
   147  		if err = store.Request(operator.GetMfaTokens, rr); err != nil {
   148  			attachFailStatus(data, fmt.Sprintf("%v", err))
   149  			break
   150  		}
   151  		var tokenValidated bool
   152  		bundle := rr.Response.Payload.(*identity.MfaTokenBundle)
   153  		for _, token := range bundle.Get() {
   154  			if token.ID != rr.MfaToken.ID {
   155  				continue
   156  			}
   157  			if err := token.ValidateCode(rr.MfaToken.Passcode); err != nil {
   158  				continue
   159  			}
   160  			tokenValidated = true
   161  			attachSuccessStatus(data, fmt.Sprintf("token id %s tested successfully", token.ID))
   162  			break
   163  		}
   164  		if tokenValidated {
   165  			break
   166  		}
   167  		attachFailStatus(data, "Invalid token passcode")
   168  	case strings.HasPrefix(endpoint, "/test/u2f"):
   169  		// Test U2F token.
   170  		var token *identity.MfaToken
   171  		action = "test-u2f"
   172  		tokenID, err := validateTestU2FTokenURL(endpoint)
   173  		data["mfa_token_id"] = tokenID
   174  		if err != nil {
   175  			status = true
   176  			attachFailStatus(data, fmt.Sprintf("Bad Request: %v", err))
   177  			break
   178  		}
   179  		// Get a list of U2F tokens.
   180  		if err = store.Request(operator.GetMfaTokens, rr); err != nil {
   181  			attachFailStatus(data, fmt.Sprintf("%v", err))
   182  			break
   183  		}
   184  		bundle := rr.Response.Payload.(*identity.MfaTokenBundle)
   185  		for _, t := range bundle.Get() {
   186  			if t.ID != tokenID {
   187  				continue
   188  			}
   189  			if t.Type != "u2f" {
   190  				continue
   191  			}
   192  			token = t
   193  			break
   194  		}
   195  		if token == nil {
   196  			status = true
   197  			attachFailStatus(data, fmt.Sprintf("Bad Request: U2F token id %s not found", tokenID))
   198  			break
   199  		}
   200  		if r.Method != "POST" {
   201  			// Authentication Ceremony parameters.
   202  			// Reference: https://www.w3.org/TR/webauthn-2/#sctn-assertion-privacy
   203  			if token.Parameters == nil {
   204  				status = true
   205  				attachFailStatus(data, fmt.Sprintf("Bad Request: U2F token id %s has no U2F parameters", tokenID))
   206  				break
   207  			}
   208  			validParams := true
   209  			for _, k := range []string{"id", "transports", "type"} {
   210  				if _, exists := token.Parameters["u2f_"+k]; !exists {
   211  					status = true
   212  					validParams = false
   213  					attachFailStatus(data, fmt.Sprintf("U2F token id %s has no %s U2F parameters", tokenID, k))
   214  					break
   215  				}
   216  			}
   217  			if !validParams {
   218  				break
   219  			}
   220  			var tokenTransports string
   221  			if len(token.Parameters["u2f_transports"]) > 0 {
   222  				tokenTransports = fmt.Sprintf(`"%s"`, strings.Join(strings.Split(token.Parameters["u2f_transports"], ","), `","`))
   223  			}
   224  			data["webauthn_challenge"] = util.GetRandomStringFromRange(64, 92)
   225  			data["webauthn_rp_name"] = "AUTHP"
   226  			data["webauthn_timeout"] = "60000"
   227  			// See https://chromium.googlesource.com/chromium/src/+/refs/heads/main/content/browser/webauth/uv_preferred.md
   228  			// data["webauthn_user_verification"] = "preferred"
   229  			data["webauthn_user_verification"] = "discouraged"
   230  			// data["webauthn_ext_uvm"] = "true"
   231  			data["webauthn_ext_uvm"] = "false"
   232  			data["webauthn_ext_loc"] = "false"
   233  			data["webauthn_tx_auth_simple"] = "Could you please verify yourself?"
   234  			var allowedCredentials []map[string]interface{}
   235  			allowedCredential := make(map[string]interface{})
   236  			allowedCredential["id"] = token.Parameters["u2f_id"]
   237  			allowedCredential["type"] = token.Parameters["u2f_type"]
   238  			allowedCredential["transports"] = tokenTransports
   239  			allowedCredentials = append(allowedCredentials, allowedCredential)
   240  			data["webauthn_credentials"] = allowedCredentials
   241  			break
   242  		}
   243  		// Validate the posted U2F token.
   244  		status = true
   245  		if err := validateAuthU2FTokenForm(r, rr); err != nil {
   246  			p.logger.Warn(
   247  				"detected malformed u2f token validation request",
   248  				zap.String("session_id", rr.Upstream.SessionID),
   249  				zap.String("request_id", rr.ID),
   250  				zap.Any("error", err),
   251  			)
   252  			attachFailStatus(data, fmt.Sprintf("Bad Request: %v", err))
   253  			break
   254  		}
   255  
   256  		if wr, err := token.WebAuthnRequest(rr.WebAuthn.Request); err != nil {
   257  			p.logger.Warn(
   258  				"u2f token validation failed",
   259  				zap.String("session_id", rr.Upstream.SessionID),
   260  				zap.String("request_id", rr.ID),
   261  				zap.Any("webauthn_request", wr),
   262  				zap.Any("error", err),
   263  			)
   264  			attachFailStatus(data, fmt.Sprintf("U2F authentication failed: %v", err))
   265  			break
   266  		} else {
   267  			p.logger.Debug(
   268  				"successfully validated u2f token",
   269  				zap.String("session_id", rr.Upstream.SessionID),
   270  				zap.String("request_id", rr.ID),
   271  				zap.Any("webauthn_request", wr),
   272  			)
   273  		}
   274  		attachSuccessStatus(data, fmt.Sprintf("U2F token id %s tested successfully", token.ID))
   275  	case strings.HasPrefix(endpoint, "/delete"):
   276  		// Delete a particular SSH key.
   277  		action = "delete"
   278  		status = true
   279  		tokenID, err := getEndpointKeyID(endpoint, "/delete/")
   280  		if err != nil {
   281  			attachFailStatus(data, fmt.Sprintf("%v", err))
   282  			break
   283  		}
   284  		rr.MfaToken.ID = tokenID
   285  		if err = store.Request(operator.DeleteMfaToken, rr); err != nil {
   286  			attachFailStatus(data, fmt.Sprintf("failed deleting token id %s: %v", tokenID, err))
   287  			break
   288  		}
   289  		attachSuccessStatus(data, fmt.Sprintf("token id %s deleted successfully", tokenID))
   290  		/*
   291  			case strings.HasPrefix(endpoint, "/view"):
   292  				// Get a particular SSH key.
   293  				action = "view"
   294  				keyID, err := getEndpointKeyID(endpoint, "/view/")
   295  				if err != nil {
   296  					attachFailStatus(data, fmt.Sprintf("%v", err))
   297  					break
   298  				}
   299  				rr.Key.Usage = "ssh"
   300  				if err = store.Request(operator.GetPublicKeys, rr); err != nil {
   301  					attachFailStatus(data, fmt.Sprintf("failed fetching key id %s: %v", keyID, err))
   302  					break
   303  				}
   304  				bundle := rr.Response.Payload.(*identity.PublicKeyBundle)
   305  				for _, k := range bundle.Get() {
   306  					if k.ID != keyID {
   307  						continue
   308  					}
   309  					var keyMap map[string]interface{}
   310  					keyBytes, _ := json.Marshal(k)
   311  					json.Unmarshal(keyBytes, &keyMap)
   312  					for _, w := range []string{"payload", "openssh"} {
   313  						if _, exists := keyMap[w]; !exists {
   314  							continue
   315  						}
   316  						delete(keyMap, w)
   317  					}
   318  					prettyKey, _ := json.MarshalIndent(keyMap, "", "  ")
   319  					attachSuccessStatus(data, "OK")
   320  					data["key"] = string(prettyKey)
   321  					if k.Payload != "" {
   322  						data["pem_key"] = k.Payload
   323  					}
   324  					if k.OpenSSH != "" {
   325  						data["openssh_key"] = k.OpenSSH
   326  					}
   327  					break
   328  				}
   329  		*/
   330  	default:
   331  		// List MFA Tokens.
   332  		if err = store.Request(operator.GetMfaTokens, rr); err != nil {
   333  			attachFailStatus(data, fmt.Sprintf("%v", err))
   334  			break
   335  		}
   336  		bundle := rr.Response.Payload.(*identity.MfaTokenBundle)
   337  		tokens := bundle.Get()
   338  		if len(tokens) > 0 {
   339  			data["mfa_tokens"] = tokens
   340  		}
   341  		attachSuccessStatus(data, "OK")
   342  	}
   343  	attachView(data, entrypoint, action, status)
   344  	return nil
   345  }