2020-12-02 17:00:04 +01:00
|
|
|
package webauthn
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2022-04-25 10:01:17 +02:00
|
|
|
"context"
|
2020-12-02 17:00:04 +01:00
|
|
|
"encoding/json"
|
2023-09-15 15:43:38 +02:00
|
|
|
"errors"
|
2020-12-02 17:00:04 +01:00
|
|
|
|
2023-05-24 13:22:00 +03:00
|
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
2022-04-27 01:01:45 +02:00
|
|
|
"github.com/zitadel/logging"
|
2020-12-02 17:00:04 +01:00
|
|
|
|
2022-04-27 01:01:45 +02:00
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/http"
|
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
2023-12-08 16:30:55 +02:00
|
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
2020-12-02 17:00:04 +01:00
|
|
|
)
|
|
|
|
|
2022-02-14 17:22:30 +01:00
|
|
|
type Config struct {
|
2022-04-25 10:01:17 +02:00
|
|
|
DisplayName string
|
|
|
|
ExternalSecure bool
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type webUser struct {
|
2021-01-15 09:32:59 +01:00
|
|
|
*domain.Human
|
2020-12-17 16:22:27 +01:00
|
|
|
accountName string
|
2020-12-02 17:00:04 +01:00
|
|
|
credentials []webauthn.Credential
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *webUser) WebAuthnID() []byte {
|
|
|
|
return []byte(u.AggregateID)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *webUser) WebAuthnName() string {
|
2020-12-17 16:22:27 +01:00
|
|
|
if u.accountName != "" {
|
|
|
|
return u.accountName
|
|
|
|
}
|
2021-01-15 09:32:59 +01:00
|
|
|
return u.GetUsername()
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (u *webUser) WebAuthnDisplayName() string {
|
2022-04-25 10:01:17 +02:00
|
|
|
if u.DisplayName != "" {
|
|
|
|
return u.DisplayName
|
|
|
|
}
|
|
|
|
return u.GetUsername()
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (u *webUser) WebAuthnIcon() string {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *webUser) WebAuthnCredentials() []webauthn.Credential {
|
|
|
|
return u.credentials
|
|
|
|
}
|
|
|
|
|
2023-06-27 14:36:07 +02:00
|
|
|
func (w *Config) BeginRegistration(ctx context.Context, user *domain.Human, accountName string, authType domain.AuthenticatorAttachment, userVerification domain.UserVerificationRequirement, rpID string, webAuthNs ...*domain.WebAuthNToken) (*domain.WebAuthNToken, error) {
|
|
|
|
webAuthNServer, err := w.serverFromContext(ctx, rpID, "")
|
2022-04-25 10:01:17 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-06-27 14:36:07 +02:00
|
|
|
creds := WebAuthNsToCredentials(webAuthNs, rpID)
|
2020-12-02 17:00:04 +01:00
|
|
|
existing := make([]protocol.CredentialDescriptor, len(creds))
|
|
|
|
for i, cred := range creds {
|
|
|
|
existing[i] = protocol.CredentialDescriptor{
|
|
|
|
Type: protocol.PublicKeyCredentialType,
|
|
|
|
CredentialID: cred.ID,
|
|
|
|
}
|
|
|
|
}
|
2022-04-25 10:01:17 +02:00
|
|
|
credentialOptions, sessionData, err := webAuthNServer.BeginRegistration(
|
2020-12-02 17:00:04 +01:00
|
|
|
&webUser{
|
2021-01-15 09:32:59 +01:00
|
|
|
Human: user,
|
2020-12-17 16:22:27 +01:00
|
|
|
accountName: accountName,
|
2020-12-02 17:00:04 +01:00
|
|
|
credentials: creds,
|
|
|
|
},
|
|
|
|
webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
|
2021-01-15 09:32:59 +01:00
|
|
|
UserVerification: UserVerificationFromDomain(userVerification),
|
|
|
|
AuthenticatorAttachment: AuthenticatorAttachmentFromDomain(authType),
|
2020-12-02 17:00:04 +01:00
|
|
|
}),
|
|
|
|
webauthn.WithConveyancePreference(protocol.PreferNoAttestation),
|
|
|
|
webauthn.WithExclusions(existing),
|
|
|
|
)
|
|
|
|
if err != nil {
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-bM8sd", "Errors.User.WebAuthN.BeginRegisterFailed")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
cred, err := json.Marshal(credentialOptions)
|
|
|
|
if err != nil {
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-D7cus", "Errors.User.WebAuthN.MarshalError")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
2021-01-15 09:32:59 +01:00
|
|
|
return &domain.WebAuthNToken{
|
2020-12-02 17:00:04 +01:00
|
|
|
Challenge: sessionData.Challenge,
|
|
|
|
CredentialCreationData: cred,
|
|
|
|
AllowedCredentialIDs: sessionData.AllowedCredentialIDs,
|
2021-01-15 09:32:59 +01:00
|
|
|
UserVerification: UserVerificationToDomain(sessionData.UserVerification),
|
2023-06-27 14:36:07 +02:00
|
|
|
RPID: webAuthNServer.Config.RPID,
|
2020-12-02 17:00:04 +01:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2024-05-07 07:38:26 +02:00
|
|
|
func (w *Config) FinishRegistration(ctx context.Context, user *domain.Human, webAuthN *domain.WebAuthNToken, tokenName string, credData []byte) (*domain.WebAuthNToken, error) {
|
2020-12-02 17:00:04 +01:00
|
|
|
if webAuthN == nil {
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(nil, "WEBAU-5M9so", "Errors.User.WebAuthN.NotFound")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
credentialData, err := protocol.ParseCredentialCreationResponseBody(bytes.NewReader(credData))
|
|
|
|
if err != nil {
|
2023-09-15 15:43:38 +02:00
|
|
|
logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn credential could not be parsed")
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-sEr8c", "Errors.User.WebAuthN.ErrorOnParseCredential")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
sessionData := WebAuthNToSessionData(webAuthN)
|
2023-06-27 14:36:07 +02:00
|
|
|
webAuthNServer, err := w.serverFromContext(ctx, webAuthN.RPID, credentialData.Response.CollectedClientData.Origin)
|
2022-04-25 10:01:17 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
credential, err := webAuthNServer.CreateCredential(
|
2020-12-02 17:00:04 +01:00
|
|
|
&webUser{
|
2021-01-15 09:32:59 +01:00
|
|
|
Human: user,
|
2020-12-02 17:00:04 +01:00
|
|
|
},
|
2021-02-08 11:30:30 +01:00
|
|
|
sessionData,
|
|
|
|
credentialData)
|
2020-12-02 17:00:04 +01:00
|
|
|
if err != nil {
|
2023-09-15 15:43:38 +02:00
|
|
|
logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn credential could not be created")
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-3Vb9s", "Errors.User.WebAuthN.CreateCredentialFailed")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2023-06-27 14:36:07 +02:00
|
|
|
webAuthN.RPID = webAuthNServer.Config.RPID
|
2020-12-02 17:00:04 +01:00
|
|
|
return webAuthN, nil
|
|
|
|
}
|
|
|
|
|
2023-06-27 14:36:07 +02:00
|
|
|
func (w *Config) BeginLogin(ctx context.Context, user *domain.Human, userVerification domain.UserVerificationRequirement, rpID string, webAuthNs ...*domain.WebAuthNToken) (*domain.WebAuthNLogin, error) {
|
|
|
|
webAuthNServer, err := w.serverFromContext(ctx, rpID, "")
|
2022-04-25 10:01:17 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
assertion, sessionData, err := webAuthNServer.BeginLogin(&webUser{
|
2021-01-15 09:32:59 +01:00
|
|
|
Human: user,
|
2023-06-27 14:36:07 +02:00
|
|
|
credentials: WebAuthNsToCredentials(webAuthNs, rpID),
|
2021-01-15 09:32:59 +01:00
|
|
|
}, webauthn.WithUserVerification(UserVerificationFromDomain(userVerification)))
|
2020-12-02 17:00:04 +01:00
|
|
|
if err != nil {
|
2023-09-15 15:43:38 +02:00
|
|
|
logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn login could not be started")
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-4G8sw", "Errors.User.WebAuthN.BeginLoginFailed")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
cred, err := json.Marshal(assertion)
|
|
|
|
if err != nil {
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-2M0s9", "Errors.User.WebAuthN.MarshalError")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
2021-01-15 09:32:59 +01:00
|
|
|
return &domain.WebAuthNLogin{
|
2020-12-02 17:00:04 +01:00
|
|
|
Challenge: sessionData.Challenge,
|
|
|
|
CredentialAssertionData: cred,
|
|
|
|
AllowedCredentialIDs: sessionData.AllowedCredentialIDs,
|
|
|
|
UserVerification: userVerification,
|
2023-06-27 14:36:07 +02:00
|
|
|
RPID: webAuthNServer.Config.RPID,
|
2020-12-02 17:00:04 +01:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-08-11 18:36:18 +03:00
|
|
|
func (w *Config) FinishLogin(ctx context.Context, user *domain.Human, webAuthN *domain.WebAuthNLogin, credData []byte, webAuthNs ...*domain.WebAuthNToken) (*webauthn.Credential, error) {
|
2020-12-02 17:00:04 +01:00
|
|
|
assertionData, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(credData))
|
|
|
|
if err != nil {
|
2023-09-15 15:43:38 +02:00
|
|
|
logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn assertion could not be parsed")
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-ADgv4", "Errors.User.WebAuthN.ValidateLoginFailed")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
webUser := &webUser{
|
2021-01-15 09:32:59 +01:00
|
|
|
Human: user,
|
2023-06-27 14:36:07 +02:00
|
|
|
credentials: WebAuthNsToCredentials(webAuthNs, webAuthN.RPID),
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
2023-06-27 14:36:07 +02:00
|
|
|
webAuthNServer, err := w.serverFromContext(ctx, webAuthN.RPID, assertionData.Response.CollectedClientData.Origin)
|
2022-04-25 10:01:17 +02:00
|
|
|
if err != nil {
|
2023-08-11 18:36:18 +03:00
|
|
|
return nil, err
|
2022-04-25 10:01:17 +02:00
|
|
|
}
|
|
|
|
credential, err := webAuthNServer.ValidateLogin(webUser, WebAuthNLoginToSessionData(webAuthN), assertionData)
|
2020-12-02 17:00:04 +01:00
|
|
|
if err != nil {
|
2023-09-15 15:43:38 +02:00
|
|
|
logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn assertion failed")
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-3M9si", "Errors.User.WebAuthN.ValidateLoginFailed")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if credential.Authenticator.CloneWarning {
|
2023-12-08 16:30:55 +02:00
|
|
|
return credential, zerrors.ThrowInternal(nil, "WEBAU-4M90s", "Errors.User.WebAuthN.CloneWarning")
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
2023-08-11 18:36:18 +03:00
|
|
|
return credential, nil
|
2020-12-02 17:00:04 +01:00
|
|
|
}
|
2022-04-25 10:01:17 +02:00
|
|
|
|
2023-06-27 14:36:07 +02:00
|
|
|
func (w *Config) serverFromContext(ctx context.Context, id, origin string) (*webauthn.WebAuthn, error) {
|
|
|
|
config := w.config(id, origin)
|
|
|
|
if id == "" {
|
|
|
|
config = w.configFromContext(ctx)
|
|
|
|
}
|
|
|
|
webAuthn, err := webauthn.New(config)
|
|
|
|
if err != nil {
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "WEBAU-UX9ta", "Errors.User.WebAuthN.ServerConfig")
|
2023-06-27 14:36:07 +02:00
|
|
|
}
|
|
|
|
return webAuthn, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Config) configFromContext(ctx context.Context) *webauthn.Config {
|
2022-09-01 09:10:07 +02:00
|
|
|
instance := authz.GetInstance(ctx)
|
2023-06-27 14:36:07 +02:00
|
|
|
return &webauthn.Config{
|
2022-04-25 10:01:17 +02:00
|
|
|
RPDisplayName: w.DisplayName,
|
2022-09-01 09:10:07 +02:00
|
|
|
RPID: instance.RequestedDomain(),
|
2023-05-24 13:22:00 +03:00
|
|
|
RPOrigins: []string{http.BuildOrigin(instance.RequestedHost(), w.ExternalSecure)},
|
|
|
|
}
|
2023-06-27 14:36:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Config) config(id, origin string) *webauthn.Config {
|
|
|
|
return &webauthn.Config{
|
|
|
|
RPDisplayName: w.DisplayName,
|
|
|
|
RPID: id,
|
|
|
|
RPOrigins: []string{origin},
|
|
|
|
}
|
2022-04-25 10:01:17 +02:00
|
|
|
}
|
2023-09-15 15:43:38 +02:00
|
|
|
|
|
|
|
func tryExtractProtocolErrMsg(err error) string {
|
|
|
|
var e *protocol.Error
|
|
|
|
if errors.As(err, &e) {
|
|
|
|
return e.Details + ": " + e.DevInfo
|
|
|
|
}
|
|
|
|
return e.Error()
|
|
|
|
}
|