From 677f7acf0a96a15b1d527a2973293b4038e3b3ec Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 24 Dec 2024 23:24:50 +1100 Subject: [PATCH] feat(protocol): credential mediation This adds support for credential mediation. --- protocol/authenticator.go | 30 ++++++++++++++++++++++++++++++ protocol/options.go | 6 ++++-- webauthn/login.go | 22 +++++++++++++++++----- webauthn/registration.go | 6 ++++++ 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/protocol/authenticator.go b/protocol/authenticator.go index b10b72d4..82bd9104 100644 --- a/protocol/authenticator.go +++ b/protocol/authenticator.go @@ -57,6 +57,36 @@ type AttestedCredentialData struct { CredentialPublicKey []byte `json:"public_key"` } +// CredentialMediationRequirement represents mediation requirements for clients. When making a request via get(options) +// or create(options), developers can set a case-by-case requirement for user mediation by choosing the appropriate +// CredentialMediationRequirement enum value. +// +// See https://www.w3.org/TR/credential-management-1/#mediation-requirements +type CredentialMediationRequirement string + +const ( + // MediationSilent indicates user mediation is suppressed for the given operation. If the operation can be performed + // without user involvement, wonderful. If user involvement is necessary, then the operation will return null rather + // than involving the user. + MediationSilent CredentialMediationRequirement = "silent" + + // MediationOptional indicates if credentials can be handed over for a given operation without user mediation, they + // will be. If user mediation is required, then the user agent will involve the user in the decision. + MediationOptional CredentialMediationRequirement = "optional" + + // MediationConditional indicates for get(), discovered credentials are presented to the user in a non-modal dialog + // along with an indication of the origin which is requesting credentials. If the user makes a gesture outside of + // the dialog, the dialog closes without resolving or rejecting the Promise returned by the get() method and without + // causing a user-visible error condition. If the user makes a gesture that selects a credential, that credential is + // returned to the caller. The prevent silent access flag is treated as being true regardless of its actual value: + // the conditional behavior always involves user mediation of some sort if applicable credentials are discovered. + MediationConditional CredentialMediationRequirement = "conditional" + + // MediationRequired indicates the user agent will not hand over credentials without user mediation, even if the + // prevent silent access flag is unset for an origin. + MediationRequired CredentialMediationRequirement = "required" +) + // AuthenticatorAttachment represents the IDL enum of the same name, and is used as part of the Authenticator Selection // Criteria. // diff --git a/protocol/options.go b/protocol/options.go index 178cf466..a070e7cc 100644 --- a/protocol/options.go +++ b/protocol/options.go @@ -5,11 +5,13 @@ import ( ) type CredentialCreation struct { - Response PublicKeyCredentialCreationOptions `json:"publicKey"` + Response PublicKeyCredentialCreationOptions `json:"publicKey"` + Mediation CredentialMediationRequirement `json:"mediation,omitempty"` } type CredentialAssertion struct { - Response PublicKeyCredentialRequestOptions `json:"publicKey"` + Response PublicKeyCredentialRequestOptions `json:"publicKey"` + Mediation CredentialMediationRequirement `json:"mediation,omitempty"` } // PublicKeyCredentialCreationOptions represents the IDL of the same name. diff --git a/webauthn/login.go b/webauthn/login.go index 7122e979..acd1e26a 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -31,6 +31,16 @@ type DiscoverableUserHandler func(rawID, userHandle []byte) (user User, err erro // // Specification: ยง5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options) func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) { + return webauthn.BeginMediatedLogin(user, "", opts...) +} + +// BeginDiscoverableLogin begins a client-side discoverable login, previously known as Resident Key logins. +func (webauthn *WebAuthn) BeginDiscoverableLogin(opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) { + return webauthn.beginLogin(nil, nil, "", opts...) +} + +// BeginMediatedLogin is similar to BeginLogin however it also allows specifying a credential mediation requirement. +func (webauthn *WebAuthn) BeginMediatedLogin(user User, mediation protocol.CredentialMediationRequirement, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) { credentials := user.WebAuthnCredentials() if len(credentials) == 0 { // If the user does not have any credentials, we cannot perform an assertion. @@ -43,15 +53,16 @@ func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol. allowedCredentials[i] = credential.Descriptor() } - return webauthn.beginLogin(user.WebAuthnID(), allowedCredentials, opts...) + return webauthn.beginLogin(user.WebAuthnID(), allowedCredentials, mediation, opts...) } -// BeginDiscoverableLogin begins a client-side discoverable login, previously known as Resident Key logins. -func (webauthn *WebAuthn) BeginDiscoverableLogin(opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) { - return webauthn.beginLogin(nil, nil, opts...) +// BeginDiscoverableMediatedLogin begins a client-side discoverable login with a mediation requirement, previously known +// as Resident Key logins. +func (webauthn *WebAuthn) BeginDiscoverableMediatedLogin(mediation protocol.CredentialMediationRequirement, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) { + return webauthn.beginLogin(nil, nil, mediation, opts...) } -func (webauthn *WebAuthn) beginLogin(userID []byte, allowedCredentials []protocol.CredentialDescriptor, opts ...LoginOption) (assertion *protocol.CredentialAssertion, session *SessionData, err error) { +func (webauthn *WebAuthn) beginLogin(userID []byte, allowedCredentials []protocol.CredentialDescriptor, mediation protocol.CredentialMediationRequirement, opts ...LoginOption) (assertion *protocol.CredentialAssertion, session *SessionData, err error) { if err = webauthn.Config.validate(); err != nil { return nil, nil, fmt.Errorf(errFmtConfigValidate, err) } @@ -62,6 +73,7 @@ func (webauthn *WebAuthn) beginLogin(userID []byte, allowedCredentials []protoco UserVerification: webauthn.Config.AuthenticatorSelection.UserVerification, AllowedCredentials: allowedCredentials, }, + Mediation: mediation, } for _, opt := range opts { diff --git a/webauthn/registration.go b/webauthn/registration.go index 22a42ffe..be921be8 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -21,6 +21,11 @@ type RegistrationOption func(*protocol.PublicKeyCredentialCreationOptions) // BeginRegistration generates a new set of registration data to be sent to the client and authenticator. func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOption) (creation *protocol.CredentialCreation, session *SessionData, err error) { + return webauthn.BeginMediatedRegistration(user, "", opts...) +} + +// BeginMediatedRegistration is similar to BeginRegistration however it also allows specifying a credential mediation requirement. +func (webauthn *WebAuthn) BeginMediatedRegistration(user User, mediation protocol.CredentialMediationRequirement, opts ...RegistrationOption) (creation *protocol.CredentialCreation, session *SessionData, err error) { if err = webauthn.Config.validate(); err != nil { return nil, nil, fmt.Errorf(errFmtConfigValidate, err) } @@ -64,6 +69,7 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio AuthenticatorSelection: webauthn.Config.AuthenticatorSelection, Attestation: webauthn.Config.AttestationPreference, }, + Mediation: mediation, } for _, opt := range opts {