zitadel/internal/webauthn/webauthn.go
Livio Amstutz 300ade66a7
feat: add WebAuthN support for passwordless login and 2fa (#966)
* at least registration prompt works

* in memory test for login

* buttons to start webauthn process

* begin eventstore impl

* begin eventstore impl

* serialize into bytes

* fix: u2f, passwordless types

* fix for localhost

* fix script

* fix: u2f, passwordless types

* fix: add u2f

* fix: verify u2f

* fix: session data in event store

* fix: u2f credentials in eventstore

* fix: webauthn pkg handles business models

* feat: tests

* feat: append events

* fix: test

* fix: check only ready webauthn creds

* fix: move u2f methods to authrepo

* frontend improvements

* fix return

* feat: add passwordless

* feat: add passwordless

* improve ui / error handling

* separate call for login

* fix login

* js

* feat: u2f login methods

* feat: remove unused session id

* feat: error handling

* feat: error handling

* feat: refactor user eventstore

* feat: finish webauthn

* feat: u2f and passwordlss in auth.proto

* u2f step

* passwordless step

* cleanup js

* EndpointPasswordLessLogin

* migration

* update mfaChecked test

* next step test

* token name

* cleanup

* attribute

* passwordless as tokens

* remove sms as otp type

* add "user" to amr for webauthn

* error handling

* fixes

* fix tests

* naming

* naming

* fixes

* session handler

* i18n

* error handling in login

* Update internal/ui/login/static/i18n/de.yaml

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* Update internal/ui/login/static/i18n/en.yaml

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* improvements

* merge fixes

* fixes

* fixes

Co-authored-by: Fabiennne <fabienne.gerschwiler@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
2020-12-02 17:00:04 +01:00

159 lines
5.3 KiB
Go

package webauthn
import (
"bytes"
"encoding/json"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
caos_errs "github.com/caos/zitadel/internal/errors"
usr_model "github.com/caos/zitadel/internal/user/model"
)
type WebAuthN struct {
web *webauthn.WebAuthn
}
func StartServer(displayName, id, origin string) (*WebAuthN, error) {
web, err := webauthn.New(&webauthn.Config{
RPDisplayName: displayName,
RPID: id,
RPOrigin: origin,
})
if err != nil {
return nil, err
}
return &WebAuthN{
web: web,
}, err
}
type webUser struct {
*usr_model.User
credentials []webauthn.Credential
}
func (u *webUser) WebAuthnID() []byte {
return []byte(u.AggregateID)
}
func (u *webUser) WebAuthnName() string {
return u.UserName
}
func (u *webUser) WebAuthnDisplayName() string {
return u.DisplayName
}
func (u *webUser) WebAuthnIcon() string {
return ""
}
func (u *webUser) WebAuthnCredentials() []webauthn.Credential {
return u.credentials
}
func (w *WebAuthN) BeginRegistration(user *usr_model.User, authType usr_model.AuthenticatorAttachment, userVerification usr_model.UserVerificationRequirement, webAuthNs ...*usr_model.WebAuthNToken) (*usr_model.WebAuthNToken, error) {
creds := WebAuthNsToCredentials(webAuthNs)
existing := make([]protocol.CredentialDescriptor, len(creds))
for i, cred := range creds {
existing[i] = protocol.CredentialDescriptor{
Type: protocol.PublicKeyCredentialType,
CredentialID: cred.ID,
}
}
credentialOptions, sessionData, err := w.web.BeginRegistration(
&webUser{
User: user,
credentials: creds,
},
webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
UserVerification: UserVerificationFromModel(userVerification),
AuthenticatorAttachment: AuthenticatorAttachmentFromModel(authType),
}),
webauthn.WithConveyancePreference(protocol.PreferNoAttestation),
webauthn.WithExclusions(existing),
)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "WEBAU-bM8sd", "Errors.User.WebAuthN.BeginRegisterFailed")
}
cred, err := json.Marshal(credentialOptions)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "WEBAU-D7cus", "Errors.User.WebAuthN.MarshalError")
}
return &usr_model.WebAuthNToken{
Challenge: sessionData.Challenge,
CredentialCreationData: cred,
AllowedCredentialIDs: sessionData.AllowedCredentialIDs,
UserVerification: UserVerificationToModel(sessionData.UserVerification),
}, nil
}
func (w *WebAuthN) FinishRegistration(user *usr_model.User, webAuthN *usr_model.WebAuthNToken, tokenName string, credData []byte) (*usr_model.WebAuthNToken, error) {
if webAuthN == nil {
return nil, caos_errs.ThrowInternal(nil, "WEBAU-5M9so", "Errors.User.WebAuthN.NotFound")
}
credentialData, err := protocol.ParseCredentialCreationResponseBody(bytes.NewReader(credData))
if err != nil {
return nil, caos_errs.ThrowInternal(err, "WEBAU-sEr8c", "Errors.User.WebAuthN.ErrorOnParseCredential")
}
sessionData := WebAuthNToSessionData(webAuthN)
credential, err := w.web.CreateCredential(
&webUser{
User: user,
},
sessionData, credentialData)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "WEBAU-3Vb9s", "Errors.User.WebAuthN.CreateCredentialFailed")
}
webAuthN.KeyID = credential.ID
webAuthN.PublicKey = credential.PublicKey
webAuthN.AttestationType = credential.AttestationType
webAuthN.AAGUID = credential.Authenticator.AAGUID
webAuthN.SignCount = credential.Authenticator.SignCount
webAuthN.WebAuthNTokenName = tokenName
return webAuthN, nil
}
func (w *WebAuthN) BeginLogin(user *usr_model.User, userVerification usr_model.UserVerificationRequirement, webAuthNs ...*usr_model.WebAuthNToken) (*usr_model.WebAuthNLogin, error) {
assertion, sessionData, err := w.web.BeginLogin(&webUser{
User: user,
credentials: WebAuthNsToCredentials(webAuthNs),
}, webauthn.WithUserVerification(UserVerificationFromModel(userVerification)))
if err != nil {
return nil, caos_errs.ThrowInternal(err, "WEBAU-4G8sw", "Errors.User.WebAuthN.BeginLoginFailed")
}
cred, err := json.Marshal(assertion)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "WEBAU-2M0s9", "Errors.User.WebAuthN.MarshalError")
}
return &usr_model.WebAuthNLogin{
Challenge: sessionData.Challenge,
CredentialAssertionData: cred,
AllowedCredentialIDs: sessionData.AllowedCredentialIDs,
UserVerification: userVerification,
}, nil
}
func (w *WebAuthN) FinishLogin(user *usr_model.User, webAuthN *usr_model.WebAuthNLogin, credData []byte, webAuthNs ...*usr_model.WebAuthNToken) ([]byte, uint32, error) {
assertionData, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(credData))
if err != nil {
return nil, 0, caos_errs.ThrowInternal(err, "WEBAU-ADgv4", "Errors.User.WebAuthN.ValidateLoginFailed")
}
webUser := &webUser{
User: user,
credentials: WebAuthNsToCredentials(webAuthNs),
}
credential, err := w.web.ValidateLogin(webUser, WebAuthNLoginToSessionData(webAuthN), assertionData)
if err != nil {
return nil, 0, caos_errs.ThrowInternal(err, "WEBAU-3M9si", "Errors.User.WebAuthN.ValidateLoginFailed")
}
if credential.Authenticator.CloneWarning {
return credential.ID, credential.Authenticator.SignCount, caos_errs.ThrowInternal(err, "WEBAU-4M90s", "Errors.User.WebAuthN.CloneWarning")
}
return credential.ID, credential.Authenticator.SignCount, nil
}