1
0
Fork 0
forked from forgejo/forgejo

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

@ -0,0 +1,23 @@
// 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_test
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
)
// This test file exists to assert that our Source exposes the interfaces that we expect
// It tightly binds the interfaces and implementation without breaking go import cycles
type sourceInterface interface {
models.LoginConfig
models.LoginSourceSettable
models.RegisterableSource
auth.PasswordAuthenticator
}
var _ (sourceInterface) = &oauth2.Source{}

View file

@ -0,0 +1,83 @@
// 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 (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/google/uuid"
"github.com/markbates/goth/gothic"
)
// SessionTableName is the table name that OAuth2 will use to store things
const SessionTableName = "oauth2_session"
// UsersStoreKey is the key for the store
const UsersStoreKey = "gitea-oauth2-sessions"
// ProviderHeaderKey is the HTTP header key
const ProviderHeaderKey = "gitea-oauth2-provider"
// Init initializes the oauth source
func Init() error {
if err := InitSigningKey(); err != nil {
return err
}
store, err := models.CreateStore(SessionTableName, UsersStoreKey)
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 initOAuth2LoginSources()
}
// ResetOAuth2 clears existing OAuth2 providers and loads them from DB
func ResetOAuth2() error {
ClearProviders()
return initOAuth2LoginSources()
}
// initOAuth2LoginSources is used to load and register all active OAuth2 providers
func initOAuth2LoginSources() error {
loginSources, _ := models.GetActiveOAuth2ProviderLoginSources()
for _, source := range loginSources {
oauth2Source, ok := source.Cfg.(*Source)
if !ok {
continue
}
err := oauth2Source.RegisterSource()
if err != nil {
log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
source.IsActive = false
if err = models.UpdateSource(source); err != nil {
log.Critical("Unable to update source %s to disable it. Error: %v", err)
return err
}
}
}
return nil
}

View file

@ -0,0 +1,378 @@
// 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

@ -0,0 +1,257 @@
// 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 (
"net/url"
"sort"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/markbates/goth"
"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"
)
// Provider describes the display values of a single OAuth2 provider
type Provider struct {
Name string
DisplayName string
Image string
CustomURLMapping *CustomURLMapping
}
// Providers contains the map of registered OAuth2 providers in Gitea (based on goth)
// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider)
// value is used to store display data
var Providers = map[string]Provider{
"bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/assets/img/auth/bitbucket.png"},
"dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/assets/img/auth/dropbox.png"},
"facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/assets/img/auth/facebook.png"},
"github": {
Name: "github", DisplayName: "GitHub", Image: "/assets/img/auth/github.png",
CustomURLMapping: &CustomURLMapping{
TokenURL: github.TokenURL,
AuthURL: github.AuthURL,
ProfileURL: github.ProfileURL,
EmailURL: github.EmailURL,
},
},
"gitlab": {
Name: "gitlab", DisplayName: "GitLab", Image: "/assets/img/auth/gitlab.png",
CustomURLMapping: &CustomURLMapping{
TokenURL: gitlab.TokenURL,
AuthURL: gitlab.AuthURL,
ProfileURL: gitlab.ProfileURL,
},
},
"gplus": {Name: "gplus", DisplayName: "Google", Image: "/assets/img/auth/google.png"},
"openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/assets/img/auth/openid_connect.svg"},
"twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/assets/img/auth/twitter.png"},
"discord": {Name: "discord", DisplayName: "Discord", Image: "/assets/img/auth/discord.png"},
"gitea": {
Name: "gitea", DisplayName: "Gitea", Image: "/assets/img/auth/gitea.png",
CustomURLMapping: &CustomURLMapping{
TokenURL: gitea.TokenURL,
AuthURL: gitea.AuthURL,
ProfileURL: gitea.ProfileURL,
},
},
"nextcloud": {
Name: "nextcloud", DisplayName: "Nextcloud", Image: "/assets/img/auth/nextcloud.png",
CustomURLMapping: &CustomURLMapping{
TokenURL: nextcloud.TokenURL,
AuthURL: nextcloud.AuthURL,
ProfileURL: nextcloud.ProfileURL,
},
},
"yandex": {Name: "yandex", DisplayName: "Yandex", Image: "/assets/img/auth/yandex.png"},
"mastodon": {
Name: "mastodon", DisplayName: "Mastodon", Image: "/assets/img/auth/mastodon.png",
CustomURLMapping: &CustomURLMapping{
AuthURL: mastodon.InstanceURL,
},
},
}
// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers
// key is used as technical name (like in the callbackURL)
// values to display
func GetActiveOAuth2Providers() ([]string, map[string]Provider, error) {
// Maybe also separate used and unused providers so we can force the registration of only 1 active provider for each type
loginSources, err := models.GetActiveOAuth2ProviderLoginSources()
if err != nil {
return nil, nil, err
}
var orderedKeys []string
providers := make(map[string]Provider)
for _, source := range loginSources {
prov := Providers[source.Cfg.(*Source).Provider]
if source.Cfg.(*Source).IconURL != "" {
prov.Image = source.Cfg.(*Source).IconURL
}
providers[source.Name] = prov
orderedKeys = append(orderedKeys, source.Name)
}
sort.Strings(orderedKeys)
return orderedKeys, providers, 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
}

View file

@ -0,0 +1,51 @@
// 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 (
"code.gitea.io/gitea/models"
jsoniter "github.com/json-iterator/go"
)
// ________ _____ __ .__ ________
// \_____ \ / _ \ __ ___/ |_| |__ \_____ \
// / | \ / /_\ \| | \ __\ | \ / ____/
// / | \/ | \ | /| | | Y \/ \
// \_______ /\____|__ /____/ |__| |___| /\_______ \
// \/ \/ \/ \/
// Source holds configuration for the OAuth2 login source.
type Source struct {
Provider string
ClientID string
ClientSecret string
OpenIDConnectAutoDiscoveryURL string
CustomURLMapping *CustomURLMapping
IconURL string
// reference to the loginSource
loginSource *models.LoginSource
}
// FromDB fills up an OAuth2Config from serialized format.
func (source *Source) FromDB(bs []byte) error {
return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
}
// ToDB exports an SMTPConfig to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
json := jsoniter.ConfigCompatibleWithStandardLibrary
return json.Marshal(source)
}
// SetLoginSource sets the related LoginSource
func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
source.loginSource = loginSource
}
func init() {
models.RegisterLoginTypeConfig(models.LoginOAuth2, &Source{})
}

View file

@ -0,0 +1,15 @@
// 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 (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/services/auth/source/db"
)
// Authenticate falls back to the db authenticator
func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
return db.Authenticate(user, login, password)
}

View file

@ -0,0 +1,42 @@
// 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 (
"net/http"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
)
// Callout redirects request/response pair to authenticate against the provider
func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
// not sure if goth is thread safe (?) when using multiple providers
request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
// 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
}
// Callback 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 (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
// not sure if goth is thread safe (?) when using multiple providers
request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
user, err := gothic.CompleteUserAuth(response, request)
if err != nil {
return user, err
}
return user, nil
}

View file

@ -0,0 +1,30 @@
// 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 (
"code.gitea.io/gitea/models"
)
// RegisterSource causes an OAuth2 configuration to be registered
func (source *Source) RegisterSource() error {
err := RegisterProvider(source.loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
return wrapOpenIDConnectInitializeError(err, source.loginSource.Name, source)
}
// UnregisterSource causes an OAuth2 configuration to be unregistered
func (source *Source) UnregisterSource() error {
RemoveProvider(source.loginSource.Name)
return nil
}
// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
func wrapOpenIDConnectInitializeError(err error, providerName string, source *Source) error {
if err != nil && source.Provider == "openidConnect" {
err = models.ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: source.OpenIDConnectAutoDiscoveryURL, Cause: err}
}
return err
}

View file

@ -0,0 +1,94 @@
// 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 (
"fmt"
"time"
"code.gitea.io/gitea/modules/timeutil"
"github.com/dgrijalva/jwt-go"
)
// ___________ __
// \__ ___/___ | | __ ____ ____
// | | / _ \| |/ // __ \ / \
// | |( <_> ) <\ ___/| | \
// |____| \____/|__|_ \\___ >___| /
// \/ \/ \/
// Token represents an Oauth grant
// TokenType represents the type of token for an oauth application
type TokenType int
const (
// TypeAccessToken is a token with short lifetime to access the api
TypeAccessToken TokenType = 0
// TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
TypeRefreshToken = iota
)
// Token represents a JWT token used to authenticate a client
type Token struct {
GrantID int64 `json:"gnt"`
Type TokenType `json:"tt"`
Counter int64 `json:"cnt,omitempty"`
jwt.StandardClaims
}
// ParseToken parses a signed jwt string
func ParseToken(jwtToken string) (*Token, error) {
parsedToken, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (interface{}, error) {
if token.Method == nil || token.Method.Alg() != DefaultSigningKey.SigningMethod().Alg() {
return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
}
return DefaultSigningKey.VerifyKey(), nil
})
if err != nil {
return nil, err
}
var token *Token
var ok bool
if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid {
return nil, fmt.Errorf("invalid token")
}
return token, nil
}
// SignToken signs the token with the JWT secret
func (token *Token) SignToken() (string, error) {
token.IssuedAt = time.Now().Unix()
jwtToken := jwt.NewWithClaims(DefaultSigningKey.SigningMethod(), token)
DefaultSigningKey.PreProcessToken(jwtToken)
return jwtToken.SignedString(DefaultSigningKey.SignKey())
}
// OIDCToken represents an OpenID Connect id_token
type OIDCToken struct {
jwt.StandardClaims
Nonce string `json:"nonce,omitempty"`
// Scope profile
Name string `json:"name,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"`
Website string `json:"website,omitempty"`
Locale string `json:"locale,omitempty"`
UpdatedAt timeutil.TimeStamp `json:"updated_at,omitempty"`
// Scope email
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
}
// SignToken signs an id_token with the (symmetric) client secret key
func (token *OIDCToken) SignToken(signingKey JWTSigningKey) (string, error) {
token.IssuedAt = time.Now().Unix()
jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
signingKey.PreProcessToken(jwtToken)
return jwtToken.SignedString(signingKey.SignKey())
}

View file

@ -0,0 +1,24 @@
// 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
// CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs
type CustomURLMapping struct {
AuthURL string
TokenURL string
ProfileURL string
EmailURL string
}
// DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls
// key is used to map the OAuth2Provider
// value is the mapping as defined for the OAuth2Provider
var DefaultCustomURLMappings = map[string]*CustomURLMapping{
"github": Providers["github"].CustomURLMapping,
"gitlab": Providers["gitlab"].CustomURLMapping,
"gitea": Providers["gitea"].CustomURLMapping,
"nextcloud": Providers["nextcloud"].CustomURLMapping,
"mastodon": Providers["mastodon"].CustomURLMapping,
}