From d2e0291cc9db9b85d9ed09d3f02c3fcef1d92950 Mon Sep 17 00:00:00 2001 From: Paul Greenberg <greenpau@outlook.com> Date: Sun, 6 Dec 2020 19:42:59 -0500 Subject: [PATCH] add mfa app testing facilities Partial Resolution: #13 --- assets/templates/basic/settings.template | 57 ++++++++++++++++- go.mod | 2 +- go.sum | 4 +- pkg/handlers/settings.go | 80 ++++++++++++++++++++++++ pkg/ui/pages.go | 57 ++++++++++++++++- 5 files changed, 193 insertions(+), 7 deletions(-) diff --git a/assets/templates/basic/settings.template b/assets/templates/basic/settings.template index 64e0e67..b634911 100644 --- a/assets/templates/basic/settings.template +++ b/assets/templates/basic/settings.template @@ -297,7 +297,12 @@ </div> <div class="card-action"> <a href="{{ pathjoin $.ActionEndpoint "/settings/mfa/delete/" .ID }}">Delete</a> - <a href="{{ pathjoin $.ActionEndpoint "/settings/mfa/test/" .ID }}">Test</a> + {{ if eq .Type "totp" }} + <a href="{{ pathjoin $.ActionEndpoint "/settings/mfa/test/app/" .ID }}">Test</a> + {{ end }} + {{ if eq .Type "u2f" }} + <a href="{{ pathjoin $.ActionEndpoint "/settings/mfa/test/u2f/" .ID }}">Test</a> + {{ end }} </div> </div> {{ end }} @@ -337,7 +342,7 @@ <input id="secret" name="secret" type="hidden" value="{{ .Data.mfa_secret }}" /> <input id="type" name="type" type="hidden" value="{{ .Data.mfa_type }}" /> <input id="period" name="period" type="hidden" value="{{ .Data.mfa_period }}" /> - <input id="period" name="digits" type="hidden" value="{{ .Data.mfa_digits }}" /> + <input id="digits" name="digits" type="hidden" value="{{ .Data.mfa_digits }}" /> </div> <div class="col s12 m6 l6"> <div class="center-align"><img src="{{ pathjoin .ActionEndpoint "/settings/mfa/barcode/" .Data.code_uri_encoded }}.png" alt="QR Code" /></div> @@ -376,6 +381,54 @@ </div> </div> {{ end }} + {{ if eq .Data.view "mfa-test-app" }} + <form action="{{ pathjoin .ActionEndpoint "/settings/mfa/test/app/" .Data.mfa_token_id }}" method="POST"> + <div class="row"> + <h1>Test MFA Authenticator Application</h1> + <div class="row"> + <div class="col s12 m6 l6"> + <p>Please open your MFA authenticator application to view your authentication code and verify your identity</p> + <div class="input-field"> + <input id="passcode" name="passcode" type="text" class="validate" pattern="[0-9]{6}" + title="Passcode should contain 6 characters and consists of 0-9 characters." + required /> + <label for="passcode">Passcode</label> + </div> + <input id="token_id" name="token_id" type="hidden" value="{{ .Data.mfa_token_id }}" /> + </div> + </div> + </div> + <div class="row"> + <button type="submit" name="submit" class="btn waves-effect waves-light navbtn active navbtn-last app-btn"> + <i class="las la-plus-circle left app-btn-icon"></i> + <span class="app-btn-text">Validate</span> + </button> + </div> + </form> + {{ end }} + {{ if eq .Data.view "mfa-test-app-status" }} + <div class="row"> + <div class="col s12"> + <h1>Test MFA Authenticator Application</h1> + <p>{{.Data.status }}: {{ .Data.status_reason }}</p> + {{ if eq .Data.status "SUCCESS" }} + <a href="{{ pathjoin .ActionEndpoint "/settings/mfa" }}"> + <button type="button" class="btn waves-effect waves-light navbtn active"> + <i class="las la-undo-alt left app-btn-icon"></i> + <span class="app-btn-text">Go Back</span> + </button> + </a> + {{ else }} + <a href="{{ pathjoin .ActionEndpoint "/settings/mfa/test/app/" .Data.mfa_token_id }}"> + <button type="button" class="btn waves-effect waves-light navbtn active"> + <i class="las la-undo-alt left app-btn-icon"></i> + <span class="app-btn-text">Try Again</span> + </button> + </a> + {{ end }} + </div> + </div> + {{ end }} {{ if eq .Data.view "mfa-delete-status" }} <div class="row"> <div class="col s12"> diff --git a/go.mod b/go.mod index 238cbdb..435bb61 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-ldap/ldap v3.0.3+incompatible github.com/greenpau/caddy-auth-jwt v1.2.1 - github.com/greenpau/go-identity v1.0.17 + github.com/greenpau/go-identity v1.0.18 github.com/satori/go.uuid v1.2.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e go.uber.org/zap v1.15.0 diff --git a/go.sum b/go.sum index 866c208..1f31109 100644 --- a/go.sum +++ b/go.sum @@ -301,8 +301,8 @@ github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.m github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/greenpau/caddy-auth-jwt v1.2.1 h1:m04X6g6gvbMIdTpPbPYZRU4RQaFy5cKQk7ym/j6m/Tg= github.com/greenpau/caddy-auth-jwt v1.2.1/go.mod h1:XS1wGcSSSS9H544mis5b640ae/hNL58Nbu/tCU6PVZo= -github.com/greenpau/go-identity v1.0.17 h1:aGs7T5tI47p9nBFn/gRyT0AMVUYnG2YsTMiu0hHNN5U= -github.com/greenpau/go-identity v1.0.17/go.mod h1:++pwfuoXgXJQM1PFtiHTH9fMlbgOY42Hs5wSwoQ3JK0= +github.com/greenpau/go-identity v1.0.18 h1:F0V3gSqPYWGiGIE3121tY05JOrYxYym8aapIrBcIYjY= +github.com/greenpau/go-identity v1.0.18/go.mod h1:++pwfuoXgXJQM1PFtiHTH9fMlbgOY42Hs5wSwoQ3JK0= github.com/greenpau/versioned v1.0.23 h1:ICqCoTG8Xv92BV+bKs52d86pDF/e0zhk3LLELsYMpl4= github.com/greenpau/versioned v1.0.23/go.mod h1:rtFCvaWWNbMH4CJnje/xicgmrM63j++rUh5juSu0k/A= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= diff --git a/pkg/handlers/settings.go b/pkg/handlers/settings.go index d85c6a8..b430b7a 100644 --- a/pkg/handlers/settings.go +++ b/pkg/handlers/settings.go @@ -156,6 +156,59 @@ func ServeSettings(w http.ResponseWriter, r *http.Request, opts map[string]inter } } } + case "test": + if len(viewParts) > 3 { + resp.Data["mfa_token_id"] = strings.TrimSpace(viewParts[3]) + if r.Method == "POST" { + resp.Data["status"] = "FAIL" + if backend != nil { + switch viewParts[2] { + case "app": + view = "mfa-test-app-status" + var passcodeValid bool + if tokenID, passcode, err := validateMfaAuthTokenForm(r); err != nil { + resp.Data["status_reason"] = fmt.Sprintf("Bad Request: %s", err) + } else { + args := make(map[string]interface{}) + args["username"] = claims.Subject + args["email"] = claims.Email + mfaTokens, err := backend.GetMfaTokens(args) + if err != nil { + resp.Data["status_reason"] = fmt.Sprintf("%s", err) + } else { + for _, mfaToken := range mfaTokens { + if mfaToken.ID != tokenID { + continue + } + if err := mfaToken.ValidateCode(passcode); err == nil { + passcodeValid = true + resp.Data["status"] = "SUCCESS" + resp.Data["status_reason"] = fmt.Sprintf("token %s validated successfully", mfaToken.ID) + break + } + } + if !passcodeValid { + resp.Data["status_reason"] = fmt.Sprintf("invalid passcode") + } + } + } + case "u2f": + view = "mfa-test-u2f-status" + resp.Data["status_reason"] = "Not implemented" + } + } else { + resp.Data["status_reason"] = "Authentication backend not found" + } + } else { + // Get token ID from path + switch viewParts[2] { + case "app": + view = "mfa-test-app" + case "u2f": + view = "mfa-test-u2f" + } + } + } } } else { // Entry Page @@ -406,6 +459,33 @@ func validateKeyInputForm(r *http.Request) (map[string]string, error) { return resp, nil } +func validateMfaAuthTokenForm(r *http.Request) (string, string, error) { + if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { + return "", "", fmt.Errorf("Unsupported content type") + } + if err := r.ParseForm(); err != nil { + return "", "", fmt.Errorf("Failed parsing submitted form") + } + + passcode := r.PostFormValue("passcode") + passcode = strings.TrimSpace(passcode) + if passcode == "" { + return "", "", fmt.Errorf("Required form passcode field is empty") + } + + if len(passcode) < 4 || len(passcode) > 8 { + return "", "", fmt.Errorf("MFA passcode is not 4-8 characters long") + } + + tokenID := r.PostFormValue("token_id") + tokenID = strings.TrimSpace(tokenID) + if tokenID == "" { + return "", "", fmt.Errorf("Required form token_id field is empty") + } + + return tokenID, passcode, nil +} + func validateAddMfaTokenForm(r *http.Request) (map[string]string, error) { resp := make(map[string]string) if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { diff --git a/pkg/ui/pages.go b/pkg/ui/pages.go index d618df4..7c159df 100644 --- a/pkg/ui/pages.go +++ b/pkg/ui/pages.go @@ -792,7 +792,12 @@ var PageTemplates = map[string]string{ </div> <div class="card-action"> <a href="{{ pathjoin $.ActionEndpoint "/settings/mfa/delete/" .ID }}">Delete</a> - <a href="{{ pathjoin $.ActionEndpoint "/settings/mfa/test/" .ID }}">Test</a> + {{ if eq .Type "totp" }} + <a href="{{ pathjoin $.ActionEndpoint "/settings/mfa/test/app/" .ID }}">Test</a> + {{ end }} + {{ if eq .Type "u2f" }} + <a href="{{ pathjoin $.ActionEndpoint "/settings/mfa/test/u2f/" .ID }}">Test</a> + {{ end }} </div> </div> {{ end }} @@ -832,7 +837,7 @@ var PageTemplates = map[string]string{ <input id="secret" name="secret" type="hidden" value="{{ .Data.mfa_secret }}" /> <input id="type" name="type" type="hidden" value="{{ .Data.mfa_type }}" /> <input id="period" name="period" type="hidden" value="{{ .Data.mfa_period }}" /> - <input id="period" name="digits" type="hidden" value="{{ .Data.mfa_digits }}" /> + <input id="digits" name="digits" type="hidden" value="{{ .Data.mfa_digits }}" /> </div> <div class="col s12 m6 l6"> <div class="center-align"><img src="{{ pathjoin .ActionEndpoint "/settings/mfa/barcode/" .Data.code_uri_encoded }}.png" alt="QR Code" /></div> @@ -871,6 +876,54 @@ var PageTemplates = map[string]string{ </div> </div> {{ end }} + {{ if eq .Data.view "mfa-test-app" }} + <form action="{{ pathjoin .ActionEndpoint "/settings/mfa/test/app/" .Data.mfa_token_id }}" method="POST"> + <div class="row"> + <h1>Test MFA Authenticator Application</h1> + <div class="row"> + <div class="col s12 m6 l6"> + <p>Please open your MFA authenticator application to view your authentication code and verify your identity</p> + <div class="input-field"> + <input id="passcode" name="passcode" type="text" class="validate" pattern="[0-9]{6}" + title="Passcode should contain 6 characters and consists of 0-9 characters." + required /> + <label for="passcode">Passcode</label> + </div> + <input id="token_id" name="token_id" type="hidden" value="{{ .Data.mfa_token_id }}" /> + </div> + </div> + </div> + <div class="row"> + <button type="submit" name="submit" class="btn waves-effect waves-light navbtn active navbtn-last app-btn"> + <i class="las la-plus-circle left app-btn-icon"></i> + <span class="app-btn-text">Validate</span> + </button> + </div> + </form> + {{ end }} + {{ if eq .Data.view "mfa-test-app-status" }} + <div class="row"> + <div class="col s12"> + <h1>Test MFA Authenticator Application</h1> + <p>{{.Data.status }}: {{ .Data.status_reason }}</p> + {{ if eq .Data.status "SUCCESS" }} + <a href="{{ pathjoin .ActionEndpoint "/settings/mfa" }}"> + <button type="button" class="btn waves-effect waves-light navbtn active"> + <i class="las la-undo-alt left app-btn-icon"></i> + <span class="app-btn-text">Go Back</span> + </button> + </a> + {{ else }} + <a href="{{ pathjoin .ActionEndpoint "/settings/mfa/test/app/" .Data.mfa_token_id }}"> + <button type="button" class="btn waves-effect waves-light navbtn active"> + <i class="las la-undo-alt left app-btn-icon"></i> + <span class="app-btn-text">Try Again</span> + </button> + </a> + {{ end }} + </div> + </div> + {{ end }} {{ if eq .Data.view "mfa-delete-status" }} <div class="row"> <div class="col s12">