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 }