summaryrefslogtreecommitdiff
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
parent41798e3e30d04e3cb01878d935cc2881ac50d7e9 (diff)
1/?: beginning fo the spnego first-factor implementation
-rw-r--r--go.mod10
-rw-r--r--go.sum10
-rw-r--r--internal/handlers/handler_firstfactor_spnego.go101
3 files changed, 121 insertions, 0 deletions
diff --git a/go.mod b/go.mod
index 5594a0635..d5abde701 100644
--- a/go.mod
+++ b/go.mod
@@ -58,6 +58,14 @@ require (
)
require (
+ github.com/hashicorp/go-uuid v1.0.3 // indirect
+ github.com/jcmturner/gofork v1.7.6 // indirect
+ gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect
+ gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect
+ gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect
+)
+
+require (
cel.dev/expr v0.19.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
@@ -118,6 +126,8 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
google.golang.org/protobuf v1.36.1 // indirect
+ gopkg.in/jcmturner/goidentity.v3 v3.0.0
+ gopkg.in/jcmturner/gokrb5.v7 v7.5.0
)
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
diff --git a/go.sum b/go.sum
index c8d2fea4f..6edc8d52a 100644
--- a/go.sum
+++ b/go.sum
@@ -364,6 +364,16 @@ google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw=
+gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
+gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM=
+gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
+gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI=
+gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
+gopkg.in/jcmturner/gokrb5.v7 v7.5.0 h1:a9tsXlIDD9SKxotJMK3niV7rPZAJeX2aD/0yg3qlIrg=
+gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
+gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU=
+gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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)
+}