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 }