mirror of
https://github.com/Luzifer/nginx-sso.git
synced 2024-12-20 04:41:17 +00:00
Initial version (#1)
* Initial draft * HCL does not support int64 * Add http stubs * Login does not need to return user details * Fields should have a label * Add example configuration * Add stub for "Simple" authenticator * Add debug logging * Implement configuration loading * Implement user detection * Fix error names in doc strings * Implement session store * Implement "Token" provider * Add login frontend * Implement login and logout * Do not show tabs when there is no choice * Fix multi-tab errors, sorting * Implement "Yubikey" authenticator * Lint: Rename error to naming convention * Apply cookie security * Prevent double-login * Adjust parameters for crowd * Implement ACL * Replace HCL config with YAML config * Remove config debug output * Remove crowd config Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
95321c666b
commit
87d719367d
9 changed files with 1260 additions and 0 deletions
189
acl.go
Normal file
189
acl.go
Normal file
|
@ -0,0 +1,189 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/go_helpers/str"
|
||||
)
|
||||
|
||||
type aclRule struct {
|
||||
Field string `yaml:"field"`
|
||||
Invert bool `yaml:"invert"`
|
||||
IsPresent *bool `yaml:"present"`
|
||||
MatchRegex *string `yaml:"regexp"`
|
||||
MatchString *string `yaml:"equals"`
|
||||
}
|
||||
|
||||
func (a aclRule) Validate() error {
|
||||
if a.Field == "" {
|
||||
return fmt.Errorf("Field is not set")
|
||||
}
|
||||
|
||||
if a.IsPresent == nil && a.MatchRegex == nil && a.MatchString == nil {
|
||||
return fmt.Errorf("No matcher (present, regexp, equals) is set")
|
||||
}
|
||||
|
||||
if a.MatchRegex != nil {
|
||||
if _, err := regexp.Compile(*a.MatchRegex); err != nil {
|
||||
return fmt.Errorf("Regexp is invalid: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a aclRule) AppliesToFields(fields map[string]string) bool {
|
||||
var field, value string
|
||||
|
||||
for f, v := range fields {
|
||||
if strings.ToLower(a.Field) == f {
|
||||
field = f
|
||||
value = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if a.IsPresent != nil {
|
||||
if !a.Invert && *a.IsPresent && field == "" {
|
||||
// Field is expected to be present but isn't, rule does not apply
|
||||
return false
|
||||
}
|
||||
if !a.Invert && !*a.IsPresent && field != "" {
|
||||
// Field is expected not to be present but is, rule does not apply
|
||||
return false
|
||||
}
|
||||
if a.Invert && *a.IsPresent && field != "" {
|
||||
// Field is expected not to be present but is, rule does not apply
|
||||
return false
|
||||
}
|
||||
if a.Invert && !*a.IsPresent && field == "" {
|
||||
// Field is expected to be present but isn't, rule does not apply
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if field == "" {
|
||||
// We found a rule which has no matching field, rule does not apply
|
||||
return false
|
||||
}
|
||||
|
||||
if a.MatchString != nil {
|
||||
if (*a.MatchString != value) == !a.Invert {
|
||||
// Value does not match expected string, rule does not apply
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if a.MatchRegex != nil {
|
||||
if regexp.MustCompile(*a.MatchRegex).MatchString(value) == a.Invert {
|
||||
// Value does not match expected regexp, rule does not apply
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type aclAccessResult uint
|
||||
|
||||
const (
|
||||
accessDunno aclAccessResult = iota
|
||||
accessAllow
|
||||
accessDeny
|
||||
)
|
||||
|
||||
type aclRuleSet struct {
|
||||
Rules []aclRule `yaml:"rules"`
|
||||
|
||||
Allow []string `yaml:"allow"`
|
||||
Deny []string `yaml:"deny"`
|
||||
}
|
||||
|
||||
func (a aclRuleSet) buildFieldSet(r *http.Request) map[string]string {
|
||||
result := map[string]string{}
|
||||
|
||||
for k := range r.Header {
|
||||
result[strings.ToLower(k)] = r.Header.Get(k)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (a aclRuleSet) HasAccess(user string, groups []string, r *http.Request) aclAccessResult {
|
||||
fields := a.buildFieldSet(r)
|
||||
|
||||
for _, rule := range a.Rules {
|
||||
if !rule.AppliesToFields(fields) {
|
||||
// At least one rule does not match the request
|
||||
return accessDunno
|
||||
}
|
||||
}
|
||||
|
||||
// All rules do apply to this request, we can judge
|
||||
|
||||
if str.StringInSlice(user, a.Deny) {
|
||||
// Explicit deny, final result
|
||||
return accessDeny
|
||||
}
|
||||
|
||||
if str.StringInSlice(user, a.Allow) {
|
||||
// Explicit allow, final result
|
||||
return accessAllow
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if str.StringInSlice("@"+group, a.Deny) {
|
||||
// Deny through group, final result
|
||||
return accessDeny
|
||||
}
|
||||
|
||||
if str.StringInSlice("@"+group, a.Allow) {
|
||||
// Allow through group, final result
|
||||
return accessAllow
|
||||
}
|
||||
}
|
||||
|
||||
// Neither user nor group are handled
|
||||
return accessDunno
|
||||
}
|
||||
|
||||
func (a aclRuleSet) Validate() error {
|
||||
for i, r := range a.Rules {
|
||||
if err := r.Validate(); err != nil {
|
||||
return fmt.Errorf("Rule on position %d is invalid: %s", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type acl struct {
|
||||
RuleSets []aclRuleSet `yaml:"rule_sets"`
|
||||
}
|
||||
|
||||
func (a acl) Validate() error {
|
||||
for i, r := range a.RuleSets {
|
||||
if err := r.Validate(); err != nil {
|
||||
return fmt.Errorf("RuleSet on position %d is invalid: %s", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a acl) HasAccess(user string, groups []string, r *http.Request) bool {
|
||||
result := accessDunno
|
||||
|
||||
for _, rs := range a.RuleSets {
|
||||
if intermediateResult := rs.HasAccess(user, groups, r); intermediateResult > result {
|
||||
result = intermediateResult
|
||||
}
|
||||
}
|
||||
|
||||
return result == accessAllow
|
||||
}
|
219
acl_test.go
Normal file
219
acl_test.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
aclTestUser = "test"
|
||||
aclTestGroups = []string{"group_a", "group_b"}
|
||||
)
|
||||
|
||||
func aclTestRequest(headers map[string]string) *http.Request {
|
||||
req, _ := http.NewRequest("GET", "http://localhost/auth", nil)
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func aclTestString(in string) *string { return &in }
|
||||
func aclTestBool(in bool) *bool { return &in }
|
||||
|
||||
func TestEmptyACL(t *testing.T) {
|
||||
a := acl{}
|
||||
|
||||
if a.HasAccess(aclTestUser, aclTestGroups, aclTestRequest(map[string]string{})) {
|
||||
t.Fatal("Empty ACL (= default action) was ALLOW instead of DENY")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleSetMatcher(t *testing.T) {
|
||||
r := aclRuleSet{
|
||||
Rules: []aclRule{
|
||||
{
|
||||
Field: "field_a",
|
||||
MatchString: aclTestString("expected"),
|
||||
},
|
||||
{
|
||||
Field: "field_c",
|
||||
MatchString: aclTestString("expected"),
|
||||
},
|
||||
},
|
||||
Allow: []string{aclTestUser},
|
||||
}
|
||||
fields := map[string]string{
|
||||
"field_a": "expected",
|
||||
"field_b": "unchecked",
|
||||
"field_c": "expected",
|
||||
}
|
||||
|
||||
if r.HasAccess(aclTestUser, aclTestGroups, aclTestRequest(fields)) != accessAllow {
|
||||
t.Error("Access was denied")
|
||||
}
|
||||
|
||||
delete(fields, "field_c")
|
||||
if r.HasAccess(aclTestUser, aclTestGroups, aclTestRequest(fields)) != accessDunno {
|
||||
t.Error("Access was not unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvertedRegexMatcher(t *testing.T) {
|
||||
fields := map[string]string{
|
||||
"field_a": "expected",
|
||||
"field_b": "unchecked",
|
||||
}
|
||||
|
||||
ar := aclRule{
|
||||
Field: "field_a",
|
||||
Invert: true,
|
||||
MatchRegex: aclTestString("^expected$"),
|
||||
}
|
||||
|
||||
if ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v matches fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
fields["field_a"] = "unexpected"
|
||||
|
||||
if !ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v does not match fields %#v", ar, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher(t *testing.T) {
|
||||
fields := map[string]string{
|
||||
"field_a": "expected",
|
||||
"field_b": "unchecked",
|
||||
}
|
||||
|
||||
ar := aclRule{
|
||||
Field: "field_a",
|
||||
MatchRegex: aclTestString("^expected$"),
|
||||
}
|
||||
|
||||
if !ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v does not match fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
fields["field_a"] = "unexpected"
|
||||
|
||||
if ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v matches fields %#v", ar, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvertedEqualsMatcher(t *testing.T) {
|
||||
fields := map[string]string{
|
||||
"field_a": "expected",
|
||||
"field_b": "unchecked",
|
||||
}
|
||||
|
||||
ar := aclRule{
|
||||
Field: "field_a",
|
||||
Invert: true,
|
||||
MatchString: aclTestString("expected"),
|
||||
}
|
||||
|
||||
if ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v matches fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
fields["field_a"] = "unexpected"
|
||||
|
||||
if !ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v does not match fields %#v", ar, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualsMatcher(t *testing.T) {
|
||||
fields := map[string]string{
|
||||
"field_a": "expected",
|
||||
"field_b": "unchecked",
|
||||
}
|
||||
|
||||
ar := aclRule{
|
||||
Field: "field_a",
|
||||
MatchString: aclTestString("expected"),
|
||||
}
|
||||
|
||||
if !ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v does not match fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
fields["field_a"] = "unexpected"
|
||||
|
||||
if ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v matches fields %#v", ar, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvertedIsPresentMatcher(t *testing.T) {
|
||||
fields := map[string]string{
|
||||
"field_a": "expected",
|
||||
"field_b": "unchecked",
|
||||
}
|
||||
|
||||
ar := aclRule{
|
||||
Field: "field_a",
|
||||
Invert: true,
|
||||
IsPresent: aclTestBool(true),
|
||||
}
|
||||
|
||||
if ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v matches fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
ar.IsPresent = aclTestBool(false)
|
||||
|
||||
if !ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v does not match fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
ar.IsPresent = aclTestBool(true)
|
||||
delete(fields, "field_a")
|
||||
|
||||
if !ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v does not match fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
ar.IsPresent = aclTestBool(false)
|
||||
if ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v matches fields %#v", ar, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPresentMatcher(t *testing.T) {
|
||||
fields := map[string]string{
|
||||
"field_a": "expected",
|
||||
"field_b": "unchecked",
|
||||
}
|
||||
|
||||
ar := aclRule{
|
||||
Field: "field_a",
|
||||
IsPresent: aclTestBool(true),
|
||||
}
|
||||
|
||||
if !ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v does not match fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
ar.IsPresent = aclTestBool(false)
|
||||
|
||||
if ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v matches fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
ar.IsPresent = aclTestBool(true)
|
||||
delete(fields, "field_a")
|
||||
|
||||
if ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v matches fields %#v", ar, fields)
|
||||
}
|
||||
|
||||
ar.IsPresent = aclTestBool(false)
|
||||
if !ar.AppliesToFields(fields) {
|
||||
t.Errorf("Rule %#v does not match fields %#v", ar, fields)
|
||||
}
|
||||
}
|
129
auth_simple.go
Normal file
129
auth_simple.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/go_helpers/str"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerAuthenticator(&authSimple{})
|
||||
}
|
||||
|
||||
type authSimple struct {
|
||||
Users map[string]string `yaml:"users"`
|
||||
Groups map[string][]string `yaml:"groups"`
|
||||
}
|
||||
|
||||
// AuthenticatorID needs to return an unique string to identify
|
||||
// this special authenticator
|
||||
func (a authSimple) AuthenticatorID() string { return "simple" }
|
||||
|
||||
// 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
|
||||
// needs to return the errAuthenticatorUnconfigured
|
||||
func (a *authSimple) Configure(yamlSource []byte) error {
|
||||
envelope := struct {
|
||||
Providers struct {
|
||||
Simple *authSimple `yaml:"simple"`
|
||||
} `yaml:"providers"`
|
||||
}{}
|
||||
|
||||
if err := yaml.Unmarshal(yamlSource, &envelope); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if envelope.Providers.Simple == nil {
|
||||
return errAuthenticatorUnconfigured
|
||||
}
|
||||
|
||||
a.Users = envelope.Providers.Simple.Users
|
||||
a.Groups = envelope.Providers.Simple.Groups
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DetectUser is used to detect a user without a login form from
|
||||
// a cookie, header or other methods
|
||||
// If no user was detected the errNoValidUserFound needs to be
|
||||
// returned
|
||||
func (a authSimple) DetectUser(r *http.Request) (string, []string, error) {
|
||||
sess, err := cookieStore.Get(r, strings.Join([]string{mainCfg.Cookie.Prefix, a.AuthenticatorID()}, "-"))
|
||||
if err != nil {
|
||||
return "", nil, errNoValidUserFound
|
||||
}
|
||||
|
||||
user, ok := sess.Values["user"].(string)
|
||||
if !ok {
|
||||
return "", nil, errNoValidUserFound
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
for group, users := range a.Groups {
|
||||
if str.StringInSlice(user, users) {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// If the user did not login correctly the errNoValidUserFound
|
||||
// needs to be returned
|
||||
func (a authSimple) Login(res http.ResponseWriter, r *http.Request) error {
|
||||
username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
|
||||
password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
|
||||
|
||||
for u, p := range a.Users {
|
||||
if u != username {
|
||||
continue
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(p), []byte(password)) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sess, _ := cookieStore.Get(r, strings.Join([]string{mainCfg.Cookie.Prefix, a.AuthenticatorID()}, "-"))
|
||||
sess.Options = mainCfg.GetSessionOpts()
|
||||
sess.Values["user"] = u
|
||||
return sess.Save(r, res)
|
||||
}
|
||||
|
||||
return errNoValidUserFound
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (a authSimple) LoginFields() (fields []loginField) {
|
||||
return []loginField{
|
||||
{
|
||||
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
|
||||
func (a authSimple) Logout(res http.ResponseWriter, r *http.Request) (err error) {
|
||||
sess, _ := cookieStore.Get(r, strings.Join([]string{mainCfg.Cookie.Prefix, a.AuthenticatorID()}, "-"))
|
||||
sess.Options = mainCfg.GetSessionOpts()
|
||||
sess.Options.MaxAge = -1 // Instant delete
|
||||
return sess.Save(r, res)
|
||||
}
|
84
auth_token.go
Normal file
84
auth_token.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerAuthenticator(&authToken{})
|
||||
}
|
||||
|
||||
type authToken struct {
|
||||
Tokens map[string]string `yaml:"tokens"`
|
||||
}
|
||||
|
||||
// AuthenticatorID needs to return an unique string to identify
|
||||
// this special authenticator
|
||||
func (a authToken) AuthenticatorID() string { return "token" }
|
||||
|
||||
// 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
|
||||
// needs to return the errAuthenticatorUnconfigured
|
||||
func (a *authToken) Configure(yamlSource []byte) error {
|
||||
envelope := struct {
|
||||
Providers struct {
|
||||
Token *authToken `yaml:"token"`
|
||||
} `yaml:"providers"`
|
||||
}{}
|
||||
|
||||
if err := yaml.Unmarshal(yamlSource, &envelope); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if envelope.Providers.Token == nil {
|
||||
return errAuthenticatorUnconfigured
|
||||
}
|
||||
|
||||
a.Tokens = envelope.Providers.Token.Tokens
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DetectUser is used to detect a user without a login form from
|
||||
// a cookie, header or other methods
|
||||
// If no user was detected the errNoValidUserFound needs to be
|
||||
// returned
|
||||
func (a authToken) DetectUser(r *http.Request) (string, []string, error) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
|
||||
if !strings.HasPrefix(authHeader, "Token ") {
|
||||
return "", nil, errNoValidUserFound
|
||||
}
|
||||
|
||||
tmp := strings.SplitN(authHeader, " ", 2)
|
||||
suppliedToken := tmp[1]
|
||||
|
||||
for user, token := range a.Tokens {
|
||||
if token == suppliedToken {
|
||||
return user, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil, errNoValidUserFound
|
||||
}
|
||||
|
||||
// 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.
|
||||
// If the user did not login correctly the errNoValidUserFound
|
||||
// needs to be returned
|
||||
func (a authToken) Login(res http.ResponseWriter, r *http.Request) error { return errNoValidUserFound }
|
||||
|
||||
// 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.
|
||||
func (a authToken) LoginFields() []loginField { return nil }
|
||||
|
||||
// Logout is called when the user visits the logout endpoint and
|
||||
// needs to destroy any persistent stored cookies
|
||||
func (a authToken) Logout(res http.ResponseWriter, r *http.Request) error { return nil }
|
136
auth_yubikey.go
Normal file
136
auth_yubikey.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/GeertJohan/yubigo"
|
||||
"github.com/Luzifer/go_helpers/str"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerAuthenticator(&authYubikey{})
|
||||
}
|
||||
|
||||
type authYubikey struct {
|
||||
ClientID string `yaml:"client_id"`
|
||||
SecretKey string `yaml:"secret_key"`
|
||||
Devices map[string]string `yaml:"devices"`
|
||||
Groups map[string][]string `yaml:"groups"`
|
||||
}
|
||||
|
||||
// AuthenticatorID needs to return an unique string to identify
|
||||
// this special authenticator
|
||||
func (a authYubikey) AuthenticatorID() string { return "yubikey" }
|
||||
|
||||
// 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
|
||||
// needs to return the errAuthenticatorUnconfigured
|
||||
func (a *authYubikey) Configure(yamlSource []byte) error {
|
||||
envelope := struct {
|
||||
Providers struct {
|
||||
Yubikey *authYubikey `yaml:"yubikey"`
|
||||
} `yaml:"providers"`
|
||||
}{}
|
||||
|
||||
if err := yaml.Unmarshal(yamlSource, &envelope); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if envelope.Providers.Yubikey == nil {
|
||||
return errAuthenticatorUnconfigured
|
||||
}
|
||||
|
||||
a.ClientID = envelope.Providers.Yubikey.ClientID
|
||||
a.SecretKey = envelope.Providers.Yubikey.SecretKey
|
||||
a.Devices = envelope.Providers.Yubikey.Devices
|
||||
a.Groups = envelope.Providers.Yubikey.Groups
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DetectUser is used to detect a user without a login form from
|
||||
// a cookie, header or other methods
|
||||
// If no user was detected the errNoValidUserFound needs to be
|
||||
// returned
|
||||
func (a authYubikey) DetectUser(r *http.Request) (string, []string, error) {
|
||||
sess, err := cookieStore.Get(r, strings.Join([]string{mainCfg.Cookie.Prefix, a.AuthenticatorID()}, "-"))
|
||||
if err != nil {
|
||||
return "", nil, errNoValidUserFound
|
||||
}
|
||||
|
||||
user, ok := sess.Values["user"].(string)
|
||||
if !ok {
|
||||
return "", nil, errNoValidUserFound
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
for group, users := range a.Groups {
|
||||
if str.StringInSlice(user, users) {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// If the user did not login correctly the errNoValidUserFound
|
||||
// needs to be returned
|
||||
func (a authYubikey) Login(res http.ResponseWriter, r *http.Request) error {
|
||||
keyInput := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "key-input"}, "-"))
|
||||
|
||||
yubiAuth, err := yubigo.NewYubiAuth(a.ClientID, a.SecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, ok, err := yubiAuth.Verify(keyInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
// Not a valid authentication
|
||||
return errNoValidUserFound
|
||||
}
|
||||
|
||||
user, ok := a.Devices[keyInput[:12]]
|
||||
if !ok {
|
||||
// We do not have a definition for that key
|
||||
return errNoValidUserFound
|
||||
}
|
||||
|
||||
sess, _ := cookieStore.Get(r, strings.Join([]string{mainCfg.Cookie.Prefix, a.AuthenticatorID()}, "-"))
|
||||
sess.Options = mainCfg.GetSessionOpts()
|
||||
sess.Values["user"] = user
|
||||
return sess.Save(r, res)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (a authYubikey) LoginFields() (fields []loginField) {
|
||||
return []loginField{
|
||||
{
|
||||
Label: "Yubikey One-Time-Password",
|
||||
Name: "key-input",
|
||||
Placeholder: "Press the button of your Yubikey...",
|
||||
Type: "text",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Logout is called when the user visits the logout endpoint and
|
||||
// needs to destroy any persistent stored cookies
|
||||
func (a authYubikey) Logout(res http.ResponseWriter, r *http.Request) (err error) {
|
||||
sess, _ := cookieStore.Get(r, strings.Join([]string{mainCfg.Cookie.Prefix, a.AuthenticatorID()}, "-"))
|
||||
sess.Options = mainCfg.GetSessionOpts()
|
||||
sess.Options.MaxAge = -1 // Instant delete
|
||||
return sess.Save(r, res)
|
||||
}
|
65
config.yaml
Normal file
65
config.yaml
Normal file
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
|
||||
login:
|
||||
title: "luzifer.io - Login"
|
||||
default_method: "simple"
|
||||
names:
|
||||
simple: "Username / Password"
|
||||
yubikey: "Yubikey"
|
||||
|
||||
cookie:
|
||||
domain: ".example.com"
|
||||
authentication_key: "Ff1uWJcLouKu9kwxgbnKcU3ps47gps72sxEz79TGHFCpJNCPtiZAFDisM4MWbstH"
|
||||
expire: 3600 # Optional, default: 3600
|
||||
prefix: "nginx-sso" # Optional, default: nginx-sso
|
||||
secure: true # Optional, default: false
|
||||
|
||||
# Optional, default: 127.0.0.1:8082
|
||||
listen:
|
||||
addr: "127.0.0.1"
|
||||
port: 8082
|
||||
|
||||
acl:
|
||||
rule_sets:
|
||||
- rules:
|
||||
- field: "host"
|
||||
equals: "test.example.com"
|
||||
- field: "x-origin-uri"
|
||||
regexp: "^/api"
|
||||
allow: ["luzifer", "@admins"]
|
||||
|
||||
providers:
|
||||
# Authentication against embedded user database
|
||||
# Supports: Users, Groups
|
||||
simple:
|
||||
# Unique username mapped to bcrypt hashed password
|
||||
users:
|
||||
luzifer: "$2a$10$FSGAF8qDWX52aBID8.WpxOyCvfSQ3JIUVFiwyd1jolb4jM3BzJmNu"
|
||||
|
||||
# Groupname to users mapping
|
||||
groups:
|
||||
admins: ["luzifer"]
|
||||
|
||||
# Authentication against embedded token directory
|
||||
# Supports: Users
|
||||
token:
|
||||
# Mapping of unique token names to the token
|
||||
tokens:
|
||||
tokenname: "MYTOKEN"
|
||||
|
||||
# Authentication against Yubikey cloud validation servers
|
||||
# Supports: Users, Groups
|
||||
yubikey:
|
||||
# Get your client / secret from https://upgrade.yubico.com/getapikey/
|
||||
client_id: "12345"
|
||||
secret_key: "foobar"
|
||||
|
||||
# First 12 characters of the OTP string mapped to the username
|
||||
devices:
|
||||
ccccccfcvuul: "luzifer"
|
||||
|
||||
# Groupname to users mapping
|
||||
groups:
|
||||
admins: ["luzifer"]
|
||||
|
||||
...
|
105
frontend/index.html
Normal file
105
frontend/index.html
Normal file
|
@ -0,0 +1,105 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
||||
<title>{{ login.Title }}</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=" crossorigin="anonymous" />
|
||||
|
||||
<style>
|
||||
html, body, .container, .row { height: 100%; }
|
||||
.vertical-align { display: flex; flex-direction: column; justify-content: center; }
|
||||
.modal-content { background-color: darkcyan; }
|
||||
.modal-heading h2 { color: white; }
|
||||
.nav-tabs>li>a { color: white; }
|
||||
.nav-tabs>li.active>a, .nav-tabs>li>a:hover { color: #333; }
|
||||
.tab-pane { padding-top: 10px; color: white; }
|
||||
</style>
|
||||
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"
|
||||
integrity="sha256-3Jy/GbSLrg0o9y5Z5n1uw0qxZECH7C6OQpVBgNFYa0g=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.min.js"
|
||||
integrity="sha256-g6iAfvZp+nDQ2TdTR/VVKJf3bGro4ub5fvWSWVRi2NE=" crossorigin="anonymous"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<div class="row vertical-align">
|
||||
<div class="col-md-offset-2 col-md-8">
|
||||
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-heading">
|
||||
<h2 class="text-center">{{ login.Title }}</h2>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Nav tabs -->
|
||||
{% if active_methods | length > 1 %}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
{% for method in active_methods sorted %}
|
||||
<li role="presentation" class="{% if method == login.DefaultMethod %}active{% endif %}">
|
||||
{% for name, desc in login.Names %}{% if method == name %}
|
||||
<a href="#{{ method }}" aria-controls="{{ method }}" role="tab" data-toggle="tab">{{ desc }}</a>
|
||||
{% endif %}{% endfor %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
{% for method, fields in active_methods sorted %}
|
||||
<div role="tabpanel" class="tab-pane {% if method == login.DefaultMethod %}active{% endif %}" id="{{ method }}">
|
||||
<form action="/login" method="post">
|
||||
{% for field in fields %}
|
||||
<div class="form-group">
|
||||
<label for="{{ method }}-{{ field.Name }}">{{ field.Label }}</label>
|
||||
<input type="{{ field.Type }}" class="form-control" placeholder="{{ field.Placeholder }}"
|
||||
name="{{ method }}-{{ field.Name }}" id="{{ method }}-{{ field.Name }}" />
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-group text-center">
|
||||
<button type="submit" class="btn btn-success btn-lg">Login</button>
|
||||
<input type="hidden" name="go" value="{{ go }}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div> <!-- /.panel-body -->
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
|
||||
</div> <!-- /.col-md-8 -->
|
||||
</div> <!-- /.row -->
|
||||
|
||||
</div> <!-- /.container -->
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"
|
||||
integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"
|
||||
integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$(e.target.hash).find('input:first').focus();
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
172
main.go
Normal file
172
main.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/Luzifer/rconfig"
|
||||
"github.com/flosch/pongo2"
|
||||
"github.com/gorilla/sessions"
|
||||
log "github.com/sirupsen/logrus"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type mainConfig struct {
|
||||
ACL acl `yaml:"acl"`
|
||||
Cookie struct {
|
||||
Domain string `yaml:"domain"`
|
||||
AuthKey string `yaml:"authentication_key"`
|
||||
Expire int `yaml:"expire"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Secure bool `yaml:"secure"`
|
||||
}
|
||||
Listen struct {
|
||||
Addr string `yaml:"addr"`
|
||||
Port int `yaml:"port"`
|
||||
} `yaml:"listen"`
|
||||
Login struct {
|
||||
Title string `yaml:"title"`
|
||||
DefaultMethod string `yaml:"default_method"`
|
||||
Names map[string]string `yaml:"names"`
|
||||
} `yaml:"login"`
|
||||
}
|
||||
|
||||
func (m mainConfig) GetSessionOpts() *sessions.Options {
|
||||
return &sessions.Options{
|
||||
Path: "/",
|
||||
Domain: m.Cookie.Domain,
|
||||
MaxAge: m.Cookie.Expire,
|
||||
Secure: m.Cookie.Secure,
|
||||
HttpOnly: true,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
cfg = struct {
|
||||
ConfigFile string `flag:"config,c" default:"config.yaml" env:"CONFIG" description:"Location of the configuration file"`
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Level of logs to display (debug, info, warn, error)"`
|
||||
TemplateDir string `flag:"frontend-dir" default:"./frontend/" env:"FRONTEND_DIR" description:"Location of the directory containing the web assets"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
}{}
|
||||
|
||||
mainCfg = mainConfig{}
|
||||
cookieStore *sessions.CookieStore
|
||||
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := rconfig.Parse(&cfg); err != nil {
|
||||
log.WithError(err).Fatal("Unable to parse commandline options")
|
||||
}
|
||||
|
||||
if l, err := log.ParseLevel(cfg.LogLevel); err != nil {
|
||||
log.WithError(err).Fatal("Unable to parse log level")
|
||||
} else {
|
||||
log.SetLevel(l)
|
||||
}
|
||||
|
||||
if cfg.VersionAndExit {
|
||||
fmt.Printf("nginx-sso %s\n", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Set sane defaults for main configuration
|
||||
mainCfg.Cookie.Prefix = "nginx-sso"
|
||||
mainCfg.Cookie.Expire = 3600
|
||||
mainCfg.Listen.Addr = "127.0.0.1"
|
||||
mainCfg.Listen.Port = 8082
|
||||
}
|
||||
|
||||
func main() {
|
||||
yamlSource, err := ioutil.ReadFile(cfg.ConfigFile)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Unable to read configuration file")
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(yamlSource, &mainCfg); err != nil {
|
||||
log.WithError(err).Fatal("Unable to load configuration file")
|
||||
}
|
||||
|
||||
if err := initializeAuthenticators(yamlSource); err != nil {
|
||||
log.WithError(err).Fatal("Unable to configure authentication")
|
||||
}
|
||||
|
||||
cookieStore = sessions.NewCookieStore([]byte(mainCfg.Cookie.AuthKey))
|
||||
|
||||
http.HandleFunc("/auth", handleAuthRequest)
|
||||
http.HandleFunc("/login", handleLoginRequest)
|
||||
http.HandleFunc("/logout", handleLogoutRequest)
|
||||
|
||||
http.ListenAndServe(fmt.Sprintf("%s:%d", mainCfg.Listen.Addr, mainCfg.Listen.Port), nil)
|
||||
}
|
||||
|
||||
func handleAuthRequest(res http.ResponseWriter, r *http.Request) {
|
||||
user, groups, err := detectUser(r)
|
||||
|
||||
switch err {
|
||||
case errNoValidUserFound:
|
||||
http.Error(res, "No valid user found", http.StatusUnauthorized)
|
||||
|
||||
case nil:
|
||||
if !mainCfg.ACL.HasAccess(user, groups, r) {
|
||||
http.Error(res, "Access denied for this resource", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
res.Header().Set("X-Username", user)
|
||||
res.WriteHeader(http.StatusOK)
|
||||
|
||||
default:
|
||||
log.WithError(err).Error("Error while handling auth request")
|
||||
http.Error(res, "Something went wrong", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func handleLoginRequest(res http.ResponseWriter, r *http.Request) {
|
||||
if _, _, err := detectUser(r); err == nil {
|
||||
// There is already a valid user
|
||||
http.Redirect(res, r, r.URL.Query().Get("go"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
err := loginUser(res, r)
|
||||
switch err {
|
||||
case errNoValidUserFound:
|
||||
http.Redirect(res, r, "/login?go="+url.QueryEscape(r.FormValue("go")), http.StatusFound)
|
||||
return
|
||||
case nil:
|
||||
http.Redirect(res, r, r.FormValue("go"), http.StatusFound)
|
||||
return
|
||||
default:
|
||||
log.WithError(err).Error("Login failed with unexpected error")
|
||||
http.Redirect(res, r, "/login?go="+url.QueryEscape(r.FormValue("go")), http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tpl := pongo2.Must(pongo2.FromFile(path.Join(cfg.TemplateDir, "index.html")))
|
||||
if err := tpl.ExecuteWriter(pongo2.Context{
|
||||
"active_methods": getFrontendAuthenticators(),
|
||||
"go": r.URL.Query().Get("go"),
|
||||
"login": mainCfg.Login,
|
||||
}, res); err != nil {
|
||||
log.WithError(err).Error("Unable to render template")
|
||||
http.Error(res, "Something went wrong", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func handleLogoutRequest(res http.ResponseWriter, r *http.Request) {
|
||||
if err := logoutUser(res, r); err != nil {
|
||||
log.WithError(err).Error("Failed to logout user")
|
||||
http.Error(res, "Something went wrong", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(res, r, r.URL.Query().Get("go"), http.StatusFound)
|
||||
}
|
161
registry.go
Normal file
161
registry.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type authenticator interface {
|
||||
// AuthenticatorID needs to return an unique string to identify
|
||||
// this special authenticator
|
||||
AuthenticatorID() (id string)
|
||||
|
||||
// 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
|
||||
// needs to return the errAuthenticatorUnconfigured
|
||||
Configure(yamlSource []byte) (err error)
|
||||
|
||||
// DetectUser is used to detect a user without a login form from
|
||||
// a cookie, header or other methods
|
||||
// If no user was detected the errNoValidUserFound needs to be
|
||||
// returned
|
||||
DetectUser(r *http.Request) (user string, groups []string, err error)
|
||||
|
||||
// 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.
|
||||
// If the user did not login correctly the errNoValidUserFound
|
||||
// needs to be returned
|
||||
Login(res http.ResponseWriter, r *http.Request) (err error)
|
||||
|
||||
// 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.
|
||||
LoginFields() (fields []loginField)
|
||||
|
||||
// Logout is called when the user visits the logout endpoint and
|
||||
// needs to destroy any persistent stored cookies
|
||||
Logout(res http.ResponseWriter, r *http.Request) (err error)
|
||||
}
|
||||
|
||||
type loginField struct {
|
||||
Label string
|
||||
Name string
|
||||
Placeholder string
|
||||
Type string
|
||||
}
|
||||
|
||||
var (
|
||||
errAuthenticatorUnconfigured = errors.New("No valid configuration found for this authenticator")
|
||||
errNoValidUserFound = errors.New("No valid users found")
|
||||
|
||||
authenticatorRegistry = []authenticator{}
|
||||
authenticatorRegistryMutex sync.RWMutex
|
||||
|
||||
activeAuthenticators = []authenticator{}
|
||||
)
|
||||
|
||||
func registerAuthenticator(a authenticator) {
|
||||
authenticatorRegistryMutex.Lock()
|
||||
defer authenticatorRegistryMutex.Unlock()
|
||||
|
||||
authenticatorRegistry = append(authenticatorRegistry, a)
|
||||
}
|
||||
|
||||
func initializeAuthenticators(yamlSource []byte) error {
|
||||
authenticatorRegistryMutex.Lock()
|
||||
defer authenticatorRegistryMutex.Unlock()
|
||||
|
||||
for _, a := range authenticatorRegistry {
|
||||
err := a.Configure(yamlSource)
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
activeAuthenticators = append(activeAuthenticators, a)
|
||||
log.WithFields(log.Fields{"authenticator": a.AuthenticatorID()}).Debug("Activated authenticator")
|
||||
case errAuthenticatorUnconfigured:
|
||||
log.WithFields(log.Fields{"authenticator": a.AuthenticatorID()}).Debug("Authenticator unconfigured")
|
||||
// This is okay.
|
||||
default:
|
||||
return fmt.Errorf("Authenticator configuration caused an error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(activeAuthenticators) == 0 {
|
||||
return fmt.Errorf("No authenticator configurations supplied")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectUser(r *http.Request) (string, []string, error) {
|
||||
authenticatorRegistryMutex.RLock()
|
||||
defer authenticatorRegistryMutex.RUnlock()
|
||||
|
||||
for _, a := range activeAuthenticators {
|
||||
user, groups, err := a.DetectUser(r)
|
||||
switch err {
|
||||
case nil:
|
||||
return user, groups, err
|
||||
case errNoValidUserFound:
|
||||
// This is okay.
|
||||
default:
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil, errNoValidUserFound
|
||||
}
|
||||
|
||||
func loginUser(res http.ResponseWriter, r *http.Request) error {
|
||||
authenticatorRegistryMutex.RLock()
|
||||
defer authenticatorRegistryMutex.RUnlock()
|
||||
|
||||
for _, a := range activeAuthenticators {
|
||||
err := a.Login(res, r)
|
||||
switch err {
|
||||
case nil:
|
||||
return nil
|
||||
case errNoValidUserFound:
|
||||
// This is okay.
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return errNoValidUserFound
|
||||
}
|
||||
|
||||
func logoutUser(res http.ResponseWriter, r *http.Request) error {
|
||||
authenticatorRegistryMutex.RLock()
|
||||
defer authenticatorRegistryMutex.RUnlock()
|
||||
|
||||
for _, a := range activeAuthenticators {
|
||||
if err := a.Logout(res, r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFrontendAuthenticators() map[string][]loginField {
|
||||
authenticatorRegistryMutex.RLock()
|
||||
defer authenticatorRegistryMutex.RUnlock()
|
||||
|
||||
output := map[string][]loginField{}
|
||||
for _, a := range activeAuthenticators {
|
||||
if len(a.LoginFields()) == 0 {
|
||||
continue
|
||||
}
|
||||
output[a.AuthenticatorID()] = a.LoginFields()
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
Loading…
Reference in a new issue