Skip to content

Commit

Permalink
chore: code review (#3679)
Browse files Browse the repository at this point in the history
  • Loading branch information
aeneasr authored Jan 8, 2024
1 parent 21ec7cb commit e25f614
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 192 deletions.
3 changes: 0 additions & 3 deletions selfservice/strategy/passkey/passkey_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ import (
"github.com/ory/x/decoderx"
)

//go:embed .schema/login.schema.json
var loginSchema []byte

func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
webauthnx.RegisterWebauthnRoute(r)
}
Expand Down
296 changes: 148 additions & 148 deletions selfservice/strategy/passkey/passkey_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,132 +31,126 @@ import (
"github.com/ory/x/randx"
)

//go:embed .schema/registration.schema.json
var registrationSchema []byte

func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) {
webauthnx.RegisterWebauthnRoute(r)
}

func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registration.Flow) error {
ctx := r.Context()

if regFlow.Type != flow.TypeBrowser {
return nil
}
// Update Registration Flow with Passkey Method
//
// swagger:model updateRegistrationFlowWithPasskeyMethod
type updateRegistrationFlowWithPasskeyMethod struct {
// Register a WebAuthn Security Key
//
// It is expected that the JSON returned by the WebAuthn registration process
// is included here.
Register string `json:"passkey_register"`

defaultSchemaURL, err := s.d.Config().DefaultIdentityTraitsSchemaURL(ctx)
if err != nil {
return err
}
nodes, err := s.registrationNodes(ctx, defaultSchemaURL)
if err != nil {
return err
}
// CSRFToken is the anti-CSRF token
CSRFToken string `json:"csrf_token"`

for _, n := range nodes {
regFlow.UI.SetNode(n)
}
// The identity's traits
//
// required: true
Traits json.RawMessage `json:"traits"`

regFlow.UI.Nodes.Append(node.NewInputField(
"method",
"passkey",
node.PasskeyGroup,
node.InputAttributeTypeSubmit,
).WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterPasskey()))
// Method
//
// Should be set to "passkey" when trying to add, update, or remove a Passkey.
//
// required: true
Method string `json:"method"`

regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
// Flow is flow ID.
//
// swagger:ignore
Flow string `json:"flow"`

return nil
// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty"`
}

func (s *Strategy) registrationNodes(ctx context.Context, schemaURL *url.URL) (node.Nodes, error) {
runner, err := schema.NewExtensionRunner(ctx)
if err != nil {
return nil, err
}
c := jsonschema.NewCompiler()
runner.Register(c)

nodes, err := container.NodesFromJSONSchema(ctx, node.DefaultGroup, schemaURL.String(), "", c)
if err != nil {
return nil, err
}
return nodes, nil
func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) {
webauthnx.RegisterWebauthnRoute(r)
}

// webauthnIdentifierNode returns the node that is used to identify the user in the WebAuthn flow.
func (s *Strategy) webauthnIdentifierNode(ctx context.Context, schemaURL *url.URL) (*node.Node, error) {
nodes, err := s.registrationNodes(ctx, schemaURL)
if err != nil {
return nil, err
}
for _, n := range nodes {
if attr, ok := n.Attributes.(*node.InputAttributes); ok {
if attr.DataWebauthnIdentifier {
return n, nil
func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithPasskeyMethod, err error) error {
if f != nil {
if p != nil {
for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes {
// we only set the value and not the whole field because we want to keep types from the initial form generation
f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
}
}

if f.Type == flow.TypeBrowser {
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
}
}

return nil, schema.NewMissingIdentifierError()
return err
}

func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, ident *identity.Identity) (err error) {
func (s *Strategy) decode(r *http.Request) (*updateRegistrationFlowWithPasskeyMethod, error) {
var p updateRegistrationFlowWithPasskeyMethod
err := registration.DecodeBody(&p, r, s.hd, s.d.Config(), registrationSchema)
return &p, err
}

func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) {
ctx := r.Context()

if regFlow.Type != flow.TypeBrowser {
return flow.ErrStrategyNotResponsible
}

params, err := s.decode(r)
p, err := s.decode(r)
if err != nil {
return s.handleRegistrationError(w, r, regFlow, params, err)
}
if params.Register == "" && params.Method != "passkey" {
return flow.ErrStrategyNotResponsible
return s.handleRegistrationError(w, r, regFlow, p, err)
}

regFlow.TransientPayload = params.TransientPayload
regFlow.TransientPayload = p.TransientPayload

if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, params.CSRFToken); err != nil {
return s.handleRegistrationError(w, r, regFlow, params, err)
if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
return s.handleRegistrationError(w, r, regFlow, p, err)
}

if len(params.Register) == 0 {
return s.createPasskey(r, w, regFlow, params)
if p.Register == "" && p.Method != "passkey" {
return flow.ErrStrategyNotResponsible
}

if err := flow.MethodEnabledAndAllowed(ctx, regFlow.GetFlowName(), params.Method, params.Method, s.d); err != nil {
return s.handleRegistrationError(w, r, regFlow, params, err)
if len(p.Register) == 0 {
return s.addPassKeyNodes(r, w, regFlow, p)
}

if len(params.Traits) == 0 {
params.Traits = json.RawMessage("{}")
p.Method = s.ID().String()
if err := flow.MethodEnabledAndAllowed(ctx, regFlow.GetFlowName(), p.Method, p.Method, s.d); err != nil {
return s.handleRegistrationError(w, r, regFlow, p, err)
}
ident.Traits = identity.Traits(params.Traits)

if len(p.Traits) == 0 {
p.Traits = json.RawMessage("{}")
}
i.Traits = identity.Traits(p.Traits)

webAuthnSession := gjson.GetBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData))
if !webAuthnSession.IsObject() {
return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack(
return s.handleRegistrationError(w, r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object.")))
}

var webAuthnSess webauthn.SessionData
if err := json.Unmarshal([]byte(webAuthnSession.Raw), &webAuthnSess); err != nil {
return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack(
return s.handleRegistrationError(w, r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err)))
}

webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(params.Register))
webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(p.Register))
if err != nil {
return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack(
return s.handleRegistrationError(w, r, regFlow, p, errors.WithStack(
herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err)))
}

webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx))
if err != nil {
return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack(
return s.handleRegistrationError(w, r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())))
}

Expand All @@ -165,40 +159,106 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *reg
if devErr := new(protocol.Error); errors.As(err, &devErr) {
s.d.Logger().WithError(err).WithField("error_devinfo", devErr.DevInfo).Error("Failed to create WebAuthn credential")
}
return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err)))
return s.handleRegistrationError(w, r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err)))
}

credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true)
credentialsConfig, err := json.Marshal(identity.CredentialsWebAuthnConfig{
credentialWebAuthnConfig, err := json.Marshal(identity.CredentialsWebAuthnConfig{
Credentials: identity.CredentialsWebAuthn{*credentialWebAuthn},
UserHandle: webAuthnSess.UserID,
})
if err != nil {
return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())))
return s.handleRegistrationError(w, r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())))
}

ident.UpsertCredentialsConfig(s.ID(), credentialsConfig, 1)
passkeyCred, _ := ident.GetCredentials(s.ID())
i.UpsertCredentialsConfig(s.ID(), credentialWebAuthnConfig, 1)
passkeyCred, _ := i.GetCredentials(s.ID())
passkeyCred.Identifiers = []string{string(webAuthnSess.UserID)}
ident.SetCredentials(s.ID(), *passkeyCred)
if err := s.validateCredentials(ctx, ident); err != nil {
return s.handleRegistrationError(w, r, regFlow, params, err)
i.SetCredentials(s.ID(), *passkeyCred)
if err := s.validateCredentials(ctx, i); err != nil {
return s.handleRegistrationError(w, r, regFlow, p, err)
}

// Remove the WebAuthn URL from the internal context now that it is set!
regFlow.InternalContext, err = sjson.DeleteBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData))
if err != nil {
return s.handleRegistrationError(w, r, regFlow, params, err)
return s.handleRegistrationError(w, r, regFlow, p, err)
}

if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil {
return s.handleRegistrationError(w, r, regFlow, params, err)
return s.handleRegistrationError(w, r, regFlow, p, err)
}

return nil
}

func (s *Strategy) createPasskey(r *http.Request, w http.ResponseWriter, regFlow *registration.Flow, params *updateRegistrationFlowWithPasskeyMethod) error {
func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registration.Flow) error {
ctx := r.Context()

if regFlow.Type != flow.TypeBrowser {
return nil
}

defaultSchemaURL, err := s.d.Config().DefaultIdentityTraitsSchemaURL(ctx)
if err != nil {
return err
}
nodes, err := s.populateRegistrationNodes(ctx, defaultSchemaURL)
if err != nil {
return err
}

for _, n := range nodes {
regFlow.UI.SetNode(n)
}

regFlow.UI.Nodes.Append(node.NewInputField(
"method",
"passkey",
node.PasskeyGroup,
node.InputAttributeTypeSubmit,
).WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterPasskey()))

regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r))

return nil
}

func (s *Strategy) populateRegistrationNodes(ctx context.Context, schemaURL *url.URL) (node.Nodes, error) {
runner, err := schema.NewExtensionRunner(ctx)
if err != nil {
return nil, err
}
c := jsonschema.NewCompiler()
runner.Register(c)

nodes, err := container.NodesFromJSONSchema(ctx, node.DefaultGroup, schemaURL.String(), "", c)
if err != nil {
return nil, err
}
return nodes, nil
}

// webauthnIdentifierNode returns the node that is used to identify the user in the WebAuthn flow.
func (s *Strategy) webauthnIdentifierNode(ctx context.Context, schemaURL *url.URL) (*node.Node, error) {
nodes, err := s.populateRegistrationNodes(ctx, schemaURL)
if err != nil {
return nil, err
}
for _, n := range nodes {
if attr, ok := n.Attributes.(*node.InputAttributes); ok {
if attr.DataWebauthnIdentifier {
return n, nil
}
}
}

return nil, schema.NewMissingIdentifierError()
}

func (s *Strategy) addPassKeyNodes(r *http.Request, w http.ResponseWriter, regFlow *registration.Flow, params *updateRegistrationFlowWithPasskeyMethod) error {
ctx := r.Context()

defaultSchemaURL, err := s.d.Config().DefaultIdentityTraitsSchemaURL(ctx)
Expand Down Expand Up @@ -284,66 +344,6 @@ func (s *Strategy) createPasskey(r *http.Request, w http.ResponseWriter, regFlow
return flow.ErrCompletedByStrategy
}

// Update Registration Flow with Passkey Method
//
// swagger:model updateRegistrationFlowWithPasskeyMethod
type updateRegistrationFlowWithPasskeyMethod struct {
// Register a WebAuthn Security Key
//
// It is expected that the JSON returned by the WebAuthn registration process
// is included here.
Register string `json:"passkey_register"`

// CSRFToken is the anti-CSRF token
CSRFToken string `json:"csrf_token"`

// The identity's traits
//
// required: true
Traits json.RawMessage `json:"traits"`

// Method
//
// Should be set to "passkey" when trying to add, update, or remove a Passkey.
//
// required: true
Method string `json:"method"`

// Flow is flow ID.
//
// swagger:ignore
Flow string `json:"flow"`

// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty"`
}

func (s *Strategy) decode(r *http.Request) (*updateRegistrationFlowWithPasskeyMethod, error) {
var p updateRegistrationFlowWithPasskeyMethod
err := registration.DecodeBody(&p, r, s.hd, s.d.Config(), registrationSchema)

return &p, err
}

func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithPasskeyMethod, err error) error {
if f != nil {
if p != nil {
for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes {
// we only set the value and not the whole field because we want to keep types from the initial form generation
f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
}
}

if f.Type == flow.TypeBrowser {
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
}
}

return err
}

func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity) error {
if err := s.d.IdentityValidator().Validate(ctx, i); err != nil {
return err
Expand Down
Loading

0 comments on commit e25f614

Please sign in to comment.