diff options
| author | Matthieu Pignolet <m@mpgn.dev> | 2025-03-09 15:54:26 +0400 |
|---|---|---|
| committer | Matthieu Pignolet <m@mpgn.dev> | 2025-03-09 15:54:26 +0400 |
| commit | eb5d2162fa4815338eeb8d716bfb1302c3863d2b (patch) | |
| tree | 36dcaad273d204a995017cd4082e2f37c074b1e6 | |
| parent | 41798e3e30d04e3cb01878d935cc2881ac50d7e9 (diff) | |
1/?: beginning fo the spnego first-factor implementation
| -rw-r--r-- | go.mod | 10 | ||||
| -rw-r--r-- | go.sum | 10 | ||||
| -rw-r--r-- | internal/handlers/handler_firstfactor_spnego.go | 101 |
3 files changed, 121 insertions, 0 deletions
@@ -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 @@ -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) +} |
