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.
This commit is contained in:
parent
d76ccf324a
commit
8714a87d52
61
README.md
61
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)
|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
25
cmd/rdpgw/database/config.go
Executable file
25
cmd/rdpgw/database/config.go
Executable file
@ -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
|
||||
}
|
||||
5
cmd/rdpgw/database/database.go
Executable file
5
cmd/rdpgw/database/database.go
Executable file
@ -0,0 +1,5 @@
|
||||
package database
|
||||
|
||||
type Database interface {
|
||||
GetPassword (username string) string
|
||||
}
|
||||
@ -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"`)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
222
cmd/rdpgw/web/ntlm.go
Normal file
222
cmd/rdpgw/web/ntlm.go
Normal file
@ -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
|
||||
}
|
||||
1
go.mod
1
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user