Skip to content

Commit

Permalink
feature: delete mfa tokens via profile api
Browse files Browse the repository at this point in the history
  • Loading branch information
greenpau committed Mar 17, 2024
1 parent e6d37cb commit ae9e524
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 34 deletions.
10 changes: 8 additions & 2 deletions internal/tag/tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion pkg/authn/apikey_form_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 5 additions & 3 deletions pkg/authn/handle_api_list_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions pkg/authn/handle_api_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
78 changes: 75 additions & 3 deletions pkg/authn/handle_api_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
7 changes: 4 additions & 3 deletions pkg/authn/handle_external_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions pkg/authn/handle_http_settings_mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
11 changes: 6 additions & 5 deletions pkg/authn/respond_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
Expand All @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions pkg/identity/mfa_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions pkg/identity/tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2022 Paul Greenberg [email protected]
//
// 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}
}
15 changes: 5 additions & 10 deletions pkg/identity/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit ae9e524

Please sign in to comment.