summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthieu Pignolet <m@mpgn.dev>2025-03-09 16:51:05 +0400
committerMatthieu Pignolet <m@mpgn.dev>2025-03-09 16:51:05 +0400
commitae29f8e6ba7d04088c8395bc529b0dfac1297e7a (patch)
tree264af31102a7a6f42e3e44c6dbd342047da3fbfb
parent0114724498a5c3b0f181027ac0efd9354c72d9d3 (diff)
2/?: implementation of firstfactor spnego
-rw-r--r--internal/handlers/handler_firstfactor_spnego.go175
1 files changed, 145 insertions, 30 deletions
diff --git a/internal/handlers/handler_firstfactor_spnego.go b/internal/handlers/handler_firstfactor_spnego.go
index 954665b08..3193eec9f 100644
--- a/internal/handlers/handler_firstfactor_spnego.go
+++ b/internal/handlers/handler_firstfactor_spnego.go
@@ -2,17 +2,18 @@ package handlers
import (
"encoding/base64"
+ "errors"
"net/http"
"strings"
+ "github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/middlewares"
+ "github.com/authelia/authelia/v4/internal/regulation"
+ "github.com/authelia/authelia/v4/internal/session"
"github.com/valyala/fasthttp"
"gopkg.in/jcmturner/goidentity.v3"
"gopkg.in/jcmturner/gokrb5.v7/gssapi"
- "gopkg.in/jcmturner/gokrb5.v7/keytab"
- "gopkg.in/jcmturner/gokrb5.v7/service"
"gopkg.in/jcmturner/gokrb5.v7/spnego"
- "gopkg.in/jcmturner/gokrb5.v7/types"
)
const (
@@ -25,64 +26,178 @@ const (
)
// spnego.SPNEGOKRB5Authenticate is a Kerberos spnego.SPNEGO authentication HTTP handler wrapper.
-func FirstFactorSPNEGO(inner fasthttp.RequestHandler, kt *keytab.Keytab, settings ...func(*service.Settings)) middlewares.RequestHandler {
+func FirstFactorSPNEGO(inner fasthttp.RequestHandler) middlewares.RequestHandler {
return func(ctx *middlewares.AutheliaCtx) {
- // Get the auth header
- s := strings.SplitN(string(ctx.Request.Header.Peek(spnego.HTTPHeaderAuthRequest)), " ", 2)
- if len(s) != 2 || s[0] != spnego.HTTPHeaderAuthResponseValueKey {
+ var (
+ details *authentication.UserDetails
+ err error
+ )
+
+ // getting the spnego headers
+ headerParts := strings.SplitN(string(ctx.Request.Header.Peek(spnego.HTTPHeaderAuthRequest)), " ", 2)
+ if len(headerParts) != 2 || headerParts[0] != spnego.HTTPHeaderAuthResponseValueKey {
// No Authorization header set so return 401 with WWW-Authenticate Negotiate header
ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnego.HTTPHeaderAuthResponseValueKey)
- ctx.Response.SetStatusCode(http.StatusUnauthorized)
- ctx.Response.SetBodyString(spnego.UnauthorizedMsg)
+ ctx.SetStatusCode(fasthttp.StatusBadRequest)
+ ctx.SetJSONError(messageMFAValidationFailed)
+
+ ctx.Logger.WithError(err).Errorf(logFmtErrPasskeyAuthenticationChallengeValidate, errStrReqBodyParse)
+
+ doMarkAuthenticationAttempt(ctx, false, regulation.NewBan(regulation.BanTypeNone, "", nil), regulation.AuthTypePasskey, nil)
+
return
}
- // Set up the spnego.SPNEGO GSS-API mechanism
- var SPNEGO *spnego.SPNEGO
- h, err := types.GetHostAddress(ctx.RemoteAddr().String())
- if err == nil {
- // put in this order so that if the user provides a ClientAddress it will override the one here.
- o := append([]func(*service.Settings){service.ClientAddress(h)}, settings...)
- SPNEGO = spnego.SPNEGOService(kt, o...)
- } else {
- SPNEGO = spnego.SPNEGOService(kt, settings...)
- SPNEGO.Log("%s - spnego.SPNEGO could not parse client address: %v", ctx.RemoteAddr(), err)
+ bodyJSON := bodyFirstFactorSPNEGOequest{}
+ if err = ctx.ParseBody(&bodyJSON); err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrParseRequestBody, regulation.AuthType1FA)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
}
- // Decode the header into an spnego.SPNEGO context token
- b, err := base64.StdEncoding.DecodeString(s[1])
+ // we then decode the base64-encoded token
+ b, err := base64.StdEncoding.DecodeString(headerParts[1])
if err != nil {
- SPNEGONegotiateKRB5MechType(SPNEGO, ctx, "%s - spnego.SPNEGO error in base64 decoding negotiation header: %v", ctx.RemoteAddr(), err)
+ ctx.SetStatusCode(fasthttp.StatusBadRequest)
+ ctx.Logger.WithError(err).Errorf(logFmtErrPasskeyAuthenticationChallengeValidate, errStrReqBodyParse)
+
+ doMarkAuthenticationAttempt(ctx, false, regulation.NewBan(regulation.BanTypeNone, "", nil), regulation.AuthTypePasskey, nil)
+
return
}
+ // and unmarshal it using the spnego library
var st spnego.SPNEGOToken
err = st.Unmarshal(b)
if err != nil {
- SPNEGONegotiateKRB5MechType(SPNEGO, ctx, "%s - spnego.SPNEGO error in unmarshaling spnego.SPNEGO token: %v", ctx.RemoteAddr(), err)
+ ctx.SetStatusCode(fasthttp.StatusBadRequest)
+ ctx.SetJSONError(messageAuthenticationFailed)
+ ctx.Logger.WithError(err).Errorf(logFmtErrPasskeyAuthenticationChallengeValidate, errStrReqBodyParse)
+
+ doMarkAuthenticationAttempt(ctx, false, regulation.NewBan(regulation.BanTypeNone, "", nil), regulation.AuthTypePasskey, nil)
+
+ return
+ }
+
+ SPNEGO, err := ctx.GetSPNEGOProvider()
+ if err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrPasskeyAuthenticationChallengeGenerate, "error occurred provisioning the configuration")
+
+ ctx.SetStatusCode(fasthttp.StatusForbidden)
+ ctx.SetJSONError(messageMFAValidationFailed)
+ doMarkAuthenticationAttempt(ctx, false, regulation.NewBan(regulation.BanTypeNone, "", nil), regulation.AuthTypePasskey, nil)
+
return
}
// Validate the context token
authed, context, status := SPNEGO.AcceptSecContext(&st)
if status.Code != gssapi.StatusComplete && status.Code != gssapi.StatusContinueNeeded {
- SPNEGOResponseReject(SPNEGO, ctx, "%s - spnego.SPNEGO validation error: %v", ctx.RemoteAddr(), status)
+ ctx.SetStatusCode(fasthttp.StatusForbidden)
+ ctx.SetJSONError(messageMFAValidationFailed)
return
}
if status.Code == gssapi.StatusContinueNeeded {
- SPNEGONegotiateKRB5MechType(SPNEGO, ctx, "%s - spnego.SPNEGO GSS-API continue needed", ctx.RemoteAddr())
+ ctx.SetStatusCode(fasthttp.StatusForbidden)
+ ctx.SetJSONError(messageMFAValidationFailed)
return
}
- if authed {
- _ = context.Value(spnego.CTXKeyCredentials).(goidentity.Identity)
+ if !authed {
- ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnegoNegTokenRespKRBAcceptCompleted)
+ doMarkAuthenticationAttempt(ctx, false, regulation.NewBan(regulation.BanTypeNone, details.Username, nil), regulation.AuthType1FA, nil)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+ }
+
+ authContext := context.Value(spnego.CTXKeyCredentials).(goidentity.Identity)
+ ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnegoNegTokenRespKRBAcceptCompleted)
+
+ username := authContext.UserName()
+
+ if details, err = ctx.Providers.UserProvider.GetDetails(username); err != nil {
+ ctx.Logger.WithError(err).Errorf("Error occurred getting details for user with username input '%s' which usually indicates they do not exist", username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+ return
+
+ }
+
+ if ban, _, expires, err := ctx.Providers.Regulator.BanCheck(ctx, details.Username); err != nil {
+ if errors.Is(err, regulation.ErrUserIsBanned) {
+ doMarkAuthenticationAttempt(ctx, false, regulation.NewBan(ban, details.Username, expires), regulation.AuthType1FA, nil)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ ctx.Logger.WithError(err).Errorf(logFmtErrRegulationFail, regulation.AuthType1FA, details.Username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
- } else {
- SPNEGOResponseReject(SPNEGO, ctx, "%s - spnego.SPNEGO Kerberos authentication failed", ctx.RemoteAddr())
return
}
+
+ doMarkAuthenticationAttempt(ctx, true, regulation.NewBan(regulation.BanTypeNone, details.Username, nil), regulation.AuthType1FA, nil)
+
+ var provider *session.Session
+
+ if provider, err = ctx.GetSessionProvider(); err != nil {
+ ctx.Logger.WithError(err).Error("Failed to get session provider during 1FA attempt")
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ if err = provider.DestroySession(ctx.RequestCtx); err != nil {
+ // This failure is not likely to be critical as we ensure to regenerate the session below.
+ ctx.Logger.WithError(err).Trace("Failed to destroy session during 1FA attempt")
+ }
+
+ userSession := provider.NewDefaultUserSession()
+
+ // Reset all values from previous session except OIDC workflow before regenerating the cookie.
+ if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrSessionReset, regulation.AuthType1FA, details.Username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ if err = provider.RegenerateSession(ctx.RequestCtx); err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrSessionRegenerate, regulation.AuthType1FA, details.Username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ ctx.Logger.Tracef(logFmtTraceProfileDetails, details.Username, details.Groups, details.Emails)
+
+ userSession.SetOneFactorPassword(ctx.Clock.Now(), details, false)
+
+ if ctx.Configuration.AuthenticationBackend.RefreshInterval.Update() {
+ userSession.RefreshTTL = ctx.Clock.Now().Add(ctx.Configuration.AuthenticationBackend.RefreshInterval.Value())
+ }
+ if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrSessionSave, "updated profile", regulation.AuthType1FA, logFmtActionAuthentication, details.Username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ if bodyJSON.Workflow == workflowOpenIDConnect {
+ handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.WorkflowID)
+ } else {
+ Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
+ }
+
}
}