Skip to content
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

Merged
merged 47 commits into from
Feb 22, 2017
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ded3513
initial stuff for oauth2 login, fails on:
willemvd Jan 10, 2017
1f94c85
login button on the signIn page to start the OAuth2 flow and a callba…
willemvd Jan 15, 2017
642f36e
Merge branch 'upstream-master' into oauth2-consumer
willemvd Jan 15, 2017
f93aad8
fix indentation
willemvd Jan 16, 2017
280913b
prevent macaron.Context in models
willemvd Jan 16, 2017
b597a86
show login button only when the OAuth2 consumer is configured (and ac…
willemvd Jan 16, 2017
8c2be7a
move overrides of goth functions to init
willemvd Jan 16, 2017
f392ce9
Merge remote-tracking branch 'upstream/master' into oauth2-consumer
willemvd Jan 16, 2017
2ba7833
create macaron group for oauth2 urls
willemvd Jan 16, 2017
8e1ea96
fix the broken url (dubble oauth2)
willemvd Jan 16, 2017
6c98fa7
no support at this moment to use this and we should only consider imp…
willemvd Jan 17, 2017
caeb911
prevent net/http in modules (other then oauth2)
willemvd Jan 23, 2017
f3f3866
use a new data sessions oauth2 folder for storing the oauth2 session …
willemvd Jan 23, 2017
047d50b
update resolving, naming and security settings of sessions oauth2 dir…
willemvd Jan 23, 2017
ab31c24
add missing 2FA when this is enabled on the user
willemvd Jan 23, 2017
fe88e87
add password option for OAuth2 user , for use with git over http and …
willemvd Jan 24, 2017
827c512
Merge remote-tracking branch 'upstream/master' into oauth2-consumer
willemvd Jan 25, 2017
83c238b
merge vendor.json incl goth library
willemvd Jan 25, 2017
c65a216
set a default provider instead of a empty option
willemvd Jan 25, 2017
914f56a
add tip for registering a GitHub OAuth application
willemvd Jan 25, 2017
b4eb93c
remove unused redirectURL
willemvd Jan 26, 2017
7a6757f
at startup of Gitea register all configured providers and also on add…
willemvd Jan 27, 2017
aae5f80
always use source.Name as provider key
willemvd Jan 27, 2017
9151dc8
custom handling of errors in oauth2 request init + show better tip
willemvd Jan 27, 2017
96d1af5
more checks if provider exists and is active (less calls to db to che…
willemvd Jan 27, 2017
c3f5d36
add ExternalLoginUser model and migration script to add it to database
willemvd Jan 27, 2017
2bf6b34
remove unused IsSocialLogin code
willemvd Jan 27, 2017
7ccbc44
create initial flow for linkAccount, todo: handle the POST of LinkAcc…
willemvd Jan 27, 2017
084c45f
link a external account to an existing account (still need to handle …
willemvd Jan 27, 2017
6594d76
remove the linked external account from the user his settings
willemvd Jan 30, 2017
19ddb15
make clear why we don't do anything here
willemvd Jan 30, 2017
57dbb74
add license header
willemvd Jan 30, 2017
32a4c58
if user is unknown we allow him to register a new account or link it …
willemvd Jan 31, 2017
770ba31
we are in 2017...
willemvd Jan 31, 2017
527d6e1
sign up with button on signin page (als change OAuth2Provider structu…
willemvd Feb 2, 2017
a7381b5
prevent panic when non-existing provider is in database or cannot be …
willemvd Feb 2, 2017
b64ee7d
fix err check so update of source is working
willemvd Feb 2, 2017
5c5214a
Merge remote-tracking branch 'upstream/master' into oauth2-consumer
willemvd Feb 5, 2017
769c747
merge master forces update of database to newer version
willemvd Feb 5, 2017
6b16f42
from gorilla/sessions docs:
willemvd Feb 6, 2017
0fa2e40
fix missed password reset
willemvd Feb 8, 2017
f54f07a
use updated goth lib that now supports getting the OAuth2 user if the…
willemvd Feb 8, 2017
873e5b7
Merge branch 'upstream-master' into oauth2-consumer
willemvd Feb 8, 2017
779e84b
manual merge the changes in signup.tmpl to signup_inner.tmpl
willemvd Feb 8, 2017
61fe261
prepare merge
willemvd Feb 21, 2017
66e28df
Merge branch 'upstream-master' into oauth2-consumer
willemvd Feb 21, 2017
2c11c44
Merge branch 'oauth2-consumer' of github.com:willemvd/gitea into oaut…
willemvd Feb 21, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ func runWeb(ctx *cli.Context) error {
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
m.Post("/reset_password", user.ResetPasswdPost)
m.Group("/oauth2", func() {
m.Get("/:provider", user.SignInOAuth)
m.Get("/:provider/callback", user.SignInOAuthCallback)
})
m.Group("/two_factor", func() {
m.Get("", user.TwoFactor)
m.Post("", bindIgnErr(auth.TwoFactorAuthForm{}), user.TwoFactorPost)
Expand Down
165 changes: 153 additions & 12 deletions models/login_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -57,6 +61,7 @@ var (
_ core.Conversion = &LDAPConfig{}
_ core.Conversion = &SMTPConfig{}
_ core.Conversion = &PAMConfig{}
_ core.Conversion = &OAuth2Config{}
)

// LDAPConfig holds configuration for LDAP login source.
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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()) &&
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Copy link
Member

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?

Copy link
Contributor Author

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)

return auths, x.Find(&auths)
}

Expand Down Expand Up @@ -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:]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this done by gofmt? otherwise revert this line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gofmt did this

return nil, ErrUserNotExist{0, login, 0}
}
}
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The 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 {
Expand Down Expand Up @@ -580,7 +634,7 @@ func UserSignIn(username, password string) (*User, error) {
}
}

sources := make([]*LoginSource, 0, 3)
sources := make([]*LoginSource, 0, 5)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

len(...) - 1 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
Expand All @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The 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) {
Copy link
Member

Choose a reason for hiding this comment

The 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
}
4 changes: 2 additions & 2 deletions modules/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func AssignForm(form interface{}, data map[string]interface{}) {
func getRuleBody(field reflect.StructField, prefix string) string {
for _, rule := range strings.Split(field.Tag.Get("binding"), ";") {
if strings.HasPrefix(rule, prefix) {
return rule[len(prefix) : len(rule)-1]
return rule[len(prefix): len(rule) - 1]
}
}
return ""
Expand Down Expand Up @@ -246,7 +246,7 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro
}

if errs[0].FieldNames[0] == field.Name {
data["Err_"+field.Name] = true
data["Err_" + field.Name] = true

trName := field.Tag.Get("locale")
if len(trName) == 0 {
Expand Down
5 changes: 4 additions & 1 deletion modules/auth/auth_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
// AuthenticationForm form for authentication
type AuthenticationForm struct {
ID int64
Type int `binding:"Range(2,5)"`
Type int `binding:"Range(2,6)"`
Name string `binding:"Required;MaxSize(30)"`
Host string
Port int
Expand All @@ -36,6 +36,9 @@ type AuthenticationForm struct {
TLS bool
SkipVerify bool
PAMServiceName string
Oauth2Provider string
Oauth2Key string
Oauth2Secret string
}

// Validate validates fields
Expand Down
57 changes: 57 additions & 0 deletions modules/auth/oauth2/oauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package oauth2
Copy link
Member

Choose a reason for hiding this comment

The 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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitea has a defined session-dir and a temp-dir :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will use that!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think you mean : setting.SessionConfig.ProviderConfig = "data/sessions" ?
But this seems only valid when the session provider is file and not for instance memory

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please prefix the directory with gitea- :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,9 @@ auths.allowed_domains_helper = Leave it empty to not restrict any domains. Multi
auths.enable_tls = Enable TLS Encryption
auths.skip_tls_verify = Skip TLS Verify
auths.pam_service_name = PAM Service Name
auths.oauth2_provider = OAuth2 provider
auths.oauth2_clientID = Client ID (Key)
auths.oauth2_clientSecret = Client Secret
auths.enable_auto_register = Enable Auto Registration
auths.tips = Tips
auths.edit = Edit Authentication Setting
Expand Down
8 changes: 6 additions & 2 deletions public/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -969,9 +969,9 @@ function initAdmin() {
// New authentication
if ($('.admin.new.authentication').length > 0) {
$('#auth_type').change(function () {
$('.ldap, .dldap, .smtp, .pam, .has-tls').hide();
$('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls').hide();

$('.ldap input[required], .dldap input[required], .smtp input[required], .pam input[required], .has-tls input[required]').removeAttr('required');
$('.ldap input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required] .has-tls input[required]').removeAttr('required');

var authType = $(this).val();
switch (authType) {
Expand All @@ -992,6 +992,10 @@ function initAdmin() {
$('.dldap').show();
$('.dldap div.required input').attr('required', 'required');
break;
case '6': // OAuth2
$('.oauth2').show();
$('.oauth2 input').attr('required', 'required');
break;
}

if (authType == '2' || authType == '5') {
Expand Down
Loading