aboutsummaryrefslogtreecommitdiff
path: root/internal/auth
diff options
context:
space:
mode:
Diffstat (limited to 'internal/auth')
-rw-r--r--internal/auth/auth.go196
-rw-r--r--internal/auth/github/config.go58
-rw-r--r--internal/auth/github/github.go50
-rw-r--r--internal/auth/github/provider.go57
-rw-r--r--internal/auth/ldap/config.go (renamed from internal/auth/ldap/ldap.go)136
-rw-r--r--internal/auth/ldap/provider.go78
-rw-r--r--internal/auth/pam/config.go13
-rw-r--r--internal/auth/pam/pam.go18
-rw-r--r--internal/auth/pam/pam_stub.go4
-rw-r--r--internal/auth/pam/provider.go54
-rw-r--r--internal/auth/smtp/config.go58
-rw-r--r--internal/auth/smtp/provider.go132
12 files changed, 603 insertions, 251 deletions
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
index fe4ad679..2ae012d3 100644
--- a/internal/auth/auth.go
+++ b/internal/auth/auth.go
@@ -5,144 +5,86 @@
package auth
import (
- "strings"
+ "fmt"
- "github.com/go-macaron/session"
- gouuid "github.com/satori/go.uuid"
- "gopkg.in/macaron.v1"
- log "unknwon.dev/clog/v2"
+ "gogs.io/gogs/internal/errutil"
+)
- "gogs.io/gogs/internal/conf"
- "gogs.io/gogs/internal/db"
- "gogs.io/gogs/internal/tool"
+type Type int
+
+// Note: New type must append to the end of list to maintain backward compatibility.
+const (
+ None Type = iota
+ Plain // 1
+ LDAP // 2
+ SMTP // 3
+ PAM // 4
+ DLDAP // 5
+ GitHub // 6
)
-func IsAPIPath(url string) bool {
- return strings.HasPrefix(url, "/api/")
+// Name returns the human-readable name for given authentication type.
+func Name(typ Type) string {
+ return map[Type]string{
+ LDAP: "LDAP (via BindDN)",
+ DLDAP: "LDAP (simple auth)", // Via direct bind
+ SMTP: "SMTP",
+ PAM: "PAM",
+ GitHub: "GitHub",
+ }[typ]
}
-// SignedInID returns the id of signed in user, along with one bool value which indicates whether user uses token
-// authentication.
-func SignedInID(c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bool) {
- if !db.HasEngine {
- return 0, false
- }
-
- // Check access token.
- if IsAPIPath(c.Req.URL.Path) {
- tokenSHA := c.Query("token")
- if len(tokenSHA) <= 0 {
- tokenSHA = c.Query("access_token")
- }
- if len(tokenSHA) == 0 {
- // Well, check with header again.
- auHead := c.Req.Header.Get("Authorization")
- if len(auHead) > 0 {
- auths := strings.Fields(auHead)
- if len(auths) == 2 && auths[0] == "token" {
- tokenSHA = auths[1]
- }
- }
- }
-
- // Let's see if token is valid.
- if len(tokenSHA) > 0 {
- t, err := db.AccessTokens.GetBySHA(tokenSHA)
- if err != nil {
- if !db.IsErrAccessTokenNotExist(err) {
- log.Error("GetAccessTokenBySHA: %v", err)
- }
- return 0, false
- }
- if err = db.AccessTokens.Save(t); err != nil {
- log.Error("UpdateAccessToken: %v", err)
- }
- return t.UserID, true
- }
- }
+var _ errutil.NotFound = (*ErrBadCredentials)(nil)
- uid := sess.Get("uid")
- if uid == nil {
- return 0, false
- }
- if id, ok := uid.(int64); ok {
- if _, err := db.GetUserByID(id); err != nil {
- if !db.IsErrUserNotExist(err) {
- log.Error("Failed to get user by ID: %v", err)
- }
- return 0, false
- }
- return id, false
- }
- return 0, false
+type ErrBadCredentials struct {
+ Args errutil.Args
}
-// SignedInUser returns the user object of signed in user, along with two bool values,
-// which indicate whether user uses HTTP Basic Authentication or token authentication respectively.
-func SignedInUser(ctx *macaron.Context, sess session.Store) (_ *db.User, isBasicAuth bool, isTokenAuth bool) {
- if !db.HasEngine {
- return nil, false, false
- }
-
- uid, isTokenAuth := SignedInID(ctx, sess)
-
- if uid <= 0 {
- if conf.Auth.EnableReverseProxyAuthentication {
- webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader)
- if len(webAuthUser) > 0 {
- u, err := db.GetUserByName(webAuthUser)
- if err != nil {
- if !db.IsErrUserNotExist(err) {
- log.Error("Failed to get user by name: %v", err)
- return nil, false, false
- }
-
- // Check if enabled auto-registration.
- if conf.Auth.EnableReverseProxyAutoRegistration {
- u := &db.User{
- Name: webAuthUser,
- Email: gouuid.NewV4().String() + "@localhost",
- Passwd: webAuthUser,
- IsActive: true,
- }
- if err = db.CreateUser(u); err != nil {
- // FIXME: should I create a system notice?
- log.Error("Failed to create user: %v", err)
- return nil, false, false
- } else {
- return u, false, false
- }
- }
- }
- return u, false, false
- }
- }
+func IsErrBadCredentials(err error) bool {
+ _, ok := err.(ErrBadCredentials)
+ return ok
+}
- // Check with basic auth.
- baHead := ctx.Req.Header.Get("Authorization")
- if len(baHead) > 0 {
- auths := strings.Fields(baHead)
- if len(auths) == 2 && auths[0] == "Basic" {
- uname, passwd, _ := tool.BasicAuthDecode(auths[1])
+func (err ErrBadCredentials) Error() string {
+ return fmt.Sprintf("bad credentials: %v", err.Args)
+}
- u, err := db.Users.Authenticate(uname, passwd, -1)
- if err != nil {
- if !db.IsErrUserNotExist(err) {
- log.Error("Failed to authenticate user: %v", err)
- }
- return nil, false, false
- }
+func (ErrBadCredentials) NotFound() bool {
+ return true
+}
- return u, true, false
- }
- }
- return nil, false, false
- }
+// ExternalAccount contains queried information returned by an authenticate provider
+// for an external account.
+type ExternalAccount struct {
+ // REQUIRED: The login to be used for authenticating against the provider.
+ Login string
+ // REQUIRED: The username of the account.
+ Name string
+ // The full name of the account.
+ FullName string
+ // The email address of the account.
+ Email string
+ // The location of the account.
+ Location string
+ // The website of the account.
+ Website string
+ // Whether the user should be prompted as a site admin.
+ Admin bool
+}
- u, err := db.GetUserByID(uid)
- if err != nil {
- log.Error("GetUserByID: %v", err)
- return nil, false, false
- }
- return u, false, isTokenAuth
+// Provider defines an authenticate provider which provides ability to authentication against
+// an external identity provider and query external account information.
+type Provider interface {
+ // Authenticate performs authentication against an external identity provider
+ // using given credentials and returns queried information of the external account.
+ Authenticate(login, password string) (*ExternalAccount, error)
+
+ // Config returns the underlying configuration of the authenticate provider.
+ Config() interface{}
+ // HasTLS returns true if the authenticate provider supports TLS.
+ HasTLS() bool
+ // UseTLS returns true if the authenticate provider is configured to use TLS.
+ UseTLS() bool
+ // SkipTLSVerify returns true if the authenticate provider is configured to skip TLS verify.
+ SkipTLSVerify() bool
}
diff --git a/internal/auth/github/config.go b/internal/auth/github/config.go
new file mode 100644
index 00000000..e4636743
--- /dev/null
+++ b/internal/auth/github/config.go
@@ -0,0 +1,58 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+ "context"
+ "crypto/tls"
+ "net/http"
+ "strings"
+
+ "github.com/google/go-github/github"
+ "github.com/pkg/errors"
+)
+
+// Config contains configuration for GitHub authentication.
+//
+// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility.
+type Config struct {
+ // the GitHub service endpoint, e.g. https://api.github.com/.
+ APIEndpoint string
+ SkipVerify bool
+}
+
+func (c *Config) doAuth(login, password string) (fullname, email, location, website string, err error) {
+ tp := github.BasicAuthTransport{
+ Username: strings.TrimSpace(login),
+ Password: strings.TrimSpace(password),
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipVerify},
+ },
+ }
+ client, err := github.NewEnterpriseClient(c.APIEndpoint, c.APIEndpoint, tp.Client())
+ if err != nil {
+ return "", "", "", "", errors.Wrap(err, "create new client")
+ }
+ user, _, err := client.Users.Get(context.Background(), "")
+ if err != nil {
+ return "", "", "", "", errors.Wrap(err, "get user info")
+ }
+
+ if user.Name != nil {
+ fullname = *user.Name
+ }
+ if user.Email != nil {
+ email = *user.Email
+ } else {
+ email = login + "+github@local"
+ }
+ if user.Location != nil {
+ location = strings.ToUpper(*user.Location)
+ }
+ if user.HTMLURL != nil {
+ website = strings.ToLower(*user.HTMLURL)
+ }
+ return fullname, email, location, website, nil
+}
diff --git a/internal/auth/github/github.go b/internal/auth/github/github.go
deleted file mode 100644
index a06608a3..00000000
--- a/internal/auth/github/github.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2018 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package github
-
-import (
- "context"
- "crypto/tls"
- "fmt"
- "net/http"
- "strings"
-
- "github.com/google/go-github/github"
-)
-
-func Authenticate(apiEndpoint, login, passwd string) (name string, email string, website string, location string, _ error) {
- tp := github.BasicAuthTransport{
- Username: strings.TrimSpace(login),
- Password: strings.TrimSpace(passwd),
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
- },
- }
- client, err := github.NewEnterpriseClient(apiEndpoint, apiEndpoint, tp.Client())
- if err != nil {
- return "", "", "", "", fmt.Errorf("create new client: %v", err)
- }
- user, _, err := client.Users.Get(context.Background(), "")
- if err != nil {
- return "", "", "", "", fmt.Errorf("get user info: %v", err)
- }
-
- if user.Name != nil {
- name = *user.Name
- }
- if user.Email != nil {
- email = *user.Email
- } else {
- email = login + "+github@local"
- }
- if user.HTMLURL != nil {
- website = strings.ToLower(*user.HTMLURL)
- }
- if user.Location != nil {
- location = strings.ToUpper(*user.Location)
- }
-
- return name, email, website, location, nil
-}
diff --git a/internal/auth/github/provider.go b/internal/auth/github/provider.go
new file mode 100644
index 00000000..4add2e54
--- /dev/null
+++ b/internal/auth/github/provider.go
@@ -0,0 +1,57 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+ "strings"
+
+ "gogs.io/gogs/internal/auth"
+)
+
+// Provider contains configuration of a PAM authentication provider.
+type Provider struct {
+ config *Config
+}
+
+// NewProvider creates a new PAM authentication provider.
+func NewProvider(cfg *Config) auth.Provider {
+ return &Provider{
+ config: cfg,
+ }
+}
+
+func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
+ fullname, email, website, location, err := p.config.doAuth(login, password)
+ if err != nil {
+ if strings.Contains(err.Error(), "401") {
+ return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+ }
+ return nil, err
+ }
+ return &auth.ExternalAccount{
+ Login: login,
+ Name: login,
+ FullName: fullname,
+ Email: email,
+ Location: location,
+ Website: website,
+ }, nil
+}
+
+func (p *Provider) Config() interface{} {
+ return p.config
+}
+
+func (p *Provider) HasTLS() bool {
+ return true
+}
+
+func (p *Provider) UseTLS() bool {
+ return true
+}
+
+func (p *Provider) SkipTLSVerify() bool {
+ return p.config.SkipVerify
+}
diff --git a/internal/auth/ldap/ldap.go b/internal/auth/ldap/config.go
index d8f8458e..0c33368d 100644
--- a/internal/auth/ldap/ldap.go
+++ b/internal/auth/ldap/config.go
@@ -2,8 +2,8 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
-// Package ldap provide functions & structure to query a LDAP ldap directory
-// For now, it's mainly tested again an MS Active Directory service, see README.md for more information
+// Package ldap provide functions & structure to query a LDAP ldap directory.
+// For now, it's mainly tested again an MS Active Directory service, see README.md for more information.
package ldap
import (
@@ -15,19 +15,31 @@ import (
log "unknwon.dev/clog/v2"
)
+// SecurityProtocol is the security protocol when the authenticate provider talks to LDAP directory.
type SecurityProtocol int
-// Note: new type must be added at the end of list to maintain compatibility.
+// ⚠️ WARNING: new type must be added at the end of list to maintain compatibility.
const (
SecurityProtocolUnencrypted SecurityProtocol = iota
SecurityProtocolLDAPS
SecurityProtocolStartTLS
)
-// Basic LDAP authentication service
-type Source struct {
+// SecurityProtocolName returns the human-readable name for given security protocol.
+func SecurityProtocolName(protocol SecurityProtocol) string {
+ return map[SecurityProtocol]string{
+ SecurityProtocolUnencrypted: "Unencrypted",
+ SecurityProtocolLDAPS: "LDAPS",
+ SecurityProtocolStartTLS: "StartTLS",
+ }[protocol]
+}
+
+// Config contains configuration for LDAP authentication.
+//
+// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility.
+type Config struct {
Host string // LDAP host
- Port int // port number
+ Port int // Port number
SecurityProtocol SecurityProtocol
SkipVerify bool
BindDN string `ini:"bind_dn,omitempty"` // DN to bind with
@@ -37,18 +49,22 @@ type Source struct {
AttributeUsername string // Username attribute
AttributeName string // First name attribute
AttributeSurname string // Surname attribute
- AttributeMail string // E-mail attribute
- AttributesInBind bool // fetch attributes in bind context (not user)
+ AttributeMail string // Email attribute
+ AttributesInBind bool // Fetch attributes in bind context (not user)
Filter string // Query filter to validate entry
AdminFilter string // Query filter to check if user is admin
- GroupEnabled bool // if the group checking is enabled
- GroupDN string `ini:"group_dn"` // Group Search Base
- GroupFilter string // Group Name Filter
+ GroupEnabled bool // Whether the group checking is enabled
+ GroupDN string `ini:"group_dn"` // Group search base
+ GroupFilter string // Group name filter
GroupMemberUID string `ini:"group_member_uid"` // Group Attribute containing array of UserUID
- UserUID string `ini:"user_uid"` // User Attribute listed in Group
+ UserUID string `ini:"user_uid"` // User Attribute listed in group
+}
+
+func (c *Config) SecurityProtocolName() string {
+ return SecurityProtocolName(c.SecurityProtocol)
}
-func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
+func (c *Config) sanitizedUserQuery(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00()*\\"
if strings.ContainsAny(username, badCharacters) {
@@ -56,10 +72,10 @@ func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
return "", false
}
- return strings.Replace(ls.Filter, "%s", username, -1), true
+ return strings.Replace(c.Filter, "%s", username, -1), true
}
-func (ls *Source) sanitizedUserDN(username string) (string, bool) {
+func (c *Config) sanitizedUserDN(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4514: "special characters"
badCharacters := "\x00()*\\,='\"#+;<>"
if strings.ContainsAny(username, badCharacters) || strings.HasPrefix(username, " ") || strings.HasSuffix(username, " ") {
@@ -67,10 +83,10 @@ func (ls *Source) sanitizedUserDN(username string) (string, bool) {
return "", false
}
- return strings.Replace(ls.UserDN, "%s", username, -1), true
+ return strings.Replace(c.UserDN, "%s", username, -1), true
}
-func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
+func (c *Config) sanitizedGroupFilter(group string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00*\\"
if strings.ContainsAny(group, badCharacters) {
@@ -81,7 +97,7 @@ func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
return group, true
}
-func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
+func (c *Config) sanitizedGroupDN(groupDn string) (string, bool) {
// See http://tools.ietf.org/search/rfc4514: "special characters"
badCharacters := "\x00()*\\'\"#+;<>"
if strings.ContainsAny(groupDn, badCharacters) || strings.HasPrefix(groupDn, " ") || strings.HasSuffix(groupDn, " ") {
@@ -92,12 +108,12 @@ func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
return groupDn, true
}
-func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
+func (c *Config) findUserDN(l *ldap.Conn, name string) (string, bool) {
log.Trace("Search for LDAP user: %s", name)
- if len(ls.BindDN) > 0 && len(ls.BindPassword) > 0 {
+ if len(c.BindDN) > 0 && len(c.BindPassword) > 0 {
// Replace placeholders with username
- bindDN := strings.Replace(ls.BindDN, "%s", name, -1)
- err := l.Bind(bindDN, ls.BindPassword)
+ bindDN := strings.Replace(c.BindDN, "%s", name, -1)
+ err := l.Bind(bindDN, c.BindPassword)
if err != nil {
log.Trace("LDAP: Failed to bind as BindDN '%s': %v", bindDN, err)
return "", false
@@ -108,23 +124,23 @@ func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
}
// A search for the user.
- userFilter, ok := ls.sanitizedUserQuery(name)
+ userFilter, ok := c.sanitizedUserQuery(name)
if !ok {
return "", false
}
- log.Trace("LDAP: Searching for DN using filter '%s' and base '%s'", userFilter, ls.UserBase)
+ log.Trace("LDAP: Searching for DN using filter %q and base %q", userFilter, c.UserBase)
search := ldap.NewSearchRequest(
- ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0,
+ c.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0,
false, userFilter, []string{}, nil)
// Ensure we found a user
sr, err := l.Search(search)
if err != nil || len(sr.Entries) < 1 {
- log.Trace("LDAP: Failed search using filter '%s': %v", userFilter, err)
+ log.Trace("LDAP: Failed to search using filter %q: %v", userFilter, err)
return "", false
} else if len(sr.Entries) > 1 {
- log.Trace("LDAP: Filter '%s' returned more than one user", userFilter)
+ log.Trace("LDAP: Filter %q returned more than one user", userFilter)
return "", false
}
@@ -137,7 +153,7 @@ func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
return userDN, true
}
-func dial(ls *Source) (*ldap.Conn, error) {
+func dial(ls *Config) (*ldap.Conn, error) {
log.Trace("LDAP: Dialing with security protocol '%v' without verifying: %v", ls.SecurityProtocol, ls.SkipVerify)
tlsCfg := &tls.Config{
@@ -174,26 +190,26 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error {
return err
}
-// searchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
-func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) {
+// searchEntry searches an LDAP source if an entry (name, passwd) is valid and in the specific filter.
+func (c *Config) searchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) {
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
if len(passwd) == 0 {
log.Trace("authentication failed for '%s' with empty password", name)
return "", "", "", "", false, false
}
- l, err := dial(ls)
+ l, err := dial(c)
if err != nil {
- log.Error("LDAP connect failed for '%s': %v", ls.Host, err)
+ log.Error("LDAP connect failed for '%s': %v", c.Host, err)
return "", "", "", "", false, false
}
defer l.Close()
var userDN string
if directBind {
- log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN)
+ log.Trace("LDAP will bind directly via UserDN template: %s", c.UserDN)
var ok bool
- userDN, ok = ls.sanitizedUserDN(name)
+ userDN, ok = c.sanitizedUserDN(name)
if !ok {
return "", "", "", "", false, false
}
@@ -201,13 +217,13 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
log.Trace("LDAP will use BindDN")
var found bool
- userDN, found = ls.findUserDN(l, name)
+ userDN, found = c.findUserDN(l, name)
if !found {
return "", "", "", "", false, false
}
}
- if directBind || !ls.AttributesInBind {
+ if directBind || !c.AttributesInBind {
// binds user (checking password) before looking-up attributes in user context
err = bindUser(l, userDN, passwd)
if err != nil {
@@ -215,18 +231,18 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
}
}
- userFilter, ok := ls.sanitizedUserQuery(name)
+ userFilter, ok := c.sanitizedUserQuery(name)
if !ok {
return "", "", "", "", false, false
}
- log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'",
- ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID, userFilter, userDN)
+ log.Trace("Fetching attributes %q, %q, %q, %q, %q with user filter %q and user DN %q",
+ c.AttributeUsername, c.AttributeName, c.AttributeSurname, c.AttributeMail, c.UserUID, userFilter, userDN)
+
search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
- []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID},
+ []string{c.AttributeUsername, c.AttributeName, c.AttributeSurname, c.AttributeMail, c.UserUID},
nil)
-
sr, err := l.Search(search)
if err != nil {
log.Error("LDAP: User search failed: %v", err)
@@ -241,27 +257,27 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
return "", "", "", "", false, false
}
- username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
- firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
- surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
- mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
- uid := sr.Entries[0].GetAttributeValue(ls.UserUID)
+ username := sr.Entries[0].GetAttributeValue(c.AttributeUsername)
+ firstname := sr.Entries[0].GetAttributeValue(c.AttributeName)
+ surname := sr.Entries[0].GetAttributeValue(c.AttributeSurname)
+ mail := sr.Entries[0].GetAttributeValue(c.AttributeMail)
+ uid := sr.Entries[0].GetAttributeValue(c.UserUID)
// Check group membership
- if ls.GroupEnabled {
- groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
+ if c.GroupEnabled {
+ groupFilter, ok := c.sanitizedGroupFilter(c.GroupFilter)
if !ok {
return "", "", "", "", false, false
}
- groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN)
+ groupDN, ok := c.sanitizedGroupDN(c.GroupDN)
if !ok {
return "", "", "", "", false, false
}
- log.Trace("LDAP: Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN)
+ log.Trace("LDAP: Fetching groups '%v' with filter '%s' and base '%s'", c.GroupMemberUID, groupFilter, groupDN)
groupSearch := ldap.NewSearchRequest(
groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
- []string{ls.GroupMemberUID},
+ []string{c.GroupMemberUID},
nil)
srg, err := l.Search(groupSearch)
@@ -274,9 +290,9 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
}
isMember := false
- if ls.UserUID == "dn" {
+ if c.UserUID == "dn" {
for _, group := range srg.Entries {
- for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
+ for _, member := range group.GetAttributeValues(c.GroupMemberUID) {
if member == sr.Entries[0].DN {
isMember = true
}
@@ -284,7 +300,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
}
} else {
for _, group := range srg.Entries {
- for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
+ for _, member := range group.GetAttributeValues(c.GroupMemberUID) {
if member == uid {
isMember = true
}
@@ -293,17 +309,17 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
}
if !isMember {
- log.Trace("LDAP: Group membership test failed [username: %s, group_member_uid: %s, user_uid: %s", username, ls.GroupMemberUID, uid)
+ log.Trace("LDAP: Group membership test failed [username: %s, group_member_uid: %s, user_uid: %s", username, c.GroupMemberUID, uid)
return "", "", "", "", false, false
}
}
isAdmin := false
- if len(ls.AdminFilter) > 0 {
- log.Trace("Checking admin with filter '%s' and base '%s'", ls.AdminFilter, userDN)
+ if len(c.AdminFilter) > 0 {
+ log.Trace("Checking admin with filter '%s' and base '%s'", c.AdminFilter, userDN)
search = ldap.NewSearchRequest(
- userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter,
- []string{ls.AttributeName},
+ userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, c.AdminFilter,
+ []string{c.AttributeName},
nil)
sr, err = l.Search(search)
@@ -316,7 +332,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
}
}
- if !directBind && ls.AttributesInBind {
+ if !directBind && c.AttributesInBind {
// binds user (checking password) after looking-up attributes in BindDN context
err = bindUser(l, userDN, passwd)
if err != nil {
diff --git a/internal/auth/ldap/provider.go b/internal/auth/ldap/provider.go
new file mode 100644
index 00000000..a42baa71
--- /dev/null
+++ b/internal/auth/ldap/provider.go
@@ -0,0 +1,78 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package ldap
+
+import (
+ "fmt"
+
+ "gogs.io/gogs/internal/auth"
+)
+
+// Provider contains configuration of an LDAP authentication provider.
+type Provider struct {
+ directBind bool
+ config *Config
+}
+
+// NewProvider creates a new LDAP authentication provider.
+func NewProvider(directBind bool, cfg *Config) auth.Provider {
+ return &Provider{
+ directBind: directBind,
+ config: cfg,
+ }
+}
+
+// Authenticate queries if login/password is valid against the LDAP directory pool,
+// and returns queried information when succeeded.
+func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
+ username, fn, sn, email, isAdmin, succeed := p.config.searchEntry(login, password, p.directBind)
+ if !succeed {
+ return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+ }
+
+ if len(username) == 0 {
+ username = login
+ }
+ if len(email) == 0 {
+ email = fmt.Sprintf("%s@localhost", username)
+ }
+
+ composeFullName := func(firstname, surname, username string) string {
+ switch {
+ case firstname == "" && surname == "":
+ return username
+ case firstname == "":
+ return surname
+ case surname == "":
+ return firstname
+ default:
+ return firstname + " " + surname
+ }
+ }
+
+ return &auth.ExternalAccount{
+ Login: login,
+ Name: username,
+ FullName: composeFullName(fn, sn, username),
+ Email: email,
+ Admin: isAdmin,
+ }, nil
+}
+
+func (p *Provider) Config() interface{} {
+ return p.config
+}
+
+func (p *Provider) HasTLS() bool {
+ return p.config.SecurityProtocol > SecurityProtocolUnencrypted
+}
+
+func (p *Provider) UseTLS() bool {
+ return p.config.SecurityProtocol > SecurityProtocolUnencrypted
+}
+
+func (p *Provider) SkipTLSVerify() bool {
+ return p.config.SkipVerify
+}
diff --git a/internal/auth/pam/config.go b/internal/auth/pam/config.go
new file mode 100644
index 00000000..7a6bc0cc
--- /dev/null
+++ b/internal/auth/pam/config.go
@@ -0,0 +1,13 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package pam
+
+// Config contains configuration for PAM authentication.
+//
+// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility.
+type Config struct {
+ // The name of the PAM service, e.g. system-auth.
+ ServiceName string
+}
diff --git a/internal/auth/pam/pam.go b/internal/auth/pam/pam.go
index 7f326d42..521cd4f0 100644
--- a/internal/auth/pam/pam.go
+++ b/internal/auth/pam/pam.go
@@ -7,29 +7,23 @@
package pam
import (
- "errors"
-
"github.com/msteinert/pam"
+ "github.com/pkg/errors"
)
-func PAMAuth(serviceName, userName, passwd string) error {
- t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) {
+func (c *Config) doAuth(login, password string) error {
+ t, err := pam.StartFunc(c.ServiceName, login, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff:
- return passwd, nil
+ return password, nil
case pam.PromptEchoOn, pam.ErrorMsg, pam.TextInfo:
return "", nil
}
- return "", errors.New("Unrecognized PAM message style")
+ return "", errors.Errorf("unrecognized PAM message style: %v - %s", s, msg)
})
-
if err != nil {
return err
}
- if err = t.Authenticate(0); err != nil {
- return err
- }
-
- return nil
+ return t.Authenticate(0)
}
diff --git a/internal/auth/pam/pam_stub.go b/internal/auth/pam/pam_stub.go
index 33ac751a..9392eab5 100644
--- a/internal/auth/pam/pam_stub.go
+++ b/internal/auth/pam/pam_stub.go
@@ -7,9 +7,9 @@
package pam
import (
- "errors"
+ "github.com/pkg/errors"
)
-func PAMAuth(serviceName, userName, passwd string) error {
+func (c *Config) doAuth(login, password string) error {
return errors.New("PAM not supported")
}
diff --git a/internal/auth/pam/provider.go b/internal/auth/pam/provider.go
new file mode 100644
index 00000000..ad1b7a8c
--- /dev/null
+++ b/internal/auth/pam/provider.go
@@ -0,0 +1,54 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package pam
+
+import (
+ "strings"
+
+ "gogs.io/gogs/internal/auth"
+)
+
+// Provider contains configuration of a PAM authentication provider.
+type Provider struct {
+ config *Config
+}
+
+// NewProvider creates a new PAM authentication provider.
+func NewProvider(cfg *Config) auth.Provider {
+ return &Provider{
+ config: cfg,
+ }
+}
+
+func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
+ err := p.config.doAuth(login, password)
+ if err != nil {
+ if strings.Contains(err.Error(), "Authentication failure") {
+ return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+ }
+ return nil, err
+ }
+
+ return &auth.ExternalAccount{
+ Login: login,
+ Name: login,
+ }, nil
+}
+
+func (p *Provider) Config() interface{} {
+ return p.config
+}
+
+func (p *Provider) HasTLS() bool {
+ return false
+}
+
+func (p *Provider) UseTLS() bool {
+ return false
+}
+
+func (p *Provider) SkipTLSVerify() bool {
+ return false
+}
diff --git a/internal/auth/smtp/config.go b/internal/auth/smtp/config.go
new file mode 100644
index 00000000..33985f45
--- /dev/null
+++ b/internal/auth/smtp/config.go
@@ -0,0 +1,58 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package smtp
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net/smtp"
+
+ "github.com/pkg/errors"
+)
+
+// Config contains configuration for SMTP authentication.
+//
+// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility.
+type Config struct {
+ Auth string
+ Host string
+ Port int
+ AllowedDomains string
+ TLS bool `ini:"tls"`
+ SkipVerify bool
+}
+
+func (c *Config) doAuth(auth smtp.Auth) error {
+ client, err := smtp.Dial(fmt.Sprintf("%s:%d", c.Host, c.Port))
+ if err != nil {
+ return err
+ }
+ defer client.Close()
+
+ if err = client.Hello("gogs"); err != nil {
+ return err
+ }
+
+ if c.TLS {
+ if ok, _ := client.Extension("STARTTLS"); ok {
+ if err = client.StartTLS(&tls.Config{
+ InsecureSkipVerify: c.SkipVerify,
+ ServerName: c.Host,
+ }); err != nil {
+ return err
+ }
+ } else {
+ return errors.New("SMTP server does not support TLS")
+ }
+ }
+
+ if ok, _ := client.Extension("AUTH"); ok {
+ if err = client.Auth(auth); err != nil {
+ return err
+ }
+ return nil
+ }
+ return errors.New("unsupported SMTP authentication method")
+}
diff --git a/internal/auth/smtp/provider.go b/internal/auth/smtp/provider.go
new file mode 100644
index 00000000..3e39aa22
--- /dev/null
+++ b/internal/auth/smtp/provider.go
@@ -0,0 +1,132 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package smtp
+
+import (
+ "net/smtp"
+ "net/textproto"
+ "strings"
+
+ "github.com/pkg/errors"
+ log "unknwon.dev/clog/v2"
+
+ "gogs.io/gogs/internal/auth"
+)
+
+// Provider contains configuration of an SMTP authentication provider.
+type Provider struct {
+ config *Config
+}
+
+// NewProvider creates a new SMTP authentication provider.
+func NewProvider(cfg *Config) auth.Provider {
+ return &Provider{
+ config: cfg,
+ }
+}
+
+// Authenticate queries if login/password is valid against the SMTP server,
+// and returns queried information when succeeded.
+func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
+ // Verify allowed domains
+ if p.config.AllowedDomains != "" {
+ fields := strings.SplitN(login, "@", 3)
+ if len(fields) != 2 {
+ return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+ }
+ domain := fields[1]
+
+ isAllowed := false
+ for _, allowed := range strings.Split(p.config.AllowedDomains, ",") {
+ if domain == allowed {
+ isAllowed = true
+ break
+ }
+ }
+
+ if !isAllowed {
+ return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+ }
+ }
+
+ var smtpAuth smtp.Auth
+ switch p.config.Auth {
+ case Plain:
+ smtpAuth = smtp.PlainAuth("", login, password, p.config.Host)
+ case Login:
+ smtpAuth = &smtpLoginAuth{login, password}
+ default:
+ return nil, errors.Errorf("unsupported SMTP authentication type %q", p.config.Auth)
+ }
+
+ if err := p.config.doAuth(smtpAuth); err != nil {
+ log.Trace("SMTP: Authentication failed: %v", err)
+
+ // Check standard error format first, then fallback to the worse case.
+ tperr, ok := err.(*textproto.Error)
+ if (ok && tperr.Code == 535) ||
+ strings.Contains(err.Error(), "Username and Password not accepted") {
+ return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+ }
+ return nil, err
+ }
+
+ username := login
+
+ // NOTE: It is not required to have "@" in `login` for a successful SMTP authentication.
+ idx := strings.Index(login, "@")
+ if idx > -1 {
+ username = login[:idx]
+ }
+
+ return &auth.ExternalAccount{
+ Login: login,
+ Name: username,
+ Email: login,
+ }, nil
+}
+
+func (p *Provider) Config() interface{} {
+ return p.config
+}
+
+func (p *Provider) HasTLS() bool {
+ return true
+}
+
+func (p *Provider) UseTLS() bool {
+ return p.config.TLS
+}
+
+func (p *Provider) SkipTLSVerify() bool {
+ return p.config.SkipVerify
+}
+
+const (
+ Plain = "PLAIN"
+ Login = "LOGIN"
+)
+
+var AuthTypes = []string{Plain, Login}
+
+type smtpLoginAuth struct {
+ username, password string
+}
+
+func (auth *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+ return "LOGIN", []byte(auth.username), nil
+}
+
+func (auth *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+ if more {
+ switch string(fromServer) {
+ case "Username:":
+ return []byte(auth.username), nil
+ case "Password:":
+ return []byte(auth.password), nil
+ }
+ }
+ return nil, nil
+}