Added ldap and pam authentication
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
stdlog "log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/jcmturner/goidentity/v6"
|
||||
"github.com/jcmturner/gokrb5/v8/keytab"
|
||||
"github.com/jcmturner/gokrb5/v8/service"
|
||||
"github.com/jcmturner/gokrb5/v8/spnego"
|
||||
)
|
||||
|
||||
// internalUserHeader is the request header the built-in authentication
|
||||
// middlewares (Kerberos, Basic) use to hand the validated username to the
|
||||
// downstream access pipeline when no external TrustedUserHeader is configured. It
|
||||
// is always stripped from inbound requests before validation, so a client cannot
|
||||
// spoof it.
|
||||
const internalUserHeader = "X-Uopi-User"
|
||||
|
||||
// kerberosAuth wraps next with SPNEGO/Kerberos ("Negotiate") authentication. On a
|
||||
// successful handshake the authenticated principal's short username (realm
|
||||
// stripped) is written into userHeader, so the existing access pipeline
|
||||
// (accessMiddleware / wsHandler, which both read userHeader) resolves identity
|
||||
// uniformly whether it originated from a trusted proxy header or a Kerberos
|
||||
// ticket.
|
||||
//
|
||||
// challenge controls behaviour when a request carries no valid Negotiate
|
||||
// credentials:
|
||||
// - challenge=true (REST/page requests): delegate to gokrb5, which replies
|
||||
// 401 + WWW-Authenticate: Negotiate so the browser performs SPNEGO.
|
||||
// - challenge=false (WebSocket upgrades): fall through unauthenticated.
|
||||
// Browsers cannot attach an Authorization header when opening a WebSocket;
|
||||
// they only send Negotiate proactively to trusted URIs. When the header is
|
||||
// present we still validate it, otherwise the session resolves to
|
||||
// default_user exactly as before.
|
||||
//
|
||||
// Any inbound value of userHeader is always discarded before validation: with
|
||||
// native Kerberos there is no trusted proxy stripping client-supplied headers, so
|
||||
// only the SPNEGO-validated identity may set it.
|
||||
func kerberosAuth(kt *keytab.Keytab, spn, userHeader string, challenge bool, log *slog.Logger, next http.Handler) http.Handler {
|
||||
opts := []func(*service.Settings){
|
||||
service.Logger(stdlog.New(slogWriter{log: log}, "", 0)),
|
||||
}
|
||||
if spn != "" {
|
||||
opts = append(opts, service.KeytabPrincipal(spn))
|
||||
}
|
||||
|
||||
// inner runs only after a successful SPNEGO handshake: it copies the resolved
|
||||
// identity into userHeader and continues down the chain.
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := goidentity.FromHTTPRequestContext(r)
|
||||
if id == nil {
|
||||
http.Error(w, "kerberos: missing identity", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
user := id.UserName()
|
||||
if i := strings.IndexByte(user, '@'); i >= 0 {
|
||||
user = user[:i]
|
||||
}
|
||||
r.Header.Set(userHeader, user)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
validate := spnego.SPNEGOKRB5Authenticate(inner, kt, opts...)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Never trust a client-supplied identity header under native Kerberos.
|
||||
r.Header.Del(userHeader)
|
||||
|
||||
negotiate := strings.HasPrefix(r.Header.Get("Authorization"), spnego.HTTPHeaderAuthResponseValueKey)
|
||||
if !negotiate && !challenge {
|
||||
// Best-effort path (WebSocket without proactive credentials): continue
|
||||
// unauthenticated; downstream resolves the session to default_user.
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// Credentials are present (validate them) or a challenge is required.
|
||||
validate.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// slogWriter adapts the std logger gokrb5 expects to slog at debug level so SPNEGO
|
||||
// validation diagnostics surface without polluting normal output.
|
||||
type slogWriter struct{ log *slog.Logger }
|
||||
|
||||
func (s slogWriter) Write(p []byte) (int, error) {
|
||||
if s.log != nil {
|
||||
s.log.Debug("kerberos", "msg", strings.TrimRight(string(p), "\n"))
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
Reference in New Issue
Block a user