From 3f28e162b3505d85d25e8e090f9f6db5b03d07b5 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Sat, 7 Jan 2017 19:08:49 +0100 Subject: [PATCH 01/16] OpenID login support - OpenID sign-in form with "Rember Me" support - Caches OpenID discovered info for 24 hours - Prompt to connect existing or register new user (if enabled) on valid but unknown OpenID URI (default route based upon finding a nickname/email match) - Allow users to associate multiple OpenID URIs to their accounts - Assigns random password to users who _register_ with valid OpenID - Introduces an [openid] app.ini section with ENABLE_OPENID_SIGNIN and ENABLE_OPENID_SIGNUP variables Tested to work with: - GNUSocial (https://gnu.io/social/) - SimpleID (http://simpleid.koinic.net) - openid.stackexchange.com - login.launchpad.net - http://openid.org.cn/ --- cmd/web.go | 21 + conf/app.ini | 6 + models/error.go | 15 + models/migrations/migrations.go | 2 + models/migrations/v23.go | 26 ++ models/models.go | 1 + models/user.go | 1 + models/user_openid.go | 117 ++++++ modules/auth/openid/openid.go | 37 ++ modules/auth/openid/openid_discovery_cache.go | 55 +++ modules/auth/user_form.go | 12 +- modules/auth/user_form_auth_openid.go | 45 ++ modules/context/context.go | 1 + modules/setting/setting.go | 8 + options/locale/locale_en-US.ini | 16 + routers/user/auth.go | 10 +- routers/user/auth_openid.go | 395 ++++++++++++++++++ routers/user/setting_openid.go | 142 +++++++ templates/user/auth/finalize_openid.tmpl | 46 ++ templates/user/auth/navbar.tmpl | 14 + templates/user/auth/signin.tmpl | 11 +- templates/user/auth/signin_inner.tmpl | 19 +- templates/user/auth/signin_openid.tmpl | 36 ++ .../user/auth/signup_openid_connect.tmpl | 47 +++ templates/user/auth/signup_openid_navbar.tmpl | 14 + .../user/auth/signup_openid_register.tmpl | 37 ++ templates/user/settings/navbar.tmpl | 7 +- templates/user/settings/openid.tmpl | 57 +++ vendor/github.com/yohcop/openid-go/LICENSE | 13 + vendor/github.com/yohcop/openid-go/README.md | 38 ++ .../github.com/yohcop/openid-go/discover.go | 57 +++ .../yohcop/openid-go/discovery_cache.go | 69 +++ vendor/github.com/yohcop/openid-go/getter.go | 31 ++ .../yohcop/openid-go/html_discovery.go | 77 ++++ .../yohcop/openid-go/nonce_store.go | 87 ++++ .../github.com/yohcop/openid-go/normalizer.go | 64 +++ vendor/github.com/yohcop/openid-go/openid.go | 15 + .../github.com/yohcop/openid-go/redirect.go | 55 +++ vendor/github.com/yohcop/openid-go/verify.go | 250 +++++++++++ vendor/github.com/yohcop/openid-go/xrds.go | 83 ++++ .../yohcop/openid-go/yadis_discovery.go | 119 ++++++ vendor/vendor.json | 6 + 42 files changed, 2147 insertions(+), 15 deletions(-) create mode 100644 models/migrations/v23.go create mode 100644 models/user_openid.go create mode 100644 modules/auth/openid/openid.go create mode 100644 modules/auth/openid/openid_discovery_cache.go create mode 100644 modules/auth/user_form_auth_openid.go create mode 100644 routers/user/auth_openid.go create mode 100644 routers/user/setting_openid.go create mode 100644 templates/user/auth/finalize_openid.tmpl create mode 100644 templates/user/auth/navbar.tmpl create mode 100644 templates/user/auth/signin_openid.tmpl create mode 100644 templates/user/auth/signup_openid_connect.tmpl create mode 100644 templates/user/auth/signup_openid_navbar.tmpl create mode 100644 templates/user/auth/signup_openid_register.tmpl create mode 100644 templates/user/settings/openid.tmpl create mode 100644 vendor/github.com/yohcop/openid-go/LICENSE create mode 100644 vendor/github.com/yohcop/openid-go/README.md create mode 100644 vendor/github.com/yohcop/openid-go/discover.go create mode 100644 vendor/github.com/yohcop/openid-go/discovery_cache.go create mode 100644 vendor/github.com/yohcop/openid-go/getter.go create mode 100644 vendor/github.com/yohcop/openid-go/html_discovery.go create mode 100644 vendor/github.com/yohcop/openid-go/nonce_store.go create mode 100644 vendor/github.com/yohcop/openid-go/normalizer.go create mode 100644 vendor/github.com/yohcop/openid-go/openid.go create mode 100644 vendor/github.com/yohcop/openid-go/redirect.go create mode 100644 vendor/github.com/yohcop/openid-go/verify.go create mode 100644 vendor/github.com/yohcop/openid-go/xrds.go create mode 100644 vendor/github.com/yohcop/openid-go/yadis_discovery.go diff --git a/cmd/web.go b/cmd/web.go index 0410ad5190ce1..17674b3069e7e 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -200,6 +200,19 @@ func runWeb(ctx *cli.Context) error { m.Group("/user", func() { m.Get("/login", user.SignIn) m.Post("/login", bindIgnErr(auth.SignInForm{}), user.SignInPost) + if setting.EnableOpenIDSignIn { + m.Combo("/login/openid"). + Get(user.SignInOpenID). + Post(bindIgnErr(auth.SignInOpenIDForm{}), user.SignInOpenIDPost) + m.Group("/openid", func() { + m.Combo("/connect"). + Get(user.ConnectOpenID). + Post(bindIgnErr(auth.ConnectOpenIDForm{}), user.ConnectOpenIDPost) + m.Combo("/register"). + Get(user.RegisterOpenID). + Post(bindIgnErr(auth.SignUpOpenIDForm{}), user.RegisterOpenIDPost) + }) + } m.Get("/sign_up", user.SignUp) m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost) m.Get("/reset_password", user.ResetPasswd) @@ -230,6 +243,14 @@ func runWeb(ctx *cli.Context) error { m.Post("/email/delete", user.DeleteEmail) m.Get("/password", user.SettingsPassword) m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost) + if setting.EnableOpenIDSignIn { + m.Group("/openid", func() { + m.Combo("").Get(user.SettingsOpenID). + Post(bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost) + m.Post("/delete", user.DeleteOpenID) + }) + } + m.Combo("/ssh").Get(user.SettingsSSHKeys). Post(bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost) m.Post("/ssh/delete", user.DeleteSSHKey) diff --git a/conf/app.ini b/conf/app.ini index 8e29e39b11987..10abc89f3e987 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -182,6 +182,12 @@ MIN_PASSWORD_LENGTH = 6 ; True when users are allowed to import local server paths IMPORT_LOCAL_PATHS = false +[openid] +; Whether to allow signin in via OpenID +ENABLE_OPENID_SIGNIN = true +; Whether to allow registering via OpenID +ENABLE_OPENID_SIGNUP = true + [service] ACTIVE_CODE_LIVE_MINUTES = 180 RESET_PASSWD_CODE_LIVE_MINUTES = 180 diff --git a/models/error.go b/models/error.go index 62529f83fa9be..68bc238907ea8 100644 --- a/models/error.go +++ b/models/error.go @@ -93,6 +93,21 @@ func (err ErrEmailAlreadyUsed) Error() string { return fmt.Sprintf("e-mail has been used [email: %s]", err.Email) } +// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error. +type ErrOpenIDAlreadyUsed struct { + OpenID string +} + +// IsErrOpenIDAlreadyUsed checks if an error is a ErrOpenIDAlreadyUsed. +func IsErrOpenIDAlreadyUsed(err error) bool { + _, ok := err.(ErrOpenIDAlreadyUsed) + return ok +} + +func (err ErrOpenIDAlreadyUsed) Error() string { + return fmt.Sprintf("OpenID has been used [oid: %s]", err.OpenID) +} + // ErrUserOwnRepos represents a "UserOwnRepos" kind of error. type ErrUserOwnRepos struct { UID int64 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index bf188dc4ce20b..4f1254b9605d1 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -94,6 +94,8 @@ var migrations = []Migration{ NewMigration("rewrite authorized_keys file via new format", useNewPublickeyFormat), // v22 -> v23 NewMigration("generate and migrate wiki Git hooks", generateAndMigrateWikiGitHooks), + // v23 -> v24 + NewMigration("add user openid table", addUserOpenID), } // Migrate database to current version diff --git a/models/migrations/v23.go b/models/migrations/v23.go new file mode 100644 index 0000000000000..efde684104d4c --- /dev/null +++ b/models/migrations/v23.go @@ -0,0 +1,26 @@ +// Copyright 2017 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "github.com/go-xorm/xorm" +) + +// UserOpenID is the list of all OpenID identities of a user. +type UserOpenID struct { + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"INDEX NOT NULL"` + URI string `xorm:"UNIQUE NOT NULL"` +} + + +func addUserOpenID(x *xorm.Engine) error { + if err := x.Sync2(new(UserOpenID)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} diff --git a/models/models.go b/models/models.go index bba4446db081c..2ae6e355fce24 100644 --- a/models/models.go +++ b/models/models.go @@ -116,6 +116,7 @@ func init() { new(RepoRedirect), new(ExternalLoginUser), new(ProtectedBranch), + new(UserOpenID), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/user.go b/models/user.go index ff898573a625c..ad303d753506a 100644 --- a/models/user.go +++ b/models/user.go @@ -964,6 +964,7 @@ func deleteUser(e *xorm.Session, u *User) error { &Action{UserID: u.ID}, &IssueUser{UID: u.ID}, &EmailAddress{UID: u.ID}, + &UserOpenID{UID: u.ID}, ); err != nil { return fmt.Errorf("deleteBeans: %v", err) } diff --git a/models/user_openid.go b/models/user_openid.go new file mode 100644 index 0000000000000..a5c88e9009d76 --- /dev/null +++ b/models/user_openid.go @@ -0,0 +1,117 @@ +// 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 models + +import ( + "errors" + + "code.gitea.io/gitea/modules/auth/openid" + "code.gitea.io/gitea/modules/log" +) + +var ( + // ErrOpenIDNotExist openid is not known + ErrOpenIDNotExist = errors.New("OpenID is unknown") +) + +// UserOpenID is the list of all OpenID identities of a user. +type UserOpenID struct { + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"INDEX NOT NULL"` + URI string `xorm:"UNIQUE NOT NULL"` +} + +// GetUserOpenIDs returns all openid addresses that belongs to given user. +func GetUserOpenIDs(uid int64) ([]*UserOpenID, error) { + openids := make([]*UserOpenID, 0, 5) + if err := x. + Where("uid=?", uid). + Find(&openids); err != nil { + return nil, err + } + + return openids, nil +} + +func isOpenIDUsed(e Engine, uri string) (bool, error) { + if len(uri) == 0 { + return true, nil + } + + return e.Get(&UserOpenID{URI: uri}) +} + +// IsOpenIDUsed returns true if the openid has been used. +func IsOpenIDUsed(openid string) (bool, error) { + return isOpenIDUsed(x, openid) +} + +// NOTE: make sure openid.URI is normalized already +func addUserOpenID(e Engine, openid *UserOpenID) error { + used, err := isOpenIDUsed(e, openid.URI) + if err != nil { + return err + } else if used { + return ErrOpenIDAlreadyUsed{openid.URI} + } + + _, err = e.Insert(openid) + return err +} + +// AddUserOpenID adds an pre-verified/normalized OpenID URI to given user. +func AddUserOpenID(openid *UserOpenID) error { + return addUserOpenID(x, openid) +} + +// DeleteUserOpenID deletes an openid address of given user. +func DeleteUserOpenID(openid *UserOpenID) (err error) { + var deleted int64 + // ask to check UID + var address = UserOpenID{ + UID: openid.UID, + } + if openid.ID > 0 { + deleted, err = x.Id(openid.ID).Delete(&address) + } else { + deleted, err = x. + Where("openid=?", openid.URI). + Delete(&address) + } + + if err != nil { + return err + } else if deleted != 1 { + return ErrOpenIDNotExist + } + return nil +} + +// GetUserByOpenID returns the user object by given OpenID if exists. +func GetUserByOpenID(uri string) (*User, error) { + if len(uri) == 0 { + return nil, ErrUserNotExist{0, uri, 0} + } + + uri, err := openid.Normalize(uri) + if err != nil { + return nil, err + } + + log.Trace("Normalized OpenID URI: " + uri) + + // Otherwise, check in openid table + oid := &UserOpenID{URI: uri} + has, err := x.Get(oid) + if err != nil { + return nil, err + } + if has { + return GetUserByID(oid.UID) + } + + return nil, ErrUserNotExist{0, uri, 0} +} + diff --git a/modules/auth/openid/openid.go b/modules/auth/openid/openid.go new file mode 100644 index 0000000000000..aebdf15155368 --- /dev/null +++ b/modules/auth/openid/openid.go @@ -0,0 +1,37 @@ +// 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 openid + +import ( + "github.com/yohcop/openid-go" + "time" +) + +// For the demo, we use in-memory infinite storage nonce and discovery +// cache. In your app, do not use this as it will eat up memory and +// never +// free it. Use your own implementation, on a better database system. +// If you have multiple servers for example, you may need to share at +// least +// the nonceStore between them. +var nonceStore = openid.NewSimpleNonceStore() +var discoveryCache = newTimedDiscoveryCache(24*time.Hour) + + +// Verify handles response from OpenID provider +func Verify(fullURL string) (id string, err error) { + return openid.Verify(fullURL, discoveryCache, nonceStore) +} + +// Normalize normalizes an OpenID URI +func Normalize(url string) (id string, err error) { + return openid.Normalize(url) +} + +// RedirectURL redirects browser +func RedirectURL(id, callbackURL, realm string) (string, error) { + return openid.RedirectURL(id, callbackURL, realm) +} + diff --git a/modules/auth/openid/openid_discovery_cache.go b/modules/auth/openid/openid_discovery_cache.go new file mode 100644 index 0000000000000..72069374ea8e1 --- /dev/null +++ b/modules/auth/openid/openid_discovery_cache.go @@ -0,0 +1,55 @@ +// 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 openid + +import ( + "github.com/yohcop/openid-go" + "sync" + "time" +) + +type timedDiscoveredInfo struct { + info openid.DiscoveredInfo + time time.Time +} + +type timedDiscoveryCache struct { + cache map[string]timedDiscoveredInfo + ttl time.Duration + mutex *sync.Mutex +} + +func newTimedDiscoveryCache(ttl time.Duration) *timedDiscoveryCache { + return &timedDiscoveryCache{cache: map[string]timedDiscoveredInfo{}, ttl: ttl, mutex: &sync.Mutex{}} +} + +func (s *timedDiscoveryCache) Put(id string, info openid.DiscoveredInfo) { + s.mutex.Lock() + defer s.mutex.Unlock() + + s.cache[id] = timedDiscoveredInfo{info: info, time: time.Now()} +} + +func (s *timedDiscoveryCache) Get(id string) openid.DiscoveredInfo { + s.mutex.Lock() + defer s.mutex.Unlock() + + // Delete old cached while we are at it. + newCache := map[string]timedDiscoveredInfo{} + now := time.Now() + for k, e := range s.cache { + diff := now.Sub(e.time) + if diff <= s.ttl { + newCache[k] = e + } + } + s.cache = newCache + + if info, has := s.cache[id]; has { + return info.info + } + return nil +} + diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index 32987e6d37904..9c6e38c4605aa 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -78,7 +78,7 @@ func (f *RegisterForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi return validate(errs, ctx.Data, f, ctx.Locale) } -// SignInForm form for signing in +// SignInForm form for signing in with user/password type SignInForm struct { UserName string `binding:"Required;MaxSize(254)"` Password string `binding:"Required;MaxSize(255)"` @@ -153,6 +153,16 @@ func (f *ChangePasswordForm) Validate(ctx *macaron.Context, errs binding.Errors) return validate(errs, ctx.Data, f, ctx.Locale) } +// AddOpenIDForm is for changing openid uri +type AddOpenIDForm struct { + Openid string `binding:"Required;MaxSize(256)"` +} + +// Validate validates the fields +func (f *AddOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + // AddSSHKeyForm form for adding SSH key type AddSSHKeyForm struct { Title string `binding:"Required;MaxSize(50)"` diff --git a/modules/auth/user_form_auth_openid.go b/modules/auth/user_form_auth_openid.go new file mode 100644 index 0000000000000..582c6dc69fce0 --- /dev/null +++ b/modules/auth/user_form_auth_openid.go @@ -0,0 +1,45 @@ +// 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 auth + +import ( + "github.com/go-macaron/binding" + "gopkg.in/macaron.v1" +) + + +// SignInOpenIDForm form for signing in with OpenID +type SignInOpenIDForm struct { + Openid string `binding:"Required;MaxSize(256)"` + Remember bool +} + +// Validate valideates the fields +func (f *SignInOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + +// SignUpOpenIDForm form for signin up with OpenID +type SignUpOpenIDForm struct { + UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"` + Email string `binding:"Required;Email;MaxSize(254)"` +} + +// Validate valideates the fields +func (f *SignUpOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + +// ConnectOpenIDForm form for connecting an existing account to an OpenID URI +type ConnectOpenIDForm struct { + UserName string `binding:"Required;MaxSize(254)"` + Password string `binding:"Required;MaxSize(255)"` +} + +// Validate valideates the fields +func (f *ConnectOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + diff --git a/modules/context/context.go b/modules/context/context.go index fa53b484ee2ea..52e50af6a142e 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -197,6 +197,7 @@ func Contexter() macaron.Handler { ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion + ctx.Data["EnableOpenIDSignIn"] = setting.EnableOpenIDSignIn c.Map(ctx) } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 520dc429df954..033a8648d2690 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -120,6 +120,10 @@ var ( MinPasswordLength int ImportLocalPaths bool + // OpenID settings + EnableOpenIDSignIn bool + EnableOpenIDSignUp bool + // Database settings UseSQLite3 bool UseMySQL bool @@ -755,6 +759,10 @@ please consider changing to GITEA_CUSTOM`) MinPasswordLength = sec.Key("MIN_PASSWORD_LENGTH").MustInt(6) ImportLocalPaths = sec.Key("IMPORT_LOCAL_PATHS").MustBool(false) + sec = Cfg.Section("openid") + EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(true) + EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(true) + sec = Cfg.Section("attachment") AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments")) if !filepath.IsAbs(AttachmentPath) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f66a7ca689e5a..7ed1cb6f6a636 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -188,6 +188,13 @@ use_scratch_code = Use a scratch code twofa_scratch_used = You have used your scratch code. You have been redirected to the two-factor settings page so you may remove your device enrollment or generate a new scratch code. twofa_passcode_incorrect = Your passcode is not correct. If you misplaced your device, use your scratch code to login. twofa_scratch_token_incorrect = Your scratch code is not correct. +login_userpass = User / Password +login_openid = OpenID +openid_connect_submit = Connect +openid_connect_title = Connect to an existing account +openid_connect_desc = The entered OpenID URIs is not know by the system, here you can associate it to an existing account. +openid_register_title = Create new account +openid_register_desc = The entered OpenID URIs is not know by the system, here you can associate it to a new account. [mail] activate_account = Please activate your account @@ -239,6 +246,7 @@ repo_name_been_taken = Repository name has already been used. org_name_been_taken = Organization name has already been taken. team_name_been_taken = Team name has already been taken. email_been_used = Email address has already been used. +openid_been_used = OpenID address '%s' has already been used. username_password_incorrect = Username or password is not correct. enterred_invalid_repo_name = Please make sure that the repository name you entered is correct. enterred_invalid_owner_name = Please make sure that the owner name you entered is correct. @@ -315,6 +323,7 @@ password_change_disabled = Non-local users are not allowed to change their passw emails = Email Addresses manage_emails = Manage email addresses +manage_openid = Manage OpenID addresses email_desc = Your primary email address will be used for notifications and other operations. primary = Primary primary_email = Set as primary @@ -322,12 +331,19 @@ delete_email = Delete email_deletion = Email Deletion email_deletion_desc = Deleting this email address will remove all related information from your account. Do you want to continue? email_deletion_success = Email has been deleted successfully! +openid_deletion = OpenID Deletion +openid_deletion_desc = Deleting this OpenID address will prevent you from signing in using it, are you sure you want to continue ? +openid_deletion_success = OpenID has been deleted successfully! add_new_email = Add new email address +add_new_openid = Add new OpenID URI add_email = Add email +add_openid = Add OpenID URI add_email_confirmation_sent = A new confirmation email has been sent to '%s', please check your inbox within the next %d hours to complete the confirmation process. add_email_success = Your new email address was successfully added. +add_openid_success = Your new OpenID address was successfully added. keep_email_private = Keep Email Address Private keep_email_private_popup = Your email address will be hidden from other users if this option is set. +openid_desc = Your OpenID addresses will let you delegate authentication to your provider of choice manage_ssh_keys = Manage SSH Keys add_key = Add Key diff --git a/routers/user/auth.go b/routers/user/auth.go index f8c6db1268f2d..4827f38b5245a 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -107,7 +107,6 @@ func checkAutoLogin(ctx *context.Context) bool { // SignIn render sign in page func SignIn(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("sign_in") // Check auto-login. if checkAutoLogin(ctx) { @@ -120,6 +119,9 @@ func SignIn(ctx *context.Context) { return } ctx.Data["OAuth2Providers"] = oauth2Providers + ctx.Data["Title"] = ctx.Tr("sign_in") + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsLogin"] = true ctx.HTML(200, tplSignIn) } @@ -127,6 +129,8 @@ func SignIn(ctx *context.Context) { // SignInPost response for sign in request func SignInPost(ctx *context.Context, form auth.SignInForm) { ctx.Data["Title"] = ctx.Tr("sign_in") + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsLogin"] = true oauth2Providers, err := models.GetActiveOAuth2Providers() if err != nil { @@ -316,6 +320,10 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR setting.CookieRememberName, u.Name, days, setting.AppSubURL) } + ctx.Session.Delete("openid_verified_uri") + ctx.Session.Delete("openid_signin_remember") + ctx.Session.Delete("openid_determined_email") + ctx.Session.Delete("openid_determined_username") ctx.Session.Delete("twofaUid") ctx.Session.Delete("twofaRemember") ctx.Session.Set("uid", u.ID) diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go new file mode 100644 index 0000000000000..11d9042e4f946 --- /dev/null +++ b/routers/user/auth_openid.go @@ -0,0 +1,395 @@ +// 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 user + +import ( + "net/url" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/auth/openid" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSignInOpenID base.TplName = "user/auth/signin_openid" + tplConnectOID base.TplName = "user/auth/signup_openid_connect" + tplSignUpOID base.TplName = "user/auth/signup_openid_register" +) + +// SignInOpenID render sign in page +func SignInOpenID(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("sign_in") + + if ctx.Query("openid.return_to") != "" { + signInOpenIDVerify(ctx) + return + } + + // Check auto-login. + isSucceed, err := AutoSignIn(ctx) + if err != nil { + ctx.Handle(500, "AutoSignIn", err) + return + } + + redirectTo := ctx.Query("redirect_to") + if len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", redirectTo, 0, setting.AppSubURL) + } else { + redirectTo, _ = url.QueryUnescape(ctx.GetCookie("redirect_to")) + } + + if isSucceed { + if len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) + ctx.Redirect(redirectTo) + } else { + ctx.Redirect(setting.AppSubURL + "/") + } + return + } + + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsLoginOpenID"] = true + ctx.HTML(200, tplSignInOpenID) +} + +// SignInOpenIDPost response for openid sign in request +func SignInOpenIDPost(ctx *context.Context, form auth.SignInOpenIDForm) { + ctx.Data["Title"] = ctx.Tr("sign_in") + + if ctx.HasError() { + ctx.HTML(200, tplSignInOpenID) + return + } + + id, err := openid.Normalize(form.Openid) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form) + return; + } + form.Openid = id + + log.Trace("OpenID uri: " + id) + + // TODO: somehow forward the form.Remember option + // either append to destination url or set a cookie + redirectTo := setting.AppURL + "user/login/openid" + url, err := openid.RedirectURL(id, redirectTo, setting.AppURL) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form) + return; + } + + // Request optional nickname and email info + // NOTE: change to `openid.sreg.required` to require it + url += "&openid.ns.sreg=http%3A%2F%2Fopenid.net%2Fextensions%2Fsreg%2F1.1" + url += "&openid.sreg.optional=nickname%2Cemail" + + log.Trace("Form-passed openid-remember: %s", form.Remember) + ctx.Session.Set("openid_signin_remember", form.Remember) + + ctx.Redirect(url) +} + +// signInOpenIDVerify handles response from OpenID provider +func signInOpenIDVerify(ctx *context.Context) { + + log.Trace("Incoming call to: " + ctx.Req.Request.URL.String()) + + fullURL := setting.AppURL + ctx.Req.Request.URL.String()[1:] + log.Trace("Full URL: " + fullURL) + + var id, err = openid.Verify(fullURL) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &auth.SignInOpenIDForm{ + Openid: id, + }) + return + } + + log.Trace("Verified ID: " + id) + + /* Now we should seek for the user and log him in, or prompt + * to register if not found */ + + u, _ := models.GetUserByOpenID(id) + if err != nil { + if ! models.IsErrUserNotExist(err) { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &auth.SignInOpenIDForm{ + Openid: id, + }) + return + } + } + if u != nil { + log.Trace("User exists, logging in") + remember, _ := ctx.Session.Get("openid_signin_remember").(bool) + log.Trace("Session stored openid-remember: %s", remember) + handleSignIn(ctx, u, remember) + return + } + + log.Trace("User with openid " + id + " does not exist, should connect or register") + + parsedURL, err := url.Parse(fullURL) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &auth.SignInOpenIDForm{ + Openid: id, + }) + return + } + values, err := url.ParseQuery(parsedURL.RawQuery) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &auth.SignInOpenIDForm{ + Openid: id, + }) + return + } + email := values.Get("openid.sreg.email") + nickname := values.Get("openid.sreg.nickname") + + log.Trace("User has email=" + email + " and nickname=" + nickname) + + if email != "" { + u, _ = models.GetUserByEmail(email) + if err != nil { + if ! models.IsErrUserNotExist(err) { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &auth.SignInOpenIDForm{ + Openid: id, + }) + return + } + } + if u != nil { + log.Trace("Local user " + u.LowerName + " has OpenID provided email " + email) + } + } + + if u == nil && nickname != "" { + u, _ = models.GetUserByName(nickname) + if err != nil { + if ! models.IsErrUserNotExist(err) { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &auth.SignInOpenIDForm{ + Openid: id, + }) + return + } + } + if u != nil { + log.Trace("Local user " + u.LowerName + " has OpenID provided nickname " + nickname) + } + } + + ctx.Session.Set("openid_verified_uri", id) + + ctx.Session.Set("openid_determined_email", email) + + if u != nil { + nickname = u.LowerName + } + + ctx.Session.Set("openid_determined_username", nickname) + + if u != nil || ! setting.EnableOpenIDSignUp { + ctx.Redirect(setting.AppSubURL + "/user/openid/connect") + } else { + ctx.Redirect(setting.AppSubURL + "/user/openid/register") + } +} + +// ConnectOpenID shows a form to connect an OpenID URI to an existing account +func ConnectOpenID(ctx *context.Context) { + oid, _ := ctx.Session.Get("openid_verified_uri").(string) + if oid == "" { + ctx.Redirect(setting.AppSubURL + "/user/login/openid") + return + } + ctx.Data["Title"] = "OpenID connect" + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsOpenIDConnect"] = true + ctx.Data["EnableOpenIDSignUp"] = setting.EnableOpenIDSignUp + ctx.Data["OpenID"] = oid + userName, _ := ctx.Session.Get("openid_determined_username").(string) + if userName != "" { + ctx.Data["user_name"] = userName + } + ctx.HTML(200, tplConnectOID) +} + +// ConnectOpenIDPost handles submission of a form to connect an OpenID URI to an existing account +func ConnectOpenIDPost(ctx *context.Context, form auth.ConnectOpenIDForm) { + oid, _ := ctx.Session.Get("openid_verified_uri").(string) + if oid == "" { + ctx.Redirect(setting.AppSubURL + "/user/login/openid") + return + } + ctx.Data["Title"] = "OpenID connect" + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsOpenIDConnect"] = true + ctx.Data["EnableOpenIDSignUp"] = setting.EnableOpenIDSignUp + ctx.Data["OpenID"] = oid + + u, err := models.UserSignIn(form.UserName, form.Password) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form) + } else { + ctx.Handle(500, "ConnectOpenIDPost", err) + } + return + } + + // add OpenID for the user + userOID := &models.UserOpenID{UID:u.ID, URI:oid} + if err = models.AddUserOpenID(userOID); err != nil { + if models.IsErrOpenIDAlreadyUsed(err) { + ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form) + return + } + ctx.Handle(500, "AddUserOpenID", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.add_openid_success")) + + remember, _ := ctx.Session.Get("openid_signin_remember").(bool) + log.Trace("Session stored openid-remember: %s", remember) + handleSignIn(ctx, u, remember) +} + +// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI +func RegisterOpenID(ctx *context.Context) { + if ! setting.EnableOpenIDSignUp { + ctx.Error(403) + return + } + oid, _ := ctx.Session.Get("openid_verified_uri").(string) + if oid == "" { + ctx.Redirect(setting.AppSubURL + "/user/login/openid") + return + } + ctx.Data["Title"] = "OpenID signup" + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsOpenIDRegister"] = true + ctx.Data["EnableOpenIDSignUp"] = setting.EnableOpenIDSignUp + ctx.Data["OpenID"] = oid + userName, _ := ctx.Session.Get("openid_determined_username").(string) + if userName != "" { + ctx.Data["user_name"] = userName + } + email, _ := ctx.Session.Get("openid_determined_email").(string) + if email != "" { + ctx.Data["email"] = email + } + ctx.HTML(200, tplSignUpOID) +} + +// RegisterOpenIDPost handles submission of a form to create a new user authenticated via an OpenID URI +func RegisterOpenIDPost(ctx *context.Context, form auth.SignUpOpenIDForm) { + if ! setting.EnableOpenIDSignUp { + ctx.Error(403) + return + } + oid, _ := ctx.Session.Get("openid_verified_uri").(string) + if oid == "" { + ctx.Redirect(setting.AppSubURL + "/user/login/openid") + return + } + + ctx.Data["Title"] = "OpenID signup" + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsOpenIDRegister"] = true + ctx.Data["EnableOpenIDSignUp"] = setting.EnableOpenIDSignUp + ctx.Data["OpenID"] = oid + +/* + // TODO: handle captcha ? + if setting.Service.EnableCaptcha && !cpt.VerifyReq(ctx.Req) { + ctx.Data["Err_Captcha"] = true + ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form) + return + } +*/ + + len := setting.MinPasswordLength + if len < 256 { len = 256 } + password, err := base.GetRandomString(len) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSignUpOID, form) + return + } + + // TODO: abstract a finalizeSignUp function ? + u := &models.User{ + Name: form.UserName, + Email: form.Email, + Passwd: password, + IsActive: !setting.Service.RegisterEmailConfirm, + } + if err := models.CreateUser(u); err != nil { + switch { + case models.IsErrUserAlreadyExist(err): + ctx.Data["Err_UserName"] = true + ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSignUpOID, &form) + case models.IsErrEmailAlreadyUsed(err): + ctx.Data["Err_Email"] = true + ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignUpOID, &form) + case models.IsErrNameReserved(err): + ctx.Data["Err_UserName"] = true + ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplSignUpOID, &form) + case models.IsErrNamePatternNotAllowed(err): + ctx.Data["Err_UserName"] = true + ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSignUpOID, &form) + default: + ctx.Handle(500, "CreateUser", err) + } + return + } + log.Trace("Account created: %s", u.Name) + + // add OpenID for the user + userOID := &models.UserOpenID{UID:u.ID, URI:oid} + if err = models.AddUserOpenID(userOID); err != nil { + if models.IsErrOpenIDAlreadyUsed(err) { + ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form) + return + } + ctx.Handle(500, "AddUserOpenID", err) + return + } + + // Auto-set admin for the only user. + if models.CountUsers() == 1 { + u.IsAdmin = true + u.IsActive = true + if err := models.UpdateUser(u); err != nil { + ctx.Handle(500, "UpdateUser", err) + return + } + } + + // Send confirmation email, no need for social account. + if setting.Service.RegisterEmailConfirm && u.ID > 1 { + models.SendActivateAccountMail(ctx.Context, u) + ctx.Data["IsSendRegisterMail"] = true + ctx.Data["Email"] = u.Email + ctx.Data["Hours"] = setting.Service.ActiveCodeLives / 60 + ctx.HTML(200, TplActivate) + + if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { + log.Error(4, "Set cache(MailResendLimit) fail: %v", err) + } + return + } + + remember, _ := ctx.Session.Get("openid_signin_remember").(bool) + log.Trace("Session stored openid-remember: %s", remember) + handleSignIn(ctx, u, remember) +} diff --git a/routers/user/setting_openid.go b/routers/user/setting_openid.go new file mode 100644 index 0000000000000..5e6052d3ef4c6 --- /dev/null +++ b/routers/user/setting_openid.go @@ -0,0 +1,142 @@ +// 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 user + +import ( + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/auth/openid" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsOpenID base.TplName = "user/settings/openid" +) + +// SettingsOpenID renders change user's openid page +func SettingsOpenID(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsOpenID"] = true + + if ctx.Query("openid.return_to") != "" { + settingsOpenIDVerify(ctx) + return + } + + openid, err := models.GetUserOpenIDs(ctx.User.ID) + if err != nil { + ctx.Handle(500, "GetUserOpenIDs", err) + return + } + ctx.Data["OpenIDs"] = openid + + ctx.HTML(200, tplSettingsOpenID) +} + +// SettingsOpenIDPost response for change user's openid +func SettingsOpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsOpenID"] = true + + if ctx.HasError() { + ctx.HTML(200, tplSettingsOpenID) + return + } + + // WARNING: specifying a wrong OpenID here could lock + // a user out of her account, would be better to + // verify/confirm the new OpenID before storing it + + // Also, consider allowing for multiple OpenID URIs + + id, err := openid.Normalize(form.Openid) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSettingsOpenID, &form) + return; + } + form.Openid = id + log.Trace("Normalized id: " + id) + + oids, err := models.GetUserOpenIDs(ctx.User.ID) + if err != nil { + ctx.Handle(500, "GetUserOpenIDs", err) + return + } + ctx.Data["OpenIDs"] = oids + + // Check that the OpenID is not already used + for _, obj := range oids { + if obj.URI == id { + ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsOpenID, &form) + return + } + } + + + redirectTo := setting.AppURL + "user/settings/openid" + url, err := openid.RedirectURL(id, redirectTo, setting.AppURL) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSettingsOpenID, &form) + return; + } + ctx.Redirect(url) +} + +func settingsOpenIDVerify(ctx *context.Context) { + log.Trace("Incoming call to: " + ctx.Req.Request.URL.String()) + + fullURL := setting.AppURL + ctx.Req.Request.URL.String()[1:] + log.Trace("Full URL: " + fullURL) + + oids, err := models.GetUserOpenIDs(ctx.User.ID) + if err != nil { + ctx.Handle(500, "GetUserOpenIDs", err) + return + } + ctx.Data["OpenIDs"] = oids + + id, err := openid.Verify(fullURL) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSettingsOpenID, &auth.AddOpenIDForm{ + Openid: id, + }) + return + } + + log.Trace("Verified ID: " + id) + + oid := &models.UserOpenID{UID:ctx.User.ID, URI:id} + if err = models.AddUserOpenID(oid); err != nil { + if models.IsErrOpenIDAlreadyUsed(err) { + ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsOpenID, &auth.AddOpenIDForm{ Openid: id }) + return + } + ctx.Handle(500, "AddUserOpenID", err) + return + } + log.Trace("Associated OpenID %s to user %s", id, ctx.User.Name) + ctx.Flash.Success(ctx.Tr("settings.add_openid_success")) + + ctx.Redirect(setting.AppSubURL + "/user/settings/openid") +} + +// DeleteOpenID response for delete user's openid +func DeleteOpenID(ctx *context.Context) { + if err := models.DeleteUserOpenID(&models.UserOpenID{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil { + ctx.Handle(500, "DeleteUserOpenID", err) + return + } + log.Trace("OpenID address deleted: %s", ctx.User.Name) + + ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success")) + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/openid", + }) +} + diff --git a/templates/user/auth/finalize_openid.tmpl b/templates/user/auth/finalize_openid.tmpl new file mode 100644 index 0000000000000..d318d332459b5 --- /dev/null +++ b/templates/user/auth/finalize_openid.tmpl @@ -0,0 +1,46 @@ +{{template "base/head" .}} +
+
+
+ {{template "user/auth/finalize_openid_navbar" .}} +
+ {{template "base/alert" .}} +

+ {{.i18n.Tr "auth.login_userpass"}} +

+
+
+ {{.CsrfTokenHtml}} +
+ + +
+
+ + +
+
+ +
+ + +
+
+ +
+ + + {{.i18n.Tr "auth.forget_password"}} +
+ {{if .ShowRegistrationButton}} + + {{end}} +
+ +
+
+
+{{template "base/footer" .}} diff --git a/templates/user/auth/navbar.tmpl b/templates/user/auth/navbar.tmpl new file mode 100644 index 0000000000000..1bd58e7c34d08 --- /dev/null +++ b/templates/user/auth/navbar.tmpl @@ -0,0 +1,14 @@ +
+ +
diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl index 5ed8612e3f7d7..936af886abad7 100644 --- a/templates/user/auth/signin.tmpl +++ b/templates/user/auth/signin.tmpl @@ -1,3 +1,12 @@ {{template "base/head" .}} -{{template "user/auth/signin_inner" .}} +
+
+
+ {{template "user/auth/navbar" .}} +
+ {{template "user/auth/signin_inner" .}} +
+
+
+
{{template "base/footer" .}} diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 91feb7c527440..7b86058776329 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -1,15 +1,12 @@ -
-
-
-
- {{.CsrfTokenHtml}} -

- {{.i18n.Tr "sign_in"}} -

+ {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} + {{template "base/alert" .}} + {{end}} +

+ {{.i18n.Tr "auth.login_userpass"}} +

- {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} - {{template "base/alert" .}} - {{end}} + + {{.CsrfTokenHtml}}
diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl new file mode 100644 index 0000000000000..405f81c129ea0 --- /dev/null +++ b/templates/user/auth/signin_openid.tmpl @@ -0,0 +1,36 @@ +{{template "base/head" .}} + From a40bc3513b0a17d4058f967407c7d3f2a4defa52 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Thu, 16 Mar 2017 12:58:37 +0100 Subject: [PATCH 03/16] Implement whitelist/blacklist support for OpenID --- conf/app.ini | 8 ++++++++ modules/setting/setting.go | 17 +++++++++++++++++ routers/user/auth_openid.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/conf/app.ini b/conf/app.ini index 10abc89f3e987..6083864fe0070 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -187,6 +187,14 @@ IMPORT_LOCAL_PATHS = false ENABLE_OPENID_SIGNIN = true ; Whether to allow registering via OpenID ENABLE_OPENID_SIGNUP = true +; Allowed URI patterns (POSIX regexp). +; Space separated. +; Only these would be allowed if non-blank. +WHITELISTED_URIS = +; Forbidden URI patterns (POSIX regexp). +; Space sepaated. +; Only used if WHITELISTED_URIS is blank. +BLACKLISTED_URIS = [service] ACTIVE_CODE_LIVE_MINUTES = 180 diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 033a8648d2690..0ac63d691f34d 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -15,6 +15,7 @@ import ( "os/exec" "path" "path/filepath" + "regexp" "runtime" "strconv" "strings" @@ -123,6 +124,8 @@ var ( // OpenID settings EnableOpenIDSignIn bool EnableOpenIDSignUp bool + OpenIDWhitelist []*regexp.Regexp + OpenIDBlacklist []*regexp.Regexp // Database settings UseSQLite3 bool @@ -762,6 +765,20 @@ please consider changing to GITEA_CUSTOM`) sec = Cfg.Section("openid") EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(true) EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(true) + pats := sec.Key("WHITELISTED_URIS").Strings(" ") + if ( len(pats) != 0 ) { + OpenIDWhitelist = make([]*regexp.Regexp, len(pats)) + for i, p := range pats { + OpenIDWhitelist[i] = regexp.MustCompilePOSIX(p) + } + } + pats = sec.Key("BLACKLISTED_URIS").Strings(" ") + if ( len(pats) != 0 ) { + OpenIDBlacklist = make([]*regexp.Regexp, len(pats)) + for i, p := range pats { + OpenIDBlacklist[i] = regexp.MustCompilePOSIX(p) + } + } sec = Cfg.Section("attachment") AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments")) diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go index 11d9042e4f946..5a1b419d0bec6 100644 --- a/routers/user/auth_openid.go +++ b/routers/user/auth_openid.go @@ -5,6 +5,7 @@ package user import ( + "fmt" "net/url" "code.gitea.io/gitea/models" @@ -60,6 +61,31 @@ func SignInOpenID(ctx *context.Context) { ctx.HTML(200, tplSignInOpenID) } +// Check if the given OpenID URI is allowed by blacklist/whitelist +func allowedOpenIDURI(uri string) (err error) { + + // In case a Whitelist is present, URI must be in it + // in order to be accepted + if len(setting.OpenIDWhitelist) != 0 { + for _, pat := range setting.OpenIDWhitelist { + if pat.MatchString(uri) { + return nil // pass + } + } + // must match one of this or be refused + return fmt.Errorf("URI not allowed by whitelist") + } + + // A blacklist match expliclty forbids + for _, pat := range setting.OpenIDBlacklist { + if pat.MatchString(uri) { + return fmt.Errorf("URI forbidden by blacklist") + } + } + + return nil +} + // SignInOpenIDPost response for openid sign in request func SignInOpenIDPost(ctx *context.Context, form auth.SignInOpenIDForm) { ctx.Data["Title"] = ctx.Tr("sign_in") @@ -78,6 +104,11 @@ func SignInOpenIDPost(ctx *context.Context, form auth.SignInOpenIDForm) { log.Trace("OpenID uri: " + id) + err = allowedOpenIDURI(id); if err != nil { + ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form) + return; + } + // TODO: somehow forward the form.Remember option // either append to destination url or set a cookie redirectTo := setting.AppURL + "user/login/openid" From 7bf72130e4070815982b42f0c38cddfc59db59d6 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Thu, 16 Mar 2017 13:02:30 +0100 Subject: [PATCH 04/16] Remove obsoleted comment --- routers/user/auth_openid.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go index 5a1b419d0bec6..bdc317f65d0c4 100644 --- a/routers/user/auth_openid.go +++ b/routers/user/auth_openid.go @@ -109,8 +109,6 @@ func SignInOpenIDPost(ctx *context.Context, form auth.SignInOpenIDForm) { return; } - // TODO: somehow forward the form.Remember option - // either append to destination url or set a cookie redirectTo := setting.AppURL + "user/login/openid" url, err := openid.RedirectURL(id, redirectTo, setting.AppURL) if err != nil { From 0c8a0efcbf7e60e4351b37fddfc7d7deff813f35 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Thu, 16 Mar 2017 14:22:31 +0100 Subject: [PATCH 05/16] Add some example OpenID uris --- options/locale/locale_en-US.ini | 1 + templates/user/auth/signin_openid.tmpl | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7ed1cb6f6a636..a2f57e1e544cf 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -195,6 +195,7 @@ openid_connect_title = Connect to an existing account openid_connect_desc = The entered OpenID URIs is not know by the system, here you can associate it to an existing account. openid_register_title = Create new account openid_register_desc = The entered OpenID URIs is not know by the system, here you can associate it to a new account. +openid_signin_desc = Example URIs: openid.stackexchange.com, login.launchpad.com, bob.openid.org.cn [mail] activate_account = Please activate your account diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl index 405f81c129ea0..20ddaeb55feda 100644 --- a/templates/user/auth/signin_openid.tmpl +++ b/templates/user/auth/signin_openid.tmpl @@ -11,9 +11,13 @@
{{.CsrfTokenHtml}} +
+ {{.i18n.Tr "auth.openid_signin_desc"}} +
From 34e054ff0c9438fcef2fae1ea7fb5c078b801a9b Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Thu, 16 Mar 2017 14:25:36 +0100 Subject: [PATCH 06/16] Avoid advertising online services :) --- options/locale/locale_en-US.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a2f57e1e544cf..cf322c7f33b9c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -195,7 +195,7 @@ openid_connect_title = Connect to an existing account openid_connect_desc = The entered OpenID URIs is not know by the system, here you can associate it to an existing account. openid_register_title = Create new account openid_register_desc = The entered OpenID URIs is not know by the system, here you can associate it to a new account. -openid_signin_desc = Example URIs: openid.stackexchange.com, login.launchpad.com, bob.openid.org.cn +openid_signin_desc = Example URIs: https://anne.me, bob.openid.org.cn, gnusocial.net/carry [mail] activate_account = Please activate your account From fb26d58e0daf6d82684fe9b03afde4e98c31d9c2 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Thu, 16 Mar 2017 19:47:12 +0100 Subject: [PATCH 07/16] Install openid logo locally --- public/img/openid-16x16.png | Bin 0 -> 230 bytes templates/user/auth/navbar.tmpl | 2 +- templates/user/auth/signin_openid.tmpl | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 public/img/openid-16x16.png diff --git a/public/img/openid-16x16.png b/public/img/openid-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..b3184808423b66f9de8852133609697d34c798ce GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPGa2=EDUoilItkI9lNRxRJNZ`ZXO zSN~jI_~^;QzyJRTYF{-5s$op>c6VXuV3qX%aySb-B8wRqxP?KOkzv*x37{Z*iKnkC z`$J{{A#L$DB@Pupp%70O#}JO|$r|iU1qx{lkAey{77K_kKC>bq#!~i#L4ux$d4gZM zMv|Alh<=z^vOxg*DoyXO {{if .EnableOpenIDSignIn}} - +   OpenID {{end}} diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl index 20ddaeb55feda..4df9d896bb1ee 100644 --- a/templates/user/auth/signin_openid.tmpl +++ b/templates/user/auth/signin_openid.tmpl @@ -16,7 +16,7 @@
From 0af2fdcdd588bafd879a162b7cd15d9fbe7b0d95 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Thu, 16 Mar 2017 19:49:47 +0100 Subject: [PATCH 08/16] Fix selected tab effects upon openid error --- routers/user/auth_openid.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go index bdc317f65d0c4..ebcfa76652c61 100644 --- a/routers/user/auth_openid.go +++ b/routers/user/auth_openid.go @@ -89,6 +89,8 @@ func allowedOpenIDURI(uri string) (err error) { // SignInOpenIDPost response for openid sign in request func SignInOpenIDPost(ctx *context.Context, form auth.SignInOpenIDForm) { ctx.Data["Title"] = ctx.Tr("sign_in") + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsLoginOpenID"] = true if ctx.HasError() { ctx.HTML(200, tplSignInOpenID) From 2469417b0c8a08b2a27a9090cc3eae786d5c1a94 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 17 Mar 2017 07:21:37 +0100 Subject: [PATCH 09/16] Add example value for openid uri whitelist --- conf/app.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/app.ini b/conf/app.ini index 6083864fe0070..d8a882def67d5 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -194,6 +194,7 @@ WHITELISTED_URIS = ; Forbidden URI patterns (POSIX regexp). ; Space sepaated. ; Only used if WHITELISTED_URIS is blank. +; Example value: loadaverage.org/badguy stackexchange.com/.*spammer BLACKLISTED_URIS = [service] From 9fb42f72975328b49ddcfd38674e20d8fe494d45 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 17 Mar 2017 07:23:53 +0100 Subject: [PATCH 10/16] Also add an example in the openid whitelist value --- conf/app.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/app.ini b/conf/app.ini index d8a882def67d5..26b93f712201b 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -190,6 +190,7 @@ ENABLE_OPENID_SIGNUP = true ; Allowed URI patterns (POSIX regexp). ; Space separated. ; Only these would be allowed if non-blank. +; Example value: trusted.domain.org trusted.domain.net WHITELISTED_URIS = ; Forbidden URI patterns (POSIX regexp). ; Space sepaated. From 0c036e6e1a3c422e6a12c6e90de1b126ebf29903 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 17 Mar 2017 08:06:40 +0100 Subject: [PATCH 11/16] Reduce memory usage in openid discovery cache cleanup Also add unit test for the cache. --- modules/auth/openid/openid_discovery_cache.go | 24 ++++++---- .../openid/openid_discovery_cache_test.go | 47 +++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 modules/auth/openid/openid_discovery_cache_test.go diff --git a/modules/auth/openid/openid_discovery_cache.go b/modules/auth/openid/openid_discovery_cache.go index 72069374ea8e1..cf9f5ae70c2bc 100644 --- a/modules/auth/openid/openid_discovery_cache.go +++ b/modules/auth/openid/openid_discovery_cache.go @@ -5,9 +5,10 @@ package openid import ( - "github.com/yohcop/openid-go" "sync" "time" + + "github.com/yohcop/openid-go" ) type timedDiscoveredInfo struct { @@ -32,20 +33,23 @@ func (s *timedDiscoveryCache) Put(id string, info openid.DiscoveredInfo) { s.cache[id] = timedDiscoveredInfo{info: info, time: time.Now()} } -func (s *timedDiscoveryCache) Get(id string) openid.DiscoveredInfo { - s.mutex.Lock() - defer s.mutex.Unlock() - - // Delete old cached while we are at it. - newCache := map[string]timedDiscoveredInfo{} +// Delete timed-out cache entries +func (s *timedDiscoveryCache) cleanTimedOut() { now := time.Now() for k, e := range s.cache { diff := now.Sub(e.time) - if diff <= s.ttl { - newCache[k] = e + if diff > s.ttl { + delete(s.cache, k) } } - s.cache = newCache +} + +func (s *timedDiscoveryCache) Get(id string) openid.DiscoveredInfo { + s.mutex.Lock() + defer s.mutex.Unlock() + + // Delete old cached while we are at it. + s.cleanTimedOut() if info, has := s.cache[id]; has { return info.info diff --git a/modules/auth/openid/openid_discovery_cache_test.go b/modules/auth/openid/openid_discovery_cache_test.go new file mode 100644 index 0000000000000..9de65a57bb712 --- /dev/null +++ b/modules/auth/openid/openid_discovery_cache_test.go @@ -0,0 +1,47 @@ +// 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 openid + +import ( + "testing" + "time" +) + +type testDiscoveredInfo struct {} +func (s *testDiscoveredInfo) ClaimedID() string { + return "claimedID" +} +func (s *testDiscoveredInfo) OpEndpoint() string { + return "opEndpoint" +} +func (s *testDiscoveredInfo) OpLocalID() string { + return "opLocalID" +} + +func TestTimedDiscoveryCache(t *testing.T) { + dc := newTimedDiscoveryCache(1*time.Second) + + // Put some initial values + dc.Put("foo", &testDiscoveredInfo{}) //openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"}) + + // Make sure we can retrieve them + if di := dc.Get("foo"); di == nil { + t.Errorf("Expected a result, got nil") + } else if di.OpEndpoint() != "opEndpoint" || di.OpLocalID() != "opLocalID" || di.ClaimedID() != "claimedID" { + t.Errorf("Expected opEndpoint opLocalID claimedID, got %v %v %v", di.OpEndpoint(), di.OpLocalID(), di.ClaimedID()) + } + + // Attempt to get a non-existent value + if di := dc.Get("bar"); di != nil { + t.Errorf("Expected nil, got %v", di) + } + + // Sleep one second and try retrive again + time.Sleep(1 * time.Second) + + if di := dc.Get("foo"); di != nil { + t.Errorf("Expected a nil, got a result") + } +} From 327ad46a752fd521d3d1cee370ceb1f35bfb7828 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 17 Mar 2017 08:11:21 +0100 Subject: [PATCH 12/16] Drop "openid" from the name of discovery cache file .. as that's implicit in the package name (@bkcsoft asked) --- .../auth/openid/{openid_discovery_cache.go => discovery_cache.go} | 0 .../{openid_discovery_cache_test.go => discovery_cache_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename modules/auth/openid/{openid_discovery_cache.go => discovery_cache.go} (100%) rename modules/auth/openid/{openid_discovery_cache_test.go => discovery_cache_test.go} (100%) diff --git a/modules/auth/openid/openid_discovery_cache.go b/modules/auth/openid/discovery_cache.go similarity index 100% rename from modules/auth/openid/openid_discovery_cache.go rename to modules/auth/openid/discovery_cache.go diff --git a/modules/auth/openid/openid_discovery_cache_test.go b/modules/auth/openid/discovery_cache_test.go similarity index 100% rename from modules/auth/openid/openid_discovery_cache_test.go rename to modules/auth/openid/discovery_cache_test.go From 9d7dc212608b6d8031017cc668db25738ba291a0 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 17 Mar 2017 08:56:25 +0100 Subject: [PATCH 13/16] Use top navbar (not being centered for some obscure reason) --- templates/user/auth/navbar.tmpl | 14 ----- templates/user/auth/signin.tmpl | 9 +-- templates/user/auth/signin_inner.tmpl | 84 ++++++++++++-------------- templates/user/auth/signin_navbar.tmpl | 11 ++++ templates/user/auth/signin_openid.tmpl | 60 +++++++++--------- 5 files changed, 83 insertions(+), 95 deletions(-) delete mode 100644 templates/user/auth/navbar.tmpl create mode 100644 templates/user/auth/signin_navbar.tmpl diff --git a/templates/user/auth/navbar.tmpl b/templates/user/auth/navbar.tmpl deleted file mode 100644 index e9b3c24881f4d..0000000000000 --- a/templates/user/auth/navbar.tmpl +++ /dev/null @@ -1,14 +0,0 @@ -
- -
diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl index 936af886abad7..7d430e925628e 100644 --- a/templates/user/auth/signin.tmpl +++ b/templates/user/auth/signin.tmpl @@ -1,12 +1,9 @@ {{template "base/head" .}}
+ {{template "user/auth/signin_navbar" .}} +
-
- {{template "user/auth/navbar" .}} -
- {{template "user/auth/signin_inner" .}} -
-
+ {{template "user/auth/signin_inner" .}}
{{template "base/footer" .}} diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 7b86058776329..23078e7d33533 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -1,54 +1,50 @@ - {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} - {{template "base/alert" .}} - {{end}} -

- {{.i18n.Tr "auth.login_userpass"}} -

-
- - {{.CsrfTokenHtml}} -
- - -
-
- - -
- {{if not .LinkAccountMode}} -
- -
- - -
+ {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} + {{template "base/alert" .}} + {{end}} + diff --git a/templates/user/auth/signin_navbar.tmpl b/templates/user/auth/signin_navbar.tmpl new file mode 100644 index 0000000000000..10569388a6cce --- /dev/null +++ b/templates/user/auth/signin_navbar.tmpl @@ -0,0 +1,11 @@ + diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl index 4df9d896bb1ee..744d4b7c40162 100644 --- a/templates/user/auth/signin_openid.tmpl +++ b/templates/user/auth/signin_openid.tmpl @@ -1,39 +1,37 @@ {{template "base/head" .}}