Skip to content

Commit

Permalink
feat(webauthn): allow encoding user.id as a string (#124)
Browse files Browse the repository at this point in the history
This allows encoding the user.id attribute as a string which is useful for implementations of the browser element which do not decode this value and you want to use a 64 byte string for.
  • Loading branch information
james-d-elliott authored Feb 19, 2023
1 parent e1d245d commit 0948c14
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 20 deletions.
3 changes: 2 additions & 1 deletion protocol/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ type UserEntity struct {
// For example, "Alex P. Müller" or "田中 倫". The Relying Party SHOULD let
// the user choose this, and SHOULD NOT restrict the choice more than necessary.
DisplayName string `json:"displayName,omitempty"`

// ID is the user handle of the user account entity. To ensure secure operation,
// authentication and authorization decisions MUST be made on the basis of this id
// member, not the displayName nor name members. See Section 6.1 of
// [RFC8266](https://www.w3.org/TR/webauthn/#biblio-rfc8266).
ID URLEncodedBase64 `json:"id"`
ID interface{} `json:"id"`
}
5 changes: 3 additions & 2 deletions webauthn/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import (
)

const (
errFmtEmptyField = "the field '%s' must be configured but it is empty"
errFmtConfigValidate = "error occurred validating the configuration: %w"
errFmtFieldEmpty = "the field '%s' must be configured but it is empty"
errFmtFieldNotValidURI = "field '%s' is not a valid URI: %w"
errFmtConfigValidate = "error occurred validating the configuration: %w"
)

const (
Expand Down
10 changes: 9 additions & 1 deletion webauthn/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio
return nil, nil, err
}

var entityUserID interface{}

if webauthn.Config.EncodeUserIDAsString {
entityUserID = string(user.WebAuthnID())
} else {
entityUserID = protocol.URLEncodedBase64(user.WebAuthnID())
}

entityUser := protocol.UserEntity{
ID: user.WebAuthnID(),
ID: entityUserID,
DisplayName: user.WebAuthnDisplayName(),
CredentialEntity: protocol.CredentialEntity{
Name: user.WebAuthnName(),
Expand Down
31 changes: 31 additions & 0 deletions webauthn/registration_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package webauthn

import (
"encoding/json"
"testing"

"github.com/go-webauthn/webauthn/protocol"
"github.com/stretchr/testify/assert"
)

func TestRegistration_FinishRegistrationFailure(t *testing.T) {
Expand All @@ -26,3 +28,32 @@ func TestRegistration_FinishRegistrationFailure(t *testing.T) {
t.Errorf("FinishRegistration() credential = %v, want nil", credential)
}
}

func TestEntityEncoding(t *testing.T) {
testCases := []struct {
name string
b64 bool
have, expected string
}{
{"ShouldEncodeBase64", true, "abc", "{\"name\":\"\",\"id\":\"YWJj\"}"},
{"ShouldEncodeString", false, "abc", "{\"name\":\"\",\"id\":\"abc\"}"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
entityUser := protocol.UserEntity{}

if tc.b64 {
entityUser.ID = protocol.URLEncodedBase64(tc.have)
} else {
entityUser.ID = tc.have
}

data, err := json.Marshal(entityUser)

assert.NoError(t, err)

assert.Equal(t, tc.expected, string(data))
})
}
}
48 changes: 32 additions & 16 deletions webauthn/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,16 @@ type WebAuthn struct {

// Config represents the WebAuthn configuration.
type Config struct {
// RPDisplayName configures the display name for the Relying Party Server. This can be any string.
RPDisplayName string

// RPID configures the Relying Party Server ID. This should generally be the origin without a scheme and port.
RPID string

// RPDisplayName configures the display name for the Relying Party Server. This can be any string.
RPDisplayName string

// RPOrigins configures the list of Relying Party Server Origins that are permitted. These should be fully
// qualified origins.
RPOrigins []string

// RPIcon
RPIcon string

// AttestationPreference sets the default attestation conveyance preferences.
AttestationPreference protocol.ConveyancePreference

Expand All @@ -48,20 +45,30 @@ type Config struct {
// Debug enables various debug options.
Debug bool

// Timeout configures the default timeout in milliseconds.
//
// Deprecated: Use Timeouts instead.
Timeout int
// EncodeUserIDAsString ensures the user.id value during registrations is encoded as a raw UTF8 string. This is
// useful when you only use printable ASCII characters for the random user.id but the browser library does not
// decode the URL Safe Base64 data.
EncodeUserIDAsString bool

// Timeouts configures various timeouts.
Timeouts TimeoutsConfig

validated bool

// RPIcon sets the icon URL for the Relying Party Server.
//
// Deprecated: this option has been removed from newer specifications due to security considerations.
RPIcon string

// RPOrigin configures the permitted Relying Party Server Origin.
//
// Deprecated: Use RPOrigins instead.
RPOrigin string

validated bool
// Timeout configures the default timeout in milliseconds.
//
// Deprecated: Use Timeouts instead.
Timeout int
}

// TimeoutsConfig represents the WebAuthn timeouts configuration.
Expand Down Expand Up @@ -91,16 +98,23 @@ func (config *Config) validate() error {
}

if len(config.RPDisplayName) == 0 {
return fmt.Errorf(errFmtEmptyField, "RPDisplayName")
return fmt.Errorf(errFmtFieldEmpty, "RPDisplayName")
}

if len(config.RPID) == 0 {
return fmt.Errorf(errFmtEmptyField, "RPID")
return fmt.Errorf(errFmtFieldEmpty, "RPID")
}

var err error

if _, err = url.Parse(config.RPID); err != nil {
return fmt.Errorf(errFmtFieldNotValidURI, "RPID", err)
}

_, err := url.Parse(config.RPID)
if err != nil {
return fmt.Errorf("RPID not valid URI: %+v", err)
if config.RPIcon != "" {
if _, err = url.Parse(config.RPIcon); err != nil {
return fmt.Errorf(errFmtFieldNotValidURI, "RPIcon", err)
}
}

defaultTimeoutConfig := defaultTimeout
Expand Down Expand Up @@ -161,6 +175,8 @@ type User interface {
// To ensure secure operation, authentication and authorization decisions MUST be made on the basis of this id
// member, not the displayName nor name members. See Section 6.1 of [RFC8266].
//
// It's recommended this value is completely random and uses the entire 64 bytes.
//
// Specification: §5.4.3. User Account Parameters for Credential Generation (https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-id)
WebAuthnID() []byte

Expand Down

0 comments on commit 0948c14

Please sign in to comment.