-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Oauth2 consumer #679
Oauth2 consumer #679
Changes from 11 commits
ded3513
1f94c85
642f36e
f93aad8
280913b
b597a86
8c2be7a
f392ce9
2ba7833
8e1ea96
6c98fa7
caeb911
f3f3866
047d50b
ab31c24
fe88e87
827c512
83c238b
c65a216
914f56a
b4eb93c
7a6757f
aae5f80
9151dc8
96d1af5
c3f5d36
2bf6b34
7ccbc44
084c45f
6594d76
19ddb15
57dbb74
32a4c58
770ba31
527d6e1
a7381b5
b64ee7d
5c5214a
769c747
6b16f42
0fa2e40
f54f07a
873e5b7
779e84b
61fe261
66e28df
2c11c44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,9 @@ import ( | |
|
||
"code.gitea.io/gitea/modules/auth/ldap" | ||
"code.gitea.io/gitea/modules/auth/pam" | ||
"code.gitea.io/gitea/modules/auth/oauth2" | ||
"code.gitea.io/gitea/modules/log" | ||
"net/http" | ||
) | ||
|
||
// LoginType represents an login type. | ||
|
@@ -30,19 +32,21 @@ type LoginType int | |
// Note: new type must append to the end of list to maintain compatibility. | ||
const ( | ||
LoginNoType LoginType = iota | ||
LoginPlain // 1 | ||
LoginLDAP // 2 | ||
LoginSMTP // 3 | ||
LoginPAM // 4 | ||
LoginDLDAP // 5 | ||
LoginPlain // 1 | ||
LoginLDAP // 2 | ||
LoginSMTP // 3 | ||
LoginPAM // 4 | ||
LoginDLDAP // 5 | ||
LoginOAuth2 // 6 | ||
) | ||
|
||
// LoginNames contains the name of LoginType values. | ||
var LoginNames = map[LoginType]string{ | ||
LoginLDAP: "LDAP (via BindDN)", | ||
LoginDLDAP: "LDAP (simple auth)", // Via direct bind | ||
LoginSMTP: "SMTP", | ||
LoginPAM: "PAM", | ||
LoginLDAP: "LDAP (via BindDN)", | ||
LoginDLDAP: "LDAP (simple auth)", // Via direct bind | ||
LoginSMTP: "SMTP", | ||
LoginPAM: "PAM", | ||
LoginOAuth2: "OAuth2", | ||
} | ||
|
||
// SecurityProtocolNames contains the name of SecurityProtocol values. | ||
|
@@ -57,6 +61,7 @@ var ( | |
_ core.Conversion = &LDAPConfig{} | ||
_ core.Conversion = &SMTPConfig{} | ||
_ core.Conversion = &PAMConfig{} | ||
_ core.Conversion = &OAuth2Config{} | ||
) | ||
|
||
// LDAPConfig holds configuration for LDAP login source. | ||
|
@@ -115,6 +120,23 @@ func (cfg *PAMConfig) ToDB() ([]byte, error) { | |
return json.Marshal(cfg) | ||
} | ||
|
||
// OAuth2Config holds configuration for the OAuth2 login source. | ||
type OAuth2Config struct { | ||
Provider string | ||
ClientID string | ||
ClientSecret string | ||
} | ||
|
||
// FromDB fills up an OAuth2Config from serialized format. | ||
func (cfg *OAuth2Config) FromDB(bs []byte) error { | ||
return json.Unmarshal(bs, cfg) | ||
} | ||
|
||
// ToDB exports an SMTPConfig to a serialized format. | ||
func (cfg *OAuth2Config) ToDB() ([]byte, error) { | ||
return json.Marshal(cfg) | ||
} | ||
|
||
// LoginSource represents an external way for authorizing users. | ||
type LoginSource struct { | ||
ID int64 `xorm:"pk autoincr"` | ||
|
@@ -162,6 +184,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { | |
source.Cfg = new(SMTPConfig) | ||
case LoginPAM: | ||
source.Cfg = new(PAMConfig) | ||
case LoginOAuth2: | ||
source.Cfg = new(OAuth2Config) | ||
default: | ||
panic("unrecognized login source type: " + com.ToStr(*val)) | ||
} | ||
|
@@ -203,6 +227,11 @@ func (source *LoginSource) IsPAM() bool { | |
return source.Type == LoginPAM | ||
} | ||
|
||
// IsOAuth2 returns true of this source is of the OAuth2 type. | ||
func (source *LoginSource) IsOAuth2() bool { | ||
return source.Type == LoginOAuth2 | ||
} | ||
|
||
// HasTLS returns true of this source supports TLS. | ||
func (source *LoginSource) HasTLS() bool { | ||
return ((source.IsLDAP() || source.IsDLDAP()) && | ||
|
@@ -250,6 +279,11 @@ func (source *LoginSource) PAM() *PAMConfig { | |
return source.Cfg.(*PAMConfig) | ||
} | ||
|
||
// OAuth2 returns OAuth2Config for this source, if of OAuth2 type. | ||
func (source *LoginSource) OAuth2() *OAuth2Config { | ||
return source.Cfg.(*OAuth2Config) | ||
} | ||
|
||
// CreateLoginSource inserts a LoginSource in the DB if not already | ||
// existing with the given name. | ||
func CreateLoginSource(source *LoginSource) error { | ||
|
@@ -266,7 +300,7 @@ func CreateLoginSource(source *LoginSource) error { | |
|
||
// LoginSources returns a slice of all login sources found in DB. | ||
func LoginSources() ([]*LoginSource, error) { | ||
auths := make([]*LoginSource, 0, 5) | ||
auths := make([]*LoginSource, 0, 6) | ||
return auths, x.Find(&auths) | ||
} | ||
|
||
|
@@ -444,7 +478,7 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC | |
idx := strings.Index(login, "@") | ||
if idx == -1 { | ||
return nil, ErrUserNotExist{0, login, 0} | ||
} else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx+1:]) { | ||
} else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx + 1:]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this done by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. gofmt did this |
||
return nil, ErrUserNotExist{0, login, 0} | ||
} | ||
} | ||
|
@@ -526,6 +560,26 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon | |
return user, CreateUser(user) | ||
} | ||
|
||
// ________ _____ __ .__ ________ | ||
// \_____ \ / _ \ __ ___/ |_| |__ \_____ \ | ||
// / | \ / /_\ \| | \ __\ | \ / ____/ | ||
// / | \/ | \ | /| | | Y \/ \ | ||
// \_______ /\____|__ /____/ |__| |___| /\_______ \ | ||
// \/ \/ \/ \/ | ||
|
||
// OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth) | ||
// key is used as technical name (for use in the callbackURL) | ||
// value is used to display | ||
var OAuth2Providers = map[string]string{ | ||
"github": "GitHub", | ||
} | ||
|
||
// LoginViaOAuth2 queries if login/password is valid against the OAuth2.0 provider, | ||
// and create a local user if success when enabled. | ||
func LoginViaOAuth2(cfg *OAuth2Config, request *http.Request, response http.ResponseWriter) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move this to modules to fix one request/response-issue |
||
oauth2.Auth(cfg.Provider, cfg.ClientID, cfg.ClientSecret, request, response) | ||
} | ||
|
||
// ExternalUserLogin attempts a login using external source types. | ||
func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) { | ||
if !source.IsActived { | ||
|
@@ -580,7 +634,7 @@ func UserSignIn(username, password string) (*User, error) { | |
} | ||
} | ||
|
||
sources := make([]*LoginSource, 0, 3) | ||
sources := make([]*LoginSource, 0, 5) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will lookup all activated loginSources, so this cannot be determined up front There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You added a single LoginSource but you incremented this number by 2, either there's a bug in current code (short count) or in your patch (long count) ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see comment above, this can be any value because it is fetched from the database and so unpredictable, so what ever you want 😄 |
||
if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil { | ||
return nil, err | ||
} | ||
|
@@ -596,3 +650,90 @@ func UserSignIn(username, password string) (*User, error) { | |
|
||
return nil, ErrUserNotExist{user.ID, user.Name, 0} | ||
} | ||
|
||
// OAuth2UserLogin attempts a login using a OAuth2 source type | ||
func OAuth2UserLogin(provider string, request *http.Request, response http.ResponseWriter) (*User, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same with this |
||
sources := make([]*LoginSource, 0, 1) | ||
if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { | ||
return nil, err | ||
} | ||
|
||
for _, source := range sources { | ||
// TODO how to put this in the xorm Find ? | ||
if source.Cfg.(*OAuth2Config).Provider == provider { | ||
LoginViaOAuth2(source.Cfg.(*OAuth2Config), request, response) | ||
return nil, nil | ||
} | ||
} | ||
|
||
return nil, errors.New("No valid provider found") | ||
} | ||
|
||
// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful | ||
// login the user | ||
func OAuth2UserLoginCallback(provider string, request *http.Request, response http.ResponseWriter) (*User, string, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And this |
||
gothUser, redirectURL, error := oauth2.ProviderCallback(provider, request, response) | ||
|
||
if error != nil { | ||
return nil, redirectURL, error | ||
} | ||
|
||
sources := make([]*LoginSource, 0, 1) | ||
if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { | ||
return nil, "", err | ||
} | ||
|
||
for _, source := range sources { | ||
// TODO how to put this in the xorm Find ? | ||
if source.OAuth2().Provider == provider { | ||
user := &User{ | ||
LoginName: gothUser.UserID, | ||
LoginType: LoginOAuth2, | ||
LoginSource: sources[0].ID, | ||
} | ||
|
||
hasUser, err := x.Get(user) | ||
if err != nil { | ||
return nil, "", err | ||
} | ||
|
||
if !hasUser { | ||
user = &User{ | ||
LowerName: strings.ToLower(gothUser.NickName), | ||
Name: gothUser.NickName, | ||
Email: gothUser.Email, | ||
LoginType: LoginOAuth2, | ||
LoginSource: sources[0].ID, | ||
LoginName: gothUser.NickName, | ||
IsActive: true, | ||
// TODO should OAuth2 imported emails be private? | ||
KeepEmailPrivate: true, | ||
} | ||
return user, redirectURL, CreateUser(user) | ||
} | ||
|
||
return user, redirectURL, nil | ||
} | ||
} | ||
|
||
return nil, "", errors.New("No valid provider found") | ||
} | ||
|
||
// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers | ||
// key is used as technical name (like in the callbackURL) | ||
// value is used to display | ||
func GetActiveOAuth2Providers() (map[string]string, error) { | ||
// Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type | ||
|
||
sources := make([]*LoginSource, 0, 1) | ||
if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { | ||
return nil, err | ||
} | ||
|
||
providers := make(map[string]string) | ||
for _, source := range sources { | ||
providers[source.OAuth2().Provider] = OAuth2Providers[source.OAuth2().Provider] | ||
} | ||
|
||
return providers, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package oauth2 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. head comment missing. |
||
|
||
import ( | ||
"code.gitea.io/gitea/modules/setting" | ||
"github.com/gorilla/sessions" | ||
"github.com/markbates/goth" | ||
"github.com/markbates/goth/gothic" | ||
"github.com/markbates/goth/providers/github" | ||
"net/http" | ||
"os" | ||
"github.com/satori/go.uuid" | ||
) | ||
|
||
var ( | ||
sessionUsersStoreKey = "gitea-oauth-sessions" | ||
providerHeaderKey = "gitea-oauth-provider" | ||
) | ||
|
||
func init() { | ||
gothic.Store = sessions.NewFilesystemStore(os.TempDir(), []byte(sessionUsersStoreKey)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gitea has a defined session-dir and a temp-dir :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will use that! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think you mean : setting.SessionConfig.ProviderConfig = "data/sessions" ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please prefix the directory with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed it to use a new data/sessions/oauth2 folder based on the setting.WorkDir method |
||
|
||
gothic.SetState = func(req *http.Request) string { | ||
return uuid.NewV4().String() | ||
} | ||
|
||
gothic.GetProviderName = func(req *http.Request) (string, error) { | ||
return req.Header.Get(providerHeaderKey), nil | ||
} | ||
} | ||
|
||
// Auth OAuth2 auth service | ||
func Auth(provider, clientID, clientSecret string, request *http.Request, response http.ResponseWriter) { | ||
// not sure if goth is thread safe (?) when using multiple providers | ||
request.Header.Set(providerHeaderKey, provider) | ||
|
||
if gothProvider, _ := goth.GetProvider(provider); gothProvider == nil { | ||
goth.UseProviders( | ||
github.New(clientID, clientSecret, setting.AppURL + "user/oauth2/" + provider + "/callback", "user:email"), | ||
) | ||
} | ||
|
||
gothic.BeginAuthHandler(response, request) | ||
} | ||
|
||
// 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, string, 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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we maybe use
len(LoginNames)
instead of a hard-coded value?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can be any value, since it will look for all the configured login sources (and so depends on the environment how many this will be)