summaryrefslogtreecommitdiff
path: root/internal/handlers/handler_firstfactor_spnego.go
diff options
context:
space:
mode:
authorMatthieu Pignolet <m@mpgn.dev>2025-03-09 15:54:26 +0400
committerMatthieu Pignolet <m@mpgn.dev>2025-03-09 15:54:26 +0400
commiteb5d2162fa4815338eeb8d716bfb1302c3863d2b (patch)
tree36dcaad273d204a995017cd4082e2f37c074b1e6 /internal/handlers/handler_firstfactor_spnego.go
parent41798e3e30d04e3cb01878d935cc2881ac50d7e9 (diff)
1/?: beginning fo the spnego first-factor implementation
Diffstat (limited to 'internal/handlers/handler_firstfactor_spnego.go')
-rw-r--r--internal/handlers/handler_firstfactor_spnego.go101
1 files changed, 101 insertions, 0 deletions
diff --git a/internal/handlers/handler_firstfactor_spnego.go b/internal/handlers/handler_firstfactor_spnego.go
new file mode 100644
index 000000000..954665b08
--- /dev/null
+++ b/internal/handlers/handler_firstfactor_spnego.go
@@ -0,0 +1,101 @@
+package handlers
+
+import (
+ "encoding/base64"
+ "net/http"
+ "strings"
+
+ "github.com/authelia/authelia/v4/internal/middlewares"
+ "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 (
+ // spnegoNegTokenRespKRBAcceptCompleted - The response on successful authentication always has this header. Capturing as const so we don't have marshaling and encoding overhead.
+ spnegoNegTokenRespKRBAcceptCompleted = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
+ // spnegoNegTokenRespReject - The response on a failed authentication always has this rejection header. Capturing as const so we don't have marshaling and encoding overhead.
+ spnegoNegTokenRespReject = "Negotiate oQcwBaADCgEC"
+ // spnegoNegTokenRespIncompleteKRB5 - Response token specifying incomplete context and KRB5 as the supported mechtype.
+ spnegoNegTokenRespIncompleteKRB5 = "Negotiate oRQwEqADCgEBoQsGCSqGSIb3EgECAg=="
+)
+
+// 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 {
+ 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 {
+ // 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)
+ 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)
+ }
+
+ // Decode the header into an spnego.SPNEGO context token
+ b, err := base64.StdEncoding.DecodeString(s[1])
+ if err != nil {
+ SPNEGONegotiateKRB5MechType(SPNEGO, ctx, "%s - spnego.SPNEGO error in base64 decoding negotiation header: %v", ctx.RemoteAddr(), err)
+ return
+ }
+ 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)
+ 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)
+ return
+ }
+
+ if status.Code == gssapi.StatusContinueNeeded {
+ SPNEGONegotiateKRB5MechType(SPNEGO, ctx, "%s - spnego.SPNEGO GSS-API continue needed", ctx.RemoteAddr())
+ return
+ }
+
+ if authed {
+ _ = context.Value(spnego.CTXKeyCredentials).(goidentity.Identity)
+
+ ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnegoNegTokenRespKRBAcceptCompleted)
+
+ } else {
+ SPNEGOResponseReject(SPNEGO, ctx, "%s - spnego.SPNEGO Kerberos authentication failed", ctx.RemoteAddr())
+ return
+ }
+ }
+}
+
+func SPNEGONegotiateKRB5MechType(s *spnego.SPNEGO, ctx *middlewares.AutheliaCtx, format string, v ...interface{}) {
+ s.Log(format, v...)
+ ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnegoNegTokenRespIncompleteKRB5)
+ ctx.Response.SetStatusCode(http.StatusUnauthorized)
+ ctx.Response.SetBodyString(spnego.UnauthorizedMsg)
+}
+
+func SPNEGOResponseReject(s *spnego.SPNEGO, ctx *middlewares.AutheliaCtx, format string, v ...interface{}) {
+ s.Log(format, v...)
+ ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnegoNegTokenRespReject)
+ ctx.Response.SetStatusCode(http.StatusUnauthorized)
+ ctx.Response.SetBodyString(spnego.UnauthorizedMsg)
+}