refactor: sql updates
parent
236fcb1e37
commit
e84ca4956a
2
go.mod
2
go.mod
|
@ -15,7 +15,7 @@ require (
|
||||||
github.com/go-ldap/ldap/v3 v3.4.4
|
github.com/go-ldap/ldap/v3 v3.4.4
|
||||||
github.com/go-rod/rod v0.112.5
|
github.com/go-rod/rod v0.112.5
|
||||||
github.com/go-sql-driver/mysql v1.7.0
|
github.com/go-sql-driver/mysql v1.7.0
|
||||||
github.com/go-webauthn/webauthn v0.7.1
|
github.com/go-webauthn/webauthn v0.7.2-0.20230214122838-1ee3a4aecef1
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -192,8 +192,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78
|
||||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/go-webauthn/revoke v0.1.9 h1:gSJ1ckA9VaKA2GN4Ukp+kiGTk1/EXtaDb1YE8RknbS0=
|
github.com/go-webauthn/revoke v0.1.9 h1:gSJ1ckA9VaKA2GN4Ukp+kiGTk1/EXtaDb1YE8RknbS0=
|
||||||
github.com/go-webauthn/revoke v0.1.9/go.mod h1:j6WKPnv0HovtEs++paan9g3ar46gm1NarktkXBaPR+w=
|
github.com/go-webauthn/revoke v0.1.9/go.mod h1:j6WKPnv0HovtEs++paan9g3ar46gm1NarktkXBaPR+w=
|
||||||
github.com/go-webauthn/webauthn v0.7.1 h1:b1/HP1bkqsW+DIO22WyG7BP9dL0rN151VpruH6cxADA=
|
github.com/go-webauthn/webauthn v0.7.2-0.20230214122838-1ee3a4aecef1 h1:q8OgN8xHBoJpZ5+ZrRwGmLVCPVhgW+kMx87AkwVGYfA=
|
||||||
github.com/go-webauthn/webauthn v0.7.1/go.mod h1:22OJd+TV8oHrjjXmPHtcPR82lR/yR5m5ilGiF8yPFrE=
|
github.com/go-webauthn/webauthn v0.7.2-0.20230214122838-1ee3a4aecef1/go.mod h1:22OJd+TV8oHrjjXmPHtcPR82lR/yR5m5ilGiF8yPFrE=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
|
|
@ -562,7 +562,7 @@ func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string
|
||||||
fmt.Printf("ID\tKID\tDescription\n")
|
fmt.Printf("ID\tKID\tDescription\n")
|
||||||
|
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
fmt.Printf("%d\t%s\t%s", device.ID, device.KID, device.Description)
|
fmt.Printf("%d\t%s\t%s", device.ID, device.KID, device.DisplayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -595,7 +595,7 @@ func (ctx *CmdCtx) StorageUserWebauthnListAllRunE(_ *cobra.Command, _ []string)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
output.WriteString(fmt.Sprintf("%d\t%s\t%s\t%s\n", device.ID, device.KID, device.Description, device.Username))
|
output.WriteString(fmt.Sprintf("%d\t%s\t%s\t%s\n", device.ID, device.KID, device.DisplayName, device.Username))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(devices) < limit {
|
if len(devices) < limit {
|
||||||
|
|
|
@ -721,7 +721,7 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "b-client",
|
ID: "b-client",
|
||||||
Description: "Normal Description",
|
Description: "Normal DisplayName",
|
||||||
Secret: MustDecodeSecret("$plaintext$b-client-secret"),
|
Secret: MustDecodeSecret("$plaintext$b-client-secret"),
|
||||||
Policy: policyOneFactor,
|
Policy: policyOneFactor,
|
||||||
UserinfoSigningAlgorithm: "RS256",
|
UserinfoSigningAlgorithm: "RS256",
|
||||||
|
@ -783,9 +783,9 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
|
||||||
assert.Equal(t, "none", config.OIDC.Clients[0].UserinfoSigningAlgorithm)
|
assert.Equal(t, "none", config.OIDC.Clients[0].UserinfoSigningAlgorithm)
|
||||||
assert.Equal(t, "RS256", config.OIDC.Clients[1].UserinfoSigningAlgorithm)
|
assert.Equal(t, "RS256", config.OIDC.Clients[1].UserinfoSigningAlgorithm)
|
||||||
|
|
||||||
// Assert Clients[0] Description is set to the Clients[0] ID, and Clients[1]'s Description is not overridden.
|
// Assert Clients[0] DisplayName is set to the Clients[0] ID, and Clients[1]'s DisplayName is not overridden.
|
||||||
assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description)
|
assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description)
|
||||||
assert.Equal(t, "Normal Description", config.OIDC.Clients[1].Description)
|
assert.Equal(t, "Normal DisplayName", config.OIDC.Clients[1].Description)
|
||||||
|
|
||||||
// Assert Clients[0] ends up configured with the default Scopes.
|
// Assert Clients[0] ends up configured with the default Scopes.
|
||||||
require.Len(t, config.OIDC.Clients[0].Scopes, 4)
|
require.Len(t, config.OIDC.Clients[0].Scopes, 4)
|
||||||
|
|
|
@ -66,7 +66,7 @@ const (
|
||||||
messageAuthenticationFailed = "Authentication failed. Check your credentials."
|
messageAuthenticationFailed = "Authentication failed. Check your credentials."
|
||||||
messageUnableToRegisterOneTimePassword = "Unable to set up one-time passwords." //nolint:gosec
|
messageUnableToRegisterOneTimePassword = "Unable to set up one-time passwords." //nolint:gosec
|
||||||
messageUnableToRegisterSecurityKey = "Unable to register your security key."
|
messageUnableToRegisterSecurityKey = "Unable to register your security key."
|
||||||
messageSecurityKeyDuplicateName = "Another one of your security keys is already registered with that name."
|
messageSecurityKeyDuplicateName = "Another one of your security keys is already registered with that display name."
|
||||||
messageUnableToResetPassword = "Unable to reset your password."
|
messageUnableToResetPassword = "Unable to reset your password."
|
||||||
messageMFAValidationFailed = "Authentication failed, please retry later."
|
messageMFAValidationFailed = "Authentication failed, please retry later."
|
||||||
messagePasswordWeak = "Your supplied password does not meet the password policy requirements"
|
messagePasswordWeak = "Your supplied password does not meet the password policy requirements"
|
||||||
|
|
|
@ -3,6 +3,7 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
@ -15,17 +16,18 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/storage"
|
"github.com/authelia/authelia/v4/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebauthnRegistrationGET returns the attestation challenge from the server.
|
// WebauthnRegistrationPUT returns the attestation challenge from the server.
|
||||||
func WebauthnRegistrationGET(ctx *middlewares.AutheliaCtx) {
|
func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
|
||||||
var (
|
var (
|
||||||
w *webauthn.WebAuthn
|
w *webauthn.WebAuthn
|
||||||
user *model.WebauthnUser
|
user *model.WebauthnUser
|
||||||
userSession session.UserSession
|
userSession session.UserSession
|
||||||
|
bodyJSON bodyRegisterWebauthnPUTRequest
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if userSession, err = ctx.GetSession(); err != nil {
|
if userSession, err = ctx.GetSession(); err != nil {
|
||||||
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation challenge", regulation.AuthTypeWebauthn)
|
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s registration challenge", regulation.AuthTypeWebauthn)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
|
@ -33,40 +35,84 @@ func WebauthnRegistrationGET(ctx *middlewares.AutheliaCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if w, err = newWebauthn(ctx); err != nil {
|
if w, err = newWebauthn(ctx); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to create provider to generate %s registration challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user, err = getWebauthnUserByRPID(ctx, userSession, w.Config.RPID); err != nil {
|
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to parse %s registration request PUT data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var credentialCreation *protocol.CredentialCreation
|
|
||||||
|
|
||||||
if credentialCreation, userSession.Webauthn, err = w.BeginRegistration(user, webauthn.WithExclusions(user.WebAuthnCredentialDescriptors())); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if length := len(bodyJSON.DisplayName); length == 0 || length > 64 {
|
||||||
|
ctx.Logger.Errorf("Failed to validate the user chosen display name for during %s registration for user '%s': the value has a length of %d but must be between 1 and 64", regulation.AuthTypeWebauthn, userSession.Username, length)
|
||||||
|
|
||||||
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, w.Config.RPID, userSession.Username)
|
||||||
|
if err != nil && err != storage.ErrNoWebauthnDevice {
|
||||||
|
ctx.Logger.Errorf("Unable to load existing %s devices for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, device := range devices {
|
||||||
|
if strings.EqualFold(device.DisplayName, bodyJSON.DisplayName) {
|
||||||
|
ctx.Logger.Errorf("Unable to generate %s registration challenge: device for for user '%s' with display name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, bodyJSON.DisplayName)
|
||||||
|
|
||||||
|
ctx.SetStatusCode(fasthttp.StatusConflict)
|
||||||
|
ctx.SetJSONError(messageSecurityKeyDuplicateName)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user, err = getWebauthnUserByRPID(ctx, userSession.Username, bodyJSON.DisplayName, w.Config.RPID); err != nil {
|
||||||
|
ctx.Logger.Errorf("Unable to load %s devices for registration challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
opts *protocol.CredentialCreation
|
||||||
|
)
|
||||||
|
|
||||||
|
data := session.Webauthn{
|
||||||
|
DisplayName: bodyJSON.DisplayName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts, data.SessionData, err = w.BeginRegistration(user, webauthn.WithExclusions(user.WebAuthnCredentialDescriptors())); err != nil {
|
||||||
|
ctx.Logger.Errorf("Unable to create %s registration challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSession.Webauthn = &data
|
||||||
|
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
if err = ctx.SaveSession(userSession); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrSessionSave, "attestation challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf(logFmtErrSessionSave, "registration challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = ctx.SetJSONBody(credentialCreation); err != nil {
|
if err = ctx.SetJSONBody(opts); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
@ -87,7 +133,6 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
response *protocol.ParsedCredentialCreationData
|
response *protocol.ParsedCredentialCreationData
|
||||||
|
|
||||||
credential *webauthn.Credential
|
credential *webauthn.Credential
|
||||||
bodyJSON bodyRegisterWebauthnRequest
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if userSession, err = ctx.GetSession(); err != nil {
|
if userSession, err = ctx.GetSession(); err != nil {
|
||||||
|
@ -98,10 +143,10 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSession.Webauthn == nil {
|
if userSession.Webauthn == nil || userSession.Webauthn.SessionData == nil {
|
||||||
ctx.Logger.Errorf("Webauthn session data is not present in order to handle %s registration for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", regulation.AuthTypeWebauthn, userSession.Username)
|
ctx.Logger.Errorf("Webauthn session data is not present in order to handle %s registration for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", regulation.AuthTypeWebauthn, userSession.Username)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -114,71 +159,41 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
|
if response, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to parse %s registration request POST data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
switch e := err.(type) {
|
||||||
|
case *protocol.Error:
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v (%s)", regulation.AuthTypeWebauthn, userSession.Username, err, e.DevInfo)
|
||||||
|
default:
|
||||||
return
|
ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(bodyJSON.Response)); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Logger.WithField("att_format", response.Response.AttestationObject.Format).Debug("Response Data")
|
|
||||||
|
|
||||||
if user, err = getWebauthnUser(ctx, userSession); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to load %s user details for registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if credential, err = w.CreateCredential(user, *userSession.Webauthn, response); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to create %s credential for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Logger.WithField("att_type", credential.AttestationType).Debug("Credential Data")
|
|
||||||
|
|
||||||
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, w.Config.RPID, userSession.Username)
|
|
||||||
if err != nil && err != storage.ErrNoWebauthnDevice {
|
|
||||||
ctx.Logger.Errorf("Unable to load existing %s devices for for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, existingDevice := range devices {
|
if user, err = getWebauthnUser(ctx, userSession); err != nil {
|
||||||
if existingDevice.Description == bodyJSON.Description {
|
ctx.Logger.Errorf("Unable to load %s user details for registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
ctx.Logger.Errorf("%s device for for user '%s' with name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, bodyJSON.Description)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
ctx.SetStatusCode(fasthttp.StatusConflict)
|
|
||||||
ctx.SetJSONError(messageSecurityKeyDuplicateName)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, bodyJSON.Description, credential)
|
if credential, err = w.CreateCredential(user, *userSession.Webauthn.SessionData, response); err != nil {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case *protocol.Error:
|
||||||
|
ctx.Logger.Errorf("Unable to create %s credential for user '%s': %+v (%s)", regulation.AuthTypeWebauthn, userSession.Username, err, e.DevInfo)
|
||||||
|
default:
|
||||||
|
ctx.Logger.Errorf("Unable to create %s credential for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Logger.WithFields(map[string]any{
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
"RPID": device.RPID,
|
|
||||||
"ID": device.ID,
|
return
|
||||||
"KID": device.KID.String(),
|
}
|
||||||
"User": device.Username,
|
|
||||||
}).Debug("Registering New Device")
|
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, userSession.Webauthn.DisplayName, credential)
|
||||||
|
|
||||||
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to save %s device registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to save %s device registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
@ -189,6 +204,7 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.Webauthn = nil
|
userSession.Webauthn = nil
|
||||||
|
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
if err = ctx.SaveSession(userSession); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the registration challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the registration challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
}
|
}
|
||||||
|
@ -196,5 +212,5 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
ctx.SetStatusCode(fasthttp.StatusCreated)
|
ctx.SetStatusCode(fasthttp.StatusCreated)
|
||||||
|
|
||||||
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Credential Description": bodyJSON.Description})
|
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Credential Display Name": device.DisplayName})
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user, err = getWebauthnUserByRPID(ctx, userSession, w.Config.RPID); err != nil {
|
if user, err = getWebauthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to load %s user details during authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to load %s user details during authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -61,7 +61,9 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
||||||
|
|
||||||
var assertion *protocol.CredentialAssertion
|
var assertion *protocol.CredentialAssertion
|
||||||
|
|
||||||
if assertion, userSession.Webauthn, err = w.BeginLogin(user, opts...); err != nil {
|
data := session.Webauthn{}
|
||||||
|
|
||||||
|
if assertion, data.SessionData, err = w.BeginLogin(user, opts...); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to create %s authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to create %s authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -69,6 +71,8 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userSession.Webauthn = &data
|
||||||
|
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
if err = ctx.SaveSession(userSession); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
|
@ -115,7 +119,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSession.Webauthn == nil {
|
if userSession.Webauthn == nil || userSession.Webauthn.SessionData == nil {
|
||||||
ctx.Logger.Errorf("Webauthn session data is not present in order to handle authentication challenge for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username)
|
ctx.Logger.Errorf("Webauthn session data is not present in order to handle authentication challenge for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -145,7 +149,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user, err = getWebauthnUserByRPID(ctx, userSession, w.Config.RPID); err != nil {
|
if user, err = getWebauthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to load %s credentials for authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to load %s credentials for authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -153,7 +157,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if credential, err = w.ValidateLogin(user, *userSession.Webauthn, assertionResponse); err != nil {
|
if credential, err = w.ValidateLogin(user, *userSession.Webauthn.SessionData, assertionResponse); err != nil {
|
||||||
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebauthn, err)
|
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebauthn, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -169,7 +173,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
|
|
||||||
found = true
|
found = true
|
||||||
|
|
||||||
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceSignIn(ctx, device.ID, device.RPID, device.LastUsedAt, device.SignCount, device.CloneWarning); err != nil {
|
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceSignIn(ctx, device); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to save %s device signin count for authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to save %s device signin count for authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
|
|
@ -40,10 +40,8 @@ type bodySignWebauthnRequest struct {
|
||||||
Response json.RawMessage `json:"response"`
|
Response json.RawMessage `json:"response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type bodyRegisterWebauthnRequest struct {
|
type bodyRegisterWebauthnPUTRequest struct {
|
||||||
Description string `json:"description"`
|
DisplayName string `json:"displayname"`
|
||||||
|
|
||||||
Response json.RawMessage `json:"response"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type bodyEditWebauthnDeviceRequest struct {
|
type bodyEditWebauthnDeviceRequest struct {
|
||||||
|
|
|
@ -13,20 +13,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getWebauthnUser(ctx *middlewares.AutheliaCtx, userSession session.UserSession) (user *model.WebauthnUser, err error) {
|
func getWebauthnUser(ctx *middlewares.AutheliaCtx, userSession session.UserSession) (user *model.WebauthnUser, err error) {
|
||||||
return getWebauthnUserByRPID(ctx, userSession, "")
|
return getWebauthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWebauthnUserByRPID(ctx *middlewares.AutheliaCtx, userSession session.UserSession, rpid string) (user *model.WebauthnUser, err error) {
|
func getWebauthnUserByRPID(ctx *middlewares.AutheliaCtx, username, displayname string, rpid string) (user *model.WebauthnUser, err error) {
|
||||||
user = &model.WebauthnUser{
|
user = &model.WebauthnUser{
|
||||||
Username: userSession.Username,
|
Username: username,
|
||||||
DisplayName: userSession.DisplayName,
|
DisplayName: displayname,
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.DisplayName == "" {
|
if user.DisplayName == "" {
|
||||||
user.DisplayName = user.Username
|
user.DisplayName = user.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Devices, err = ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, rpid, userSession.Username); err != nil {
|
if user.Devices, err = ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, rpid, user.Username); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
||||||
ID: 1,
|
ID: 1,
|
||||||
RPID: "example.com",
|
RPID: "example.com",
|
||||||
Username: "john",
|
Username: "john",
|
||||||
Description: "Primary",
|
DisplayName: "Primary",
|
||||||
KID: model.NewBase64([]byte("abc123")),
|
KID: model.NewBase64([]byte("abc123")),
|
||||||
AttestationType: "fido-u2f",
|
AttestationType: "fido-u2f",
|
||||||
PublicKey: []byte("data"),
|
PublicKey: []byte("data"),
|
||||||
|
@ -37,7 +37,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
||||||
ID: 2,
|
ID: 2,
|
||||||
RPID: "example.com",
|
RPID: "example.com",
|
||||||
Username: "john",
|
Username: "john",
|
||||||
Description: "Secondary",
|
DisplayName: "Secondary",
|
||||||
KID: model.NewBase64([]byte("123abc")),
|
KID: model.NewBase64([]byte("123abc")),
|
||||||
AttestationType: "packed",
|
AttestationType: "packed",
|
||||||
Transport: "usb,nfc",
|
Transport: "usb,nfc",
|
||||||
|
@ -47,7 +47,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession, "example.com")
|
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com")
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, user)
|
require.NotNil(t, user)
|
||||||
|
@ -66,7 +66,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
||||||
assert.Equal(t, 1, user.Devices[0].ID)
|
assert.Equal(t, 1, user.Devices[0].ID)
|
||||||
assert.Equal(t, "example.com", user.Devices[0].RPID)
|
assert.Equal(t, "example.com", user.Devices[0].RPID)
|
||||||
assert.Equal(t, "john", user.Devices[0].Username)
|
assert.Equal(t, "john", user.Devices[0].Username)
|
||||||
assert.Equal(t, "Primary", user.Devices[0].Description)
|
assert.Equal(t, "Primary", user.Devices[0].DisplayName)
|
||||||
assert.Equal(t, "", user.Devices[0].Transport)
|
assert.Equal(t, "", user.Devices[0].Transport)
|
||||||
assert.Equal(t, "fido-u2f", user.Devices[0].AttestationType)
|
assert.Equal(t, "fido-u2f", user.Devices[0].AttestationType)
|
||||||
assert.Equal(t, []byte("data"), user.Devices[0].PublicKey)
|
assert.Equal(t, []byte("data"), user.Devices[0].PublicKey)
|
||||||
|
@ -83,7 +83,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
||||||
assert.Equal(t, 2, user.Devices[1].ID)
|
assert.Equal(t, 2, user.Devices[1].ID)
|
||||||
assert.Equal(t, "example.com", user.Devices[1].RPID)
|
assert.Equal(t, "example.com", user.Devices[1].RPID)
|
||||||
assert.Equal(t, "john", user.Devices[1].Username)
|
assert.Equal(t, "john", user.Devices[1].Username)
|
||||||
assert.Equal(t, "Secondary", user.Devices[1].Description)
|
assert.Equal(t, "Secondary", user.Devices[1].DisplayName)
|
||||||
assert.Equal(t, "usb,nfc", user.Devices[1].Transport)
|
assert.Equal(t, "usb,nfc", user.Devices[1].Transport)
|
||||||
assert.Equal(t, "packed", user.Devices[1].AttestationType)
|
assert.Equal(t, "packed", user.Devices[1].AttestationType)
|
||||||
assert.Equal(t, []byte("data"), user.Devices[1].PublicKey)
|
assert.Equal(t, []byte("data"), user.Devices[1].PublicKey)
|
||||||
|
@ -111,7 +111,7 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
|
||||||
ID: 1,
|
ID: 1,
|
||||||
RPID: "example.com",
|
RPID: "example.com",
|
||||||
Username: "john",
|
Username: "john",
|
||||||
Description: "Primary",
|
DisplayName: "Primary",
|
||||||
KID: model.NewBase64([]byte("abc123")),
|
KID: model.NewBase64([]byte("abc123")),
|
||||||
AttestationType: "fido-u2f",
|
AttestationType: "fido-u2f",
|
||||||
PublicKey: []byte("data"),
|
PublicKey: []byte("data"),
|
||||||
|
@ -120,7 +120,7 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession, "example.com")
|
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com")
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, user)
|
require.NotNil(t, user)
|
||||||
|
@ -138,7 +138,7 @@ func TestWebauthnGetUserWithErr(t *testing.T) {
|
||||||
|
|
||||||
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "example.com", "john").Return(nil, errors.New("not found"))
|
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "example.com", "john").Return(nil, errors.New("not found"))
|
||||||
|
|
||||||
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession, "example.com")
|
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com")
|
||||||
|
|
||||||
assert.EqualError(t, err, "not found")
|
assert.EqualError(t, err, "not found")
|
||||||
assert.Nil(t, user)
|
assert.Nil(t, user)
|
||||||
|
|
|
@ -850,15 +850,15 @@ func (mr *MockStorageMockRecorder) UpdateWebauthnDeviceDescription(arg0, arg1, a
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateWebauthnDeviceSignIn mocks base method.
|
// UpdateWebauthnDeviceSignIn mocks base method.
|
||||||
func (m *MockStorage) UpdateWebauthnDeviceSignIn(arg0 context.Context, arg1 int, arg2 string, arg3 sql.NullTime, arg4 uint32, arg5 bool) error {
|
func (m *MockStorage) UpdateWebauthnDeviceSignIn(arg0 context.Context, arg1 model.WebauthnDevice) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "UpdateWebauthnDeviceSignIn", arg0, arg1, arg2, arg3, arg4, arg5)
|
ret := m.ctrl.Call(m, "UpdateWebauthnDeviceSignIn", arg0, arg1)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateWebauthnDeviceSignIn indicates an expected call of UpdateWebauthnDeviceSignIn.
|
// UpdateWebauthnDeviceSignIn indicates an expected call of UpdateWebauthnDeviceSignIn.
|
||||||
func (mr *MockStorageMockRecorder) UpdateWebauthnDeviceSignIn(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
|
func (mr *MockStorageMockRecorder) UpdateWebauthnDeviceSignIn(arg0, arg1 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebauthnDeviceSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateWebauthnDeviceSignIn), arg0, arg1, arg2, arg3, arg4, arg5)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebauthnDeviceSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateWebauthnDeviceSignIn), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,10 +72,17 @@ func (w WebauthnUser) WebAuthnCredentials() (credentials []webauthn.Credential)
|
||||||
ID: device.KID.Bytes(),
|
ID: device.KID.Bytes(),
|
||||||
PublicKey: device.PublicKey,
|
PublicKey: device.PublicKey,
|
||||||
AttestationType: device.AttestationType,
|
AttestationType: device.AttestationType,
|
||||||
|
Flags: webauthn.CredentialFlags{
|
||||||
|
UserPresent: device.Present,
|
||||||
|
UserVerified: device.Verified,
|
||||||
|
BackupEligible: device.BackupEligible,
|
||||||
|
BackupState: device.BackupState,
|
||||||
|
},
|
||||||
Authenticator: webauthn.Authenticator{
|
Authenticator: webauthn.Authenticator{
|
||||||
AAGUID: aaguid,
|
AAGUID: aaguid,
|
||||||
SignCount: device.SignCount,
|
SignCount: device.SignCount,
|
||||||
CloneWarning: device.CloneWarning,
|
CloneWarning: device.CloneWarning,
|
||||||
|
Attachment: protocol.AuthenticatorAttachment(device.Attachment),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +117,7 @@ func (w WebauthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.Cr
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebauthnDeviceFromCredential creates a WebauthnDevice from a webauthn.Credential.
|
// NewWebauthnDeviceFromCredential creates a WebauthnDevice from a webauthn.Credential.
|
||||||
func NewWebauthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebauthnDevice) {
|
func NewWebauthnDeviceFromCredential(rpid, username, displayname string, credential *webauthn.Credential) (device WebauthnDevice) {
|
||||||
transport := make([]string, len(credential.Transport))
|
transport := make([]string, len(credential.Transport))
|
||||||
|
|
||||||
for i, t := range credential.Transport {
|
for i, t := range credential.Transport {
|
||||||
|
@ -121,13 +128,19 @@ func NewWebauthnDeviceFromCredential(rpid, username, description string, credent
|
||||||
RPID: rpid,
|
RPID: rpid,
|
||||||
Username: username,
|
Username: username,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
Description: description,
|
DisplayName: displayname,
|
||||||
KID: NewBase64(credential.ID),
|
KID: NewBase64(credential.ID),
|
||||||
PublicKey: credential.PublicKey,
|
|
||||||
AttestationType: credential.AttestationType,
|
AttestationType: credential.AttestationType,
|
||||||
|
Attachment: string(credential.Authenticator.Attachment),
|
||||||
|
Transport: strings.Join(transport, ","),
|
||||||
SignCount: credential.Authenticator.SignCount,
|
SignCount: credential.Authenticator.SignCount,
|
||||||
CloneWarning: credential.Authenticator.CloneWarning,
|
CloneWarning: credential.Authenticator.CloneWarning,
|
||||||
Transport: strings.Join(transport, ","),
|
Discoverable: false,
|
||||||
|
Present: credential.Flags.UserPresent,
|
||||||
|
Verified: credential.Flags.UserVerified,
|
||||||
|
BackupEligible: credential.Flags.BackupEligible,
|
||||||
|
BackupState: credential.Flags.BackupState,
|
||||||
|
PublicKey: credential.PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
aaguid, err := uuid.Parse(hex.EncodeToString(credential.Authenticator.AAGUID))
|
aaguid, err := uuid.Parse(hex.EncodeToString(credential.Authenticator.AAGUID))
|
||||||
|
@ -144,14 +157,20 @@ type WebauthnDeviceJSON struct {
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||||
RPID string `json:"rpid"`
|
RPID string `json:"rpid"`
|
||||||
Description string `json:"description"`
|
DisplayName string `json:"displayname"`
|
||||||
KID []byte `json:"kid"`
|
KID []byte `json:"kid"`
|
||||||
PublicKey []byte `json:"public_key"`
|
AAGUID string `json:"aaguid,omitempty"`
|
||||||
|
Attachment string `json:"attachment"`
|
||||||
AttestationType string `json:"attestation_type"`
|
AttestationType string `json:"attestation_type"`
|
||||||
Transports []string `json:"transports"`
|
Transports []string `json:"transports"`
|
||||||
AAGUID string `json:"aaguid,omitempty"`
|
|
||||||
SignCount uint32 `json:"sign_count"`
|
SignCount uint32 `json:"sign_count"`
|
||||||
CloneWarning bool `json:"clone_warning"`
|
CloneWarning bool `json:"clone_warning"`
|
||||||
|
Discoverable bool `json:"discoverable"`
|
||||||
|
Present bool `json:"present"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
BackupEligible bool `json:"backup_eligible"`
|
||||||
|
BackupState bool `json:"backup_state"`
|
||||||
|
PublicKey []byte `json:"public_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebauthnDevice represents a Webauthn Device in the database storage.
|
// WebauthnDevice represents a Webauthn Device in the database storage.
|
||||||
|
@ -161,14 +180,20 @@ type WebauthnDevice struct {
|
||||||
LastUsedAt sql.NullTime `db:"last_used_at"`
|
LastUsedAt sql.NullTime `db:"last_used_at"`
|
||||||
RPID string `db:"rpid"`
|
RPID string `db:"rpid"`
|
||||||
Username string `db:"username"`
|
Username string `db:"username"`
|
||||||
Description string `db:"description"`
|
DisplayName string `db:"displayname"`
|
||||||
KID Base64 `db:"kid"`
|
KID Base64 `db:"kid"`
|
||||||
PublicKey []byte `db:"public_key"`
|
|
||||||
AttestationType string `db:"attestation_type"`
|
|
||||||
Transport string `db:"transport"`
|
|
||||||
AAGUID uuid.NullUUID `db:"aaguid"`
|
AAGUID uuid.NullUUID `db:"aaguid"`
|
||||||
|
AttestationType string `db:"attestation_type"`
|
||||||
|
Attachment string `db:"attachment"`
|
||||||
|
Transport string `db:"transport"`
|
||||||
SignCount uint32 `db:"sign_count"`
|
SignCount uint32 `db:"sign_count"`
|
||||||
CloneWarning bool `db:"clone_warning"`
|
CloneWarning bool `db:"clone_warning"`
|
||||||
|
Discoverable bool `db:"discoverable"`
|
||||||
|
Present bool `db:"present"`
|
||||||
|
Verified bool `db:"verified"`
|
||||||
|
BackupEligible bool `db:"backup_eligible"`
|
||||||
|
BackupState bool `db:"backup_state"`
|
||||||
|
PublicKey []byte `db:"public_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON returns the WebauthnDevice in a JSON friendly manner.
|
// MarshalJSON returns the WebauthnDevice in a JSON friendly manner.
|
||||||
|
@ -177,13 +202,19 @@ func (w *WebauthnDevice) MarshalJSON() (data []byte, err error) {
|
||||||
ID: w.ID,
|
ID: w.ID,
|
||||||
CreatedAt: w.CreatedAt,
|
CreatedAt: w.CreatedAt,
|
||||||
RPID: w.RPID,
|
RPID: w.RPID,
|
||||||
Description: w.Description,
|
DisplayName: w.DisplayName,
|
||||||
KID: w.KID.data,
|
KID: w.KID.data,
|
||||||
PublicKey: w.PublicKey,
|
|
||||||
AttestationType: w.AttestationType,
|
AttestationType: w.AttestationType,
|
||||||
|
Attachment: w.Attachment,
|
||||||
Transports: []string{},
|
Transports: []string{},
|
||||||
SignCount: w.SignCount,
|
SignCount: w.SignCount,
|
||||||
CloneWarning: w.CloneWarning,
|
CloneWarning: w.CloneWarning,
|
||||||
|
Discoverable: w.Discoverable,
|
||||||
|
Present: w.Present,
|
||||||
|
Verified: w.Verified,
|
||||||
|
BackupEligible: w.BackupEligible,
|
||||||
|
BackupState: w.BackupState,
|
||||||
|
PublicKey: w.PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
if w.AAGUID.Valid {
|
if w.AAGUID.Valid {
|
||||||
|
@ -234,14 +265,19 @@ func (d *WebauthnDevice) MarshalYAML() (any, error) {
|
||||||
LastUsedAt: d.LastUsed(),
|
LastUsedAt: d.LastUsed(),
|
||||||
RPID: d.RPID,
|
RPID: d.RPID,
|
||||||
Username: d.Username,
|
Username: d.Username,
|
||||||
Description: d.Description,
|
DisplayName: d.DisplayName,
|
||||||
KID: d.KID.String(),
|
KID: d.KID.String(),
|
||||||
PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey),
|
|
||||||
AttestationType: d.AttestationType,
|
|
||||||
Transport: d.Transport,
|
|
||||||
AAGUID: d.AAGUID.UUID.String(),
|
AAGUID: d.AAGUID.UUID.String(),
|
||||||
|
AttestationType: d.AttestationType,
|
||||||
|
Attachment: d.Attachment,
|
||||||
|
Transport: d.Transport,
|
||||||
SignCount: d.SignCount,
|
SignCount: d.SignCount,
|
||||||
CloneWarning: d.CloneWarning,
|
CloneWarning: d.CloneWarning,
|
||||||
|
Present: d.Present,
|
||||||
|
Verified: d.Verified,
|
||||||
|
BackupEligible: d.BackupEligible,
|
||||||
|
BackupState: d.BackupState,
|
||||||
|
PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
return yaml.Marshal(o)
|
return yaml.Marshal(o)
|
||||||
|
@ -280,11 +316,17 @@ func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
||||||
d.CreatedAt = o.CreatedAt
|
d.CreatedAt = o.CreatedAt
|
||||||
d.RPID = o.RPID
|
d.RPID = o.RPID
|
||||||
d.Username = o.Username
|
d.Username = o.Username
|
||||||
d.Description = o.Description
|
d.DisplayName = o.DisplayName
|
||||||
d.AttestationType = o.AttestationType
|
d.AttestationType = o.AttestationType
|
||||||
|
d.Attachment = o.Attachment
|
||||||
d.Transport = o.Transport
|
d.Transport = o.Transport
|
||||||
d.SignCount = o.SignCount
|
d.SignCount = o.SignCount
|
||||||
d.CloneWarning = o.CloneWarning
|
d.CloneWarning = o.CloneWarning
|
||||||
|
d.Discoverable = o.Discoverable
|
||||||
|
d.Present = o.Present
|
||||||
|
d.Verified = o.Verified
|
||||||
|
d.BackupEligible = o.BackupEligible
|
||||||
|
d.BackupState = o.BackupState
|
||||||
|
|
||||||
if o.LastUsedAt != nil {
|
if o.LastUsedAt != nil {
|
||||||
d.LastUsedAt = sql.NullTime{Valid: true, Time: *o.LastUsedAt}
|
d.LastUsedAt = sql.NullTime{Valid: true, Time: *o.LastUsedAt}
|
||||||
|
@ -299,14 +341,20 @@ type WebauthnDeviceData struct {
|
||||||
LastUsedAt *time.Time `yaml:"last_used_at"`
|
LastUsedAt *time.Time `yaml:"last_used_at"`
|
||||||
RPID string `yaml:"rpid"`
|
RPID string `yaml:"rpid"`
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
Description string `yaml:"description"`
|
DisplayName string `yaml:"displayname"`
|
||||||
KID string `yaml:"kid"`
|
KID string `yaml:"kid"`
|
||||||
PublicKey string `yaml:"public_key"`
|
|
||||||
AttestationType string `yaml:"attestation_type"`
|
|
||||||
Transport string `yaml:"transport"`
|
|
||||||
AAGUID string `yaml:"aaguid"`
|
AAGUID string `yaml:"aaguid"`
|
||||||
|
AttestationType string `yaml:"attestation_type"`
|
||||||
|
Attachment string `yaml:"attachment"`
|
||||||
|
Transport string `yaml:"transport"`
|
||||||
SignCount uint32 `yaml:"sign_count"`
|
SignCount uint32 `yaml:"sign_count"`
|
||||||
CloneWarning bool `yaml:"clone_warning"`
|
CloneWarning bool `yaml:"clone_warning"`
|
||||||
|
Discoverable bool `yaml:"discoverable"`
|
||||||
|
Present bool `yaml:"present"`
|
||||||
|
Verified bool `yaml:"verified"`
|
||||||
|
BackupEligible bool `yaml:"backup_eligible"`
|
||||||
|
BackupState bool `yaml:"backup_state"`
|
||||||
|
PublicKey string `yaml:"public_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebauthnDeviceExport represents a WebauthnDevice export file.
|
// WebauthnDeviceExport represents a WebauthnDevice export file.
|
||||||
|
|
|
@ -70,7 +70,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "b-client",
|
ID: "b-client",
|
||||||
Description: "Normal Description",
|
Description: "Normal DisplayName",
|
||||||
Secret: MustDecodeSecret("$plaintext$b-client-secret"),
|
Secret: MustDecodeSecret("$plaintext$b-client-secret"),
|
||||||
Policy: "two_factor",
|
Policy: "two_factor",
|
||||||
RedirectURIs: []string{
|
RedirectURIs: []string{
|
||||||
|
|
|
@ -240,7 +240,7 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
|
||||||
|
|
||||||
// Management of the webauthn devices.
|
// Management of the webauthn devices.
|
||||||
r.GET("/api/secondfactor/webauthn/credentials", middleware1FA(handlers.WebauthnDevicesGET))
|
r.GET("/api/secondfactor/webauthn/credentials", middleware1FA(handlers.WebauthnDevicesGET))
|
||||||
r.GET("/api/secondfactor/webauthn/credential/register", middleware1FA(handlers.WebauthnRegistrationGET))
|
r.PUT("/api/secondfactor/webauthn/credential/register", middleware1FA(handlers.WebauthnRegistrationPUT))
|
||||||
r.POST("/api/secondfactor/webauthn/credential/register", middleware1FA(handlers.WebauthnRegistrationPOST))
|
r.POST("/api/secondfactor/webauthn/credential/register", middleware1FA(handlers.WebauthnRegistrationPOST))
|
||||||
r.PUT("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebauthnDevicePUT))
|
r.PUT("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebauthnDevicePUT))
|
||||||
r.DELETE("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebauthnDeviceDELETE))
|
r.DELETE("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebauthnDeviceDELETE))
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"Added": "Added {{when, datetime}}",
|
"Added": "Added {{when, datetime}}",
|
||||||
"Are you sure you want to remove the Webauthn credential from from your account": "Are you sure you want to remove the Webauthn credential {{description}} from your account?",
|
"Are you sure you want to remove the Webauthn credential from from your account": "Are you sure you want to remove the Webauthn credential {{description}} from your account?",
|
||||||
"Attestation Type": "Attestation Type",
|
"Attestation Type": "Attestation Type",
|
||||||
"Authenticator Attestation GUID": "Authenticator Attestation GUID",
|
"Authenticator GUID": "Authenticator GUID",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Click to add a Webauthn credential to your account": "Click to add a Webauthn credential to your account",
|
"Click to add a Webauthn credential to your account": "Click to add a Webauthn credential to your account",
|
||||||
"Click to copy the": "Click to copy the",
|
"Click to copy the": "Click to copy the",
|
||||||
|
@ -19,7 +19,8 @@
|
||||||
"Edit Webauthn Credential": "Edit Webauthn Credential",
|
"Edit Webauthn Credential": "Edit Webauthn Credential",
|
||||||
"Enabled": "Enabled",
|
"Enabled": "Enabled",
|
||||||
"Enter a new name for this Webauthn credential": "Enter a new name for this Webauthn credential:",
|
"Enter a new name for this Webauthn credential": "Enter a new name for this Webauthn credential:",
|
||||||
"Extended Webauthn credential information for security key": "Extended Webauthn credential information for security key {{description}}",
|
"Enter a display name for this credential": "Enter a display name for this credential",
|
||||||
|
"Extended Webauthn credential information for security key": "Extended Webauthn credential information for security key {{displayname}}",
|
||||||
"Identifier": "Identifier",
|
"Identifier": "Identifier",
|
||||||
"Last Used": "Last Used {{when, datetime}}",
|
"Last Used": "Last Used {{when, datetime}}",
|
||||||
"Manage your security keys": "Manage your security keys",
|
"Manage your security keys": "Manage your security keys",
|
||||||
|
@ -28,7 +29,7 @@
|
||||||
"No Registered Webauthn Credentials": "No Registered Webauthn Credentials",
|
"No Registered Webauthn Credentials": "No Registered Webauthn Credentials",
|
||||||
"Overview": "Overview",
|
"Overview": "Overview",
|
||||||
"Provide the details for the new security key": "Provide the details for the new security key",
|
"Provide the details for the new security key": "Provide the details for the new security key",
|
||||||
"Register Webauthn Credential (Security Key)": "Register Webauthn Credential (Security Key)",
|
"Register Webauthn Credential": "Register Webauthn Credential",
|
||||||
"Relying Party ID": "Relying Party ID",
|
"Relying Party ID": "Relying Party ID",
|
||||||
"Remove": "Remove",
|
"Remove": "Remove",
|
||||||
"Remove this Webauthn credential": "Remove this Webauthn credential",
|
"Remove this Webauthn credential": "Remove this Webauthn credential",
|
||||||
|
|
|
@ -36,7 +36,7 @@ type UserSession struct {
|
||||||
AuthenticationMethodRefs oidc.AuthenticationMethodsReferences
|
AuthenticationMethodRefs oidc.AuthenticationMethodsReferences
|
||||||
|
|
||||||
// Webauthn holds the session registration data for this session.
|
// Webauthn holds the session registration data for this session.
|
||||||
Webauthn *webauthn.SessionData
|
Webauthn *Webauthn
|
||||||
|
|
||||||
// This boolean is set to true after identity verification and checked
|
// This boolean is set to true after identity verification and checked
|
||||||
// while doing the query actually updating the password.
|
// while doing the query actually updating the password.
|
||||||
|
@ -45,6 +45,11 @@ type UserSession struct {
|
||||||
RefreshTTL time.Time
|
RefreshTTL time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Webauthn struct {
|
||||||
|
*webauthn.SessionData
|
||||||
|
DisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
// Identity identity of the user who is being verified.
|
// Identity identity of the user who is being verified.
|
||||||
type Identity struct {
|
type Identity struct {
|
||||||
Username string
|
Username string
|
||||||
|
|
|
@ -1,3 +1,33 @@
|
||||||
DROP INDEX webauthn_devices_lookup_key ON webauthn_devices;
|
ALTER TABLE webauthn_devices
|
||||||
ALTER TABLE webauthn_devices MODIFY COLUMN rpid VARCHAR(512);
|
RENAME _bkp_UP_V0008_webauthn_devices;
|
||||||
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, description);
|
|
||||||
|
CREATE TABLE IF NOT EXISTS webauthn_devices (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
rpid VARCHAR(512) NOT NULL,
|
||||||
|
username VARCHAR(100) NOT NULL,
|
||||||
|
displayname VARCHAR(30) NOT NULL,
|
||||||
|
kid VARCHAR(512) NOT NULL,
|
||||||
|
aaguid CHAR(36) NOT NULL,
|
||||||
|
attestation_type VARCHAR(32),
|
||||||
|
attachment VARCHAR(64) NOT NULL,
|
||||||
|
transport VARCHAR(20) DEFAULT '',
|
||||||
|
sign_count INTEGER DEFAULT 0,
|
||||||
|
clone_warning BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
discoverable BOOLEAN NOT NULL,
|
||||||
|
present BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
backup_eligible BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
backup_state BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
public_key BLOB NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX webauthn_devices_kid_key ON webauthn_devices (kid);
|
||||||
|
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, displayname);
|
||||||
|
|
||||||
|
INSERT INTO webauthn_devices (created_at, last_used_at, rpid, username, displayname, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key)
|
||||||
|
SELECT id, created_at, last_used_at, CAST(rpid AS CHAR) AS rpid, username, description, kid, aaguid, attestation_type, 'cross-platform', transport, sign_count, clone_warning, FALSE, FALSE, FALSE, FALSE, public_key
|
||||||
|
FROM _bkp_UP_V0008_webauthn_devices;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS _bkp_UP_V0008_webauthn_devices;
|
||||||
|
|
|
@ -1,3 +1,33 @@
|
||||||
DROP INDEX webauthn_devices_lookup_key;
|
ALTER TABLE webauthn_devices
|
||||||
ALTER TABLE webauthn_devices ALTER COLUMN rpid SET DATA TYPE VARCHAR(512);
|
RENAME TO _bkp_UP_V0008_webauthn_devices;
|
||||||
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, description);
|
|
||||||
|
CREATE TABLE IF NOT EXISTS webauthn_devices (
|
||||||
|
id SERIAL CONSTRAINT webauthn_devices_pkey PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL,
|
||||||
|
rpid VARCHAR(512) NOT NULL,
|
||||||
|
username VARCHAR(100) NOT NULL,
|
||||||
|
displayname VARCHAR(64) NOT NULL,
|
||||||
|
kid VARCHAR(512) NOT NULL,
|
||||||
|
aaguid CHAR(36) NOT NULL,
|
||||||
|
attestation_type VARCHAR(32),
|
||||||
|
attachment VARCHAR(64) NOT NULL,
|
||||||
|
transport VARCHAR(20) DEFAULT '',
|
||||||
|
sign_count INTEGER DEFAULT 0,
|
||||||
|
clone_warning BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
discoverable BOOLEAN NOT NULL,
|
||||||
|
present BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
backup_eligible BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
backup_state BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
public_key BYTEA NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX webauthn_devices_kid_key ON webauthn_devices (kid);
|
||||||
|
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, displayname);
|
||||||
|
|
||||||
|
INSERT INTO webauthn_devices (created_at, last_used_at, rpid, username, displayname, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key)
|
||||||
|
SELECT created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, 'cross-platform', transport, sign_count, clone_warning, FALSE, FALSE, FALSE, FALSE, FALSE, public_key
|
||||||
|
FROM _bkp_UP_V0008_webauthn_devices;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS _bkp_UP_V0008_webauthn_devices;
|
||||||
|
|
|
@ -1,2 +1,36 @@
|
||||||
DROP INDEX webauthn_devices_lookup_key;
|
DROP INDEX IF EXISTS webauthn_devices_lookup_key;
|
||||||
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, description);
|
DROP INDEX IF EXISTS webauthn_devices_kid_key;
|
||||||
|
|
||||||
|
ALTER TABLE webauthn_devices
|
||||||
|
RENAME TO _bkp_UP_V0008_webauthn_devices;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS webauthn_devices (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at DATETIME NULL DEFAULT NULL,
|
||||||
|
rpid VARCHAR(512) NOT NULL,
|
||||||
|
username VARCHAR(100) NOT NULL,
|
||||||
|
displayname VARCHAR(64) NOT NULL,
|
||||||
|
kid VARCHAR(512) NOT NULL,
|
||||||
|
aaguid CHAR(36) NULL,
|
||||||
|
attestation_type VARCHAR(32),
|
||||||
|
attachment VARCHAR(64),
|
||||||
|
transport VARCHAR(20) DEFAULT '',
|
||||||
|
sign_count INTEGER DEFAULT 0,
|
||||||
|
clone_warning BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
discoverable BOOLEAN NOT NULL,
|
||||||
|
present BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
backup_eligible BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
backup_state BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
public_key BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX webauthn_devices_kid_key ON webauthn_devices (kid);
|
||||||
|
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, displayname);
|
||||||
|
|
||||||
|
INSERT INTO webauthn_devices (created_at, last_used_at, rpid, username, displayname, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key)
|
||||||
|
SELECT created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, 'cross-platform', transport, sign_count, clone_warning, FALSE, FALSE, FALSE, FALSE, FALSE, public_key
|
||||||
|
FROM _bkp_UP_V0008_webauthn_devices;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS _bkp_UP_V0008_webauthn_devices;
|
||||||
|
|
|
@ -40,7 +40,7 @@ type Provider interface {
|
||||||
|
|
||||||
SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error)
|
SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error)
|
||||||
UpdateWebauthnDeviceDescription(ctx context.Context, username string, deviceID int, description string) (err error)
|
UpdateWebauthnDeviceDescription(ctx context.Context, username string, deviceID int, description string) (err error)
|
||||||
UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error)
|
UpdateWebauthnDeviceSignIn(ctx context.Context, device model.WebauthnDevice) (err error)
|
||||||
DeleteWebauthnDevice(ctx context.Context, kid string) (err error)
|
DeleteWebauthnDevice(ctx context.Context, kid string) (err error)
|
||||||
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error)
|
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error)
|
||||||
LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error)
|
LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error)
|
||||||
|
|
|
@ -51,11 +51,11 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
|
||||||
sqlSelectWebauthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByUsername, tableWebauthnDevices),
|
sqlSelectWebauthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByUsername, tableWebauthnDevices),
|
||||||
sqlSelectWebauthnDevicesByRPIDByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByRPIDByUsername, tableWebauthnDevices),
|
sqlSelectWebauthnDevicesByRPIDByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByRPIDByUsername, tableWebauthnDevices),
|
||||||
sqlSelectWebauthnDeviceByID: fmt.Sprintf(queryFmtSelectWebauthnDeviceByID, tableWebauthnDevices),
|
sqlSelectWebauthnDeviceByID: fmt.Sprintf(queryFmtSelectWebauthnDeviceByID, tableWebauthnDevices),
|
||||||
sqlUpdateWebauthnDeviceDescriptionByUsernameAndID: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID, tableWebauthnDevices),
|
sqlUpdateWebauthnDeviceDescriptionByUsernameAndID: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDeviceDisplayNameByUsernameAndID, tableWebauthnDevices),
|
||||||
sqlUpdateWebauthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices),
|
sqlUpdateWebauthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices),
|
||||||
sqlDeleteWebauthnDevice: fmt.Sprintf(queryFmtDeleteWebauthnDevice, tableWebauthnDevices),
|
sqlDeleteWebauthnDevice: fmt.Sprintf(queryFmtDeleteWebauthnDevice, tableWebauthnDevices),
|
||||||
sqlDeleteWebauthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsername, tableWebauthnDevices),
|
sqlDeleteWebauthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsername, tableWebauthnDevices),
|
||||||
sqlDeleteWebauthnDeviceByUsernameAndDescription: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDescription, tableWebauthnDevices),
|
sqlDeleteWebauthnDeviceByUsernameAndDisplayName: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDisplayName, tableWebauthnDevices),
|
||||||
|
|
||||||
sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),
|
sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),
|
||||||
sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices),
|
sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices),
|
||||||
|
@ -172,7 +172,7 @@ type SQLProvider struct {
|
||||||
|
|
||||||
sqlDeleteWebauthnDevice string
|
sqlDeleteWebauthnDevice string
|
||||||
sqlDeleteWebauthnDeviceByUsername string
|
sqlDeleteWebauthnDeviceByUsername string
|
||||||
sqlDeleteWebauthnDeviceByUsernameAndDescription string
|
sqlDeleteWebauthnDeviceByUsernameAndDisplayName string
|
||||||
|
|
||||||
// Table: duo_devices.
|
// Table: duo_devices.
|
||||||
sqlUpsertDuoDevice string
|
sqlUpsertDuoDevice string
|
||||||
|
@ -842,10 +842,10 @@ func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.Webau
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = p.db.ExecContext(ctx, p.sqlInsertWebauthnDevice,
|
if _, err = p.db.ExecContext(ctx, p.sqlInsertWebauthnDevice,
|
||||||
device.CreatedAt, device.LastUsedAt,
|
device.CreatedAt, device.LastUsedAt, device.RPID, device.Username, device.DisplayName,
|
||||||
device.RPID, device.Username, device.Description,
|
device.KID, device.AAGUID, device.AttestationType, device.Attachment, device.Transport,
|
||||||
device.KID, device.PublicKey,
|
device.SignCount, device.CloneWarning, device.Discoverable, device.Present, device.Verified,
|
||||||
device.AttestationType, device.Transport, device.AAGUID, device.SignCount, device.CloneWarning,
|
device.BackupEligible, device.BackupState, device.PublicKey,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("error upserting Webauthn device for user '%s' kid '%x': %w", device.Username, device.KID, err)
|
return fmt.Errorf("error upserting Webauthn device for user '%s' kid '%x': %w", device.Username, device.KID, err)
|
||||||
}
|
}
|
||||||
|
@ -863,9 +863,12 @@ func (p *SQLProvider) UpdateWebauthnDeviceDescription(ctx context.Context, usern
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateWebauthnDeviceSignIn updates a registered Webauthn devices sign in information.
|
// UpdateWebauthnDeviceSignIn updates a registered Webauthn devices sign in information.
|
||||||
func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error) {
|
func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, device model.WebauthnDevice) (err error) {
|
||||||
if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceRecordSignIn, rpid, lastUsedAt, signCount, cloneWarning, id); err != nil {
|
if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceRecordSignIn,
|
||||||
return fmt.Errorf("error updating Webauthn signin metadata for id '%x': %w", id, err)
|
device.RPID, device.LastUsedAt, device.SignCount, device.Discoverable, device.Present, device.Verified,
|
||||||
|
device.BackupEligible, device.BackupState, device.CloneWarning, device.ID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("error updating Webauthn signin metadata for id '%x': %w", device.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -881,18 +884,18 @@ func (p *SQLProvider) DeleteWebauthnDevice(ctx context.Context, kid string) (err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteWebauthnDeviceByUsername deletes registered Webauthn devices by username or username and description.
|
// DeleteWebauthnDeviceByUsername deletes registered Webauthn devices by username or username and description.
|
||||||
func (p *SQLProvider) DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error) {
|
func (p *SQLProvider) DeleteWebauthnDeviceByUsername(ctx context.Context, username, displayname string) (err error) {
|
||||||
if len(username) == 0 {
|
if len(username) == 0 {
|
||||||
return fmt.Errorf("error deleting webauthn device with username '%s' and description '%s': username must not be empty", username, description)
|
return fmt.Errorf("error deleting webauthn device with username '%s' and displayname '%s': username must not be empty", username, displayname)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(description) == 0 {
|
if len(displayname) == 0 {
|
||||||
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsername, username); err != nil {
|
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsername, username); err != nil {
|
||||||
return fmt.Errorf("error deleting webauthn devices for username '%s': %w", username, err)
|
return fmt.Errorf("error deleting webauthn devices for username '%s': %w", username, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsernameAndDescription, username, description); err != nil {
|
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsernameAndDisplayName, username, displayname); err != nil {
|
||||||
return fmt.Errorf("error deleting webauthn device with username '%s' and description '%s': %w", username, description, err)
|
return fmt.Errorf("error deleting webauthn device with username '%s' and displayname '%s': %w", username, displayname, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo
|
||||||
provider.sqlUpdateWebauthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignIn)
|
provider.sqlUpdateWebauthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignIn)
|
||||||
provider.sqlDeleteWebauthnDevice = provider.db.Rebind(provider.sqlDeleteWebauthnDevice)
|
provider.sqlDeleteWebauthnDevice = provider.db.Rebind(provider.sqlDeleteWebauthnDevice)
|
||||||
provider.sqlDeleteWebauthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsername)
|
provider.sqlDeleteWebauthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsername)
|
||||||
provider.sqlDeleteWebauthnDeviceByUsernameAndDescription = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsernameAndDescription)
|
provider.sqlDeleteWebauthnDeviceByUsernameAndDisplayName = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsernameAndDisplayName)
|
||||||
|
|
||||||
provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice)
|
provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice)
|
||||||
provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice)
|
provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice)
|
||||||
|
|
|
@ -174,7 +174,7 @@ func schemaEncryptionChangeKeyWebauthn(ctx context.Context, provider *SQLProvide
|
||||||
return fmt.Errorf("error selecting Webauthn devices: %w", err)
|
return fmt.Errorf("error selecting Webauthn devices: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
query := provider.db.Rebind(fmt.Sprintf(queryFmtUpdateWebauthnDevicePublicKey, tableWebauthnDevices))
|
query := provider.db.Rebind(fmt.Sprintf(queryFmtUpdateWebauthnDevicesEncryptedData, tableWebauthnDevices))
|
||||||
|
|
||||||
for _, d := range devices {
|
for _, d := range devices {
|
||||||
if d.PublicKey, err = provider.decrypt(d.PublicKey); err != nil {
|
if d.PublicKey, err = provider.decrypt(d.PublicKey); err != nil {
|
||||||
|
|
|
@ -120,50 +120,41 @@ const (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
queryFmtSelectWebauthnDevices = `
|
queryFmtSelectWebauthnDevices = `
|
||||||
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
|
SELECT id, created_at, last_used_at, rpid, username, displayname, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key
|
||||||
FROM %s
|
FROM %s
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
OFFSET ?;`
|
OFFSET ?;`
|
||||||
|
|
||||||
queryFmtSelectWebauthnDevicesEncryptedData = `
|
|
||||||
SELECT id, public_key
|
|
||||||
FROM %s;`
|
|
||||||
|
|
||||||
queryFmtSelectWebauthnDevicesByUsername = `
|
queryFmtSelectWebauthnDevicesByUsername = `
|
||||||
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
|
SELECT id, created_at, last_used_at, rpid, username, displayname, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE username = ?;`
|
WHERE username = ?;`
|
||||||
|
|
||||||
queryFmtSelectWebauthnDevicesByRPIDByUsername = `
|
queryFmtSelectWebauthnDevicesByRPIDByUsername = `
|
||||||
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
|
SELECT id, created_at, last_used_at, rpid, username, displayname, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE rpid = ? AND username = ?;`
|
WHERE rpid = ? AND username = ?;`
|
||||||
|
|
||||||
queryFmtSelectWebauthnDeviceByID = `
|
queryFmtSelectWebauthnDeviceByID = `
|
||||||
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
|
SELECT id, created_at, last_used_at, rpid, username, displayname, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE id = ?;`
|
WHERE id = ?;`
|
||||||
|
|
||||||
queryFmtUpdateWebauthnDevicePublicKey = `
|
queryFmtUpdateUpdateWebauthnDeviceDisplayNameByUsernameAndID = `
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET public_key = ?
|
SET displayname = ?
|
||||||
WHERE id = ?;`
|
|
||||||
|
|
||||||
queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID = `
|
|
||||||
UPDATE %s
|
|
||||||
SET description = ?
|
|
||||||
WHERE username = ? AND id = ?;`
|
WHERE username = ? AND id = ?;`
|
||||||
|
|
||||||
queryFmtUpdateWebauthnDeviceRecordSignIn = `
|
queryFmtUpdateWebauthnDeviceRecordSignIn = `
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET
|
SET
|
||||||
rpid = ?, last_used_at = ?, sign_count = ?,
|
rpid = ?, last_used_at = ?, sign_count = ?, discoverable = ?, present = ?, verified = ?, backup_eligible = ?, backup_state = ?,
|
||||||
clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END
|
clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END
|
||||||
WHERE id = ?;`
|
WHERE id = ?;`
|
||||||
|
|
||||||
queryFmtUpsertInsertDevice = `
|
queryFmtUpsertInsertDevice = `
|
||||||
INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning)
|
INSERT INTO %s (created_at, last_used_at, rpid, username, displayname, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
||||||
|
|
||||||
queryFmtDeleteWebauthnDevice = `
|
queryFmtDeleteWebauthnDevice = `
|
||||||
DELETE FROM %s
|
DELETE FROM %s
|
||||||
|
@ -173,9 +164,18 @@ const (
|
||||||
DELETE FROM %s
|
DELETE FROM %s
|
||||||
WHERE username = ?;`
|
WHERE username = ?;`
|
||||||
|
|
||||||
queryFmtDeleteWebauthnDeviceByUsernameAndDescription = `
|
queryFmtDeleteWebauthnDeviceByUsernameAndDisplayName = `
|
||||||
DELETE FROM %s
|
DELETE FROM %s
|
||||||
WHERE username = ? AND description = ?;`
|
WHERE username = ? AND displayname = ?;`
|
||||||
|
|
||||||
|
queryFmtSelectWebauthnDevicesEncryptedData = `
|
||||||
|
SELECT id, public_key
|
||||||
|
FROM %s;`
|
||||||
|
|
||||||
|
queryFmtUpdateWebauthnDevicesEncryptedData = `
|
||||||
|
UPDATE %s
|
||||||
|
SET public_key = ?
|
||||||
|
WHERE id = ?;`
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"@mui/styles": "5.11.7",
|
"@mui/styles": "5.11.7",
|
||||||
"@simplewebauthn/browser": "7.1.0",
|
"@simplewebauthn/browser": "7.1.0",
|
||||||
"@simplewebauthn/typescript-types": "7.0.0",
|
"@simplewebauthn/typescript-types": "7.0.0",
|
||||||
"axios": "1.3.2",
|
"axios": "1.3.3",
|
||||||
"broadcast-channel": "4.20.2",
|
"broadcast-channel": "4.20.2",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
|
|
|
@ -27,7 +27,7 @@ specifiers:
|
||||||
'@typescript-eslint/eslint-plugin': 5.51.0
|
'@typescript-eslint/eslint-plugin': 5.51.0
|
||||||
'@typescript-eslint/parser': 5.51.0
|
'@typescript-eslint/parser': 5.51.0
|
||||||
'@vitejs/plugin-react': 3.1.0
|
'@vitejs/plugin-react': 3.1.0
|
||||||
axios: 1.3.2
|
axios: 1.3.3
|
||||||
broadcast-channel: 4.20.2
|
broadcast-channel: 4.20.2
|
||||||
classnames: 2.3.2
|
classnames: 2.3.2
|
||||||
date-fns: 2.29.3
|
date-fns: 2.29.3
|
||||||
|
@ -81,7 +81,7 @@ dependencies:
|
||||||
'@mui/styles': 5.11.7_pmekkgnqduwlme35zpnqhenc34
|
'@mui/styles': 5.11.7_pmekkgnqduwlme35zpnqhenc34
|
||||||
'@simplewebauthn/browser': 7.1.0
|
'@simplewebauthn/browser': 7.1.0
|
||||||
'@simplewebauthn/typescript-types': 7.0.0
|
'@simplewebauthn/typescript-types': 7.0.0
|
||||||
axios: 1.3.2
|
axios: 1.3.3
|
||||||
broadcast-channel: 4.20.2
|
broadcast-channel: 4.20.2
|
||||||
classnames: 2.3.2
|
classnames: 2.3.2
|
||||||
date-fns: 2.29.3
|
date-fns: 2.29.3
|
||||||
|
@ -4313,8 +4313,8 @@ packages:
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/axios/1.3.2:
|
/axios/1.3.3:
|
||||||
resolution: {integrity: sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==}
|
resolution: {integrity: sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.1
|
follow-redirects: 1.15.1
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
|
|
|
@ -110,14 +110,34 @@ export interface WebauthnDevice {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
last_used_at?: string;
|
last_used_at?: string;
|
||||||
rpid: string;
|
rpid: string;
|
||||||
description: string;
|
displayname: string;
|
||||||
kid: Uint8Array;
|
kid: Uint8Array;
|
||||||
public_key: Uint8Array;
|
|
||||||
attestation_type: string;
|
|
||||||
transports: string[];
|
|
||||||
aaguid?: string;
|
aaguid?: string;
|
||||||
|
attestation_type: string;
|
||||||
|
attachment: string;
|
||||||
|
transports: string[];
|
||||||
sign_count: number;
|
sign_count: number;
|
||||||
clone_warning: boolean;
|
clone_warning: boolean;
|
||||||
|
discoverable: boolean;
|
||||||
|
present: boolean;
|
||||||
|
verified: boolean;
|
||||||
|
backup_eligible: boolean;
|
||||||
|
backup_state: boolean;
|
||||||
|
public_key: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTransportName(transport: string) {
|
||||||
|
switch (transport.toLowerCase()) {
|
||||||
|
case "internal":
|
||||||
|
return "Internal";
|
||||||
|
case "ble":
|
||||||
|
return "Bluetooth";
|
||||||
|
case "nfc":
|
||||||
|
case "usb":
|
||||||
|
return transport.toUpperCase();
|
||||||
|
default:
|
||||||
|
return transport;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WebauthnTouchState {
|
export enum WebauthnTouchState {
|
||||||
|
|
|
@ -104,10 +104,20 @@ function getAssertionResultFromDOMException(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAttestationCreationOptions(): Promise<PublicKeyCredentialCreationOptionsStatus> {
|
export async function getAttestationCreationOptions(
|
||||||
let response: AxiosResponse<ServiceResponse<CredentialCreation>>;
|
displayname: string,
|
||||||
|
): Promise<PublicKeyCredentialCreationOptionsStatus> {
|
||||||
response = await axios.get<ServiceResponse<CredentialCreation>>(WebauthnRegistrationPath);
|
const response = await axios.put<ServiceResponse<CredentialCreation>>(
|
||||||
|
WebauthnRegistrationPath,
|
||||||
|
{
|
||||||
|
displayname: displayname,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status < 300 || status === 409;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (response.data.status !== "OK" || response.data.data == null) {
|
if (response.data.status !== "OK" || response.data.data == null) {
|
||||||
return {
|
return {
|
||||||
|
@ -194,12 +204,8 @@ export async function getAuthenticationResult(options: PublicKeyCredentialReques
|
||||||
|
|
||||||
async function postRegistrationResponse(
|
async function postRegistrationResponse(
|
||||||
response: RegistrationResponseJSON,
|
response: RegistrationResponseJSON,
|
||||||
description: string,
|
|
||||||
): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> {
|
): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> {
|
||||||
return axios.post<OptionalDataServiceResponse<any>>(WebauthnRegistrationPath, {
|
return axios.post<OptionalDataServiceResponse<any>>(WebauthnRegistrationPath, response);
|
||||||
response: response,
|
|
||||||
description: description,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postAuthenticationResponse(
|
export async function postAuthenticationResponse(
|
||||||
|
@ -216,14 +222,14 @@ export async function postAuthenticationResponse(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function finishRegistration(response: RegistrationResponseJSON, description: string) {
|
export async function finishRegistration(response: RegistrationResponseJSON) {
|
||||||
let result = {
|
let result = {
|
||||||
status: AttestationResult.Failure,
|
status: AttestationResult.Failure,
|
||||||
message: "Device registration failed.",
|
message: "Device registration failed.",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await postRegistrationResponse(response, description);
|
const resp = await postRegistrationResponse(response);
|
||||||
if (resp.data.status === "OK" && (resp.status === 200 || resp.status === 201)) {
|
if (resp.data.status === "OK" && (resp.status === 200 || resp.status === 201)) {
|
||||||
return {
|
return {
|
||||||
status: AttestationResult.Success,
|
status: AttestationResult.Success,
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default function WebauthnDeviceDeleteDialog(props: Props) {
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{translate("Are you sure you want to remove the Webauthn credential from from your account", {
|
{translate("Are you sure you want to remove the Webauthn credential from from your account", {
|
||||||
description: props.device.description,
|
description: props.device.displayname,
|
||||||
})}
|
})}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
import { WebauthnDevice, toTransportName } from "@models/Webauthn";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -33,7 +33,7 @@ export default function WebauthnDetailsDeleteDialog(props: Props) {
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText sx={{ mb: 3 }}>
|
<DialogContentText sx={{ mb: 3 }}>
|
||||||
{translate("Extended Webauthn credential information for security key", {
|
{translate("Extended Webauthn credential information for security key", {
|
||||||
description: props.device.description,
|
displayname: props.device.displayname,
|
||||||
})}
|
})}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||||
|
@ -46,16 +46,39 @@ export default function WebauthnDetailsDeleteDialog(props: Props) {
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
<PropertyText name={translate("Description")} value={props.device.description} />
|
<PropertyText name={translate("Display Name")} value={props.device.displayname} />
|
||||||
<PropertyText name={translate("Relying Party ID")} value={props.device.rpid} />
|
<PropertyText name={translate("Relying Party ID")} value={props.device.rpid} />
|
||||||
<PropertyText
|
<PropertyText
|
||||||
name={translate("Authenticator Attestation GUID")}
|
name={translate("Authenticator GUID")}
|
||||||
value={props.device.aaguid === undefined ? "N/A" : props.device.aaguid}
|
value={props.device.aaguid === undefined ? "N/A" : props.device.aaguid}
|
||||||
/>
|
/>
|
||||||
<PropertyText name={translate("Attestation Type")} value={props.device.attestation_type} />
|
<PropertyText name={translate("Attestation Type")} value={props.device.attestation_type} />
|
||||||
|
<PropertyText name={translate("Attachment")} value={props.device.attachment} />
|
||||||
|
<PropertyText
|
||||||
|
name={translate("Discoverable")}
|
||||||
|
value={props.device.discoverable ? translate("Yes") : translate("No")}
|
||||||
|
/>
|
||||||
|
<PropertyText
|
||||||
|
name={translate("User Verified")}
|
||||||
|
value={props.device.verified ? translate("Yes") : translate("No")}
|
||||||
|
/>
|
||||||
|
<PropertyText
|
||||||
|
name={translate("Backup State")}
|
||||||
|
value={
|
||||||
|
props.device.backup_eligible
|
||||||
|
? props.device.backup_state
|
||||||
|
? translate("Backed Up")
|
||||||
|
: translate("Eligible")
|
||||||
|
: translate("Not Eligible")
|
||||||
|
}
|
||||||
|
/>
|
||||||
<PropertyText
|
<PropertyText
|
||||||
name={translate("Transports")}
|
name={translate("Transports")}
|
||||||
value={props.device.transports.length === 0 ? "N/A" : props.device.transports.join(", ")}
|
value={
|
||||||
|
props.device.transports.length === 0
|
||||||
|
? "N/A"
|
||||||
|
: props.device.transports.map((transport) => toTransportName(transport)).join(", ")
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<PropertyText
|
<PropertyText
|
||||||
name={translate("Clone Warning")}
|
name={translate("Clone Warning")}
|
||||||
|
|
|
@ -118,7 +118,7 @@ export default function WebauthnDeviceItem(props: Props) {
|
||||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||||
{props.device.description}
|
{props.device.displayname}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
display="inline"
|
display="inline"
|
||||||
|
|
|
@ -6,9 +6,9 @@ import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Grid,
|
Grid,
|
||||||
Stack,
|
|
||||||
Step,
|
Step,
|
||||||
StepLabel,
|
StepLabel,
|
||||||
Stepper,
|
Stepper,
|
||||||
|
@ -23,15 +23,10 @@ import { useTranslation } from "react-i18next";
|
||||||
import InformationIcon from "@components/InformationIcon";
|
import InformationIcon from "@components/InformationIcon";
|
||||||
import WebauthnRegisterIcon from "@components/WebauthnRegisterIcon";
|
import WebauthnRegisterIcon from "@components/WebauthnRegisterIcon";
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import {
|
import { AttestationResult, AttestationResultFailureString, WebauthnTouchState } from "@models/Webauthn";
|
||||||
AttestationResult,
|
|
||||||
AttestationResultFailureString,
|
|
||||||
RegistrationResult,
|
|
||||||
WebauthnTouchState,
|
|
||||||
} from "@models/Webauthn";
|
|
||||||
import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistration } from "@services/Webauthn";
|
import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistration } from "@services/Webauthn";
|
||||||
|
|
||||||
const steps = ["Confirm device", "Choose name"];
|
const steps = ["Display Name", "Verification"];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -47,21 +42,20 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
|
|
||||||
const [state, setState] = useState(WebauthnTouchState.WaitTouch);
|
const [state, setState] = useState(WebauthnTouchState.WaitTouch);
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [result, setResult] = useState<RegistrationResult | null>(null);
|
|
||||||
const [options, setOptions] = useState<PublicKeyCredentialCreationOptionsJSON | null>(null);
|
const [options, setOptions] = useState<PublicKeyCredentialCreationOptionsJSON | null>(null);
|
||||||
const [timeout, setTimeout] = useState<number | null>(null);
|
const [timeout, setTimeout] = useState<number | null>(null);
|
||||||
const [deviceName, setName] = useState("");
|
const [credentialDisplayName, setCredentialDisplayName] = useState("");
|
||||||
|
const [errorDisplayName, setErrorDisplayName] = useState(false);
|
||||||
|
|
||||||
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||||
const [nameError, setNameError] = useState(false);
|
|
||||||
|
|
||||||
const resetStates = () => {
|
const resetStates = () => {
|
||||||
setState(WebauthnTouchState.WaitTouch);
|
setState(WebauthnTouchState.WaitTouch);
|
||||||
setActiveStep(0);
|
|
||||||
setResult(null);
|
|
||||||
setOptions(null);
|
setOptions(null);
|
||||||
|
setActiveStep(0);
|
||||||
setTimeout(null);
|
setTimeout(null);
|
||||||
setName("");
|
setCredentialDisplayName("");
|
||||||
|
setErrorDisplayName(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
|
@ -70,53 +64,41 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
props.setCancelled();
|
props.setCancelled();
|
||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
const finishAttestation = async () => {
|
const performCredentialCreation = useCallback(async () => {
|
||||||
if (!result || !result.response) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!deviceName.length) {
|
|
||||||
setNameError(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await finishRegistration(result.response, deviceName);
|
|
||||||
switch (res.status) {
|
|
||||||
case AttestationResult.Success:
|
|
||||||
handleClose();
|
|
||||||
break;
|
|
||||||
case AttestationResult.Failure:
|
|
||||||
createErrorNotification(res.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startRegistration = useCallback(async () => {
|
|
||||||
if (options === null) {
|
if (options === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(options.timeout ? options.timeout : null);
|
setTimeout(options.timeout ? options.timeout : null);
|
||||||
|
setActiveStep(1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(WebauthnTouchState.WaitTouch);
|
setState(WebauthnTouchState.WaitTouch);
|
||||||
setActiveStep(0);
|
|
||||||
|
|
||||||
const res = await startWebauthnRegistration(options);
|
const resultCredentialCreation = await startWebauthnRegistration(options);
|
||||||
|
|
||||||
setTimeout(null);
|
setTimeout(null);
|
||||||
|
|
||||||
if (res.result === AttestationResult.Success) {
|
if (resultCredentialCreation.result === AttestationResult.Success) {
|
||||||
if (res.response == null) {
|
if (resultCredentialCreation.response == null) {
|
||||||
throw new Error("Attestation request succeeded but credential is empty");
|
throw new Error("Credential Creation Request succeeded but Registration Response is empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
setResult(res);
|
const response = await finishRegistration(resultCredentialCreation.response);
|
||||||
setActiveStep(1);
|
|
||||||
|
switch (response.status) {
|
||||||
|
case AttestationResult.Success:
|
||||||
|
handleClose();
|
||||||
|
break;
|
||||||
|
case AttestationResult.Failure:
|
||||||
|
createErrorNotification(response.message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createErrorNotification(AttestationResultFailureString(res.result));
|
createErrorNotification(AttestationResultFailureString(resultCredentialCreation.result));
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebauthnTouchState.Failure);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -124,7 +106,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
"Failed to register your device. The identity verification process might have timed out.",
|
"Failed to register your device. The identity verification process might have timed out.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [options, createErrorNotification]);
|
}, [options, createErrorNotification, handleClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state !== WebauthnTouchState.Failure || activeStep !== 0 || !props.open) {
|
if (state !== WebauthnTouchState.Failure || activeStep !== 0 || !props.open) {
|
||||||
|
@ -135,35 +117,100 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
}, [props, state, activeStep, handleClose]);
|
}, [props, state, activeStep, handleClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async function () {
|
||||||
if (options === null || !props.open || activeStep !== 0) {
|
if (!props.open || activeStep !== 0 || options === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await startRegistration();
|
await performCredentialCreation();
|
||||||
})();
|
})();
|
||||||
}, [options, props.open, activeStep, startRegistration]);
|
}, [props.open, activeStep, options, performCredentialCreation]);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleNext = useCallback(async () => {
|
||||||
(async () => {
|
if (credentialDisplayName.length === 0 || credentialDisplayName.length > 64) {
|
||||||
if (!props.open || activeStep !== 0) {
|
setErrorDisplayName(true);
|
||||||
return;
|
createErrorNotification(
|
||||||
}
|
translate("The Display Name must be more than 1 character and less than 64 characters."),
|
||||||
|
);
|
||||||
|
|
||||||
const res = await getAttestationCreationOptions();
|
return;
|
||||||
if (res.status !== 200 || !res.options) {
|
}
|
||||||
|
|
||||||
|
const res = await getAttestationCreationOptions(credentialDisplayName);
|
||||||
|
|
||||||
|
switch (res.status) {
|
||||||
|
case 200:
|
||||||
|
if (res.options) {
|
||||||
|
setOptions(res.options);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Credential Creation Options Request succeeded but Credential Creation Options is empty.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 409:
|
||||||
|
setErrorDisplayName(true);
|
||||||
|
createErrorNotification(translate("A Webauthn Credential with that Display Name already exists."));
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
"You must open the link from the same device and browser that initiated the registration process.",
|
translate("Error occurred obtaining the Webauthn Credential creation options."),
|
||||||
);
|
);
|
||||||
return;
|
}
|
||||||
|
}, [createErrorNotification, credentialDisplayName, performCredentialCreation, translate]);
|
||||||
|
|
||||||
|
const handleCredentialDisplayName = useCallback(
|
||||||
|
(displayname: string) => {
|
||||||
|
setCredentialDisplayName(displayname);
|
||||||
|
|
||||||
|
if (errorDisplayName) {
|
||||||
|
setErrorDisplayName(false);
|
||||||
}
|
}
|
||||||
setOptions(res.options);
|
},
|
||||||
})();
|
[errorDisplayName],
|
||||||
}, [setOptions, createErrorNotification, props.open, activeStep]);
|
);
|
||||||
|
|
||||||
function renderStep(step: number) {
|
function renderStep(step: number) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 0:
|
case 0:
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box className={styles.icon}>
|
||||||
|
<InformationIcon />
|
||||||
|
</Box>
|
||||||
|
<Typography className={styles.instruction}>
|
||||||
|
{translate("Enter a display name for this credential")}
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
inputRef={nameRef}
|
||||||
|
id="name-textfield"
|
||||||
|
label={translate("Display Name")}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
value={credentialDisplayName}
|
||||||
|
error={errorDisplayName}
|
||||||
|
disabled={false}
|
||||||
|
onChange={(v) => handleCredentialDisplayName(v.target.value)}
|
||||||
|
autoCapitalize="none"
|
||||||
|
onKeyDown={(ev) => {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
(async () => {
|
||||||
|
await handleNext();
|
||||||
|
})();
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Box className={styles.icon}>
|
<Box className={styles.icon}>
|
||||||
|
@ -174,52 +221,6 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
</Typography>
|
</Typography>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<Box id="webauthn-registration-name">
|
|
||||||
<Box className={styles.icon}>
|
|
||||||
<InformationIcon />
|
|
||||||
</Box>
|
|
||||||
<Typography className={styles.instruction}>{translate("Enter a name for this key")}</Typography>
|
|
||||||
<Grid container spacing={1}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<TextField
|
|
||||||
inputRef={nameRef}
|
|
||||||
id="name-textfield"
|
|
||||||
label={translate("Name")}
|
|
||||||
variant="outlined"
|
|
||||||
required
|
|
||||||
value={deviceName}
|
|
||||||
error={nameError}
|
|
||||||
disabled={false}
|
|
||||||
onChange={(v) => setName(v.target.value.substring(0, 30))}
|
|
||||||
onFocus={() => setNameError(false)}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="webauthn-name"
|
|
||||||
onKeyDown={(ev) => {
|
|
||||||
if (ev.key === "Enter") {
|
|
||||||
if (!deviceName.length) {
|
|
||||||
setNameError(true);
|
|
||||||
} else {
|
|
||||||
(async () => {
|
|
||||||
await finishAttestation();
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Stack direction="row" spacing={1} justifyContent="center" paddingTop={1}>
|
|
||||||
<Button color="primary" variant="contained" onClick={finishAttestation}>
|
|
||||||
{translate("Finish")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,8 +234,13 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
|
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
|
||||||
<DialogTitle>{translate("Register Webauthn Credential (Security Key)")}</DialogTitle>
|
<DialogTitle>{translate("Register Webauthn Credential")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
<DialogContentText sx={{ mb: 3 }}>
|
||||||
|
{translate(
|
||||||
|
"This page allows registration of a new Security Key backed by modern Webauthn Credential technology.",
|
||||||
|
)}
|
||||||
|
</DialogContentText>
|
||||||
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Stepper activeStep={activeStep}>
|
<Stepper activeStep={activeStep}>
|
||||||
|
@ -257,9 +263,24 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} disabled={activeStep === 0 && state !== WebauthnTouchState.Failure}>
|
<Button
|
||||||
|
color={activeStep === 1 && state !== WebauthnTouchState.Failure ? "primary" : "error"}
|
||||||
|
disabled={activeStep === 1 && state !== WebauthnTouchState.Failure}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
{translate("Cancel")}
|
{translate("Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{activeStep === 0 ? (
|
||||||
|
<Button
|
||||||
|
color={credentialDisplayName.length !== 0 ? "success" : "primary"}
|
||||||
|
disabled={activeStep !== 0}
|
||||||
|
onClick={async () => {
|
||||||
|
await handleNext();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate("Next")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue