//go:build pam package auth import ( "fmt" "net/http" "github.com/msteinert/pam" "github.com/rs/zerolog/log" ) type pamProvider struct{} func NewPAM() (AuthProvider, error) { return pamProvider{}, nil } func (pamProvider) Middleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { pamAuth(next, w, r) }) } } func pamAuth(next http.Handler, w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok { w.Header().Add("WWW-Authenticate", `Basic realm="Please provide your system credentials", charset="UTF-8"`) http.Error(w, "HTTP Basic auth is required", http.StatusUnauthorized) return } t, err := pam.StartFunc("login", user, func(s pam.Style, msg string) (string, error) { log.Debug().Str("style", fmt.Sprintf("%v", s)).Msg(msg) switch s { case pam.PromptEchoOff: return pass, nil case pam.PromptEchoOn, pam.ErrorMsg, pam.TextInfo: return "", nil default: return "", fmt.Errorf("unsupported PAM conversation style: %v", s) } }) if err != nil { log.Warn().Err(err).Msg("failed to start PAM conversation") http.Error(w, "Temporary authentication error, try again later", http.StatusServiceUnavailable) return } if err := t.Authenticate(0); err != nil { log.Debug().Str("user", user).Err(err).Msg("auth error") http.Error(w, "Invalid username or password", http.StatusUnauthorized) return } if err := t.AcctMgmt(0); err != nil { log.Debug().Str("user", user).Err(err).Msg("account unavailable") http.Error(w, "Account unavailable", http.StatusUnauthorized) return } user, err = t.GetItem(pam.User) if err != nil { log.Warn().Str("user", user).Err(err).Msg("failed to get PAM username") http.Error(w, "Temporary authentication error, try again later", http.StatusServiceUnavailable) return } authCtx := AuthContext{ AuthMethod: "pam", UserName: user, } ctx := NewContext(r.Context(), &authCtx) r = r.WithContext(ctx) next.ServeHTTP(w, r) }