mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
8f05846e21
This implements Webauthn. Old devices can be used to authenticate via the appid compatibility layer which should be automatic. New devices will be registered via Webauthn, and devices which do not support FIDO2 will no longer be able to be registered. At this time it does not fully support multiple devices (backend does, frontend doesn't allow registration of additional devices). Does not support passwordless.
205 lines
6.1 KiB
Go
205 lines
6.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
|
|
"github.com/duo-labs/webauthn/protocol"
|
|
"github.com/duo-labs/webauthn/webauthn"
|
|
|
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
|
"github.com/authelia/authelia/v4/internal/models"
|
|
"github.com/authelia/authelia/v4/internal/regulation"
|
|
)
|
|
|
|
// SecondFactorWebauthnAssertionGET handler starts the assertion ceremony.
|
|
func SecondFactorWebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
|
var (
|
|
w *webauthn.WebAuthn
|
|
user *models.WebauthnUser
|
|
err error
|
|
)
|
|
|
|
userSession := ctx.GetSession()
|
|
|
|
if w, err = newWebauthn(ctx); err != nil {
|
|
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
if user, err = getWebAuthnUser(ctx, userSession); err != nil {
|
|
ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
var opts = []webauthn.LoginOption{
|
|
webauthn.WithAllowedCredentials(user.WebAuthnCredentialDescriptors()),
|
|
}
|
|
|
|
extensions := make(map[string]interface{})
|
|
|
|
if user.HasFIDOU2F() {
|
|
extensions["appid"] = w.Config.RPOrigin
|
|
}
|
|
|
|
if len(extensions) != 0 {
|
|
opts = append(opts, webauthn.WithAssertionExtensions(extensions))
|
|
}
|
|
|
|
var assertion *protocol.CredentialAssertion
|
|
|
|
if assertion, userSession.Webauthn, err = w.BeginLogin(user, opts...); err != nil {
|
|
ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
if err = ctx.SaveSession(userSession); err != nil {
|
|
ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
if err = ctx.SetJSONBody(assertion); err != nil {
|
|
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// SecondFactorWebauthnAssertionPOST handler completes the assertion ceremony after verifying the challenge.
|
|
func SecondFactorWebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
|
var (
|
|
err error
|
|
w *webauthn.WebAuthn
|
|
|
|
requestBody signWebauthnRequestBody
|
|
)
|
|
|
|
if err = ctx.ParseBody(&requestBody); err != nil {
|
|
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
userSession := ctx.GetSession()
|
|
|
|
if userSession.Webauthn == nil {
|
|
ctx.Logger.Errorf("Webauthn session data is not present in order to handle assertion 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)
|
|
|
|
return
|
|
}
|
|
|
|
if w, err = newWebauthn(ctx); err != nil {
|
|
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
var (
|
|
assertionResponse *protocol.ParsedCredentialAssertionData
|
|
credential *webauthn.Credential
|
|
user *models.WebauthnUser
|
|
)
|
|
|
|
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
|
|
ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
if user, err = getWebAuthnUser(ctx, userSession); 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
|
|
}
|
|
|
|
if credential, err = w.ValidateLogin(user, *userSession.Webauthn, assertionResponse); err != nil {
|
|
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebauthn, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
var found bool
|
|
|
|
for _, device := range user.Devices {
|
|
if bytes.Equal(device.KID.Bytes(), credential.ID) {
|
|
device.UpdateSignInInfo(w.Config, ctx.Clock.Now(), credential.Authenticator.SignCount)
|
|
|
|
found = true
|
|
|
|
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceSignIn(ctx, device.ID, device.RPID, device.LastUsedAt, device.SignCount, device.CloneWarning); err != nil {
|
|
ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s' device '%x' count '%d': unable to find device", regulation.AuthTypeWebauthn, userSession.Username, credential.ID, credential.Authenticator.SignCount)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil {
|
|
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeWebauthn, nil); err != nil {
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
userSession.SetTwoFactor(ctx.Clock.Now())
|
|
userSession.Webauthn = nil
|
|
|
|
if err = ctx.SaveSession(userSession); err != nil {
|
|
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the assertion challenge and authentication time", regulation.AuthTypeWebauthn, userSession.Username, err)
|
|
|
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
|
|
return
|
|
}
|
|
|
|
if userSession.OIDCWorkflowSession != nil {
|
|
handleOIDCWorkflowResponse(ctx)
|
|
} else {
|
|
Handle2FAResponse(ctx, requestBody.TargetURL)
|
|
}
|
|
}
|