2019-04-21 17:55:52 +00:00
|
|
|
package crowd
|
2018-02-04 13:51:08 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
crowd "github.com/jda/go-crowd"
|
|
|
|
log "github.com/sirupsen/logrus"
|
2023-07-29 11:58:50 +00:00
|
|
|
yaml "gopkg.in/yaml.v3"
|
2019-02-21 23:10:43 +00:00
|
|
|
|
|
|
|
"github.com/Luzifer/nginx-sso/plugins"
|
2018-02-04 13:51:08 +00:00
|
|
|
)
|
|
|
|
|
2019-04-21 17:55:52 +00:00
|
|
|
type AuthCrowd struct {
|
2018-02-04 13:51:08 +00:00
|
|
|
URL string `yaml:"url"`
|
|
|
|
AppName string `yaml:"app_name"`
|
|
|
|
AppPassword string `yaml:"app_pass"`
|
|
|
|
|
|
|
|
crowd crowd.Crowd
|
|
|
|
}
|
|
|
|
|
2019-04-21 17:55:52 +00:00
|
|
|
func New() *AuthCrowd {
|
|
|
|
return &AuthCrowd{}
|
|
|
|
}
|
|
|
|
|
2018-02-04 13:51:08 +00:00
|
|
|
// AuthenticatorID needs to return an unique string to identify
|
|
|
|
// this special authenticator
|
2019-04-21 17:55:52 +00:00
|
|
|
func (a AuthCrowd) AuthenticatorID() string { return "crowd" }
|
2018-02-04 13:51:08 +00:00
|
|
|
|
|
|
|
// Configure loads the configuration for the Authenticator from the
|
|
|
|
// global config.yaml file which is passed as a byte-slice.
|
|
|
|
// If no configuration for the Authenticator is supplied the function
|
2019-02-21 23:27:02 +00:00
|
|
|
// needs to return the plugins.ErrProviderUnconfigured
|
2019-04-21 17:55:52 +00:00
|
|
|
func (a *AuthCrowd) Configure(yamlSource []byte) error {
|
2018-02-04 13:51:08 +00:00
|
|
|
envelope := struct {
|
|
|
|
Providers struct {
|
2019-04-21 17:55:52 +00:00
|
|
|
Crowd *AuthCrowd `yaml:"crowd"`
|
2018-02-04 13:51:08 +00:00
|
|
|
} `yaml:"providers"`
|
|
|
|
}{}
|
|
|
|
|
|
|
|
if err := yaml.Unmarshal(yamlSource, &envelope); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if envelope.Providers.Crowd == nil {
|
2019-02-21 23:27:02 +00:00
|
|
|
return plugins.ErrProviderUnconfigured
|
2018-02-04 13:51:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
a.URL = envelope.Providers.Crowd.URL
|
|
|
|
a.AppName = envelope.Providers.Crowd.AppName
|
|
|
|
a.AppPassword = envelope.Providers.Crowd.AppPassword
|
|
|
|
|
|
|
|
if a.AppName == "" || a.AppPassword == "" {
|
2019-02-21 23:27:02 +00:00
|
|
|
return plugins.ErrProviderUnconfigured
|
2018-02-04 13:51:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
a.crowd, err = crowd.New(a.AppName, a.AppPassword, a.URL)
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// DetectUser is used to detect a user without a login form from
|
|
|
|
// a cookie, header or other methods
|
2019-02-21 23:27:02 +00:00
|
|
|
// If no user was detected the plugins.ErrNoValidUserFound needs to be
|
2018-02-04 13:51:08 +00:00
|
|
|
// returned
|
2019-04-21 17:55:52 +00:00
|
|
|
func (a AuthCrowd) DetectUser(res http.ResponseWriter, r *http.Request) (string, []string, error) {
|
2018-02-04 13:51:08 +00:00
|
|
|
cc, err := a.crowd.GetCookieConfig()
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cookie, err := r.Cookie(cc.Name)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
// Fine, we do have a cookie
|
|
|
|
case http.ErrNoCookie:
|
|
|
|
// Also fine, there is no cookie
|
2019-02-21 23:27:02 +00:00
|
|
|
return "", nil, plugins.ErrNoValidUserFound
|
2018-02-04 13:51:08 +00:00
|
|
|
default:
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
ssoToken := cookie.Value
|
|
|
|
sess, err := a.crowd.GetSession(ssoToken)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Debug("Getting crowd session failed")
|
2019-02-21 23:27:02 +00:00
|
|
|
return "", nil, plugins.ErrNoValidUserFound
|
2018-02-04 13:51:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
user := sess.User.UserName
|
|
|
|
cGroups, err := a.crowd.GetDirectGroups(user)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
groups := []string{}
|
|
|
|
for _, g := range cGroups {
|
|
|
|
groups = append(groups, g.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
return user, groups, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Login is called when the user submits the login form and needs
|
|
|
|
// to authenticate the user or throw an error. If the user has
|
|
|
|
// successfully logged in the persistent cookie should be written
|
|
|
|
// in order to use DetectUser for the next login.
|
2019-02-21 23:27:02 +00:00
|
|
|
// If the user did not login correctly the plugins.ErrNoValidUserFound
|
2018-02-04 13:51:08 +00:00
|
|
|
// needs to be returned
|
2019-04-21 17:55:52 +00:00
|
|
|
func (a AuthCrowd) Login(res http.ResponseWriter, r *http.Request) (string, []plugins.MFAConfig, error) {
|
2018-02-04 13:51:08 +00:00
|
|
|
username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
|
|
|
|
password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
|
|
|
|
|
|
|
|
cc, err := a.crowd.GetCookieConfig()
|
|
|
|
if err != nil {
|
2018-12-24 09:07:49 +00:00
|
|
|
return "", nil, err
|
2018-02-04 13:51:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sess, err := a.crowd.NewSession(username, password, r.RemoteAddr)
|
|
|
|
if err != nil {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"username": username,
|
|
|
|
}).WithError(err).Debug("Crowd authentication failed")
|
2019-02-21 23:27:02 +00:00
|
|
|
return "", nil, plugins.ErrNoValidUserFound
|
2018-02-04 13:51:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
http.SetCookie(res, &http.Cookie{
|
|
|
|
Name: cc.Name,
|
|
|
|
Value: sess.Token,
|
|
|
|
Path: "/",
|
|
|
|
Domain: cc.Domain,
|
|
|
|
Expires: sess.Expires,
|
|
|
|
Secure: cc.Secure,
|
|
|
|
HttpOnly: true,
|
|
|
|
})
|
|
|
|
|
2018-12-24 09:07:49 +00:00
|
|
|
return username, nil, nil
|
2018-02-04 13:51:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// LoginFields needs to return the fields required for this login
|
|
|
|
// method. If no login using this method is possible the function
|
|
|
|
// needs to return nil.
|
2019-04-21 17:55:52 +00:00
|
|
|
func (a AuthCrowd) LoginFields() (fields []plugins.LoginField) {
|
2019-02-21 23:10:43 +00:00
|
|
|
return []plugins.LoginField{
|
2018-02-04 13:51:08 +00:00
|
|
|
{
|
|
|
|
Label: "Username",
|
|
|
|
Name: "username",
|
|
|
|
Placeholder: "Username",
|
|
|
|
Type: "text",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Label: "Password",
|
|
|
|
Name: "password",
|
|
|
|
Placeholder: "****",
|
|
|
|
Type: "password",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Logout is called when the user visits the logout endpoint and
|
|
|
|
// needs to destroy any persistent stored cookies
|
2019-04-21 17:55:52 +00:00
|
|
|
func (a AuthCrowd) Logout(res http.ResponseWriter, r *http.Request) (err error) {
|
2018-02-04 13:51:08 +00:00
|
|
|
cc, err := a.crowd.GetCookieConfig()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
http.SetCookie(res, &http.Cookie{
|
|
|
|
Name: cc.Name,
|
|
|
|
Value: "",
|
|
|
|
Path: "/",
|
|
|
|
Domain: cc.Domain,
|
|
|
|
MaxAge: -1,
|
|
|
|
Secure: cc.Secure,
|
|
|
|
HttpOnly: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2018-12-24 09:07:49 +00:00
|
|
|
|
|
|
|
// SupportsMFA returns the MFA detection capabilities of the login
|
|
|
|
// provider. If the provider can provide mfaConfig objects from its
|
|
|
|
// configuration return true. If this is true the login interface
|
|
|
|
// will display an additional field for this provider for the user
|
|
|
|
// to fill in their MFA token.
|
2019-04-21 17:55:52 +00:00
|
|
|
func (a AuthCrowd) SupportsMFA() bool { return false }
|