diff options
| author | Matthieu Pignolet <m@mpgn.dev> | 2025-03-09 16:51:05 +0400 | 
|---|---|---|
| committer | Matthieu Pignolet <m@mpgn.dev> | 2025-03-09 16:51:05 +0400 | 
| commit | ae29f8e6ba7d04088c8395bc529b0dfac1297e7a (patch) | |
| tree | 264af31102a7a6f42e3e44c6dbd342047da3fbfb /internal/handlers/handler_firstfactor_spnego.go | |
| parent | 0114724498a5c3b0f181027ac0efd9354c72d9d3 (diff) | |
2/?: implementation of firstfactor spnego
Diffstat (limited to 'internal/handlers/handler_firstfactor_spnego.go')
| -rw-r--r-- | internal/handlers/handler_firstfactor_spnego.go | 175 | 
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) +		} +  	}  }  | 
