From 8714a87d52d3dbf322c67c8409270a5a59428175 Mon Sep 17 00:00:00 2001 From: Dries Mys Date: Sun, 14 Apr 2024 22:48:18 +0200 Subject: [PATCH] Support for NTLM authentication added To support NTLM authentication, a database is added as an authentication source. Currently, only the configuration file is supported as a database. Database authentication supports Basic and NTLM authentication protcols. ServerConfig.BasicAuthEnabled renamed to LocalEnabled as Basic auth can be used with Database or Local. --- README.md | 61 ++++++-- cmd/rdpgw/config/configuration.go | 19 ++- cmd/rdpgw/database/config.go | 25 ++++ cmd/rdpgw/database/database.go | 5 + cmd/rdpgw/main.go | 28 +++- cmd/rdpgw/web/basic.go | 77 +++++++---- cmd/rdpgw/web/ntlm.go | 222 ++++++++++++++++++++++++++++++ go.mod | 1 + 8 files changed, 394 insertions(+), 44 deletions(-) create mode 100755 cmd/rdpgw/database/config.go create mode 100755 cmd/rdpgw/database/database.go create mode 100644 cmd/rdpgw/web/ntlm.go diff --git a/README.md b/README.md index 860cdea..c5b7f28 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ to connect. The gateway has several security phases. In the authentication phase the client's credentials are verified. Depending the authentication mechanism used, the client's credentials are verified against -an OpenID Connect provider, Kerberos or a local PAM service. +an OpenID Connect provider, Kerberos, a local PAM service or a local database. If OpenID Connect is used the user will need to connect to a webpage provided by the gateway to authenticate, which in turn will redirect @@ -45,6 +45,9 @@ If local authentication is used the client will need to provide a username and p against PAM. This requires, to ensure privilege separation, that ```rdpgw-auth``` is also running and a valid PAM configuration is provided per typical configuration. +If database authentication is used, the allowed user credentials for the gateway should be configured in the +configuration file. + Finally, RDP hosts that the client wants to connect to are verified against what was provided by / allowed by the server. Next to that the client's ip address needs to match the one it obtained the gateway token with if using OpenID Connect. Due to proxies and NAT this is not always possible and thus can be disabled. However, this @@ -58,7 +61,7 @@ settings. ## Authentication RDPGW wants to be secure when you set it up from the start. It supports several authentication -mechanisms such as OpenID Connect, Kerberos and PAM. +mechanisms such as OpenID Connect, Kerberos, PAM or a database. Technically, cookies are encrypted and signed on the client side relying on [Gorilla Sessions](https://www.gorillatoolkit.org/pkg/sessions). PAA tokens (gateway access tokens) @@ -72,7 +75,7 @@ if you want. ### Mixing authentication mechanisms -It is technically possible to mix authentication mechanisms. Currently, you can mix local and Kerberos. If you enable +It is technically possible to mix authentication mechanisms. Currently, you can mix local with Kerberos or database. If you enable OpenID Connect it is not possible to mix it with local or Kerberos at the moment. ### Open ID Connect @@ -137,7 +140,7 @@ Caps: The client can then connect directly to the gateway without the need for a RDP file. -### PAM / Local / Basic Auth +### PAM / Local (Basic Auth) ![PAM](docs/images/flow-pam.svg) The gateway can also support authentication against PAM. Sometimes this is referred to as local or passwd authentication, @@ -146,7 +149,7 @@ but it also supports LDAP authentication or even Active Directory if you have th `rdpgw-auth` that is used to authenticate the user. This program needs to be run as root or setuid. __NOTE__: The default windows client ``mstsc`` does not support basic auth. You will need to use a different client or -switch to OpenID Connect or Kerberos. +switch to OpenID Connect, Kerberos or database authentication. __NOTE__: Using PAM for passwd (i.e. LDAP is fine) within a container is not recommended. It is better to use OpenID Connect or Kerberos. If you do want to use it within a container you can choose to run the helper program outside the @@ -180,6 +183,33 @@ Make sure to run both the gateway and `rdpgw-auth`. The gateway will connect to The client can then connect to the gateway directly by using a remote desktop client. +### Database (Basic Auth or NTLM) + +The gateway can also support authentication using a local database. +Currently, only the configuration file is supported as a database. +In the future, support for real databases (e.g. sqlite) may be added. + +Database authentication has the advantage that it is easy to setup, especially in case the gateway is used for a limited number of users. +Unlike PAM / local, database authentication supports the default windows client ``mstsc``. + +__WARNING__: The password is currently saved in plain text. So, you should keep the config file as secure as possible and avoid +reusing the same password for other applications. The password is stored in plain text to support the NTLM authentication protocol. + +To enable database authentication make sure to set the following variables in the configuration file. + +```yaml +Server: + Authentication: + - database +Users: + - {Username: "username1", Password: "secure_password"} # Modify this password! +Caps: + TokenAuth: false +``` + +The client can then connect to the gateway directly by using a remote desktop client using the gateway credentials +configured in the YAML configuration file. + ## TLS The gateway requires a valid TLS certificate. This means a certificate that is signed by a valid CA that is in the store @@ -209,16 +239,17 @@ TLS termination. ```yaml # web server configuration. Server: - # can be set to openid, kerberos and local. If openid is used rdpgw expects + # can be set to openid, kerberos, local and database. If openid is used rdpgw expects # a configured openid provider, make sure to set caps.tokenauth to true. If local # rdpgw connects to rdpgw-auth over a socket to verify users and password. Note: # rdpgw-auth needs to be run as root or setuid in order to work. If kerberos is - # used a keytab and krb5conf need to be supplied. local and kerberos authentication - # can be stacked, so that the clients selects what it wants. + # used a keytab and krb5conf need to be supplied. local can be stacked with + # kerberos or database authentication, so that the clients selects what it wants. Authentication: # - kerberos # - local - openid + # - database # The socket to connect to if using local auth. Ensure rdpgw auth is configured to # use the same socket. # AuthSocket: /tmp/rdpgw-auth.sock @@ -267,6 +298,9 @@ OpenId: # Keytab: /etc/keytabs/rdpgw.keytab # Krb5conf: /etc/krb5.conf # enabled / disabled capabilities +# Users: +# - {Username: "username1", Password: "secure_password"} +# - {Username: "username2", Password: "secure_password2"} Caps: SmartCardAuth: false # required for openid connect @@ -369,12 +403,15 @@ In this way you can integrate, for example, it with [pam-jwt](https://github.com ## Client Caveats The several clients that Microsoft provides come with their own caveats. The most important one is that the default client on Windows ``mstsc`` does -not support basic authentication. This means you need to use either OpenID Connect -or Kerberos. +not support basic authentication. This means you need to use either OpenID Connect, +Kerberos or database authentication. In addition to that, ``mstsc``, when configuring a gateway directly in the client requires -you to "save the credentials" for the gateway otherwise the client will not connect at all -(it won't send any packages to the gateway) and it will keep on asking for new credentials. +you to either: + * "save the credentials" for the gateway + * or specify a (random) domain name in the username field (e.g. ``.\username``) when prompted for the gateway credentials, + +otherwise the client will not connect at all (it won't send any packages to the gateway) and it will keep on asking for new credentials. Finally, ``mstsc`` requires a valid certificate on the gateway. diff --git a/cmd/rdpgw/config/configuration.go b/cmd/rdpgw/config/configuration.go index 95a2d10..3202d34 100644 --- a/cmd/rdpgw/config/configuration.go +++ b/cmd/rdpgw/config/configuration.go @@ -31,6 +31,7 @@ type Configuration struct { Server ServerConfig `koanf:"server"` OpenId OpenIDConfig `koanf:"openid"` Kerberos KerberosConfig `koanf:"kerberos"` + Users []UserConfig `koanf:"users"` Caps RDGCapsConfig `koanf:"caps"` Security SecurityConfig `koanf:"security"` Client ClientConfig `koanf:"client"` @@ -66,6 +67,11 @@ type OpenIDConfig struct { ClientSecret string `koanf:"clientsecret"` } +type UserConfig struct { + Username string `koanf:"username"` + Password string `koanf:"password"` +} + type RDGCapsConfig struct { SmartCardAuth bool `koanf:"smartcardauth"` TokenAuth bool `koanf:"tokenauth"` @@ -184,6 +190,7 @@ func Load(configFile string) Configuration { k.UnmarshalWithConf("Security", &Conf.Security, koanfTag) k.UnmarshalWithConf("Client", &Conf.Client, koanfTag) k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag) + k.UnmarshalWithConf("Users", &Conf.Users, koanfTag) if len(Conf.Security.PAATokenEncryptionKey) != 32 { Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32) @@ -221,9 +228,13 @@ func Load(configFile string) Configuration { log.Fatalf("host selection is set to `signed` but `querytokensigningkey` is not set") } - if Conf.Server.BasicAuthEnabled() && Conf.Server.Tls == "disable" { + if Conf.Server.LocalEnabled() && Conf.Server.Tls == "disable" { log.Fatalf("basicauth=local and tls=disable are mutually exclusive") } + + if Conf.Server.DatabaseEnabled() && Conf.Server.KerberosEnabled() { + log.Fatalf("database and kerberos authentication are not stackable") + } if !Conf.Caps.TokenAuth && Conf.Server.OpenIDEnabled() { log.Fatalf("openid is configured but tokenauth disabled") @@ -250,10 +261,14 @@ func (s *ServerConfig) KerberosEnabled() bool { return s.matchAuth("kerberos") } -func (s *ServerConfig) BasicAuthEnabled() bool { +func (s *ServerConfig) LocalEnabled() bool { return s.matchAuth("local") || s.matchAuth("basic") } +func (s *ServerConfig) DatabaseEnabled() bool { + return s.matchAuth("database") +} + func (s *ServerConfig) matchAuth(needle string) bool { for _, q := range s.Authentication { if q == needle { diff --git a/cmd/rdpgw/database/config.go b/cmd/rdpgw/database/config.go new file mode 100755 index 0000000..37b475c --- /dev/null +++ b/cmd/rdpgw/database/config.go @@ -0,0 +1,25 @@ +package database + +import ( + "github.com/bolkedebruin/rdpgw/cmd/rdpgw/config" +) + +type Config struct { + users map[string]config.UserConfig +} + +func NewConfig(users []config.UserConfig) *Config { + usersMap := map[string]config.UserConfig{} + + for _, user := range users { + usersMap[user.Username] = user + } + + return &Config{ + users: usersMap, + } +} + +func (c *Config) GetPassword (username string) string { + return c.users[username].Password +} \ No newline at end of file diff --git a/cmd/rdpgw/database/database.go b/cmd/rdpgw/database/database.go new file mode 100755 index 0000000..56ccd85 --- /dev/null +++ b/cmd/rdpgw/database/database.go @@ -0,0 +1,5 @@ +package database + +type Database interface { + GetPassword (username string) string +} \ No newline at end of file diff --git a/cmd/rdpgw/main.go b/cmd/rdpgw/main.go index 750d04d..ec72ec4 100644 --- a/cmd/rdpgw/main.go +++ b/cmd/rdpgw/main.go @@ -8,6 +8,7 @@ import ( "github.com/bolkedebruin/gokrb5/v8/service" "github.com/bolkedebruin/gokrb5/v8/spnego" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/config" + "github.com/bolkedebruin/rdpgw/cmd/rdpgw/database" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/kdcproxy" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/security" @@ -221,7 +222,7 @@ func main() { r.HandleFunc("/callback", o.HandleCallback) // only enable un-auth endpoint for openid only config - if !conf.Server.KerberosEnabled() || !conf.Server.BasicAuthEnabled() { + if !conf.Server.KerberosEnabled() && !conf.Server.LocalEnabled() && !conf.Server.DatabaseEnabled() { rdp.Name("gw").HandlerFunc(gw.HandleGatewayProtocol) } } @@ -229,11 +230,30 @@ func main() { // for stacking of authentication auth := web.NewAuthMux() rdp.MatcherFunc(web.NoAuthz).HandlerFunc(auth.SetAuthenticate) + + var db database.Database + + // database + if conf.Server.DatabaseEnabled() { + log.Printf("enabling database authentication") + db = database.NewConfig(conf.Users) + ntlm := web.NewNTLMAuthHandler(db) + rdp.NewRoute().HeadersRegexp("Authorization", "NTLM").HandlerFunc(ntlm.NTLMAuth(gw.HandleGatewayProtocol)) + rdp.NewRoute().HeadersRegexp("Authorization", "Negotiate").HandlerFunc(ntlm.NTLMAuth(gw.HandleGatewayProtocol)) + auth.Register(`NTLM`) + } // basic auth - if conf.Server.BasicAuthEnabled() { - log.Printf("enabling basic authentication") - q := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket, Timeout: conf.Server.BasicAuthTimeout} + if conf.Server.LocalEnabled() || conf.Server.DatabaseEnabled() { + q := web.BasicAuthHandler{} + if conf.Server.LocalEnabled() { + log.Printf("enabling local authentication") + q.SocketAddress = conf.Server.AuthSocket + q.Timeout = conf.Server.BasicAuthTimeout + } + if conf.Server.DatabaseEnabled() { + q.Database = db + } rdp.NewRoute().HeadersRegexp("Authorization", "Basic").HandlerFunc(q.BasicAuth(gw.HandleGatewayProtocol)) auth.Register(`Basic realm="restricted", charset="UTF-8"`) } diff --git a/cmd/rdpgw/web/basic.go b/cmd/rdpgw/web/basic.go index 9f829f6..299738c 100644 --- a/cmd/rdpgw/web/basic.go +++ b/cmd/rdpgw/web/basic.go @@ -2,6 +2,7 @@ package web import ( "context" + "github.com/bolkedebruin/rdpgw/cmd/rdpgw/database" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity" "github.com/bolkedebruin/rdpgw/shared/auth" "google.golang.org/grpc" @@ -19,38 +20,16 @@ const ( type BasicAuthHandler struct { SocketAddress string Timeout int + Database database.Database } func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if ok { - ctx := r.Context() + authenticated := h.authenticate(w, r, username, password) - conn, err := grpc.Dial(h.SocketAddress, grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { - return net.Dial(protocolGrpc, addr) - })) - if err != nil { - log.Printf("Cannot reach authentication provider: %s", err) - http.Error(w, "Server error", http.StatusInternalServerError) - return - } - defer conn.Close() - - c := auth.NewAuthenticateClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(h.Timeout)) - defer cancel() - - req := &auth.UserPass{Username: username, Password: password} - res, err := c.Authenticate(ctx, req) - if err != nil { - log.Printf("Error talking to authentication provider: %s", err) - http.Error(w, "Server error", http.StatusInternalServerError) - return - } - - if !res.Authenticated { + if !authenticated { log.Printf("User %s is not authenticated for this service", username) } else { log.Printf("User %s authenticated", username) @@ -61,7 +40,6 @@ func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc { next.ServeHTTP(w, identity.AddToRequestCtx(id, r)) return } - } // If the Authentication header is not present, is invalid, or the // username or password is wrong, then set a WWW-Authenticate @@ -71,3 +49,50 @@ func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc { http.Error(w, "Unauthorized", http.StatusUnauthorized) } } + +func (h *BasicAuthHandler) authenticate(w http.ResponseWriter, r *http.Request, username string, password string) (authenticated bool) { + return h.authenticateDatabase(username, password) || + h.authenticateSocket(w, r, username, password) +} + +func (h *BasicAuthHandler) authenticateSocket(w http.ResponseWriter, r *http.Request, username string, password string) (authenticated bool) { + if h.SocketAddress == "" { + return false + } + + ctx := r.Context() + + conn, err := grpc.Dial(h.SocketAddress, grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { + return net.Dial(protocolGrpc, addr) + })) + if err != nil { + log.Printf("Cannot reach authentication provider: %s", err) + http.Error(w, "Server error", http.StatusInternalServerError) + return false + } + defer conn.Close() + + c := auth.NewAuthenticateClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(h.Timeout)) + defer cancel() + + req := &auth.UserPass{Username: username, Password: password} + res, err := c.Authenticate(ctx, req) + if err != nil { + log.Printf("Error talking to authentication provider: %s", err) + http.Error(w, "Server error", http.StatusInternalServerError) + return false + } + + return res.Authenticated +} + +func (h *BasicAuthHandler) authenticateDatabase(username string, password string) (authenticated bool) { + if h.Database == nil { + return false + } + + expectedPassword := h.Database.GetPassword (username) + return expectedPassword != "" && password == expectedPassword +} \ No newline at end of file diff --git a/cmd/rdpgw/web/ntlm.go b/cmd/rdpgw/web/ntlm.go new file mode 100644 index 0000000..dfcbe47 --- /dev/null +++ b/cmd/rdpgw/web/ntlm.go @@ -0,0 +1,222 @@ +package web + +import ( + "encoding/base64" + "errors" + "fmt" + "github.com/bolkedebruin/rdpgw/cmd/rdpgw/database" + "github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity" + "github.com/patrickmn/go-cache" + "github.com/m7913d/go-ntlm/ntlm" + "log" + "net/http" + "time" +) + +const ( + cacheExpiration = time.Minute + cleanupInterval = time.Minute * 5 +) + +type ntlmAuthMode uint32 +const ( + authNone ntlmAuthMode = iota + authNTLM + authNegotiate +) + +type NTLMAuthHandler struct { + contextCache *cache.Cache + + // Information about the server, returned to the client during authentication + ServerName string // e.g. EXAMPLE1 + DomainName string // e.g. EXAMPLE + DnsServerName string // e.g. example1.example.com + DnsDomainName string // e.g. example.com + DnsTreeName string // e.g. example.com + + Database database.Database +} + +func NewNTLMAuthHandler (database database.Database) (*NTLMAuthHandler) { + return &NTLMAuthHandler{ + contextCache: cache.New(cacheExpiration, cleanupInterval), + Database: database, + } +} + +func (h *NTLMAuthHandler) NTLMAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + //log.Printf("NTLM request") + + authPayload, authMode, err := h.getAuthPayload(r) + if err != nil { + log.Printf("Failed parsing auth header: %s", err) + w.Header().Add("WWW-Authenticate", `NTLM,Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + c := h.getContext(r) + if c.Auth(w, authPayload, authMode) { + username := c.GetUsername() + log.Printf("NTLM: User %s authenticated", username) + id := identity.FromRequestCtx(r) + id.SetUserName(username) + id.SetAuthenticated(true) + id.SetAuthTime(time.Now()) + next.ServeHTTP(w, identity.AddToRequestCtx(id, r)) + } + } +} + +func (h *NTLMAuthHandler) getAuthPayload (r *http.Request) (payload string, authMode ntlmAuthMode, err error) { + authorisationEncoded := r.Header.Get("Authorization") + if authorisationEncoded[0:5] == "NTLM " { + return authorisationEncoded[5:], authNTLM, nil + } + if authorisationEncoded[0:10] == "Negotiate " { + return authorisationEncoded[10:], authNegotiate, nil + } + return "", authNone, errors.New(fmt.Sprintf("Invalid NTLM Authorisation header: %s", authorisationEncoded)) +} + +func (h *NTLMAuthHandler) getContext (r *http.Request) (*ntlmContext) { + if c_, found := h.contextCache.Get(r.RemoteAddr); found { + if c, ok := c_.(*ntlmContext); ok { + return c + } + } + c := new(ntlmContext) + c.h = h + h.contextCache.Set(r.RemoteAddr, c, cache.DefaultExpiration) + return c +} + +type ntlmContext struct { + session ntlm.ServerSession + h *NTLMAuthHandler +} + +func (c *ntlmContext) GetUsername () (username string) { + username, _, _ = c.session.GetUserInfo() + return username +} + +func (c *ntlmContext) Auth(w http.ResponseWriter, authorisationEncoded string, authMode ntlmAuthMode) (succeeded bool) { + authorisation, err := base64.StdEncoding.DecodeString(authorisationEncoded) + if err != nil { + log.Printf("Failed to decode NTLM Authorisation header: %s due to: %s", authorisationEncoded, err) + http.Error(w, "Server error", http.StatusInternalServerError) + return false + } + + nm, err := ntlm.ParseNegotiateMessage(authorisation) + if err == nil { + c.negotiate(w, nm, authMode) + return false + } + if (nm != nil && nm.MessageType == 1) { + log.Printf("Failed to parse NTLM Authorisation header: %s due to %s", authorisationEncoded, err) + http.Error(w, "Server error", http.StatusInternalServerError) + return false + } else if c.session == nil { + log.Printf("New NTLM auth sequence should start with negotioate request: %s", authorisationEncoded) + http.Error(w, "Server error", http.StatusInternalServerError) + return false + } + + am, err := ntlm.ParseAuthenticateMessage(authorisation, 2) + if err == nil { + return c.authenticate(w, am) + } + + log.Printf("Failed to parse NTLM Authorisation header: %s due to %s", authorisationEncoded, err) + http.Error(w, "Server error", http.StatusInternalServerError) + return false +} + +func (c *ntlmContext) getAuthPrefix (authMode ntlmAuthMode) (prefix string) { + if authMode == authNTLM { + return "NTLM " + } + if authMode == authNegotiate { + return "Negotiate " + } + return "" +} + +func (c *ntlmContext) requestAuthenticate (w http.ResponseWriter) { + w.Header().Add("WWW-Authenticate", `NTLM,Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) +} + +func (c *ntlmContext) negotiate(w http.ResponseWriter, nm *ntlm.NegotiateMessage, authMode ntlmAuthMode) { + //log.Printf("NTLM negotiate request: %v", nm) + + session, err := ntlm.CreateServerSession(ntlm.Version2, ntlm.ConnectionOrientedMode) + + if err != nil { + c.session = nil; + log.Printf("Failed to create NTLM server session: %s", err) + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + + c.session = session + c.session.SetDomainName(c.h.DomainName) + c.session.SetComputerName(c.h.ServerName) + c.session.SetDnsDomainName(c.h.DnsDomainName) + c.session.SetDnsComputerName(c.h.DnsServerName) + c.session.SetDnsTreeName(c.h.DnsTreeName) + + err = c.session.ProcessNegotiateMessage(nm) + if err != nil { + log.Printf("Failed to process NTLM negotiate message: %s", err) + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + + cm, err := c.session.GenerateChallengeMessage() + if err != nil { + log.Printf("Failed to generate NTLM challenge message: %s", err) + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + + log.Printf("Sending NTLM challenge request") + //log.Printf("Sending NTLM challenge request: %v", cm) + + cmBytes := cm.Bytes() + w.Header().Add("WWW-Authenticate", c.getAuthPrefix(authMode)+base64.StdEncoding.EncodeToString(cmBytes)) + http.Error(w, "Unauthorized", http.StatusUnauthorized) +} + +func (c *ntlmContext) authenticate(w http.ResponseWriter, am *ntlm.AuthenticateMessage) (succeeded bool) { + //log.Printf("NTLM Authenticate request: %v", am) + + if c.session == nil { + log.Printf("NTLM Authenticate requires active session: first call negotioate") + http.Error(w, "Server error", http.StatusInternalServerError) + return false + } + + username := am.UserName.String() + password := c.h.Database.GetPassword (username) + if password == "" { + log.Printf("NTLM: unknown username specified: %s", username) + c.requestAuthenticate(w) + return false + } + + c.session.SetUserInfo(username,password,"") + + err := c.session.ProcessAuthenticateMessage(am) + if err != nil { + log.Printf("Failed to process NTLM authenticate message: %s", err) + c.requestAuthenticate(w) + return false + } + + return true +} diff --git a/go.mod b/go.mod index 8c378e3..42bcf43 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/knadh/koanf/providers/env v0.1.0 github.com/knadh/koanf/providers/file v0.1.0 github.com/knadh/koanf/v2 v2.1.0 + github.com/m7913d/go-ntlm v0.0.1 github.com/msteinert/pam/v2 v2.0.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.19.0