From ae9e524366927ad40f70d2216d65086b585f4fbb Mon Sep 17 00:00:00 2001 From: Paul Greenberg Date: Sat, 16 Mar 2024 21:24:07 -0400 Subject: [PATCH] feature: delete mfa tokens via profile api --- internal/tag/tag_test.go | 10 +++- pkg/authn/apikey_form_validator.go | 3 +- pkg/authn/handle_api_list_users.go | 8 +-- pkg/authn/handle_api_metadata.go | 7 +-- pkg/authn/handle_api_profile.go | 78 +++++++++++++++++++++++++-- pkg/authn/handle_external_login.go | 7 +-- pkg/authn/handle_http_settings_mfa.go | 4 +- pkg/authn/respond_json.go | 11 ++-- pkg/identity/mfa_token.go | 7 ++- pkg/identity/tag.go | 26 +++++++++ pkg/identity/user.go | 15 ++---- 11 files changed, 142 insertions(+), 34 deletions(-) create mode 100644 pkg/identity/tag.go diff --git a/internal/tag/tag_test.go b/internal/tag/tag_test.go index 724b65d..e62868a 100644 --- a/internal/tag/tag_test.go +++ b/internal/tag/tag_test.go @@ -17,6 +17,9 @@ package tag import ( "bufio" "fmt" + "strings" + "unicode" + "github.com/greenpau/go-authcrunch" "github.com/greenpau/go-authcrunch/internal/tests" "github.com/greenpau/go-authcrunch/internal/testutils" @@ -52,8 +55,6 @@ import ( "github.com/greenpau/go-authcrunch/pkg/user" "github.com/greenpau/go-authcrunch/pkg/util" "github.com/greenpau/go-authcrunch/pkg/util/cfg" - "strings" - "unicode" "os" "path/filepath" @@ -878,6 +879,11 @@ func TestTagCompliance(t *testing.T) { entry: &redirects.RedirectURIMatchConfig{}, opts: &Options{}, }, + { + name: "test identity.Tag struct", + entry: &identity.Tag{}, + opts: &Options{}, + }, } for _, tc := range testcases { diff --git a/pkg/authn/apikey_form_validator.go b/pkg/authn/apikey_form_validator.go index dba1b52..02f7f71 100644 --- a/pkg/authn/apikey_form_validator.go +++ b/pkg/authn/apikey_form_validator.go @@ -16,9 +16,10 @@ package authn import ( "fmt" - "github.com/greenpau/go-authcrunch/pkg/requests" "net/http" "strings" + + "github.com/greenpau/go-authcrunch/pkg/requests" ) func validateAPIKeyInputForm(r *http.Request, rr *requests.Request) error { diff --git a/pkg/authn/handle_api_list_users.go b/pkg/authn/handle_api_list_users.go index 9e9fad0..c7462e4 100644 --- a/pkg/authn/handle_api_list_users.go +++ b/pkg/authn/handle_api_list_users.go @@ -17,14 +17,16 @@ package authn import ( "context" "encoding/json" + // "github.com/greenpau/go-authcrunch/pkg/identity" - "github.com/greenpau/go-authcrunch/pkg/requests" - "github.com/greenpau/go-authcrunch/pkg/user" "net/http" "time" + + "github.com/greenpau/go-authcrunch/pkg/requests" + "github.com/greenpau/go-authcrunch/pkg/user" ) -func (p *Portal) handleAPIListUsers(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, usr *user.User) error { +func (p *Portal) handleAPIListUsers(_ context.Context, w http.ResponseWriter, _ *http.Request, rr *requests.Request, _ *user.User) error { rr.Response.Code = http.StatusOK resp := make(map[string]interface{}) resp["timestamp"] = time.Now().UTC().Format(time.RFC3339Nano) diff --git a/pkg/authn/handle_api_metadata.go b/pkg/authn/handle_api_metadata.go index c15ef93..3a6fa4f 100644 --- a/pkg/authn/handle_api_metadata.go +++ b/pkg/authn/handle_api_metadata.go @@ -17,14 +17,15 @@ package authn import ( "context" "encoding/json" + "net/http" + "time" + "github.com/greenpau/go-authcrunch/pkg/identity" "github.com/greenpau/go-authcrunch/pkg/requests" "github.com/greenpau/go-authcrunch/pkg/user" - "net/http" - "time" ) -func (p *Portal) handleAPIMetadata(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, usr *user.User) error { +func (p *Portal) handleAPIMetadata(_ context.Context, w http.ResponseWriter, _ *http.Request, rr *requests.Request, _ *user.User) error { rr.Response.Code = http.StatusOK resp := identity.Version() resp["timestamp"] = time.Now().UTC().Format(time.RFC3339Nano) diff --git a/pkg/authn/handle_api_profile.go b/pkg/authn/handle_api_profile.go index cb9b04c..25c3522 100644 --- a/pkg/authn/handle_api_profile.go +++ b/pkg/authn/handle_api_profile.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "time" @@ -67,13 +68,42 @@ func (p *Portal) handleAPIProfile(ctx context.Context, w http.ResponseWriter, r return handleAPIProfileResponse(w, rr, http.StatusForbidden, resp) } - reqKind := "fetch_user_dashboard_data" + // Unpack the request and determine the type of the request. + var reqKind = "unknown" + + // Read the request body + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + resp["message"] = "Profile API failed to parse request body" + return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp) + } + var bodyData map[string]interface{} + if err := json.Unmarshal(body, &bodyData); err != nil { + resp["message"] = "Profile API failed to parse request JSON body" + return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp) + } + + if v, exists := bodyData["kind"]; exists { + reqKind = v.(string) + } + + switch reqKind { + case "fetch_user_dashboard_data": + case "delete_user_multi_factor_verifier": + case "fetch_user_multi_factor_verifiers": + default: + resp["message"] = "Profile API recieved unsupported request type" + return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp) + } + + // Determine supported authentication methods. switch usr.Authenticator.Method { case "local": default: resp["message"] = fmt.Sprintf("%s is not supported with profile API", usr.Authenticator.Method) - return handleAPIProfileResponse(w, rr, 501, resp) + return handleAPIProfileResponse(w, rr, http.StatusNotImplemented, resp) } backend := p.getIdentityStoreByRealm(usr.Authenticator.Realm) @@ -219,8 +249,50 @@ func (p *Portal) handleAPIProfile(ctx context.Context, w http.ResponseWriter, r entry["connected_accounts"] = []interface{}{} resp["entry"] = entry return handleAPIProfileResponse(w, rr, http.StatusOK, resp) + case "fetch_user_multi_factor_verifiers": + fetchedData := make(map[string]interface{}) + fetchedData["endpoint"] = "/list" + if err := p.handleHTTPMfaSettings(ctx, r, rr, usr, backend, fetchedData); err != nil { + resp["message"] = "failed to extract user MFA/2FA" + p.logger.Debug( + "failed to extract user MFA/2FA", + zap.String("session_id", rr.Upstream.SessionID), + zap.String("request_id", rr.ID), + zap.Error(err), + ) + return handleAPIProfileResponse(w, rr, http.StatusInternalServerError, resp) + } + + if mfaTokens, exists := fetchedData["mfa_tokens"]; exists { + resp["entries"] = mfaTokens + } else { + resp["entries"] = []string{} + } + return handleAPIProfileResponse(w, rr, http.StatusOK, resp) + case "delete_user_multi_factor_verifier": + fetchedData := make(map[string]interface{}) + var verifierID string + if v, exists := bodyData["id"]; exists { + verifierID = v.(string) + } else { + resp["message"] = "Profile API did not find id in the request payload" + return handleAPIProfileResponse(w, rr, http.StatusBadRequest, resp) + } + fetchedData["endpoint"] = fmt.Sprintf("/delete/%s", verifierID) + if err := p.handleHTTPMfaSettings(ctx, r, rr, usr, backend, fetchedData); err != nil { + resp["message"] = "failed to delete user MFA/2FA" + p.logger.Debug( + "failed to delete user MFA/2FA", + zap.String("session_id", rr.Upstream.SessionID), + zap.String("request_id", rr.ID), + zap.Error(err), + ) + return handleAPIProfileResponse(w, rr, http.StatusInternalServerError, resp) + } + resp["entry"] = verifierID + return handleAPIProfileResponse(w, rr, http.StatusOK, resp) default: resp["message"] = fmt.Sprintf("unsupported %s request kind with profile API", reqKind) - return handleAPIProfileResponse(w, rr, 501, resp) + return handleAPIProfileResponse(w, rr, http.StatusNotImplemented, resp) } } diff --git a/pkg/authn/handle_external_login.go b/pkg/authn/handle_external_login.go index a600ee6..3ae4473 100644 --- a/pkg/authn/handle_external_login.go +++ b/pkg/authn/handle_external_login.go @@ -16,11 +16,12 @@ package authn import ( "context" + "net/http" + "strings" + "github.com/greenpau/go-authcrunch/pkg/authn/enums/operator" "github.com/greenpau/go-authcrunch/pkg/requests" "go.uber.org/zap" - "net/http" - "strings" ) func (p *Portal) handleHTTPExternalLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, authMethod string) error { @@ -105,7 +106,7 @@ func (p *Portal) handleHTTPExternalLogin(ctx context.Context, w http.ResponseWri return nil } -func (p *Portal) handleJavascriptCallbackIntercept(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (p *Portal) handleJavascriptCallbackIntercept(_ context.Context, w http.ResponseWriter, _ *http.Request) error { p.disableClientCache(w) w.WriteHeader(200) diff --git a/pkg/authn/handle_http_settings_mfa.go b/pkg/authn/handle_http_settings_mfa.go index af27a26..e997f5a 100644 --- a/pkg/authn/handle_http_settings_mfa.go +++ b/pkg/authn/handle_http_settings_mfa.go @@ -32,7 +32,7 @@ import ( "go.uber.org/zap" ) -func (p *Portal) handleHTTPMfaBarcode(ctx context.Context, w http.ResponseWriter, r *http.Request, endpoint string) error { +func (p *Portal) handleHTTPMfaBarcode(ctx context.Context, w http.ResponseWriter, _ *http.Request, endpoint string) error { qrCodeEncoded := strings.TrimPrefix(endpoint, "/mfa/barcode/") qrCodeEncoded = strings.TrimSuffix(qrCodeEncoded, ".png") codeURI, err := base64.StdEncoding.DecodeString(qrCodeEncoded) @@ -49,7 +49,7 @@ func (p *Portal) handleHTTPMfaBarcode(ctx context.Context, w http.ResponseWriter } func (p *Portal) handleHTTPMfaSettings( - ctx context.Context, r *http.Request, rr *requests.Request, + _ context.Context, r *http.Request, rr *requests.Request, usr *user.User, store ids.IdentityStore, data map[string]interface{}, ) error { var action string diff --git a/pkg/authn/respond_json.go b/pkg/authn/respond_json.go index 95a6850..2c96eaf 100644 --- a/pkg/authn/respond_json.go +++ b/pkg/authn/respond_json.go @@ -17,12 +17,13 @@ package authn import ( "context" "encoding/json" - "github.com/greenpau/go-authcrunch/pkg/requests" - addrutil "github.com/greenpau/go-authcrunch/pkg/util/addr" - "go.uber.org/zap" "net/http" "strings" "time" + + "github.com/greenpau/go-authcrunch/pkg/requests" + addrutil "github.com/greenpau/go-authcrunch/pkg/util/addr" + "go.uber.org/zap" ) // AccessDeniedResponse is the access denied response. @@ -40,7 +41,7 @@ func newAccessDeniedResponse(msg string) *AccessDeniedResponse { } } -func (p *Portal) handleJSONErrorWithLog(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, code int, msg string) error { +func (p *Portal) handleJSONErrorWithLog(ctx context.Context, w http.ResponseWriter, _ *http.Request, rr *requests.Request, code int, msg string) error { p.logger.Warn( "Access denied", zap.String("session_id", rr.Upstream.SessionID), @@ -58,7 +59,7 @@ func (p *Portal) handleJSONErrorWithLog(ctx context.Context, w http.ResponseWrit return p.handleJSONError(ctx, w, code, "Access denied") } -func (p *Portal) handleJSONError(ctx context.Context, w http.ResponseWriter, code int, msg string) error { +func (p *Portal) handleJSONError(_ context.Context, w http.ResponseWriter, code int, msg string) error { resp := newAccessDeniedResponse(msg) respBytes, _ := json.Marshal(resp) w.WriteHeader(code) diff --git a/pkg/identity/mfa_token.go b/pkg/identity/mfa_token.go index f2c1114..02d0d6a 100644 --- a/pkg/identity/mfa_token.go +++ b/pkg/identity/mfa_token.go @@ -63,8 +63,11 @@ type MfaToken struct { Parameters map[string]string `json:"parameters,omitempty" xml:"parameters,omitempty" yaml:"parameters,omitempty"` Flags map[string]bool `json:"flags,omitempty" xml:"flags,omitempty" yaml:"flags,omitempty"` SignatureCounter uint32 `json:"signature_counter,omitempty" xml:"signature_counter,omitempty" yaml:"signature_counter,omitempty"` - pubkeyECDSA *ecdsa.PublicKey - pubkeyRSA *rsa.PublicKey + Tags []Tag `json:"tags,omitempty" xml:"tags,omitempty" yaml:"tags,omitempty"` + Labels []string `json:"labels,omitempty" xml:"labels,omitempty" yaml:"labels,omitempty"` + + pubkeyECDSA *ecdsa.PublicKey + pubkeyRSA *rsa.PublicKey } // MfaDevice is the hardware device associated with MfaToken. diff --git a/pkg/identity/tag.go b/pkg/identity/tag.go new file mode 100644 index 0000000..275289f --- /dev/null +++ b/pkg/identity/tag.go @@ -0,0 +1,26 @@ +// Copyright 2022 Paul Greenberg greenpau@outlook.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package identity + +// Tag represents key-value tag. +type Tag struct { + Key string `json:"key,omitempty" xml:"key,omitempty" yaml:"key,omitempty"` + Value string `json:"value,omitempty" xml:"value,omitempty" yaml:"value,omitempty"` +} + +// NewTag returns an instance of Tag +func NewTag(key, value string) *Tag { + return &Tag{Key: key, Value: value} +} diff --git a/pkg/identity/user.go b/pkg/identity/user.go index 6d10c2d..b127f57 100644 --- a/pkg/identity/user.go +++ b/pkg/identity/user.go @@ -15,10 +15,11 @@ package identity import ( - "github.com/greenpau/go-authcrunch/pkg/errors" - "github.com/greenpau/go-authcrunch/pkg/requests" "strings" "time" + + "github.com/greenpau/go-authcrunch/pkg/errors" + "github.com/greenpau/go-authcrunch/pkg/requests" ) // UserMetadata is metadata associated with a user. @@ -206,18 +207,12 @@ func (user *User) AddEmailAddress(s string) error { // HasEmailAddresses checks whether a user has email address. func (user *User) HasEmailAddresses() bool { - if len(user.EmailAddresses) == 0 { - return false - } - return true + return len(user.EmailAddresses) != 0 } // HasRoles checks whether a user has a role. func (user *User) HasRoles() bool { - if len(user.Roles) == 0 { - return false - } - return true + return len(user.Roles) != 0 } // HasRole checks whether a user has a specific role.