Refactor: Move login out of models (#16199)

`models` does far too much. In particular it handles all `UserSignin`.

It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in.

Therefore we should move this code out of `models`.

This code has to depend on `models` - therefore it belongs in `services`.

There is a package in `services` called `auth` and clearly this functionality belongs in there.

Plan:

- [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication.
- [x] Move `models.UserSignIn` into `auth`
- [x] Move `models.ExternalUserLogin`
- [x] Move most of the `LoginVia*` methods to `auth` or subpackages
- [x] Move Resynchronize functionality to `auth`
  - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files.
- [x] Move the rest of the LDAP functionality in to the ldap subpackage
- [x] Re-factor the login sources to express an interfaces `auth.Source`?
  - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future
- [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable
- [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2
  - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models.
  - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 
- [x] More simplifications of login_source.go may need to be done
- Allow wiring in of notify registration -  *this can now easily be done - but I think we should do it in another PR*  - see #16178 
- More refactors...?
  - OpenID should probably become an auth Method but I think that can be left for another PR
  - Methods should also probably be cleaned up  - again another PR I think.
  - SSPI still needs more refactors.* Rename auth.Auth auth.Method
* Restructure ssh_key.go

- move functions from models/user.go that relate to ssh_key to ssh_key
- split ssh_key.go to try create clearer function domains for allow for
future refactors here.

Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
zeripath 2021-07-24 11:16:34 +01:00 committed by GitHub
parent f135a818f5
commit 5d2e11eedb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 3803 additions and 2951 deletions

View file

@ -1,123 +0,0 @@
Gitea LDAP Authentication Module
===============================
## About
This authentication module attempts to authorize and authenticate a user
against an LDAP server. It provides two methods of authentication: LDAP via
BindDN, and LDAP simple authentication.
LDAP via BindDN functions like most LDAP authentication systems. First, it
queries the LDAP server using a Bind DN and searches for the user that is
attempting to sign in. If the user is found, the module attempts to bind to the
server using the user's supplied credentials. If this succeeds, the user has
been authenticated, and his account information is retrieved and passed to the
Gogs login infrastructure.
LDAP simple authentication does not utilize a Bind DN. Instead, it binds
directly with the LDAP server using the user's supplied credentials. If the bind
succeeds and no filter rules out the user, the user is authenticated.
LDAP via BindDN is recommended for most users. By using a Bind DN, the server
can perform authorization by restricting which entries the Bind DN account can
read. Further, using a Bind DN with reduced permissions can reduce security risk
in the face of application bugs.
## Usage
To use this module, add an LDAP authentication source via the Authentications
section in the admin panel. Both the LDAP via BindDN and the simple auth LDAP
share the following fields:
* Authorization Name **(required)**
* A name to assign to the new method of authorization.
* Host **(required)**
* The address where the LDAP server can be reached.
* Example: mydomain.com
* Port **(required)**
* The port to use when connecting to the server.
* Example: 636
* Enable TLS Encryption (optional)
* Whether to use TLS when connecting to the LDAP server.
* Admin Filter (optional)
* An LDAP filter specifying if a user should be given administrator
privileges. If a user accounts passes the filter, the user will be
privileged as an administrator.
* Example: (objectClass=adminAccount)
* First name attribute (optional)
* The attribute of the user's LDAP record containing the user's first name.
This will be used to populate their account information.
* Example: givenName
* Surname attribute (optional)
* The attribute of the user's LDAP record containing the user's surname This
will be used to populate their account information.
* Example: sn
* E-mail attribute **(required)**
* The attribute of the user's LDAP record containing the user's email
address. This will be used to populate their account information.
* Example: mail
**LDAP via BindDN** adds the following fields:
* Bind DN (optional)
* The DN to bind to the LDAP server with when searching for the user. This
may be left blank to perform an anonymous search.
* Example: cn=Search,dc=mydomain,dc=com
* Bind Password (optional)
* The password for the Bind DN specified above, if any. _Note: The password
is stored in plaintext at the server. As such, ensure that your Bind DN
has as few privileges as possible._
* User Search Base **(required)**
* The LDAP base at which user accounts will be searched for.
* Example: ou=Users,dc=mydomain,dc=com
* User Filter **(required)**
* An LDAP filter declaring how to find the user record that is attempting to
authenticate. The '%s' matching parameter will be substituted with the
user's username.
* Example: (&(objectClass=posixAccount)(uid=%s))
**LDAP using simple auth** adds the following fields:
* User DN **(required)**
* A template to use as the user's DN. The `%s` matching parameter will be
substituted with the user's username.
* Example: cn=%s,ou=Users,dc=mydomain,dc=com
* Example: uid=%s,ou=Users,dc=mydomain,dc=com
* User Search Base (optional)
* The LDAP base at which user accounts will be searched for.
* Example: ou=Users,dc=mydomain,dc=com
* User Filter **(required)**
* An LDAP filter declaring when a user should be allowed to log in. The `%s`
matching parameter will be substituted with the user's username.
* Example: (&(objectClass=posixAccount)(cn=%s))
* Example: (&(objectClass=posixAccount)(uid=%s))
**Verify group membership in LDAP** uses the following fields:
* Group Search Base (optional)
* The LDAP DN used for groups.
* Example: ou=group,dc=mydomain,dc=com
* Group Name Filter (optional)
* An LDAP filter declaring how to find valid groups in the above DN.
* Example: (|(cn=gitea_users)(cn=admins))
* User Attribute in Group (optional)
* Which user LDAP attribute is listed in the group.
* Example: uid
* Group Attribute for User (optional)
* Which group LDAP attribute contains an array above user attribute names.
* Example: memberUid

View file

@ -1,486 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea 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 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 (
"crypto/tls"
"fmt"
"strings"
"code.gitea.io/gitea/modules/log"
"github.com/go-ldap/ldap/v3"
)
// SecurityProtocol protocol type
type SecurityProtocol int
// Note: new type must be added at the end of list to maintain compatibility.
const (
SecurityProtocolUnencrypted SecurityProtocol = iota
SecurityProtocolLDAPS
SecurityProtocolStartTLS
)
// Source Basic LDAP authentication service
type Source struct {
Name string // canonical name (ie. corporate.ad)
Host string // LDAP host
Port int // port number
SecurityProtocol SecurityProtocol
SkipVerify bool
BindDN string // DN to bind with
BindPasswordEncrypt string // Encrypted Bind BN password
BindPassword string // Bind DN password
UserBase string // Base search path for users
UserDN string // Template for the DN of the user for simple auth
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)
AttributeSSHPublicKey string // LDAP SSH Public Key attribute
SearchPageSize uint32 // Search with paging page size
Filter string // Query filter to validate entry
AdminFilter string // Query filter to check if user is admin
RestrictedFilter string // Query filter to check if user is restricted
Enabled bool // if this source is disabled
AllowDeactivateAll bool // Allow an empty search response to deactivate all users from this source
GroupsEnabled bool // if the group checking is enabled
GroupDN string // Group Search Base
GroupFilter string // Group Name Filter
GroupMemberUID string // Group Attribute containing array of UserUID
UserUID string // User Attribute listed in Group
}
// SearchResult : user data
type SearchResult struct {
Username string // Username
Name string // Name
Surname string // Surname
Mail string // E-mail address
SSHPublicKey []string // SSH Public Key
IsAdmin bool // if user is administrator
IsRestricted bool // if user is restricted
}
func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00()*\\"
if strings.ContainsAny(username, badCharacters) {
log.Debug("'%s' contains invalid query characters. Aborting.", username)
return "", false
}
return fmt.Sprintf(ls.Filter, username), true
}
func (ls *Source) sanitizedUserDN(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4514: "special characters"
badCharacters := "\x00()*\\,='\"#+;<>"
if strings.ContainsAny(username, badCharacters) {
log.Debug("'%s' contains invalid DN characters. Aborting.", username)
return "", false
}
return fmt.Sprintf(ls.UserDN, username), true
}
func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00*\\"
if strings.ContainsAny(group, badCharacters) {
log.Trace("Group filter invalid query characters: %s", group)
return "", false
}
return group, true
}
func (ls *Source) 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, " ") {
log.Trace("Group DN contains invalid query characters: %s", groupDn)
return "", false
}
return groupDn, true
}
func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
log.Trace("Search for LDAP user: %s", name)
// A search for the user.
userFilter, ok := ls.sanitizedUserQuery(name)
if !ok {
return "", false
}
log.Trace("Searching for DN using filter %s and base %s", userFilter, ls.UserBase)
search := ldap.NewSearchRequest(
ls.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.Debug("Failed search using filter[%s]: %v", userFilter, err)
return "", false
} else if len(sr.Entries) > 1 {
log.Debug("Filter '%s' returned more than one user.", userFilter)
return "", false
}
userDN := sr.Entries[0].DN
if userDN == "" {
log.Error("LDAP search was successful, but found no DN!")
return "", false
}
return userDN, true
}
func dial(ls *Source) (*ldap.Conn, error) {
log.Trace("Dialing LDAP with security protocol (%v) without verifying: %v", ls.SecurityProtocol, ls.SkipVerify)
tlsCfg := &tls.Config{
ServerName: ls.Host,
InsecureSkipVerify: ls.SkipVerify,
}
if ls.SecurityProtocol == SecurityProtocolLDAPS {
return ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port), tlsCfg)
}
conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port))
if err != nil {
return nil, fmt.Errorf("Dial: %v", err)
}
if ls.SecurityProtocol == SecurityProtocolStartTLS {
if err = conn.StartTLS(tlsCfg); err != nil {
conn.Close()
return nil, fmt.Errorf("StartTLS: %v", err)
}
}
return conn, nil
}
func bindUser(l *ldap.Conn, userDN, passwd string) error {
log.Trace("Binding with userDN: %s", userDN)
err := l.Bind(userDN, passwd)
if err != nil {
log.Debug("LDAP auth. failed for %s, reason: %v", userDN, err)
return err
}
log.Trace("Bound successfully with userDN: %s", userDN)
return err
}
func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool {
if len(ls.AdminFilter) == 0 {
return false
}
log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN)
search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter,
[]string{ls.AttributeName},
nil)
sr, err := l.Search(search)
if err != nil {
log.Error("LDAP Admin Search failed unexpectedly! (%v)", err)
} else if len(sr.Entries) < 1 {
log.Trace("LDAP Admin Search found no matching entries.")
} else {
return true
}
return false
}
func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
if len(ls.RestrictedFilter) == 0 {
return false
}
if ls.RestrictedFilter == "*" {
return true
}
log.Trace("Checking restricted with filter %s and base %s", ls.RestrictedFilter, userDN)
search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.RestrictedFilter,
[]string{ls.AttributeName},
nil)
sr, err := l.Search(search)
if err != nil {
log.Error("LDAP Restrictred Search failed unexpectedly! (%v)", err)
} else if len(sr.Entries) < 1 {
log.Trace("LDAP Restricted Search found no matching entries.")
} else {
return true
}
return false
}
// 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) *SearchResult {
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
if len(passwd) == 0 {
log.Debug("Auth. failed for %s, password cannot be empty", name)
return nil
}
l, err := dial(ls)
if err != nil {
log.Error("LDAP Connect error, %s:%v", ls.Host, err)
ls.Enabled = false
return nil
}
defer l.Close()
var userDN string
if directBind {
log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN)
var ok bool
userDN, ok = ls.sanitizedUserDN(name)
if !ok {
return nil
}
err = bindUser(l, userDN, passwd)
if err != nil {
return nil
}
if ls.UserBase != "" {
// not everyone has a CN compatible with input name so we need to find
// the real userDN in that case
userDN, ok = ls.findUserDN(l, name)
if !ok {
return nil
}
}
} else {
log.Trace("LDAP will use BindDN.")
var found bool
if ls.BindDN != "" && ls.BindPassword != "" {
err := l.Bind(ls.BindDN, ls.BindPassword)
if err != nil {
log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err)
return nil
}
log.Trace("Bound as BindDN %s", ls.BindDN)
} else {
log.Trace("Proceeding with anonymous LDAP search.")
}
userDN, found = ls.findUserDN(l, name)
if !found {
return nil
}
}
if !ls.AttributesInBind {
// binds user (checking password) before looking-up attributes in user context
err = bindUser(l, userDN, passwd)
if err != nil {
return nil
}
}
userFilter, ok := ls.sanitizedUserQuery(name)
if !ok {
return nil
}
var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
if len(strings.TrimSpace(ls.UserUID)) > 0 {
attribs = append(attribs, ls.UserUID)
}
if isAttributeSSHPublicKeySet {
attribs = append(attribs, ls.AttributeSSHPublicKey)
}
log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.UserUID, userFilter, userDN)
search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
attribs, nil)
sr, err := l.Search(search)
if err != nil {
log.Error("LDAP Search failed unexpectedly! (%v)", err)
return nil
} else if len(sr.Entries) < 1 {
if directBind {
log.Trace("User filter inhibited user login.")
} else {
log.Trace("LDAP Search found no matching entries.")
}
return nil
}
var sshPublicKey []string
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)
// Check group membership
if ls.GroupsEnabled {
groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
if !ok {
return nil
}
groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN)
if !ok {
return nil
}
log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN)
groupSearch := ldap.NewSearchRequest(
groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
[]string{ls.GroupMemberUID},
nil)
srg, err := l.Search(groupSearch)
if err != nil {
log.Error("LDAP group search failed: %v", err)
return nil
} else if len(srg.Entries) < 1 {
log.Error("LDAP group search failed: 0 entries")
return nil
}
isMember := false
Entries:
for _, group := range srg.Entries {
for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
if (ls.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid {
isMember = true
break Entries
}
}
}
if !isMember {
log.Error("LDAP group membership test failed")
return nil
}
}
if isAttributeSSHPublicKeySet {
sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey)
}
isAdmin := checkAdmin(l, ls, userDN)
var isRestricted bool
if !isAdmin {
isRestricted = checkRestricted(l, ls, userDN)
}
if !directBind && ls.AttributesInBind {
// binds user (checking password) after looking-up attributes in BindDN context
err = bindUser(l, userDN, passwd)
if err != nil {
return nil
}
}
return &SearchResult{
Username: username,
Name: firstname,
Surname: surname,
Mail: mail,
SSHPublicKey: sshPublicKey,
IsAdmin: isAdmin,
IsRestricted: isRestricted,
}
}
// UsePagedSearch returns if need to use paged search
func (ls *Source) UsePagedSearch() bool {
return ls.SearchPageSize > 0
}
// SearchEntries : search an LDAP source for all users matching userFilter
func (ls *Source) SearchEntries() ([]*SearchResult, error) {
l, err := dial(ls)
if err != nil {
log.Error("LDAP Connect error, %s:%v", ls.Host, err)
ls.Enabled = false
return nil, err
}
defer l.Close()
if ls.BindDN != "" && ls.BindPassword != "" {
err := l.Bind(ls.BindDN, ls.BindPassword)
if err != nil {
log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err)
return nil, err
}
log.Trace("Bound as BindDN %s", ls.BindDN)
} else {
log.Trace("Proceeding with anonymous LDAP search.")
}
userFilter := fmt.Sprintf(ls.Filter, "*")
var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
if isAttributeSSHPublicKeySet {
attribs = append(attribs, ls.AttributeSSHPublicKey)
}
log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, userFilter, ls.UserBase)
search := ldap.NewSearchRequest(
ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
attribs, nil)
var sr *ldap.SearchResult
if ls.UsePagedSearch() {
sr, err = l.SearchWithPaging(search, ls.SearchPageSize)
} else {
sr, err = l.Search(search)
}
if err != nil {
log.Error("LDAP Search failed unexpectedly! (%v)", err)
return nil, err
}
result := make([]*SearchResult, len(sr.Entries))
for i, v := range sr.Entries {
result[i] = &SearchResult{
Username: v.GetAttributeValue(ls.AttributeUsername),
Name: v.GetAttributeValue(ls.AttributeName),
Surname: v.GetAttributeValue(ls.AttributeSurname),
Mail: v.GetAttributeValue(ls.AttributeMail),
IsAdmin: checkAdmin(l, ls, v.DN),
}
if !result[i].IsAdmin {
result[i].IsRestricted = checkRestricted(l, ls, v.DN)
}
if isAttributeSSHPublicKeySet {
result[i].SSHPublicKey = v.GetAttributeValues(ls.AttributeSSHPublicKey)
}
}
return result, nil
}

View file

@ -1,378 +0,0 @@
// Copyright 2021 The Gitea 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 oauth2
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/dgrijalva/jwt-go"
ini "gopkg.in/ini.v1"
)
// ErrInvalidAlgorithmType represents an invalid algorithm error.
type ErrInvalidAlgorithmType struct {
Algorightm string
}
func (err ErrInvalidAlgorithmType) Error() string {
return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorightm)
}
// JWTSigningKey represents a algorithm/key pair to sign JWTs
type JWTSigningKey interface {
IsSymmetric() bool
SigningMethod() jwt.SigningMethod
SignKey() interface{}
VerifyKey() interface{}
ToJWK() (map[string]string, error)
PreProcessToken(*jwt.Token)
}
type hmacSigningKey struct {
signingMethod jwt.SigningMethod
secret []byte
}
func (key hmacSigningKey) IsSymmetric() bool {
return true
}
func (key hmacSigningKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key hmacSigningKey) SignKey() interface{} {
return key.secret
}
func (key hmacSigningKey) VerifyKey() interface{} {
return key.secret
}
func (key hmacSigningKey) ToJWK() (map[string]string, error) {
return map[string]string{
"kty": "oct",
"alg": key.SigningMethod().Alg(),
}, nil
}
func (key hmacSigningKey) PreProcessToken(*jwt.Token) {}
type rsaSingingKey struct {
signingMethod jwt.SigningMethod
key *rsa.PrivateKey
id string
}
func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) {
kid, err := createPublicKeyFingerprint(key.Public().(*rsa.PublicKey))
if err != nil {
return rsaSingingKey{}, err
}
return rsaSingingKey{
signingMethod,
key,
base64.RawURLEncoding.EncodeToString(kid),
}, nil
}
func (key rsaSingingKey) IsSymmetric() bool {
return false
}
func (key rsaSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key rsaSingingKey) SignKey() interface{} {
return key.key
}
func (key rsaSingingKey) VerifyKey() interface{} {
return key.key.Public()
}
func (key rsaSingingKey) ToJWK() (map[string]string, error) {
pubKey := key.key.Public().(*rsa.PublicKey)
return map[string]string{
"kty": "RSA",
"alg": key.SigningMethod().Alg(),
"kid": key.id,
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()),
"n": base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()),
}, nil
}
func (key rsaSingingKey) PreProcessToken(token *jwt.Token) {
token.Header["kid"] = key.id
}
type ecdsaSingingKey struct {
signingMethod jwt.SigningMethod
key *ecdsa.PrivateKey
id string
}
func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) {
kid, err := createPublicKeyFingerprint(key.Public().(*ecdsa.PublicKey))
if err != nil {
return ecdsaSingingKey{}, err
}
return ecdsaSingingKey{
signingMethod,
key,
base64.RawURLEncoding.EncodeToString(kid),
}, nil
}
func (key ecdsaSingingKey) IsSymmetric() bool {
return false
}
func (key ecdsaSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key ecdsaSingingKey) SignKey() interface{} {
return key.key
}
func (key ecdsaSingingKey) VerifyKey() interface{} {
return key.key.Public()
}
func (key ecdsaSingingKey) ToJWK() (map[string]string, error) {
pubKey := key.key.Public().(*ecdsa.PublicKey)
return map[string]string{
"kty": "EC",
"alg": key.SigningMethod().Alg(),
"kid": key.id,
"crv": pubKey.Params().Name,
"x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()),
"y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()),
}, nil
}
func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) {
token.Header["kid"] = key.id
}
// createPublicKeyFingerprint creates a fingerprint of the given key.
// The fingerprint is the sha256 sum of the PKIX structure of the key.
func createPublicKeyFingerprint(key interface{}) ([]byte, error) {
bytes, err := x509.MarshalPKIXPublicKey(key)
if err != nil {
return nil, err
}
checksum := sha256.Sum256(bytes)
return checksum[:], nil
}
// CreateJWTSingingKey creates a signing key from an algorithm / key pair.
func CreateJWTSingingKey(algorithm string, key interface{}) (JWTSigningKey, error) {
var signingMethod jwt.SigningMethod
switch algorithm {
case "HS256":
signingMethod = jwt.SigningMethodHS256
case "HS384":
signingMethod = jwt.SigningMethodHS384
case "HS512":
signingMethod = jwt.SigningMethodHS512
case "RS256":
signingMethod = jwt.SigningMethodRS256
case "RS384":
signingMethod = jwt.SigningMethodRS384
case "RS512":
signingMethod = jwt.SigningMethodRS512
case "ES256":
signingMethod = jwt.SigningMethodES256
case "ES384":
signingMethod = jwt.SigningMethodES384
case "ES512":
signingMethod = jwt.SigningMethodES512
default:
return nil, ErrInvalidAlgorithmType{algorithm}
}
switch signingMethod.(type) {
case *jwt.SigningMethodECDSA:
privateKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return newECDSASingingKey(signingMethod, privateKey)
case *jwt.SigningMethodRSA:
privateKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return newRSASingingKey(signingMethod, privateKey)
default:
secret, ok := key.([]byte)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return hmacSigningKey{signingMethod, secret}, nil
}
}
// DefaultSigningKey is the default signing key for JWTs.
var DefaultSigningKey JWTSigningKey
// InitSigningKey creates the default signing key from settings or creates a random key.
func InitSigningKey() error {
var err error
var key interface{}
switch setting.OAuth2.JWTSigningAlgorithm {
case "HS256":
fallthrough
case "HS384":
fallthrough
case "HS512":
key, err = loadOrCreateSymmetricKey()
case "RS256":
fallthrough
case "RS384":
fallthrough
case "RS512":
fallthrough
case "ES256":
fallthrough
case "ES384":
fallthrough
case "ES512":
key, err = loadOrCreateAsymmetricKey()
default:
return ErrInvalidAlgorithmType{setting.OAuth2.JWTSigningAlgorithm}
}
if err != nil {
return fmt.Errorf("Error while loading or creating symmetric key: %v", err)
}
signingKey, err := CreateJWTSingingKey(setting.OAuth2.JWTSigningAlgorithm, key)
if err != nil {
return err
}
DefaultSigningKey = signingKey
return nil
}
// loadOrCreateSymmetricKey checks if the configured secret is valid.
// If it is not valid a new secret is created and saved in the configuration file.
func loadOrCreateSymmetricKey() (interface{}, error) {
key := make([]byte, 32)
n, err := base64.RawURLEncoding.Decode(key, []byte(setting.OAuth2.JWTSecretBase64))
if err != nil || n != 32 {
key, err = generate.NewJwtSecret()
if err != nil {
log.Fatal("error generating JWT secret: %v", err)
return nil, err
}
setting.CreateOrAppendToCustomConf(func(cfg *ini.File) {
secretBase64 := base64.RawURLEncoding.EncodeToString(key)
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64)
})
}
return key, nil
}
// loadOrCreateAsymmetricKey checks if the configured private key exists.
// If it does not exist a new random key gets generated and saved on the configured path.
func loadOrCreateAsymmetricKey() (interface{}, error) {
keyPath := setting.OAuth2.JWTSigningPrivateKeyFile
isExist, err := util.IsExist(keyPath)
if err != nil {
log.Fatal("Unable to check if %s exists. Error: %v", keyPath, err)
}
if !isExist {
err := func() error {
key, err := func() (interface{}, error) {
if strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS") {
return rsa.GenerateKey(rand.Reader, 4096)
}
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}()
if err != nil {
return err
}
bytes, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return err
}
privateKeyPEM := &pem.Block{Type: "PRIVATE KEY", Bytes: bytes}
if err := os.MkdirAll(filepath.Dir(keyPath), os.ModePerm); err != nil {
return err
}
f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer func() {
if err = f.Close(); err != nil {
log.Error("Close: %v", err)
}
}()
return pem.Encode(f, privateKeyPEM)
}()
if err != nil {
log.Fatal("Error generating private key: %v", err)
return nil, err
}
}
bytes, err := ioutil.ReadFile(keyPath)
if err != nil {
return nil, err
}
block, _ := pem.Decode(bytes)
if block == nil {
return nil, fmt.Errorf("no valid PEM data found in %s", keyPath)
} else if block.Type != "PRIVATE KEY" {
return nil, fmt.Errorf("expected PRIVATE KEY, got %s in %s", block.Type, keyPath)
}
return x509.ParsePKCS8PrivateKey(block.Bytes)
}

View file

@ -1,299 +0,0 @@
// Copyright 2017 The Gitea 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 oauth2
import (
"net/http"
"net/url"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
uuid "github.com/google/uuid"
"github.com/lafriks/xormstore"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/bitbucket"
"github.com/markbates/goth/providers/discord"
"github.com/markbates/goth/providers/dropbox"
"github.com/markbates/goth/providers/facebook"
"github.com/markbates/goth/providers/gitea"
"github.com/markbates/goth/providers/github"
"github.com/markbates/goth/providers/gitlab"
"github.com/markbates/goth/providers/google"
"github.com/markbates/goth/providers/mastodon"
"github.com/markbates/goth/providers/nextcloud"
"github.com/markbates/goth/providers/openidConnect"
"github.com/markbates/goth/providers/twitter"
"github.com/markbates/goth/providers/yandex"
"xorm.io/xorm"
)
var (
sessionUsersStoreKey = "gitea-oauth2-sessions"
providerHeaderKey = "gitea-oauth2-provider"
)
// CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs
type CustomURLMapping struct {
AuthURL string
TokenURL string
ProfileURL string
EmailURL string
}
// Init initialize the setup of the OAuth2 library
func Init(x *xorm.Engine) error {
store, err := xormstore.NewOptions(x, xormstore.Options{
TableName: "oauth2_session",
}, []byte(sessionUsersStoreKey))
if err != nil {
return err
}
// according to the Goth lib:
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
store.MaxLength(setting.OAuth2.MaxTokenLength)
gothic.Store = store
gothic.SetState = func(req *http.Request) string {
return uuid.New().String()
}
gothic.GetProviderName = func(req *http.Request) (string, error) {
return req.Header.Get(providerHeaderKey), nil
}
return nil
}
// Auth OAuth2 auth service
func Auth(provider string, request *http.Request, response http.ResponseWriter) error {
// not sure if goth is thread safe (?) when using multiple providers
request.Header.Set(providerHeaderKey, provider)
// don't use the default gothic begin handler to prevent issues when some error occurs
// normally the gothic library will write some custom stuff to the response instead of our own nice error page
//gothic.BeginAuthHandler(response, request)
url, err := gothic.GetAuthURL(response, request)
if err == nil {
http.Redirect(response, request, url, http.StatusTemporaryRedirect)
}
return err
}
// ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url
// this will trigger a new authentication request, but because we save it in the session we can use that
func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, error) {
// not sure if goth is thread safe (?) when using multiple providers
request.Header.Set(providerHeaderKey, provider)
user, err := gothic.CompleteUserAuth(response, request)
if err != nil {
return user, err
}
return user, nil
}
// RegisterProvider register a OAuth2 provider in goth lib
func RegisterProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) error {
provider, err := createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL, customURLMapping)
if err == nil && provider != nil {
goth.UseProviders(provider)
}
return err
}
// RemoveProvider removes the given OAuth2 provider from the goth lib
func RemoveProvider(providerName string) {
delete(goth.GetProviders(), providerName)
}
// ClearProviders clears all OAuth2 providers from the goth lib
func ClearProviders() {
goth.ClearProviders()
}
// used to create different types of goth providers
func createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) (goth.Provider, error) {
callbackURL := setting.AppURL + "user/oauth2/" + url.PathEscape(providerName) + "/callback"
var provider goth.Provider
var err error
switch providerType {
case "bitbucket":
provider = bitbucket.New(clientID, clientSecret, callbackURL, "account")
case "dropbox":
provider = dropbox.New(clientID, clientSecret, callbackURL)
case "facebook":
provider = facebook.New(clientID, clientSecret, callbackURL, "email")
case "github":
authURL := github.AuthURL
tokenURL := github.TokenURL
profileURL := github.ProfileURL
emailURL := github.EmailURL
if customURLMapping != nil {
if len(customURLMapping.AuthURL) > 0 {
authURL = customURLMapping.AuthURL
}
if len(customURLMapping.TokenURL) > 0 {
tokenURL = customURLMapping.TokenURL
}
if len(customURLMapping.ProfileURL) > 0 {
profileURL = customURLMapping.ProfileURL
}
if len(customURLMapping.EmailURL) > 0 {
emailURL = customURLMapping.EmailURL
}
}
scopes := []string{}
if setting.OAuth2Client.EnableAutoRegistration {
scopes = append(scopes, "user:email")
}
provider = github.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, emailURL, scopes...)
case "gitlab":
authURL := gitlab.AuthURL
tokenURL := gitlab.TokenURL
profileURL := gitlab.ProfileURL
if customURLMapping != nil {
if len(customURLMapping.AuthURL) > 0 {
authURL = customURLMapping.AuthURL
}
if len(customURLMapping.TokenURL) > 0 {
tokenURL = customURLMapping.TokenURL
}
if len(customURLMapping.ProfileURL) > 0 {
profileURL = customURLMapping.ProfileURL
}
}
provider = gitlab.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, "read_user")
case "gplus": // named gplus due to legacy gplus -> google migration (Google killed Google+). This ensures old connections still work
scopes := []string{"email"}
if setting.OAuth2Client.UpdateAvatar || setting.OAuth2Client.EnableAutoRegistration {
scopes = append(scopes, "profile")
}
provider = google.New(clientID, clientSecret, callbackURL, scopes...)
case "openidConnect":
if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL, setting.OAuth2Client.OpenIDConnectScopes...); err != nil {
log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, openIDConnectAutoDiscoveryURL, err)
}
case "twitter":
provider = twitter.NewAuthenticate(clientID, clientSecret, callbackURL)
case "discord":
provider = discord.New(clientID, clientSecret, callbackURL, discord.ScopeIdentify, discord.ScopeEmail)
case "gitea":
authURL := gitea.AuthURL
tokenURL := gitea.TokenURL
profileURL := gitea.ProfileURL
if customURLMapping != nil {
if len(customURLMapping.AuthURL) > 0 {
authURL = customURLMapping.AuthURL
}
if len(customURLMapping.TokenURL) > 0 {
tokenURL = customURLMapping.TokenURL
}
if len(customURLMapping.ProfileURL) > 0 {
profileURL = customURLMapping.ProfileURL
}
}
provider = gitea.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL)
case "nextcloud":
authURL := nextcloud.AuthURL
tokenURL := nextcloud.TokenURL
profileURL := nextcloud.ProfileURL
if customURLMapping != nil {
if len(customURLMapping.AuthURL) > 0 {
authURL = customURLMapping.AuthURL
}
if len(customURLMapping.TokenURL) > 0 {
tokenURL = customURLMapping.TokenURL
}
if len(customURLMapping.ProfileURL) > 0 {
profileURL = customURLMapping.ProfileURL
}
}
provider = nextcloud.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL)
case "yandex":
// See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/
provider = yandex.New(clientID, clientSecret, callbackURL, "login:email", "login:info", "login:avatar")
case "mastodon":
instanceURL := mastodon.InstanceURL
if customURLMapping != nil && len(customURLMapping.AuthURL) > 0 {
instanceURL = customURLMapping.AuthURL
}
provider = mastodon.NewCustomisedURL(clientID, clientSecret, callbackURL, instanceURL)
}
// always set the name if provider is created so we can support multiple setups of 1 provider
if err == nil && provider != nil {
provider.SetName(providerName)
}
return provider, err
}
// GetDefaultTokenURL return the default token url for the given provider
func GetDefaultTokenURL(provider string) string {
switch provider {
case "github":
return github.TokenURL
case "gitlab":
return gitlab.TokenURL
case "gitea":
return gitea.TokenURL
case "nextcloud":
return nextcloud.TokenURL
}
return ""
}
// GetDefaultAuthURL return the default authorize url for the given provider
func GetDefaultAuthURL(provider string) string {
switch provider {
case "github":
return github.AuthURL
case "gitlab":
return gitlab.AuthURL
case "gitea":
return gitea.AuthURL
case "nextcloud":
return nextcloud.AuthURL
case "mastodon":
return mastodon.InstanceURL
}
return ""
}
// GetDefaultProfileURL return the default profile url for the given provider
func GetDefaultProfileURL(provider string) string {
switch provider {
case "github":
return github.ProfileURL
case "gitlab":
return gitlab.ProfileURL
case "gitea":
return gitea.ProfileURL
case "nextcloud":
return nextcloud.ProfileURL
}
return ""
}
// GetDefaultEmailURL return the default email url for the given provider
func GetDefaultEmailURL(provider string) string {
if provider == "github" {
return github.EmailURL
}
return ""
}

View file

@ -218,7 +218,7 @@ func (ctx *APIContext) CheckForOTP() {
}
// APIAuth converts auth.Auth as a middleware
func APIAuth(authMethod auth.Auth) func(*APIContext) {
func APIAuth(authMethod auth.Method) func(*APIContext) {
return func(ctx *APIContext) {
// Get user from session if logged in.
ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)

View file

@ -627,7 +627,7 @@ func getCsrfOpts() CsrfOptions {
}
// Auth converts auth.Auth as a middleware
func Auth(authMethod auth.Auth) func(*Context) {
func Auth(authMethod auth.Method) func(*Context) {
return func(ctx *Context) {
ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if ctx.User != nil {

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/migrations"
repository_service "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/auth"
mirror_service "code.gitea.io/gitea/services/mirror"
)
@ -80,7 +81,7 @@ func registerSyncExternalUsers() {
UpdateExisting: true,
}, func(ctx context.Context, _ *models.User, config Config) error {
realConfig := config.(*UpdateExistingConfig)
return models.SyncExternalUsers(ctx, realConfig.UpdateExisting)
return auth.SyncExternalUsers(ctx, realConfig.UpdateExisting)
})
}