github.com/greenpau/go-authcrunch@v1.1.4/pkg/authn/mfa_form_validator.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  	"fmt"
    19  	"github.com/greenpau/go-authcrunch/pkg/requests"
    20  	"net/http"
    21  	"strconv"
    22  	"strings"
    23  )
    24  
    25  func validateAuthU2FTokenForm(r *http.Request, rr *requests.Request) error {
    26  	if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
    27  		return fmt.Errorf("Unsupported content type")
    28  	}
    29  	if err := r.ParseForm(); err != nil {
    30  		return fmt.Errorf("Failed parsing submitted form")
    31  	}
    32  	rr.WebAuthn.Request = strings.TrimSpace(r.PostFormValue("webauthn_request"))
    33  	if rr.WebAuthn.Request == "" {
    34  		return fmt.Errorf("Required form %s field not found", "webauthn_request")
    35  	}
    36  	rr.MfaToken.Type = "u2f"
    37  	tokenID := r.PostFormValue("token_id")
    38  	tokenID = strings.TrimSpace(tokenID)
    39  	if tokenID != "" {
    40  		rr.MfaToken.ID = tokenID
    41  	}
    42  	return nil
    43  }
    44  
    45  func validateMfaAuthTokenForm(r *http.Request, rr *requests.Request) error {
    46  	if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
    47  		return fmt.Errorf("Unsupported content type")
    48  	}
    49  	if err := r.ParseForm(); err != nil {
    50  		return fmt.Errorf("Failed parsing submitted form")
    51  	}
    52  	passcode := r.PostFormValue("passcode")
    53  	passcode = strings.TrimSpace(passcode)
    54  	if passcode == "" {
    55  		return fmt.Errorf("Required form passcode field is empty")
    56  	}
    57  
    58  	if len(passcode) < 4 || len(passcode) > 8 {
    59  		return fmt.Errorf("MFA passcode is not 4-8 characters long")
    60  	}
    61  	for _, c := range passcode {
    62  		if c < '0' || c > '9' {
    63  			return fmt.Errorf("MFA passcode contains non-numeric value")
    64  		}
    65  	}
    66  	rr.MfaToken.Passcode = passcode
    67  	tokenID := r.PostFormValue("token_id")
    68  	tokenID = strings.TrimSpace(tokenID)
    69  	if tokenID != "" {
    70  		rr.MfaToken.ID = tokenID
    71  	}
    72  	return nil
    73  }
    74  
    75  func validateAddU2FTokenForm(r *http.Request, rr *requests.Request) error {
    76  	if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
    77  		return fmt.Errorf("Unsupported content type")
    78  	}
    79  	if err := r.ParseForm(); err != nil {
    80  		return fmt.Errorf("Failed parsing submitted form")
    81  	}
    82  	for _, k := range []string{"webauthn_register", "webauthn_challenge"} {
    83  		v := strings.TrimSpace(r.PostFormValue(k))
    84  		if v == "" {
    85  			return fmt.Errorf("Required form %s field not found", k)
    86  		}
    87  		switch k {
    88  		case "webauthn_register":
    89  			rr.WebAuthn.Register = v
    90  		case "webauthn_challenge":
    91  			rr.WebAuthn.Challenge = v
    92  		}
    93  	}
    94  	rr.MfaToken.Type = "u2f"
    95  	return nil
    96  }
    97  
    98  func validateAddMfaTokenForm(r *http.Request, rr *requests.Request) error {
    99  	if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
   100  		return fmt.Errorf("Unsupported content type")
   101  	}
   102  	if err := r.ParseForm(); err != nil {
   103  		return fmt.Errorf("Failed parsing submitted form")
   104  	}
   105  	for _, k := range []string{"passcode", "secret", "type"} {
   106  		if r.PostFormValue(k) == "" {
   107  			return fmt.Errorf("Required form %s field not found", k)
   108  		}
   109  	}
   110  
   111  	// Passcode
   112  	code := r.PostFormValue("passcode")
   113  	if code == "" {
   114  		return fmt.Errorf("MFA passcode is empty")
   115  	}
   116  	if len(code) < 4 || len(code) > 8 {
   117  		return fmt.Errorf("MFA passcode is not 4-8 characters")
   118  	}
   119  	rr.MfaToken.Passcode = code
   120  
   121  	// Comment
   122  	comment := r.PostFormValue("comment")
   123  	if comment != "" {
   124  		rr.MfaToken.Comment = comment
   125  	}
   126  	// Secret
   127  	secret := r.PostFormValue("secret")
   128  	if secret == "" {
   129  		return fmt.Errorf("MFA secret is empty")
   130  	}
   131  	rr.MfaToken.Secret = secret
   132  
   133  	// Type
   134  	secretType := r.PostFormValue("type")
   135  	switch secretType {
   136  	case "":
   137  		return fmt.Errorf("MFA type is empty")
   138  	case "totp":
   139  	default:
   140  		return fmt.Errorf("MFA type is unsupported")
   141  	}
   142  	rr.MfaToken.Type = secretType
   143  
   144  	// Period
   145  	period := r.PostFormValue("period")
   146  	if period == "" {
   147  		return fmt.Errorf("MFA period is empty")
   148  	}
   149  	periodInt, err := strconv.Atoi(period)
   150  	if err != nil {
   151  		return fmt.Errorf("MFA period is invalid")
   152  	}
   153  	if period != strconv.Itoa(periodInt) {
   154  		return fmt.Errorf("MFA period is invalid")
   155  	}
   156  	if periodInt < 30 || periodInt > 180 {
   157  		return fmt.Errorf("MFA period is invalid")
   158  	}
   159  	rr.MfaToken.Period = periodInt
   160  
   161  	// Digits
   162  	digits := r.PostFormValue("digits")
   163  	if digits == "" {
   164  		return fmt.Errorf("MFA digits is empty")
   165  	}
   166  	digitsInt, err := strconv.Atoi(digits)
   167  	if err != nil {
   168  		return fmt.Errorf("MFA digits is invalid")
   169  	}
   170  	if digits != strconv.Itoa(digitsInt) {
   171  		return fmt.Errorf("MFA digits is invalid")
   172  	}
   173  	if digitsInt < 4 || digitsInt > 8 {
   174  		return fmt.Errorf("MFA digits is invalid")
   175  	}
   176  	rr.MfaToken.Digits = digitsInt
   177  	return nil
   178  }
   179  
   180  func validateTestMfaTokenURL(endpoint string) (string, string, error) {
   181  	arr, err := parseEndpointPath(endpoint, "/test/app/", 2)
   182  	if err != nil {
   183  		return "", "", fmt.Errorf("malformed URL: %v", err)
   184  	}
   185  	tokenID, err := parseID(arr[1])
   186  	if err != nil {
   187  		return "", "", fmt.Errorf("malformed URL: %v", err)
   188  	}
   189  	switch arr[0] {
   190  	case "4", "6", "8":
   191  	default:
   192  		return "", "", fmt.Errorf("malformed URL")
   193  	}
   194  	return tokenID, arr[0], nil
   195  }
   196  
   197  func validateTestU2FTokenURL(endpoint string) (string, error) {
   198  	arr, err := parseEndpointPath(endpoint, "/test/u2f/", 2)
   199  	if err != nil {
   200  		return "", fmt.Errorf("malformed URL: %v", err)
   201  	}
   202  	switch arr[0] {
   203  	case "generic":
   204  	default:
   205  		return "", fmt.Errorf("unsupported URL identifier")
   206  	}
   207  	tokenID, err := parseID(arr[1])
   208  	if err != nil {
   209  		return "", fmt.Errorf("malformed URL: %v", err)
   210  	}
   211  	return tokenID, nil
   212  }
   213  
   214  func parseEndpointPath(p, prefix string, length int) ([]string, error) {
   215  	p = strings.TrimPrefix(p, prefix)
   216  	arr := strings.Split(p, "/")
   217  	if len(arr) != length {
   218  		return arr, fmt.Errorf("unexpected endpoint path")
   219  	}
   220  	return arr, nil
   221  }
   222  
   223  func parseID(s string) (string, error) {
   224  	s = strings.TrimSpace(s)
   225  	if len(s) == 0 {
   226  		return "", fmt.Errorf("id is empty")
   227  	}
   228  	if len(s) > 96 {
   229  		return "", fmt.Errorf("id is too long")
   230  	}
   231  	return s, nil
   232  }