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)
|
||
|
}
|
||
|
}
|