Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 9 additions & 41 deletions internal/api/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import (
"bytes"
"crypto/subtle"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/aaronarduino/goqrsvg"
Expand Down Expand Up @@ -77,8 +75,6 @@ type WebAuthnChallengeData struct {
}

type WebAuthnParams struct {
RPID string `json:"rpId,omitempty"`
RPOrigins []string `json:"rpOrigins,omitempty"`
Type string `json:"type"` // "create" or "request"
CredentialResponse json.RawMessage `json:"credential_response"`
}
Expand All @@ -87,39 +83,14 @@ type UnenrollFactorResponse struct {
ID uuid.UUID `json:"id"`
}

func (w *WebAuthnParams) ToConfig() (*webauthn.WebAuthn, error) {
if w.RPID == "" {
return nil, fmt.Errorf("webAuthn RP ID cannot be empty")
}

if len(w.RPOrigins) == 0 {
return nil, fmt.Errorf("webAuthn RP Origins cannot be empty")
}

var validOrigins []string
var invalidOrigins []string

for _, origin := range w.RPOrigins {
parsedURL, err := url.Parse(origin)
if err != nil || (parsedURL.Scheme != "https" && !(parsedURL.Scheme == "http" && parsedURL.Hostname() == "localhost")) || parsedURL.Host == "" {
invalidOrigins = append(invalidOrigins, origin)
} else {
validOrigins = append(validOrigins, origin)
}
}
func (a *API) getWebAuthnMFA() (*webauthn.WebAuthn, error) {
rpConfig := a.config.WebAuthn

if len(invalidOrigins) > 0 {
return nil, fmt.Errorf("invalid RP origins: %s", strings.Join(invalidOrigins, ", "))
}

wconfig := &webauthn.Config{
// DisplayName is optional in spec but required to be non-empty in libary, we use the RPID as a placeholder.
RPDisplayName: w.RPID,
RPID: w.RPID,
RPOrigins: validOrigins,
}

return webauthn.New(wconfig)
return webauthn.New(&webauthn.Config{
RPDisplayName: rpConfig.RPDisplayName,
RPID: rpConfig.RPID,
RPOrigins: rpConfig.RPOrigins,
})
}

const (
Expand Down Expand Up @@ -500,10 +471,7 @@ func (a *API) challengeWebAuthnFactor(w http.ResponseWriter, r *http.Request) er
if err := retrieveRequestParams(r, params); err != nil {
return err
}
if params.WebAuthn == nil {
return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "web_authn config required")
}
webAuthn, err := params.WebAuthn.ToConfig()
webAuthn, err := a.getWebAuthnMFA()
if err != nil {
return err
}
Expand Down Expand Up @@ -917,7 +885,7 @@ func (a *API) verifyWebAuthnFactor(w http.ResponseWriter, r *http.Request, param
case params.WebAuthn.CredentialResponse == nil:
return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "credential_response required")
default:
webAuthn, err = params.WebAuthn.ToConfig()
webAuthn, err = a.getWebAuthnMFA()
if err != nil {
return err
}
Expand Down
12 changes: 7 additions & 5 deletions internal/api/mfa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ func (ts *MFATestSuite) SetupTest() {

ts.Config.MFA.WebAuthn.EnrollEnabled = true
ts.Config.MFA.WebAuthn.VerifyEnabled = true
ts.Config.WebAuthn = conf.WebAuthnConfiguration{
RPID: "localhost",
RPDisplayName: "Test App",
RPOrigins: []string{"http://localhost:3000"},
ChallengeExpiryDuration: 5 * time.Minute,
}

key, err := totp.Generate(totp.GenerateOpts{
Issuer: ts.TestDomain,
Expand Down Expand Up @@ -721,13 +727,9 @@ func (ts *MFATestSuite) TestMFAFollowedByPasswordSignIn() {

func (ts *MFATestSuite) TestChallengeWebAuthnFactor() {
factor := models.NewWebAuthnFactor(ts.TestUser, "WebAuthnfactor")
validWebAuthnConfiguration := &WebAuthnParams{
RPID: "localhost",
RPOrigins: []string{"http://localhost:3000"},
}
require.NoError(ts.T(), ts.API.db.Create(factor), "Error saving new test factor")
token := ts.generateAAL1Token(ts.TestUser, &ts.TestSession.ID)
w := performChallengeWebAuthnFlow(ts, factor.ID, token, validWebAuthnConfiguration)
w := performChallengeWebAuthnFlow(ts, factor.ID, token, &WebAuthnParams{})
require.Equal(ts.T(), http.StatusOK, w.Code)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ func (c *GlobalConfiguration) Validate() error {
}
}

if c.Passkey.Enabled {
if c.Passkey.Enabled || c.MFA.WebAuthn.EnrollEnabled || c.MFA.WebAuthn.VerifyEnabled {
if err := c.WebAuthn.Validate(); err != nil {
return err
}
Expand Down
26 changes: 0 additions & 26 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1040,21 +1040,6 @@ paths:
enum:
- sms
- whatsapp
webauthn:
type: object
required:
- rpId
- rpOrigins
properties:
rpId:
type: string
description: The relying party identifier (usually the domain)
rpOrigins:
type: array
items:
type: string
minItems: 1
description: List of allowed origins for WebAuthn

responses:
200:
Expand Down Expand Up @@ -1103,20 +1088,9 @@ paths:
webauthn:
type: object
required:
- rpId
- rpOrigins
- type
- credential_response
properties:
rpId:
type: string
description: The relying party identifier
rpOrigins:
type: array
items:
type: string
minItems: 1
description: List of allowed origins for WebAuthn
type:
type: string
enum: [create, request]
Expand Down
Loading