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-rod/rod v0.112.5
|
||||
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/mock v1.6.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-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/webauthn v0.7.1 h1:b1/HP1bkqsW+DIO22WyG7BP9dL0rN151VpruH6cxADA=
|
||||
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 h1:q8OgN8xHBoJpZ5+ZrRwGmLVCPVhgW+kMx87AkwVGYfA=
|
||||
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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
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")
|
||||
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
@ -721,7 +721,7 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
|
|||
},
|
||||
{
|
||||
ID: "b-client",
|
||||
Description: "Normal Description",
|
||||
Description: "Normal DisplayName",
|
||||
Secret: MustDecodeSecret("$plaintext$b-client-secret"),
|
||||
Policy: policyOneFactor,
|
||||
UserinfoSigningAlgorithm: "RS256",
|
||||
|
@ -783,9 +783,9 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
|
|||
assert.Equal(t, "none", config.OIDC.Clients[0].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, "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.
|
||||
require.Len(t, config.OIDC.Clients[0].Scopes, 4)
|
||||
|
|
|
@ -66,7 +66,7 @@ const (
|
|||
messageAuthenticationFailed = "Authentication failed. Check your credentials."
|
||||
messageUnableToRegisterOneTimePassword = "Unable to set up one-time passwords." //nolint:gosec
|
||||
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."
|
||||
messageMFAValidationFailed = "Authentication failed, please retry later."
|
||||
messagePasswordWeak = "Your supplied password does not meet the password policy requirements"
|
||||
|
|
|
@ -3,6 +3,7 @@ package handlers
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
|
@ -15,17 +16,18 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
)
|
||||
|
||||
// WebauthnRegistrationGET returns the attestation challenge from the server.
|
||||
func WebauthnRegistrationGET(ctx *middlewares.AutheliaCtx) {
|
||||
// WebauthnRegistrationPUT returns the attestation challenge from the server.
|
||||
func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
|
||||
var (
|
||||
w *webauthn.WebAuthn
|
||||
user *model.WebauthnUser
|
||||
userSession session.UserSession
|
||||
bodyJSON bodyRegisterWebauthnPUTRequest
|
||||
err error
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -33,40 +35,84 @@ func WebauthnRegistrationGET(ctx *middlewares.AutheliaCtx) {
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if user, err = getWebauthnUserByRPID(ctx, userSession, w.Config.RPID); err != nil {
|
||||
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge 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)
|
||||
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
|
||||
ctx.Logger.Errorf("Unable to parse %s registration request PUT data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err = ctx.SetJSONBody(credentialCreation); err != nil {
|
||||
if err = ctx.SetJSONBody(opts); err != nil {
|
||||
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
|
@ -87,7 +133,6 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
response *protocol.ParsedCredentialCreationData
|
||||
|
||||
credential *webauthn.Credential
|
||||
bodyJSON bodyRegisterWebauthnRequest
|
||||
)
|
||||
|
||||
if userSession, err = ctx.GetSession(); err != nil {
|
||||
|
@ -98,10 +143,10 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -114,71 +159,41 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
|
||||
ctx.Logger.Errorf("Unable to parse %s registration request POST data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
if response, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
|
||||
switch e := err.(type) {
|
||||
case *protocol.Error:
|
||||
ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v (%s)", regulation.AuthTypeWebauthn, userSession.Username, err, e.DevInfo)
|
||||
default:
|
||||
ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
}
|
||||
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, existingDevice := range devices {
|
||||
if existingDevice.Description == bodyJSON.Description {
|
||||
ctx.Logger.Errorf("%s device for for user '%s' with name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, bodyJSON.Description)
|
||||
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, messageUnableToRegisterSecurityKey)
|
||||
ctx.SetStatusCode(fasthttp.StatusConflict)
|
||||
ctx.SetJSONError(messageSecurityKeyDuplicateName)
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
|
||||
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{
|
||||
"RPID": device.RPID,
|
||||
"ID": device.ID,
|
||||
"KID": device.KID.String(),
|
||||
"User": device.Username,
|
||||
}).Debug("Registering New Device")
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, userSession.Webauthn.DisplayName, credential)
|
||||
|
||||
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)
|
||||
|
@ -189,6 +204,7 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
}
|
||||
|
||||
userSession.Webauthn = nil
|
||||
|
||||
if err = ctx.SaveSession(userSession); err != nil {
|
||||
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.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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
@ -61,7 +61,9 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
|||
|
||||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
@ -69,6 +71,8 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
userSession.Webauthn = &data
|
||||
|
||||
if err = ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
||||
|
@ -115,7 +119,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
@ -145,7 +149,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
@ -153,7 +157,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
@ -169,7 +173,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
|
||||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
|
|
@ -40,10 +40,8 @@ type bodySignWebauthnRequest struct {
|
|||
Response json.RawMessage `json:"response"`
|
||||
}
|
||||
|
||||
type bodyRegisterWebauthnRequest struct {
|
||||
Description string `json:"description"`
|
||||
|
||||
Response json.RawMessage `json:"response"`
|
||||
type bodyRegisterWebauthnPUTRequest struct {
|
||||
DisplayName string `json:"displayname"`
|
||||
}
|
||||
|
||||
type bodyEditWebauthnDeviceRequest struct {
|
||||
|
|
|
@ -13,20 +13,20 @@ import (
|
|||
)
|
||||
|
||||
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{
|
||||
Username: userSession.Username,
|
||||
DisplayName: userSession.DisplayName,
|
||||
Username: username,
|
||||
DisplayName: displayname,
|
||||
}
|
||||
|
||||
if user.DisplayName == "" {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
|||
ID: 1,
|
||||
RPID: "example.com",
|
||||
Username: "john",
|
||||
Description: "Primary",
|
||||
DisplayName: "Primary",
|
||||
KID: model.NewBase64([]byte("abc123")),
|
||||
AttestationType: "fido-u2f",
|
||||
PublicKey: []byte("data"),
|
||||
|
@ -37,7 +37,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
|||
ID: 2,
|
||||
RPID: "example.com",
|
||||
Username: "john",
|
||||
Description: "Secondary",
|
||||
DisplayName: "Secondary",
|
||||
KID: model.NewBase64([]byte("123abc")),
|
||||
AttestationType: "packed",
|
||||
Transport: "usb,nfc",
|
||||
|
@ -47,7 +47,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
|||
},
|
||||
}, 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.NotNil(t, user)
|
||||
|
@ -66,7 +66,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
|||
assert.Equal(t, 1, user.Devices[0].ID)
|
||||
assert.Equal(t, "example.com", user.Devices[0].RPID)
|
||||
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, "fido-u2f", user.Devices[0].AttestationType)
|
||||
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, "example.com", user.Devices[1].RPID)
|
||||
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, "packed", user.Devices[1].AttestationType)
|
||||
assert.Equal(t, []byte("data"), user.Devices[1].PublicKey)
|
||||
|
@ -111,7 +111,7 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
|
|||
ID: 1,
|
||||
RPID: "example.com",
|
||||
Username: "john",
|
||||
Description: "Primary",
|
||||
DisplayName: "Primary",
|
||||
KID: model.NewBase64([]byte("abc123")),
|
||||
AttestationType: "fido-u2f",
|
||||
PublicKey: []byte("data"),
|
||||
|
@ -120,7 +120,7 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
|
|||
},
|
||||
}, 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.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"))
|
||||
|
||||
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.Nil(t, user)
|
||||
|
|
|
@ -850,15 +850,15 @@ func (mr *MockStorageMockRecorder) UpdateWebauthnDeviceDescription(arg0, arg1, a
|
|||
}
|
||||
|
||||
// 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()
|
||||
ret := m.ctrl.Call(m, "UpdateWebauthnDeviceSignIn", arg0, arg1, arg2, arg3, arg4, arg5)
|
||||
ret := m.ctrl.Call(m, "UpdateWebauthnDeviceSignIn", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// 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()
|
||||
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(),
|
||||
PublicKey: device.PublicKey,
|
||||
AttestationType: device.AttestationType,
|
||||
Flags: webauthn.CredentialFlags{
|
||||
UserPresent: device.Present,
|
||||
UserVerified: device.Verified,
|
||||
BackupEligible: device.BackupEligible,
|
||||
BackupState: device.BackupState,
|
||||
},
|
||||
Authenticator: webauthn.Authenticator{
|
||||
AAGUID: aaguid,
|
||||
SignCount: device.SignCount,
|
||||
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.
|
||||
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))
|
||||
|
||||
for i, t := range credential.Transport {
|
||||
|
@ -121,13 +128,19 @@ func NewWebauthnDeviceFromCredential(rpid, username, description string, credent
|
|||
RPID: rpid,
|
||||
Username: username,
|
||||
CreatedAt: time.Now(),
|
||||
Description: description,
|
||||
DisplayName: displayname,
|
||||
KID: NewBase64(credential.ID),
|
||||
PublicKey: credential.PublicKey,
|
||||
AttestationType: credential.AttestationType,
|
||||
Attachment: string(credential.Authenticator.Attachment),
|
||||
Transport: strings.Join(transport, ","),
|
||||
SignCount: credential.Authenticator.SignCount,
|
||||
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))
|
||||
|
@ -144,14 +157,20 @@ type WebauthnDeviceJSON struct {
|
|||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||
RPID string `json:"rpid"`
|
||||
Description string `json:"description"`
|
||||
DisplayName string `json:"displayname"`
|
||||
KID []byte `json:"kid"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
AAGUID string `json:"aaguid,omitempty"`
|
||||
Attachment string `json:"attachment"`
|
||||
AttestationType string `json:"attestation_type"`
|
||||
Transports []string `json:"transports"`
|
||||
AAGUID string `json:"aaguid,omitempty"`
|
||||
SignCount uint32 `json:"sign_count"`
|
||||
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.
|
||||
|
@ -161,14 +180,20 @@ type WebauthnDevice struct {
|
|||
LastUsedAt sql.NullTime `db:"last_used_at"`
|
||||
RPID string `db:"rpid"`
|
||||
Username string `db:"username"`
|
||||
Description string `db:"description"`
|
||||
DisplayName string `db:"displayname"`
|
||||
KID Base64 `db:"kid"`
|
||||
PublicKey []byte `db:"public_key"`
|
||||
AttestationType string `db:"attestation_type"`
|
||||
Transport string `db:"transport"`
|
||||
AAGUID uuid.NullUUID `db:"aaguid"`
|
||||
AttestationType string `db:"attestation_type"`
|
||||
Attachment string `db:"attachment"`
|
||||
Transport string `db:"transport"`
|
||||
SignCount uint32 `db:"sign_count"`
|
||||
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.
|
||||
|
@ -177,13 +202,19 @@ func (w *WebauthnDevice) MarshalJSON() (data []byte, err error) {
|
|||
ID: w.ID,
|
||||
CreatedAt: w.CreatedAt,
|
||||
RPID: w.RPID,
|
||||
Description: w.Description,
|
||||
DisplayName: w.DisplayName,
|
||||
KID: w.KID.data,
|
||||
PublicKey: w.PublicKey,
|
||||
AttestationType: w.AttestationType,
|
||||
Attachment: w.Attachment,
|
||||
Transports: []string{},
|
||||
SignCount: w.SignCount,
|
||||
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 {
|
||||
|
@ -234,14 +265,19 @@ func (d *WebauthnDevice) MarshalYAML() (any, error) {
|
|||
LastUsedAt: d.LastUsed(),
|
||||
RPID: d.RPID,
|
||||
Username: d.Username,
|
||||
Description: d.Description,
|
||||
DisplayName: d.DisplayName,
|
||||
KID: d.KID.String(),
|
||||
PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey),
|
||||
AttestationType: d.AttestationType,
|
||||
Transport: d.Transport,
|
||||
AAGUID: d.AAGUID.UUID.String(),
|
||||
AttestationType: d.AttestationType,
|
||||
Attachment: d.Attachment,
|
||||
Transport: d.Transport,
|
||||
SignCount: d.SignCount,
|
||||
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)
|
||||
|
@ -280,11 +316,17 @@ func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
|||
d.CreatedAt = o.CreatedAt
|
||||
d.RPID = o.RPID
|
||||
d.Username = o.Username
|
||||
d.Description = o.Description
|
||||
d.DisplayName = o.DisplayName
|
||||
d.AttestationType = o.AttestationType
|
||||
d.Attachment = o.Attachment
|
||||
d.Transport = o.Transport
|
||||
d.SignCount = o.SignCount
|
||||
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 {
|
||||
d.LastUsedAt = sql.NullTime{Valid: true, Time: *o.LastUsedAt}
|
||||
|
@ -299,14 +341,20 @@ type WebauthnDeviceData struct {
|
|||
LastUsedAt *time.Time `yaml:"last_used_at"`
|
||||
RPID string `yaml:"rpid"`
|
||||
Username string `yaml:"username"`
|
||||
Description string `yaml:"description"`
|
||||
DisplayName string `yaml:"displayname"`
|
||||
KID string `yaml:"kid"`
|
||||
PublicKey string `yaml:"public_key"`
|
||||
AttestationType string `yaml:"attestation_type"`
|
||||
Transport string `yaml:"transport"`
|
||||
AAGUID string `yaml:"aaguid"`
|
||||
AttestationType string `yaml:"attestation_type"`
|
||||
Attachment string `yaml:"attachment"`
|
||||
Transport string `yaml:"transport"`
|
||||
SignCount uint32 `yaml:"sign_count"`
|
||||
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.
|
||||
|
|
|
@ -70,7 +70,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes
|
|||
},
|
||||
{
|
||||
ID: "b-client",
|
||||
Description: "Normal Description",
|
||||
Description: "Normal DisplayName",
|
||||
Secret: MustDecodeSecret("$plaintext$b-client-secret"),
|
||||
Policy: "two_factor",
|
||||
RedirectURIs: []string{
|
||||
|
|
|
@ -240,7 +240,7 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
|
|||
|
||||
// Management of the webauthn devices.
|
||||
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.PUT("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebauthnDevicePUT))
|
||||
r.DELETE("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebauthnDeviceDELETE))
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"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?",
|
||||
"Attestation Type": "Attestation Type",
|
||||
"Authenticator Attestation GUID": "Authenticator Attestation GUID",
|
||||
"Authenticator GUID": "Authenticator GUID",
|
||||
"Cancel": "Cancel",
|
||||
"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",
|
||||
|
@ -19,7 +19,8 @@
|
|||
"Edit Webauthn Credential": "Edit Webauthn Credential",
|
||||
"Enabled": "Enabled",
|
||||
"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",
|
||||
"Last Used": "Last Used {{when, datetime}}",
|
||||
"Manage your security keys": "Manage your security keys",
|
||||
|
@ -28,7 +29,7 @@
|
|||
"No Registered Webauthn Credentials": "No Registered Webauthn Credentials",
|
||||
"Overview": "Overview",
|
||||
"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",
|
||||
"Remove": "Remove",
|
||||
"Remove this Webauthn credential": "Remove this Webauthn credential",
|
||||
|
|
|
@ -36,7 +36,7 @@ type UserSession struct {
|
|||
AuthenticationMethodRefs oidc.AuthenticationMethodsReferences
|
||||
|
||||
// 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
|
||||
// while doing the query actually updating the password.
|
||||
|
@ -45,6 +45,11 @@ type UserSession struct {
|
|||
RefreshTTL time.Time
|
||||
}
|
||||
|
||||
type Webauthn struct {
|
||||
*webauthn.SessionData
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// Identity identity of the user who is being verified.
|
||||
type Identity struct {
|
||||
Username string
|
||||
|
|
|
@ -1,3 +1,33 @@
|
|||
DROP INDEX webauthn_devices_lookup_key ON webauthn_devices;
|
||||
ALTER TABLE webauthn_devices MODIFY COLUMN rpid VARCHAR(512);
|
||||
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, description);
|
||||
ALTER TABLE webauthn_devices
|
||||
RENAME _bkp_UP_V0008_webauthn_devices;
|
||||
|
||||
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 COLUMN rpid SET DATA TYPE VARCHAR(512);
|
||||
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, description);
|
||||
ALTER TABLE webauthn_devices
|
||||
RENAME TO _bkp_UP_V0008_webauthn_devices;
|
||||
|
||||
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;
|
||||
CREATE UNIQUE INDEX webauthn_devices_lookup_key ON webauthn_devices (rpid, username, description);
|
||||
DROP INDEX IF EXISTS webauthn_devices_lookup_key;
|
||||
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)
|
||||
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)
|
||||
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (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),
|
||||
sqlSelectWebauthnDevicesByRPIDByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByRPIDByUsername, tableWebauthnDevices),
|
||||
sqlSelectWebauthnDeviceByID: fmt.Sprintf(queryFmtSelectWebauthnDeviceByID, tableWebauthnDevices),
|
||||
sqlUpdateWebauthnDeviceDescriptionByUsernameAndID: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID, tableWebauthnDevices),
|
||||
sqlUpdateWebauthnDeviceDescriptionByUsernameAndID: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDeviceDisplayNameByUsernameAndID, tableWebauthnDevices),
|
||||
sqlUpdateWebauthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices),
|
||||
sqlDeleteWebauthnDevice: fmt.Sprintf(queryFmtDeleteWebauthnDevice, tableWebauthnDevices),
|
||||
sqlDeleteWebauthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsername, tableWebauthnDevices),
|
||||
sqlDeleteWebauthnDeviceByUsernameAndDescription: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDescription, tableWebauthnDevices),
|
||||
sqlDeleteWebauthnDeviceByUsernameAndDisplayName: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDisplayName, tableWebauthnDevices),
|
||||
|
||||
sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),
|
||||
sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices),
|
||||
|
@ -172,7 +172,7 @@ type SQLProvider struct {
|
|||
|
||||
sqlDeleteWebauthnDevice string
|
||||
sqlDeleteWebauthnDeviceByUsername string
|
||||
sqlDeleteWebauthnDeviceByUsernameAndDescription string
|
||||
sqlDeleteWebauthnDeviceByUsernameAndDisplayName string
|
||||
|
||||
// Table: duo_devices.
|
||||
sqlUpsertDuoDevice string
|
||||
|
@ -842,10 +842,10 @@ func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.Webau
|
|||
}
|
||||
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlInsertWebauthnDevice,
|
||||
device.CreatedAt, device.LastUsedAt,
|
||||
device.RPID, device.Username, device.Description,
|
||||
device.KID, device.PublicKey,
|
||||
device.AttestationType, device.Transport, device.AAGUID, device.SignCount, device.CloneWarning,
|
||||
device.CreatedAt, device.LastUsedAt, device.RPID, device.Username, device.DisplayName,
|
||||
device.KID, device.AAGUID, device.AttestationType, device.Attachment, device.Transport,
|
||||
device.SignCount, device.CloneWarning, device.Discoverable, device.Present, device.Verified,
|
||||
device.BackupEligible, device.BackupState, device.PublicKey,
|
||||
); err != nil {
|
||||
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.
|
||||
func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceRecordSignIn, rpid, lastUsedAt, signCount, cloneWarning, id); err != nil {
|
||||
return fmt.Errorf("error updating Webauthn signin metadata for id '%x': %w", id, err)
|
||||
func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, device model.WebauthnDevice) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceRecordSignIn,
|
||||
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
|
||||
|
@ -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.
|
||||
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 {
|
||||
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 {
|
||||
return fmt.Errorf("error deleting webauthn devices for username '%s': %w", username, err)
|
||||
}
|
||||
} else {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsernameAndDescription, username, description); err != nil {
|
||||
return fmt.Errorf("error deleting webauthn device with username '%s' and description '%s': %w", username, description, err)
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsernameAndDisplayName, username, displayname); err != nil {
|
||||
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.sqlDeleteWebauthnDevice = provider.db.Rebind(provider.sqlDeleteWebauthnDevice)
|
||||
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.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)
|
||||
}
|
||||
|
||||
query := provider.db.Rebind(fmt.Sprintf(queryFmtUpdateWebauthnDevicePublicKey, tableWebauthnDevices))
|
||||
query := provider.db.Rebind(fmt.Sprintf(queryFmtUpdateWebauthnDevicesEncryptedData, tableWebauthnDevices))
|
||||
|
||||
for _, d := range devices {
|
||||
if d.PublicKey, err = provider.decrypt(d.PublicKey); err != nil {
|
||||
|
|
|
@ -120,50 +120,41 @@ const (
|
|||
|
||||
const (
|
||||
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
|
||||
LIMIT ?
|
||||
OFFSET ?;`
|
||||
|
||||
queryFmtSelectWebauthnDevicesEncryptedData = `
|
||||
SELECT id, public_key
|
||||
FROM %s;`
|
||||
|
||||
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
|
||||
WHERE username = ?;`
|
||||
|
||||
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
|
||||
WHERE rpid = ? AND username = ?;`
|
||||
|
||||
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
|
||||
WHERE id = ?;`
|
||||
|
||||
queryFmtUpdateWebauthnDevicePublicKey = `
|
||||
queryFmtUpdateUpdateWebauthnDeviceDisplayNameByUsernameAndID = `
|
||||
UPDATE %s
|
||||
SET public_key = ?
|
||||
WHERE id = ?;`
|
||||
|
||||
queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID = `
|
||||
UPDATE %s
|
||||
SET description = ?
|
||||
SET displayname = ?
|
||||
WHERE username = ? AND id = ?;`
|
||||
|
||||
queryFmtUpdateWebauthnDeviceRecordSignIn = `
|
||||
UPDATE %s
|
||||
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
|
||||
WHERE id = ?;`
|
||||
|
||||
queryFmtUpsertInsertDevice = `
|
||||
INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
||||
|
||||
queryFmtDeleteWebauthnDevice = `
|
||||
DELETE FROM %s
|
||||
|
@ -173,9 +164,18 @@ const (
|
|||
DELETE FROM %s
|
||||
WHERE username = ?;`
|
||||
|
||||
queryFmtDeleteWebauthnDeviceByUsernameAndDescription = `
|
||||
queryFmtDeleteWebauthnDeviceByUsernameAndDisplayName = `
|
||||
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 (
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"@mui/styles": "5.11.7",
|
||||
"@simplewebauthn/browser": "7.1.0",
|
||||
"@simplewebauthn/typescript-types": "7.0.0",
|
||||
"axios": "1.3.2",
|
||||
"axios": "1.3.3",
|
||||
"broadcast-channel": "4.20.2",
|
||||
"classnames": "2.3.2",
|
||||
"date-fns": "2.29.3",
|
||||
|
|
|
@ -27,7 +27,7 @@ specifiers:
|
|||
'@typescript-eslint/eslint-plugin': 5.51.0
|
||||
'@typescript-eslint/parser': 5.51.0
|
||||
'@vitejs/plugin-react': 3.1.0
|
||||
axios: 1.3.2
|
||||
axios: 1.3.3
|
||||
broadcast-channel: 4.20.2
|
||||
classnames: 2.3.2
|
||||
date-fns: 2.29.3
|
||||
|
@ -81,7 +81,7 @@ dependencies:
|
|||
'@mui/styles': 5.11.7_pmekkgnqduwlme35zpnqhenc34
|
||||
'@simplewebauthn/browser': 7.1.0
|
||||
'@simplewebauthn/typescript-types': 7.0.0
|
||||
axios: 1.3.2
|
||||
axios: 1.3.3
|
||||
broadcast-channel: 4.20.2
|
||||
classnames: 2.3.2
|
||||
date-fns: 2.29.3
|
||||
|
@ -4313,8 +4313,8 @@ packages:
|
|||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/axios/1.3.2:
|
||||
resolution: {integrity: sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==}
|
||||
/axios/1.3.3:
|
||||
resolution: {integrity: sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.1
|
||||
form-data: 4.0.0
|
||||
|
|
|
@ -110,14 +110,34 @@ export interface WebauthnDevice {
|
|||
created_at: string;
|
||||
last_used_at?: string;
|
||||
rpid: string;
|
||||
description: string;
|
||||
displayname: string;
|
||||
kid: Uint8Array;
|
||||
public_key: Uint8Array;
|
||||
attestation_type: string;
|
||||
transports: string[];
|
||||
aaguid?: string;
|
||||
attestation_type: string;
|
||||
attachment: string;
|
||||
transports: string[];
|
||||
sign_count: number;
|
||||
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 {
|
||||
|
|
|
@ -104,10 +104,20 @@ function getAssertionResultFromDOMException(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getAttestationCreationOptions(): Promise<PublicKeyCredentialCreationOptionsStatus> {
|
||||
let response: AxiosResponse<ServiceResponse<CredentialCreation>>;
|
||||
|
||||
response = await axios.get<ServiceResponse<CredentialCreation>>(WebauthnRegistrationPath);
|
||||
export async function getAttestationCreationOptions(
|
||||
displayname: string,
|
||||
): Promise<PublicKeyCredentialCreationOptionsStatus> {
|
||||
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) {
|
||||
return {
|
||||
|
@ -194,12 +204,8 @@ export async function getAuthenticationResult(options: PublicKeyCredentialReques
|
|||
|
||||
async function postRegistrationResponse(
|
||||
response: RegistrationResponseJSON,
|
||||
description: string,
|
||||
): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> {
|
||||
return axios.post<OptionalDataServiceResponse<any>>(WebauthnRegistrationPath, {
|
||||
response: response,
|
||||
description: description,
|
||||
});
|
||||
return axios.post<OptionalDataServiceResponse<any>>(WebauthnRegistrationPath, response);
|
||||
}
|
||||
|
||||
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 = {
|
||||
status: AttestationResult.Failure,
|
||||
message: "Device registration failed.",
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await postRegistrationResponse(response, description);
|
||||
const resp = await postRegistrationResponse(response);
|
||||
if (resp.data.status === "OK" && (resp.status === 200 || resp.status === 201)) {
|
||||
return {
|
||||
status: AttestationResult.Success,
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function WebauthnDeviceDeleteDialog(props: Props) {
|
|||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{translate("Are you sure you want to remove the Webauthn credential from from your account", {
|
||||
description: props.device.description,
|
||||
description: props.device.displayname,
|
||||
})}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { WebauthnDevice } from "@models/Webauthn";
|
||||
import { WebauthnDevice, toTransportName } from "@models/Webauthn";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
@ -33,7 +33,7 @@ export default function WebauthnDetailsDeleteDialog(props: Props) {
|
|||
<DialogContent>
|
||||
<DialogContentText sx={{ mb: 3 }}>
|
||||
{translate("Extended Webauthn credential information for security key", {
|
||||
description: props.device.description,
|
||||
displayname: props.device.displayname,
|
||||
})}
|
||||
</DialogContentText>
|
||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||
|
@ -46,16 +46,39 @@ export default function WebauthnDetailsDeleteDialog(props: Props) {
|
|||
/>
|
||||
</Stack>
|
||||
</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("Authenticator Attestation GUID")}
|
||||
name={translate("Authenticator GUID")}
|
||||
value={props.device.aaguid === undefined ? "N/A" : props.device.aaguid}
|
||||
/>
|
||||
<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
|
||||
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
|
||||
name={translate("Clone Warning")}
|
||||
|
|
|
@ -118,7 +118,7 @@ export default function WebauthnDeviceItem(props: Props) {
|
|||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||
<Box>
|
||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||
{props.device.description}
|
||||
{props.device.displayname}
|
||||
</Typography>
|
||||
<Typography
|
||||
display="inline"
|
||||
|
|
|
@ -6,9 +6,9 @@ import {
|
|||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
Stack,
|
||||
Step,
|
||||
StepLabel,
|
||||
Stepper,
|
||||
|
@ -23,15 +23,10 @@ import { useTranslation } from "react-i18next";
|
|||
import InformationIcon from "@components/InformationIcon";
|
||||
import WebauthnRegisterIcon from "@components/WebauthnRegisterIcon";
|
||||
import { useNotifications } from "@hooks/NotificationsContext";
|
||||
import {
|
||||
AttestationResult,
|
||||
AttestationResultFailureString,
|
||||
RegistrationResult,
|
||||
WebauthnTouchState,
|
||||
} from "@models/Webauthn";
|
||||
import { AttestationResult, AttestationResultFailureString, WebauthnTouchState } from "@models/Webauthn";
|
||||
import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistration } from "@services/Webauthn";
|
||||
|
||||
const steps = ["Confirm device", "Choose name"];
|
||||
const steps = ["Display Name", "Verification"];
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
@ -47,21 +42,20 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
|||
|
||||
const [state, setState] = useState(WebauthnTouchState.WaitTouch);
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [result, setResult] = useState<RegistrationResult | null>(null);
|
||||
const [options, setOptions] = useState<PublicKeyCredentialCreationOptionsJSON | 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 [nameError, setNameError] = useState(false);
|
||||
|
||||
const resetStates = () => {
|
||||
setState(WebauthnTouchState.WaitTouch);
|
||||
setActiveStep(0);
|
||||
setResult(null);
|
||||
setOptions(null);
|
||||
setActiveStep(0);
|
||||
setTimeout(null);
|
||||
setName("");
|
||||
setCredentialDisplayName("");
|
||||
setErrorDisplayName(false);
|
||||
};
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
|
@ -70,53 +64,41 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
|||
props.setCancelled();
|
||||
}, [props]);
|
||||
|
||||
const finishAttestation = 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 () => {
|
||||
const performCredentialCreation = useCallback(async () => {
|
||||
if (options === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(options.timeout ? options.timeout : null);
|
||||
setActiveStep(1);
|
||||
|
||||
try {
|
||||
setState(WebauthnTouchState.WaitTouch);
|
||||
setActiveStep(0);
|
||||
|
||||
const res = await startWebauthnRegistration(options);
|
||||
const resultCredentialCreation = await startWebauthnRegistration(options);
|
||||
|
||||
setTimeout(null);
|
||||
|
||||
if (res.result === AttestationResult.Success) {
|
||||
if (res.response == null) {
|
||||
throw new Error("Attestation request succeeded but credential is empty");
|
||||
if (resultCredentialCreation.result === AttestationResult.Success) {
|
||||
if (resultCredentialCreation.response == null) {
|
||||
throw new Error("Credential Creation Request succeeded but Registration Response is empty.");
|
||||
}
|
||||
|
||||
setResult(res);
|
||||
setActiveStep(1);
|
||||
const response = await finishRegistration(resultCredentialCreation.response);
|
||||
|
||||
switch (response.status) {
|
||||
case AttestationResult.Success:
|
||||
handleClose();
|
||||
break;
|
||||
case AttestationResult.Failure:
|
||||
createErrorNotification(response.message);
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
createErrorNotification(AttestationResultFailureString(res.result));
|
||||
createErrorNotification(AttestationResultFailureString(resultCredentialCreation.result));
|
||||
setState(WebauthnTouchState.Failure);
|
||||
} catch (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.",
|
||||
);
|
||||
}
|
||||
}, [options, createErrorNotification]);
|
||||
}, [options, createErrorNotification, handleClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state !== WebauthnTouchState.Failure || activeStep !== 0 || !props.open) {
|
||||
|
@ -135,35 +117,100 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
|||
}, [props, state, activeStep, handleClose]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (options === null || !props.open || activeStep !== 0) {
|
||||
(async function () {
|
||||
if (!props.open || activeStep !== 0 || options === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await startRegistration();
|
||||
await performCredentialCreation();
|
||||
})();
|
||||
}, [options, props.open, activeStep, startRegistration]);
|
||||
}, [props.open, activeStep, options, performCredentialCreation]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!props.open || activeStep !== 0) {
|
||||
return;
|
||||
}
|
||||
const handleNext = useCallback(async () => {
|
||||
if (credentialDisplayName.length === 0 || credentialDisplayName.length > 64) {
|
||||
setErrorDisplayName(true);
|
||||
createErrorNotification(
|
||||
translate("The Display Name must be more than 1 character and less than 64 characters."),
|
||||
);
|
||||
|
||||
const res = await getAttestationCreationOptions();
|
||||
if (res.status !== 200 || !res.options) {
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
"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);
|
||||
})();
|
||||
}, [setOptions, createErrorNotification, props.open, activeStep]);
|
||||
},
|
||||
[errorDisplayName],
|
||||
);
|
||||
|
||||
function renderStep(step: number) {
|
||||
switch (step) {
|
||||
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 (
|
||||
<Fragment>
|
||||
<Box className={styles.icon}>
|
||||
|
@ -174,52 +221,6 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
|||
</Typography>
|
||||
</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 (
|
||||
<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>
|
||||
<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 item xs={12}>
|
||||
<Stepper activeStep={activeStep}>
|
||||
|
@ -257,9 +263,24 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
|||
</Grid>
|
||||
</DialogContent>
|
||||
<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")}
|
||||
</Button>
|
||||
{activeStep === 0 ? (
|
||||
<Button
|
||||
color={credentialDisplayName.length !== 0 ? "success" : "primary"}
|
||||
disabled={activeStep !== 0}
|
||||
onClick={async () => {
|
||||
await handleNext();
|
||||
}}
|
||||
>
|
||||
{translate("Next")}
|
||||
</Button>
|
||||
) : null}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue