From 71d16f69ff9448e55f371ce8354d978f8dbe2cba Mon Sep 17 00:00:00 2001
From: Sandro Santilli <strk@kbt.io>
Date: Fri, 17 Mar 2017 15:16:08 +0100
Subject: [PATCH] Login via OpenID-2.0 (#618)

---
 cmd/web.go                                    |  21 +
 conf/app.ini                                  |  32 ++
 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/discovery_cache.go        |  59 +++
 modules/auth/openid/discovery_cache_test.go   |  47 ++
 modules/auth/openid/openid.go                 |  37 ++
 modules/auth/user_form.go                     |  12 +-
 modules/auth/user_form_auth_openid.go         |  45 ++
 modules/context/context.go                    |   1 +
 modules/setting/setting.go                    |  25 +
 options/locale/locale_en-US.ini               |  17 +
 public/img/openid-16x16.png                   | Bin 0 -> 230 bytes
 routers/user/auth.go                          |  10 +-
 routers/user/auth_openid.go                   | 426 ++++++++++++++++++
 routers/user/setting_openid.go                | 142 ++++++
 templates/user/auth/finalize_openid.tmpl      |  46 ++
 templates/user/auth/signin.tmpl               |   7 +-
 templates/user/auth/signin_inner.tmpl         | 100 ++--
 templates/user/auth/signin_navbar.tmpl        |  11 +
 templates/user/auth/signin_openid.tmpl        |  37 ++
 .../user/auth/signup_openid_connect.tmpl      |  45 ++
 templates/user/auth/signup_openid_navbar.tmpl |  11 +
 .../user/auth/signup_openid_register.tmpl     |  34 ++
 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 +
 44 files changed, 2298 insertions(+), 57 deletions(-)
 create mode 100644 models/migrations/v23.go
 create mode 100644 models/user_openid.go
 create mode 100644 modules/auth/openid/discovery_cache.go
 create mode 100644 modules/auth/openid/discovery_cache_test.go
 create mode 100644 modules/auth/openid/openid.go
 create mode 100644 modules/auth/user_form_auth_openid.go
 create mode 100644 public/img/openid-16x16.png
 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/signin_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 0410ad5190..17674b3069 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 8e29e39b11..c2d41b8535 100644
--- a/conf/app.ini
+++ b/conf/app.ini
@@ -182,6 +182,38 @@ MIN_PASSWORD_LENGTH = 6
 ; True when users are allowed to import local server paths
 IMPORT_LOCAL_PATHS = false
 
+[openid]
+;
+; OpenID is an open standard and decentralized authentication protocol.
+; Your identity is the address of a webpage you provide, which describes
+; how to prove you are in control of that page.
+;
+; For more info: https://en.wikipedia.org/wiki/OpenID
+;
+; Current implementation supports OpenID-2.0
+;
+; Tested to work providers at the time of writing:
+;  - Any GNUSocial node (your.hostname.tld/username)
+;  - Any SimpleID provider (http://simpleid.koinic.net)
+;  - http://openid.org.cn/
+;  - openid.stackexchange.com
+;  - login.launchpad.net
+;
+; Whether to allow signin in via OpenID
+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.
+; Example value: trusted.domain.org trusted.domain.net
+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]
 ACTIVE_CODE_LIVE_MINUTES = 180
 RESET_PASSWD_CODE_LIVE_MINUTES = 180
diff --git a/models/error.go b/models/error.go
index 62529f83fa..68bc238907 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 bf188dc4ce..4f1254b960 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 0000000000..efde684104
--- /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 bba4446db0..2ae6e355fc 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 ff898573a6..ad303d7535 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 0000000000..a5c88e9009
--- /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/discovery_cache.go b/modules/auth/openid/discovery_cache.go
new file mode 100644
index 0000000000..cf9f5ae70c
--- /dev/null
+++ b/modules/auth/openid/discovery_cache.go
@@ -0,0 +1,59 @@
+// 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 (
+	"sync"
+	"time"
+
+	"github.com/yohcop/openid-go"
+)
+
+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()}
+}
+
+// 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 {
+			delete(s.cache, k)
+		}
+	}
+}
+
+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
+	}
+	return nil
+}
+
diff --git a/modules/auth/openid/discovery_cache_test.go b/modules/auth/openid/discovery_cache_test.go
new file mode 100644
index 0000000000..9de65a57bb
--- /dev/null
+++ b/modules/auth/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")
+	}
+}
diff --git a/modules/auth/openid/openid.go b/modules/auth/openid/openid.go
new file mode 100644
index 0000000000..aebdf15155
--- /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/user_form.go b/modules/auth/user_form.go
index 32987e6d37..9c6e38c460 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 0000000000..582c6dc69f
--- /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 fa53b484ee..52e50af6a1 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 520dc429df..0ac63d691f 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"
@@ -120,6 +121,12 @@ var (
 	MinPasswordLength    int
 	ImportLocalPaths     bool
 
+	// OpenID settings
+	EnableOpenIDSignIn bool
+	EnableOpenIDSignUp bool
+	OpenIDWhitelist    []*regexp.Regexp
+	OpenIDBlacklist    []*regexp.Regexp
+
 	// Database settings
 	UseSQLite3    bool
 	UseMySQL      bool
@@ -755,6 +762,24 @@ 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)
+	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"))
 	if !filepath.IsAbs(AttachmentPath) {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index f66a7ca689..cf322c7f33 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -188,6 +188,14 @@ 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.
+openid_signin_desc = Example URIs: https://anne.me, bob.openid.org.cn, gnusocial.net/carry
 
 [mail]
 activate_account = Please activate your account
@@ -239,6 +247,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 +324,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 +332,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/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<OGj)t+ax6qXR(-ZidcQS7g}LTvwbt*Yz|&`v3!j^?W_S
UsQ%k=K=T<qUHx3vIVCg!0Gnw?+5i9m

literal 0
HcmV?d00001

diff --git a/routers/user/auth.go b/routers/user/auth.go
index f8c6db1268..4827f38b52 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 0000000000..ebcfa76652
--- /dev/null
+++ b/routers/user/auth_openid.go
@@ -0,0 +1,426 @@
+// 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 (
+	"fmt"
+	"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)
+}
+
+// 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")
+	ctx.Data["PageIsSignIn"] = true
+	ctx.Data["PageIsLoginOpenID"] = true
+
+	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)
+
+	err = allowedOpenIDURI(id); if err != nil {
+		ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
+                return;
+	}
+
+	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 0000000000..5e6052d3ef
--- /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 0000000000..d318d33245
--- /dev/null
+++ b/templates/user/auth/finalize_openid.tmpl
@@ -0,0 +1,46 @@
+{{template "base/head" .}}
+<div class="user signin">
+	<div class="ui container">
+		<div class="ui grid">
+			{{template "user/auth/finalize_openid_navbar" .}}
+			<div class="twelve wide column content">
+				{{template "base/alert" .}}
+				<h4 class="ui top attached header">
+					{{.i18n.Tr "auth.login_userpass"}}
+				</h4>
+				<div class="ui attached segment">
+					<form class="ui form" action="{{.Link}}" method="post">
+					{{.CsrfTokenHtml}}
+					<div class="required inline field {{if .Err_UserName}}error{{end}}">
+						<label for="user_name">{{.i18n.Tr "home.uname_holder"}}</label>
+						<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
+					</div>
+					<div class="required inline field {{if .Err_Password}}error{{end}}">
+						<label for="password">{{.i18n.Tr "password"}}</label>
+						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
+					</div>
+					<div class="inline field">
+						<label></label>
+						<div class="ui checkbox">
+							<label>{{.i18n.Tr "auth.remember_me"}}</label>
+							<input name="remember" type="checkbox">
+						</div>
+					</div>
+
+					<div class="inline field">
+						<label></label>
+						<button class="ui green button">{{.i18n.Tr "sign_in"}}</button>
+						<a href="{{AppSubUrl}}/user/forget_password">{{.i18n.Tr "auth.forget_password"}}</a>
+					</div>
+					{{if .ShowRegistrationButton}}
+						<div class="inline field">
+							<label></label>
+							<a href="{{AppSubUrl}}/user/sign_up">{{.i18n.Tr "auth.sign_up_now" | Str2html}}</a>
+						</div>
+					{{end}}
+				</div>
+			</form>
+		</div>
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl
index 5ed8612e3f..bbb471cacb 100644
--- a/templates/user/auth/signin.tmpl
+++ b/templates/user/auth/signin.tmpl
@@ -1,3 +1,8 @@
 {{template "base/head" .}}
-{{template "user/auth/signin_inner" .}}
+<div class="user signin{{if .LinkAccountMode}} icon{{end}}">
+	{{template "user/auth/signin_navbar" .}}
+	<div class="ui container">
+		{{template "user/auth/signin_inner" .}}
+	</div>
+</div>
 {{template "base/footer" .}}
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 91feb7c527..c8df0870b7 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -1,57 +1,51 @@
-<div class="user signin{{if .LinkAccountMode}} icon{{end}}">
-	<div class="ui middle very relaxed page grid">
-		<div class="column">
+		{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}}
+		{{template "base/alert" .}}
+		{{end}}
+		<h4 class="ui top attached header">
+			{{.i18n.Tr "auth.login_userpass"}}
+		</h4>
+		<div class="ui attached segment">
 			<form class="ui form" action="{{if not .LinkAccountMode}}{{.Link}}{{else}}{{.SignInLink}}{{end}}" method="post">
-				{{.CsrfTokenHtml}}
-				<h3 class="ui top attached header">
-					{{.i18n.Tr "sign_in"}}
-				</h3>
-				<div class="ui attached segment">
-					{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}}
-					{{template "base/alert" .}}
-					{{end}}
-					<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
-						<label for="user_name">{{.i18n.Tr "home.uname_holder"}}</label>
-						<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
-					</div>
-					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
-						<label for="password">{{.i18n.Tr "password"}}</label>
-						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
-					</div>
-					{{if not .LinkAccountMode}}
-					<div class="inline field">
-						<label></label>
-						<div class="ui checkbox">
-							<label>{{.i18n.Tr "auth.remember_me"}}</label>
-							<input name="remember" type="checkbox">
-						</div>
-					</div>
-					{{end}}
-
-					<div class="inline field">
-						<label></label>
-						<button class="ui green button">{{.i18n.Tr "sign_in"}}</button>
-						<a href="{{AppSubUrl}}/user/forgot_password">{{.i18n.Tr "auth.forgot_password"}}</a>
-					</div>
-
-					{{if .ShowRegistrationButton}}
-						<div class="inline field">
-							<label></label>
-							<a href="{{AppSubUrl}}/user/sign_up">{{.i18n.Tr "auth.sign_up_now" | Str2html}}</a>
-						</div>
-					{{end}}
-
-					{{if .OAuth2Providers}}
-					<div class="ui attached segment">
-						<div class="oauth2 center">
-							<div>
-								<p>{{.i18n.Tr "sign_in_with"}}</p>{{range $key, $value := .OAuth2Providers}}<a href="{{AppSubUrl}}/user/oauth2/{{$key}}"><img alt="{{$value.DisplayName}}" title="{{$value.DisplayName}}" src="{{AppSubUrl}}{{$value.Image}}"></a>{{end}}
-							</div>
-						</div>
-					</div>
-					{{end}}
+			{{.CsrfTokenHtml}}
+			<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
+				<label for="user_name">{{.i18n.Tr "home.uname_holder"}}</label>
+				<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
+			</div>
+			<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
+				<label for="password">{{.i18n.Tr "password"}}</label>
+				<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
+			</div>
+			{{if not .LinkAccountMode}}
+			<div class="inline field">
+				<label></label>
+				<div class="ui checkbox">
+					<label>{{.i18n.Tr "auth.remember_me"}}</label>
+					<input name="remember" type="checkbox">
 				</div>
+			</div>
+			{{end}}
+
+			<div class="inline field">
+				<label></label>
+				<button class="ui green button">{{.i18n.Tr "sign_in"}}</button>
+				<a href="{{AppSubUrl}}/user/forgot_password">{{.i18n.Tr "auth.forgot_password"}}</a>
+			</div>
+
+			{{if .ShowRegistrationButton}}
+				<div class="inline field">
+					<label></label>
+					<a href="{{AppSubUrl}}/user/sign_up">{{.i18n.Tr "auth.sign_up_now" | Str2html}}</a>
+				</div>
+			{{end}}
+
+			{{if .OAuth2Providers}}
+			<div class="ui attached segment">
+				<div class="oauth2 center">
+					<div>
+						<p>{{.i18n.Tr "sign_in_with"}}</p>{{range $key, $value := .OAuth2Providers}}<a href="{{AppSubUrl}}/user/oauth2/{{$key}}"><img alt="{{$value.DisplayName}}" title="{{$value.DisplayName}}" src="{{AppSubUrl}}{{$value.Image}}"></a>{{end}}
+					</div>
+				</div>
+			</div>
+			{{end}}
 			</form>
 		</div>
-	</div>
-</div>
diff --git a/templates/user/auth/signin_navbar.tmpl b/templates/user/auth/signin_navbar.tmpl
new file mode 100644
index 0000000000..2bee1325f7
--- /dev/null
+++ b/templates/user/auth/signin_navbar.tmpl
@@ -0,0 +1,11 @@
+<div class="ui secondary pointing tabular top attached borderless menu stackable new-menu navbar">
+	<a class="{{if .PageIsLogin}}active{{end}} item" href="{{AppSubUrl}}/user/login">
+		{{.i18n.Tr "auth.login_userpass"}}
+	</a>
+	{{if .EnableOpenIDSignIn}}
+		<a class="{{if .PageIsLoginOpenID}}active{{end}} item" href="{{AppSubUrl}}/user/login/openid">
+			<img align="left" width="16" height="16" src="{{AppSubUrl}}/img/openid-16x16.png"/>
+			OpenID
+		</a>
+	{{end}}
+</div>
diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl
new file mode 100644
index 0000000000..ccc689ce33
--- /dev/null
+++ b/templates/user/auth/signin_openid.tmpl
@@ -0,0 +1,37 @@
+{{template "base/head" .}}
+<div class="user signin openid">
+	{{template "user/auth/signin_navbar" .}}
+	<div class="ui container">
+		{{template "base/alert" .}}
+		<h4 class="ui top attached header">
+			OpenID
+		</h4>
+		<div class="ui attached segment">
+			<form class="ui form" action="{{.Link}}" method="post">
+			{{.CsrfTokenHtml}}
+			<div class="inline field">
+				{{.i18n.Tr "auth.openid_signin_desc"}}
+			</div>
+			<div class="required inline field {{if .Err_OpenID}}error{{end}}">
+				<label for="openid">
+				<img alt="OpenID URI" height="16" src="{{AppSubUrl}}/img/openid-16x16.png"/>
+				OpenID URI
+				</label>
+				<input id="openid" name="openid" value="{{.openid}}" autofocus required>
+			</div>
+			<div class="inline field">
+				<label></label>
+				<div class="ui checkbox">
+					<label>{{.i18n.Tr "auth.remember_me"}}</label>
+					<input name="remember" type="checkbox">
+				</div>
+			</div>
+			<div class="inline field">
+				<label></label>
+				<button class="ui green button">{{.i18n.Tr "sign_in"}}</button>
+			</div>
+			</form>
+		</div>
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/user/auth/signup_openid_connect.tmpl b/templates/user/auth/signup_openid_connect.tmpl
new file mode 100644
index 0000000000..476141e0d0
--- /dev/null
+++ b/templates/user/auth/signup_openid_connect.tmpl
@@ -0,0 +1,45 @@
+{{template "base/head" .}}
+<div class="user signup">
+	{{template "user/auth/signup_openid_navbar" .}}
+	<div class="ui container">
+				{{template "base/alert" .}}
+				<h4 class="ui top attached header">
+					{{.i18n.Tr "auth.openid_connect_title"}}
+				</h4>
+				<div class="ui attached segment">
+					<p>
+						{{.i18n.Tr "auth.openid_connect_desc"}}
+					</p>
+					<form class="ui form" action="{{.Link}}" method="post">
+					{{.CsrfTokenHtml}}
+					<div class="required inline field {{if .Err_UserName}}error{{end}}">
+						<label for="user_name">{{.i18n.Tr "home.uname_holder"}}</label>
+						<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
+					</div>
+					<div class="required inline field {{if .Err_Password}}error{{end}}">
+						<label for="password">{{.i18n.Tr "password"}}</label>
+						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
+					</div>
+					<div class="inline field">
+						OpenID: {{ .OpenID }}
+					</div>
+					{{if .EnableCaptcha}}
+						<div class="inline field">
+							<label></label>
+							{{.Captcha.CreateHtml}}
+						</div>
+						<div class="required inline field {{if .Err_Captcha}}error{{end}}">
+							<label for="captcha">{{.i18n.Tr "captcha"}}</label>
+							<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off">
+						</div>
+					{{end}}
+					<div class="inline field">
+						<label></label>
+						<button class="ui green button">{{.i18n.Tr "auth.openid_connect_submit"}}</button>
+						<a href="{{AppSubUrl}}/user/forgot_password">{{.i18n.Tr "auth.forgot_password"}}</a>
+					</div>
+					</form>
+				</div>
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/user/auth/signup_openid_navbar.tmpl b/templates/user/auth/signup_openid_navbar.tmpl
new file mode 100644
index 0000000000..86f6ee7d81
--- /dev/null
+++ b/templates/user/auth/signup_openid_navbar.tmpl
@@ -0,0 +1,11 @@
+<div class="ui secondary pointing tabular top attached borderless menu stackable new-menu navbar">
+	<a class="{{if .PageIsOpenIDConnect}}active{{end}} item" href="{{AppSubUrl}}/user/openid/connect">
+		{{.i18n.Tr "auth.openid_connect_title"}}
+	</a>
+	{{if .EnableOpenIDSignUp}}
+		<a class="{{if .PageIsOpenIDRegister}}active{{end}} item" href="{{AppSubUrl}}/user/openid/register">
+			{{.i18n.Tr "auth.openid_register_title"}}
+		</a>
+	{{end}}
+</div>
+
diff --git a/templates/user/auth/signup_openid_register.tmpl b/templates/user/auth/signup_openid_register.tmpl
new file mode 100644
index 0000000000..4970494da1
--- /dev/null
+++ b/templates/user/auth/signup_openid_register.tmpl
@@ -0,0 +1,34 @@
+{{template "base/head" .}}
+<div class="user signup">
+	{{template "user/auth/signup_openid_navbar" .}}
+	<div class="ui container">
+				{{template "base/alert" .}}
+				<h4 class="ui top attached header">
+					{{.i18n.Tr "auth.openid_register_title"}}
+				</h4>
+				<div class="ui attached segment">
+					<p>
+						{{.i18n.Tr "auth.openid_register_desc"}}
+					</p>
+					<form class="ui form" action="{{.Link}}" method="post">
+					{{.CsrfTokenHtml}}
+					<div class="required inline field {{if .Err_UserName}}error{{end}}">
+						<label for="user_name">{{.i18n.Tr "username"}}</label>
+						<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
+					</div>
+					<div class="required inline field {{if .Err_Email}}error{{end}}">
+						<label for="email">{{.i18n.Tr "email"}}</label>
+						<input id="email" name="email" type="email" value="{{.email}}" required>
+					</div>
+					<div class="inline field">
+						OpenID: {{ .OpenID }}
+					</div>
+					<div class="inline field">
+						<label></label>
+						<button class="ui green button">{{.i18n.Tr "auth.create_new_account"}}</button>
+					</div>
+					</form>
+				</div>
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl
index 611c091ba4..d798d40219 100644
--- a/templates/user/settings/navbar.tmpl
+++ b/templates/user/settings/navbar.tmpl
@@ -11,6 +11,11 @@
 	<a class="{{if .PageIsSettingsEmails}}active{{end}} item" href="{{AppSubUrl}}/user/settings/email">
 		{{.i18n.Tr "settings.emails"}}
 	</a>
+	{{if .EnableOpenIDSignIn}}
+		<a class="{{if .PageIsSettingsOpenID}}active{{end}} item" href="{{AppSubUrl}}/user/settings/openid">
+			OpenID
+		</a>
+	{{end}}
 	<a class="{{if .PageIsSettingsSSHKeys}}active{{end}} item" href="{{AppSubUrl}}/user/settings/ssh">
 		{{.i18n.Tr "settings.ssh_keys"}}
 	</a>
@@ -26,4 +31,4 @@
 	<a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{AppSubUrl}}/user/settings/delete">
 		{{.i18n.Tr "settings.delete"}}
 	</a>
-</div>
\ No newline at end of file
+</div>
diff --git a/templates/user/settings/openid.tmpl b/templates/user/settings/openid.tmpl
new file mode 100644
index 0000000000..0528cbaa1d
--- /dev/null
+++ b/templates/user/settings/openid.tmpl
@@ -0,0 +1,57 @@
+{{template "base/head" .}}
+<div class="user settings openid">
+	<div class="ui container">
+		<div class="ui grid">
+			{{template "user/settings/navbar" .}}
+			<div class="twelve wide column content">
+				{{template "base/alert" .}}
+				<h4 class="ui top attached header">
+					{{.i18n.Tr "settings.manage_openid"}}
+				</h4>
+				<div class="ui attached segment">
+					<div class="ui openid list">
+						<div class="item">
+							{{.i18n.Tr "settings.openid_desc"}}
+						</div>
+						{{range .OpenIDs}}
+							<div class="item ui grid">
+								<div class="column">
+									<strong>{{.URI}}</strong>
+									<div class="ui right">
+										<button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
+											{{$.i18n.Tr "settings.delete_key"}}
+										</button>
+									</div>
+								</div>
+							</div>
+						{{end}}
+					</div>
+				</div>
+				<div class="ui attached bottom segment">
+					<form class="ui form" action="{{.Link}}" method="post">
+						{{.CsrfTokenHtml}}
+						<div class="required field {{if .Err_OpenID}}error{{end}}">
+							<label for="openid">{{.i18n.Tr "settings.add_new_openid"}}</label>
+							<input id="openid" name="openid" type="openid" autofocus required>
+						</div>
+						<button class="ui green button">
+							{{.i18n.Tr "settings.add_openid"}}
+						</button>
+					</form>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<div class="ui small basic delete modal">
+	<div class="ui icon header">
+		<i class="trash icon"></i>
+		{{.i18n.Tr "settings.openid_deletion"}}
+	</div>
+	<div class="content">
+		<p>{{.i18n.Tr "settings.openid_deletion_desc"}}</p>
+	</div>
+	{{template "base/delete_modal_actions" .}}
+</div>
+{{template "base/footer" .}}
diff --git a/vendor/github.com/yohcop/openid-go/LICENSE b/vendor/github.com/yohcop/openid-go/LICENSE
new file mode 100644
index 0000000000..4e8c3333bb
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2015 Yohann Coppel
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/vendor/github.com/yohcop/openid-go/README.md b/vendor/github.com/yohcop/openid-go/README.md
new file mode 100644
index 0000000000..f617126d2e
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/README.md
@@ -0,0 +1,38 @@
+# openid.go
+
+This is a consumer (Relying party) implementation of OpenId 2.0,
+written in Go.
+
+    go get -u github.com/yohcop/openid-go
+
+[![Build Status](https://travis-ci.org/yohcop/openid-go.svg?branch=master)](https://travis-ci.org/yohcop/openid-go)
+
+## Github
+
+Be awesome! Feel free to clone and use according to the licence.
+If you make a useful change that can benefit others, send a
+pull request! This ensures that one version has all the good stuff
+and doesn't fall behind.
+
+## Code example
+
+See `_example/` for a simple webserver using the openID
+implementation. Also, read the comment about the NonceStore towards
+the top of that file. The example must be run for the openid-go
+directory, like so:
+
+    go run _example/server.go
+
+## App Engine
+
+In order to use this on Google App Engine, you need to create an instance with a custom `*http.Client` provided by [urlfetch](https://cloud.google.com/appengine/docs/go/urlfetch/).
+
+```go
+oid := openid.NewOpenID(urlfetch.Client(appengine.NewContext(r)))
+oid.RedirectURL(...)
+oid.Verify(...)
+```
+
+## License
+
+Distributed under the [Apache v2.0 license](http://www.apache.org/licenses/LICENSE-2.0.html).
diff --git a/vendor/github.com/yohcop/openid-go/discover.go b/vendor/github.com/yohcop/openid-go/discover.go
new file mode 100644
index 0000000000..2943b9cd18
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/discover.go
@@ -0,0 +1,57 @@
+package openid
+
+// 7.3.1.  Discovered Information
+// Upon successful completion of discovery, the Relying Party will
+// have one or more sets of the following information (see the
+// Terminology section for definitions). If more than one set of the
+// following information has been discovered, the precedence rules
+// defined in [XRI_Resolution_2.0] are to be applied.
+//   - OP Endpoint URL
+//   - Protocol Version
+// If the end user did not enter an OP Identifier, the following
+// information will also be present:
+//   - Claimed Identifier
+//   - OP-Local Identifier
+// If the end user entered an OP Identifier, there is no Claimed
+// Identifier. For the purposes of making OpenID Authentication
+// requests, the value
+// "http://specs.openid.net/auth/2.0/identifier_select" MUST be
+// used as both the Claimed Identifier and the OP-Local Identifier
+// when an OP Identifier is entered.
+func Discover(id string) (opEndpoint, opLocalID, claimedID string, err error) {
+	return defaultInstance.Discover(id)
+}
+
+func (oid *OpenID) Discover(id string) (opEndpoint, opLocalID, claimedID string, err error) {
+	// From OpenID specs, 7.2: Normalization
+	if id, err = Normalize(id); err != nil {
+		return
+	}
+
+	// From OpenID specs, 7.3: Discovery.
+
+	// If the identifier is an XRI, [XRI_Resolution_2.0] will yield an
+	// XRDS document that contains the necessary information. It
+	// should also be noted that Relying Parties can take advantage of
+	// XRI Proxy Resolvers, such as the one provided by XDI.org at
+	// http://www.xri.net. This will remove the need for the RPs to
+	// perform XRI Resolution locally.
+
+	// XRI not supported.
+
+	// If it is a URL, the Yadis protocol [Yadis] SHALL be first
+	// attempted. If it succeeds, the result is again an XRDS
+	// document.
+	if opEndpoint, opLocalID, err = yadisDiscovery(id, oid.urlGetter); err != nil {
+		// If the Yadis protocol fails and no valid XRDS document is
+		// retrieved, or no Service Elements are found in the XRDS
+		// document, the URL is retrieved and HTML-Based discovery SHALL be
+		// attempted.
+		opEndpoint, opLocalID, claimedID, err = htmlDiscovery(id, oid.urlGetter)
+	}
+
+	if err != nil {
+		return "", "", "", err
+	}
+	return
+}
diff --git a/vendor/github.com/yohcop/openid-go/discovery_cache.go b/vendor/github.com/yohcop/openid-go/discovery_cache.go
new file mode 100644
index 0000000000..5fe2e99ccc
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/discovery_cache.go
@@ -0,0 +1,69 @@
+package openid
+
+import (
+	"sync"
+)
+
+type DiscoveredInfo interface {
+	OpEndpoint() string
+	OpLocalID() string
+	ClaimedID() string
+	// ProtocolVersion: it's always openId 2.
+}
+
+type DiscoveryCache interface {
+	Put(id string, info DiscoveredInfo)
+	// Return a discovered info, or nil.
+	Get(id string) DiscoveredInfo
+}
+
+type SimpleDiscoveredInfo struct {
+	opEndpoint string
+	opLocalID  string
+	claimedID  string
+}
+
+func (s *SimpleDiscoveredInfo) OpEndpoint() string {
+	return s.opEndpoint
+}
+
+func (s *SimpleDiscoveredInfo) OpLocalID() string {
+	return s.opLocalID
+}
+
+func (s *SimpleDiscoveredInfo) ClaimedID() string {
+	return s.claimedID
+}
+
+type SimpleDiscoveryCache struct {
+	cache map[string]DiscoveredInfo
+	mutex *sync.Mutex
+}
+
+func NewSimpleDiscoveryCache() *SimpleDiscoveryCache {
+	return &SimpleDiscoveryCache{cache: map[string]DiscoveredInfo{}, mutex: &sync.Mutex{}}
+}
+
+func (s *SimpleDiscoveryCache) Put(id string, info DiscoveredInfo) {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	s.cache[id] = info
+}
+
+func (s *SimpleDiscoveryCache) Get(id string) DiscoveredInfo {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	if info, has := s.cache[id]; has {
+		return info
+	}
+	return nil
+}
+
+func compareDiscoveredInfo(a DiscoveredInfo, opEndpoint, opLocalID, claimedID string) bool {
+	return a != nil &&
+		a.OpEndpoint() == opEndpoint &&
+		a.OpLocalID() == opLocalID &&
+		a.ClaimedID() == claimedID
+}
diff --git a/vendor/github.com/yohcop/openid-go/getter.go b/vendor/github.com/yohcop/openid-go/getter.go
new file mode 100644
index 0000000000..a21019f0ff
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/getter.go
@@ -0,0 +1,31 @@
+package openid
+
+import (
+	"net/http"
+	"net/url"
+)
+
+// Interface that simplifies testing.
+type httpGetter interface {
+	Get(uri string, headers map[string]string) (resp *http.Response, err error)
+	Post(uri string, form url.Values) (resp *http.Response, err error)
+}
+
+type defaultGetter struct {
+	client *http.Client
+}
+
+func (dg *defaultGetter) Get(uri string, headers map[string]string) (resp *http.Response, err error) {
+	request, err := http.NewRequest("GET", uri, nil)
+	if err != nil {
+		return
+	}
+	for h, v := range headers {
+		request.Header.Add(h, v)
+	}
+	return dg.client.Do(request)
+}
+
+func (dg *defaultGetter) Post(uri string, form url.Values) (resp *http.Response, err error) {
+	return dg.client.PostForm(uri, form)
+}
diff --git a/vendor/github.com/yohcop/openid-go/html_discovery.go b/vendor/github.com/yohcop/openid-go/html_discovery.go
new file mode 100644
index 0000000000..145e6c4d9f
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/html_discovery.go
@@ -0,0 +1,77 @@
+package openid
+
+import (
+	"errors"
+	"io"
+
+	"golang.org/x/net/html"
+)
+
+func htmlDiscovery(id string, getter httpGetter) (opEndpoint, opLocalID, claimedID string, err error) {
+	resp, err := getter.Get(id, nil)
+	if err != nil {
+		return "", "", "", err
+	}
+	opEndpoint, opLocalID, err = findProviderFromHeadLink(resp.Body)
+	return opEndpoint, opLocalID, resp.Request.URL.String(), err
+}
+
+func findProviderFromHeadLink(input io.Reader) (opEndpoint, opLocalID string, err error) {
+	tokenizer := html.NewTokenizer(input)
+	inHead := false
+	for {
+		tt := tokenizer.Next()
+		switch tt {
+		case html.ErrorToken:
+			// Even if the document is malformed after we found a
+			// valid <link> tag, ignore and let's be happy with our
+			// openid2.provider and potentially openid2.local_id as well.
+			if len(opEndpoint) > 0 {
+				return
+			}
+			return "", "", tokenizer.Err()
+		case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
+			tk := tokenizer.Token()
+			if tk.Data == "head" {
+				if tt == html.StartTagToken {
+					inHead = true
+				} else {
+					if len(opEndpoint) > 0 {
+						return
+					}
+					return "", "", errors.New(
+						"LINK with rel=openid2.provider not found")
+				}
+			} else if inHead && tk.Data == "link" {
+				provider := false
+				localID := false
+				href := ""
+				for _, attr := range tk.Attr {
+					if attr.Key == "rel" {
+						if attr.Val == "openid2.provider" {
+							provider = true
+						} else if attr.Val == "openid2.local_id" {
+							localID = true
+						} else if attr.Val == "openid.server" {
+							provider = true
+						}
+					} else if attr.Key == "href" {
+						href = attr.Val
+					}
+				}
+				if provider && !localID && len(href) > 0 {
+					opEndpoint = href
+				} else if !provider && localID && len(href) > 0 {
+					opLocalID = href
+				}
+			}
+		}
+	}
+	// At this point we should probably have returned either from
+	// a closing </head> or a tokenizer error (no </head> found).
+	// But just in case.
+	if len(opEndpoint) > 0 {
+		return
+	}
+	return "", "", errors.New("LINK rel=openid2.provider not found")
+}
diff --git a/vendor/github.com/yohcop/openid-go/nonce_store.go b/vendor/github.com/yohcop/openid-go/nonce_store.go
new file mode 100644
index 0000000000..684a1acb0e
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/nonce_store.go
@@ -0,0 +1,87 @@
+package openid
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"sync"
+	"time"
+)
+
+var maxNonceAge = flag.Duration("openid-max-nonce-age",
+	60*time.Second,
+	"Maximum accepted age for openid nonces. The bigger, the more"+
+		"memory is needed to store used nonces.")
+
+type NonceStore interface {
+	// Returns nil if accepted, an error otherwise.
+	Accept(endpoint, nonce string) error
+}
+
+type Nonce struct {
+	T time.Time
+	S string
+}
+
+type SimpleNonceStore struct {
+	store map[string][]*Nonce
+	mutex *sync.Mutex
+}
+
+func NewSimpleNonceStore() *SimpleNonceStore {
+	return &SimpleNonceStore{store: map[string][]*Nonce{}, mutex: &sync.Mutex{}}
+}
+
+func (d *SimpleNonceStore) Accept(endpoint, nonce string) error {
+	// Value: A string 255 characters or less in length, that MUST be
+	// unique to this particular successful authentication response.
+	if len(nonce) < 20 || len(nonce) > 256 {
+		return errors.New("Invalid nonce")
+	}
+
+	// The nonce MUST start with the current time on the server, and MAY
+	// contain additional ASCII characters in the range 33-126 inclusive
+	// (printable non-whitespace characters), as necessary to make each
+	// response unique. The date and time MUST be formatted as specified in
+	// section 5.6 of [RFC3339], with the following restrictions:
+
+	// All times must be in the UTC timezone, indicated with a "Z".  No
+	// fractional seconds are allowed For example:
+	// 2005-05-15T17:11:51ZUNIQUE
+	ts, err := time.Parse(time.RFC3339, nonce[0:20])
+	if err != nil {
+		return err
+	}
+	now := time.Now()
+	diff := now.Sub(ts)
+	if diff > *maxNonceAge {
+		return fmt.Errorf("Nonce too old: %ds", diff.Seconds())
+	}
+
+	s := nonce[20:]
+
+	// Meh.. now we have to use a mutex, to protect that map from
+	// concurrent access. Could put a go routine in charge of it
+	// though.
+	d.mutex.Lock()
+	defer d.mutex.Unlock()
+
+	if nonces, hasOp := d.store[endpoint]; hasOp {
+		// Delete old nonces while we are at it.
+		newNonces := []*Nonce{{ts, s}}
+		for _, n := range nonces {
+			if n.T == ts && n.S == s {
+				// If return early, just ignore the filtered list
+				// we have been building so far...
+				return errors.New("Nonce already used")
+			}
+			if now.Sub(n.T) < *maxNonceAge {
+				newNonces = append(newNonces, n)
+			}
+		}
+		d.store[endpoint] = newNonces
+	} else {
+		d.store[endpoint] = []*Nonce{{ts, s}}
+	}
+	return nil
+}
diff --git a/vendor/github.com/yohcop/openid-go/normalizer.go b/vendor/github.com/yohcop/openid-go/normalizer.go
new file mode 100644
index 0000000000..8c7aa34a68
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/normalizer.go
@@ -0,0 +1,64 @@
+package openid
+
+import (
+	"errors"
+	"net/url"
+	"strings"
+)
+
+func Normalize(id string) (string, error) {
+	id = strings.TrimSpace(id)
+	if len(id) == 0 {
+		return "", errors.New("No id provided")
+	}
+
+	// 7.2 from openID 2.0 spec.
+
+	//If the user's input starts with the "xri://" prefix, it MUST be
+	//stripped off, so that XRIs are used in the canonical form.
+	if strings.HasPrefix(id, "xri://") {
+		id = id[6:]
+		return id, errors.New("XRI identifiers not supported")
+	}
+
+	// If the first character of the resulting string is an XRI
+	// Global Context Symbol ("=", "@", "+", "$", "!") or "(", as
+	// defined in Section 2.2.1 of [XRI_Syntax_2.0], then the input
+	// SHOULD be treated as an XRI.
+	if b := id[0]; b == '=' || b == '@' || b == '+' || b == '$' || b == '!' {
+		return id, errors.New("XRI identifiers not supported")
+	}
+
+	// Otherwise, the input SHOULD be treated as an http URL; if it
+	// does not include a "http" or "https" scheme, the Identifier
+	// MUST be prefixed with the string "http://". If the URL
+	// contains a fragment part, it MUST be stripped off together
+	// with the fragment delimiter character "#". See Section 11.5.2 for
+	// more information.
+	if !strings.HasPrefix(id, "http://") && !strings.HasPrefix(id,
+		"https://") {
+		id = "http://" + id
+	}
+	if fragmentIndex := strings.Index(id, "#"); fragmentIndex != -1 {
+		id = id[0:fragmentIndex]
+	}
+	if u, err := url.ParseRequestURI(id); err != nil {
+		return "", err
+	} else {
+		if u.Host == "" {
+			return "", errors.New("Invalid address provided as id")
+		}
+		if u.Path == "" {
+			u.Path = "/"
+		}
+		id = u.String()
+	}
+
+	// URL Identifiers MUST then be further normalized by both
+	// following redirects when retrieving their content and finally
+	// applying the rules in Section 6 of [RFC3986] to the final
+	// destination URL. This final URL MUST be noted by the Relying
+	// Party as the Claimed Identifier and be used when requesting
+	// authentication.
+	return id, nil
+}
diff --git a/vendor/github.com/yohcop/openid-go/openid.go b/vendor/github.com/yohcop/openid-go/openid.go
new file mode 100644
index 0000000000..5878a478e9
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/openid.go
@@ -0,0 +1,15 @@
+package openid
+
+import (
+	"net/http"
+)
+
+type OpenID struct {
+	urlGetter httpGetter
+}
+
+func NewOpenID(client *http.Client) *OpenID {
+	return &OpenID{urlGetter: &defaultGetter{client: client}}
+}
+
+var defaultInstance = NewOpenID(http.DefaultClient)
diff --git a/vendor/github.com/yohcop/openid-go/redirect.go b/vendor/github.com/yohcop/openid-go/redirect.go
new file mode 100644
index 0000000000..9af7a845bb
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/redirect.go
@@ -0,0 +1,55 @@
+package openid
+
+import (
+	"net/url"
+	"strings"
+)
+
+func RedirectURL(id, callbackURL, realm string) (string, error) {
+	return defaultInstance.RedirectURL(id, callbackURL, realm)
+}
+
+func (oid *OpenID) RedirectURL(id, callbackURL, realm string) (string, error) {
+	opEndpoint, opLocalID, claimedID, err := oid.Discover(id)
+	if err != nil {
+		return "", err
+	}
+	return BuildRedirectURL(opEndpoint, opLocalID, claimedID, callbackURL, realm)
+}
+
+func BuildRedirectURL(opEndpoint, opLocalID, claimedID, returnTo, realm string) (string, error) {
+	values := make(url.Values)
+	values.Add("openid.ns", "http://specs.openid.net/auth/2.0")
+	values.Add("openid.mode", "checkid_setup")
+	values.Add("openid.return_to", returnTo)
+
+	// 9.1.  Request Parameters
+	// "openid.claimed_id" and "openid.identity" SHALL be either both present or both absent.
+	if len(claimedID) > 0 {
+		values.Add("openid.claimed_id", claimedID)
+		if len(opLocalID) > 0 {
+			values.Add("openid.identity", opLocalID)
+		} else {
+			// If a different OP-Local Identifier is not specified,
+			// the claimed identifier MUST be used as the value for openid.identity.
+			values.Add("openid.identity", claimedID)
+		}
+	} else {
+		// 7.3.1.  Discovered Information
+		// If the end user entered an OP Identifier, there is no Claimed Identifier.
+		// For the purposes of making OpenID Authentication requests, the value
+		// "http://specs.openid.net/auth/2.0/identifier_select" MUST be used as both the
+		// Claimed Identifier and the OP-Local Identifier when an OP Identifier is entered.
+		values.Add("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select")
+		values.Add("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select")
+	}
+
+	if len(realm) > 0 {
+		values.Add("openid.realm", realm)
+	}
+
+	if strings.Contains(opEndpoint, "?") {
+		return opEndpoint + "&" + values.Encode(), nil
+	}
+	return opEndpoint + "?" + values.Encode(), nil
+}
diff --git a/vendor/github.com/yohcop/openid-go/verify.go b/vendor/github.com/yohcop/openid-go/verify.go
new file mode 100644
index 0000000000..6090ecac59
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/verify.go
@@ -0,0 +1,250 @@
+package openid
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/url"
+	"strings"
+)
+
+func Verify(uri string, cache DiscoveryCache, nonceStore NonceStore) (id string, err error) {
+	return defaultInstance.Verify(uri, cache, nonceStore)
+}
+
+func (oid *OpenID) Verify(uri string, cache DiscoveryCache, nonceStore NonceStore) (id string, err error) {
+	parsedURL, err := url.Parse(uri)
+	if err != nil {
+		return "", err
+	}
+	values, err := url.ParseQuery(parsedURL.RawQuery)
+	if err != nil {
+		return "", err
+	}
+
+	// 11.  Verifying Assertions
+	// When the Relying Party receives a positive assertion, it MUST
+	// verify the following before accepting the assertion:
+
+	// - The value of "openid.signed" contains all the required fields.
+	//   (Section 10.1)
+	if err = verifySignedFields(values); err != nil {
+		return "", err
+	}
+
+	// - The signature on the assertion is valid (Section 11.4)
+	if err = verifySignature(uri, values, oid.urlGetter); err != nil {
+		return "", err
+	}
+
+	// - The value of "openid.return_to" matches the URL of the current
+	//   request (Section 11.1)
+	if err = verifyReturnTo(parsedURL, values); err != nil {
+		return "", err
+	}
+
+	// - Discovered information matches the information in the assertion
+	//   (Section 11.2)
+	if err = oid.verifyDiscovered(parsedURL, values, cache); err != nil {
+		return "", err
+	}
+
+	// - An assertion has not yet been accepted from this OP with the
+	//   same value for "openid.response_nonce" (Section 11.3)
+	if err = verifyNonce(values, nonceStore); err != nil {
+		return "", err
+	}
+
+	// If all four of these conditions are met, assertion is now
+	// verified. If the assertion contained a Claimed Identifier, the
+	// user is now authenticated with that identifier.
+	return values.Get("openid.claimed_id"), nil
+}
+
+// 10.1. Positive Assertions
+// openid.signed - Comma-separated list of signed fields.
+// This entry consists of the fields without the "openid." prefix that the signature covers.
+// This list MUST contain at least "op_endpoint", "return_to" "response_nonce" and "assoc_handle",
+// and if present in the response, "claimed_id" and "identity".
+func verifySignedFields(vals url.Values) error {
+	ok := map[string]bool{
+		"op_endpoint":    false,
+		"return_to":      false,
+		"response_nonce": false,
+		"assoc_handle":   false,
+		"claimed_id":     vals.Get("openid.claimed_id") == "",
+		"identity":       vals.Get("openid.identity") == "",
+	}
+	signed := strings.Split(vals.Get("openid.signed"), ",")
+	for _, sf := range signed {
+		ok[sf] = true
+	}
+	for k, v := range ok {
+		if !v {
+			return fmt.Errorf("%v must be signed but isn't", k)
+		}
+	}
+	return nil
+}
+
+// 11.1.  Verifying the Return URL
+// To verify that the "openid.return_to" URL matches the URL that is processing this assertion:
+// - The URL scheme, authority, and path MUST be the same between the two
+//   URLs.
+// - Any query parameters that are present in the "openid.return_to" URL
+//   MUST also be present with the same values in the URL of the HTTP
+//   request the RP received.
+func verifyReturnTo(uri *url.URL, vals url.Values) error {
+	returnTo := vals.Get("openid.return_to")
+	rp, err := url.Parse(returnTo)
+	if err != nil {
+		return err
+	}
+	if uri.Scheme != rp.Scheme ||
+		uri.Host != rp.Host ||
+		uri.Path != rp.Path {
+		return errors.New(
+			"Scheme, host or path don't match in return_to URL")
+	}
+	qp, err := url.ParseQuery(rp.RawQuery)
+	if err != nil {
+		return err
+	}
+	return compareQueryParams(qp, vals)
+}
+
+// Any parameter in q1 must also be present in q2, and values must match.
+func compareQueryParams(q1, q2 url.Values) error {
+	for k := range q1 {
+		v1 := q1.Get(k)
+		v2 := q2.Get(k)
+		if v1 != v2 {
+			return fmt.Errorf(
+				"URLs query params don't match: Param %s different: %s vs %s",
+				k, v1, v2)
+		}
+	}
+	return nil
+}
+
+func (oid *OpenID) verifyDiscovered(uri *url.URL, vals url.Values, cache DiscoveryCache) error {
+	version := vals.Get("openid.ns")
+	if version != "http://specs.openid.net/auth/2.0" {
+		return errors.New("Bad protocol version")
+	}
+
+	endpoint := vals.Get("openid.op_endpoint")
+	if len(endpoint) == 0 {
+		return errors.New("missing openid.op_endpoint url param")
+	}
+	localID := vals.Get("openid.identity")
+	if len(localID) == 0 {
+		return errors.New("no localId to verify")
+	}
+	claimedID := vals.Get("openid.claimed_id")
+	if len(claimedID) == 0 {
+		// If no Claimed Identifier is present in the response, the
+		// assertion is not about an identifier and the RP MUST NOT use the
+		// User-supplied Identifier associated with the current OpenID
+		// authentication transaction to identify the user. Extension
+		// information in the assertion MAY still be used.
+		// --- This library does not support this case. So claimed
+		//     identifier must be present.
+		return errors.New("no claimed_id to verify")
+	}
+
+	// 11.2.  Verifying Discovered Information
+
+	// If the Claimed Identifier in the assertion is a URL and contains a
+	// fragment, the fragment part and the fragment delimiter character "#"
+	// MUST NOT be used for the purposes of verifying the discovered
+	// information.
+	claimedIDVerify := claimedID
+	if fragmentIndex := strings.Index(claimedID, "#"); fragmentIndex != -1 {
+		claimedIDVerify = claimedID[0:fragmentIndex]
+	}
+
+	// If the Claimed Identifier is included in the assertion, it
+	// MUST have been discovered by the Relying Party and the
+	// information in the assertion MUST be present in the
+	// discovered information. The Claimed Identifier MUST NOT be an
+	// OP Identifier.
+	if discovered := cache.Get(claimedIDVerify); discovered != nil &&
+		discovered.OpEndpoint() == endpoint &&
+		discovered.OpLocalID() == localID &&
+		discovered.ClaimedID() == claimedIDVerify {
+		return nil
+	}
+
+	// If the Claimed Identifier was not previously discovered by the
+	// Relying Party (the "openid.identity" in the request was
+	// "http://specs.openid.net/auth/2.0/identifier_select" or a different
+	// Identifier, or if the OP is sending an unsolicited positive
+	// assertion), the Relying Party MUST perform discovery on the Claimed
+	// Identifier in the response to make sure that the OP is authorized to
+	// make assertions about the Claimed Identifier.
+	if ep, _, _, err := oid.Discover(claimedID); err == nil {
+		if ep == endpoint {
+			// This claimed ID points to the same endpoint, therefore this
+			// endpoint is authorized to make assertions about that claimed ID.
+			// TODO: There may be multiple endpoints found during discovery.
+			// They should all be checked.
+			cache.Put(claimedIDVerify, &SimpleDiscoveredInfo{opEndpoint: endpoint, opLocalID: localID, claimedID: claimedIDVerify})
+			return nil
+		}
+	}
+
+	return errors.New("Could not verify the claimed ID")
+}
+
+func verifyNonce(vals url.Values, store NonceStore) error {
+	nonce := vals.Get("openid.response_nonce")
+	endpoint := vals.Get("openid.op_endpoint")
+	return store.Accept(endpoint, nonce)
+}
+
+func verifySignature(uri string, vals url.Values, getter httpGetter) error {
+	// To have the signature verification performed by the OP, the
+	// Relying Party sends a direct request to the OP. To verify the
+	// signature, the OP uses a private association that was generated
+	// when it issued the positive assertion.
+
+	// 11.4.2.1.  Request Parameters
+	params := make(url.Values)
+	// openid.mode: Value: "check_authentication"
+	params.Add("openid.mode", "check_authentication")
+	// Exact copies of all fields from the authentication response,
+	// except for "openid.mode".
+	for k, vs := range vals {
+		if k == "openid.mode" {
+			continue
+		}
+		for _, v := range vs {
+			params.Add(k, v)
+		}
+	}
+	resp, err := getter.Post(vals.Get("openid.op_endpoint"), params)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	content, err := ioutil.ReadAll(resp.Body)
+	response := string(content)
+	lines := strings.Split(response, "\n")
+
+	isValid := false
+	nsValid := false
+	for _, l := range lines {
+		if l == "is_valid:true" {
+			isValid = true
+		} else if l == "ns:http://specs.openid.net/auth/2.0" {
+			nsValid = true
+		}
+	}
+	if isValid && nsValid {
+		// Yay !
+		return nil
+	}
+
+	return errors.New("Could not verify assertion with provider")
+}
diff --git a/vendor/github.com/yohcop/openid-go/xrds.go b/vendor/github.com/yohcop/openid-go/xrds.go
new file mode 100644
index 0000000000..34ecf046f1
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/xrds.go
@@ -0,0 +1,83 @@
+package openid
+
+import (
+	"encoding/xml"
+	"errors"
+	"strings"
+)
+
+// TODO: As per 11.2 in openid 2 specs, a service may have multiple
+//       URIs. We don't care for discovery really, but we do care for
+//       verification though.
+type XrdsIdentifier struct {
+	Type     []string `xml:"Type"`
+	URI      string   `xml:"URI"`
+	LocalID  string   `xml:"LocalID"`
+	Priority int      `xml:"priority,attr"`
+}
+
+type Xrd struct {
+	Service []*XrdsIdentifier `xml:"Service"`
+}
+
+type XrdsDocument struct {
+	XMLName xml.Name `xml:"XRDS"`
+	Xrd     *Xrd     `xml:"XRD"`
+}
+
+func parseXrds(input []byte) (opEndpoint, opLocalID string, err error) {
+	xrdsDoc := &XrdsDocument{}
+	err = xml.Unmarshal(input, xrdsDoc)
+	if err != nil {
+		return
+	}
+
+	if xrdsDoc.Xrd == nil {
+		return "", "", errors.New("XRDS document missing XRD tag")
+	}
+
+	// 7.3.2.2.  Extracting Authentication Data
+	// Once the Relying Party has obtained an XRDS document, it
+	// MUST first search the document (following the rules
+	// described in [XRI_Resolution_2.0]) for an OP Identifier
+	// Element. If none is found, the RP will search for a Claimed
+	// Identifier Element.
+	for _, service := range xrdsDoc.Xrd.Service {
+		// 7.3.2.1.1.  OP Identifier Element
+		// An OP Identifier Element is an <xrd:Service> element with the
+		// following information:
+		// An <xrd:Type> tag whose text content is
+		//     "http://specs.openid.net/auth/2.0/server".
+		// An <xrd:URI> tag whose text content is the OP Endpoint URL
+		if service.hasType("http://specs.openid.net/auth/2.0/server") {
+			opEndpoint = strings.TrimSpace(service.URI)
+			return
+		}
+	}
+	for _, service := range xrdsDoc.Xrd.Service {
+		// 7.3.2.1.2.  Claimed Identifier Element
+		// A Claimed Identifier Element is an <xrd:Service> element
+		// with the following information:
+		// An <xrd:Type> tag whose text content is
+		//     "http://specs.openid.net/auth/2.0/signon".
+		// An <xrd:URI> tag whose text content is the OP Endpoint
+		//     URL.
+		// An <xrd:LocalID> tag (optional) whose text content is the
+		//     OP-Local Identifier.
+		if service.hasType("http://specs.openid.net/auth/2.0/signon") {
+			opEndpoint = strings.TrimSpace(service.URI)
+			opLocalID = strings.TrimSpace(service.LocalID)
+			return
+		}
+	}
+	return "", "", errors.New("Could not find a compatible service")
+}
+
+func (xrdsi *XrdsIdentifier) hasType(tpe string) bool {
+	for _, t := range xrdsi.Type {
+		if t == tpe {
+			return true
+		}
+	}
+	return false
+}
diff --git a/vendor/github.com/yohcop/openid-go/yadis_discovery.go b/vendor/github.com/yohcop/openid-go/yadis_discovery.go
new file mode 100644
index 0000000000..1c2b690ff1
--- /dev/null
+++ b/vendor/github.com/yohcop/openid-go/yadis_discovery.go
@@ -0,0 +1,119 @@
+package openid
+
+import (
+	"errors"
+	"io"
+	"io/ioutil"
+	"strings"
+
+	"golang.org/x/net/html"
+)
+
+var yadisHeaders = map[string]string{
+	"Accept": "application/xrds+xml"}
+
+func yadisDiscovery(id string, getter httpGetter) (opEndpoint string, opLocalID string, err error) {
+	// Section 6.2.4 of Yadis 1.0 specifications.
+	// The Yadis Protocol is initiated by the Relying Party Agent
+	// with an initial HTTP request using the Yadis URL.
+
+	// This request MUST be either a GET or a HEAD request.
+
+	// A GET or HEAD request MAY include an HTTP Accept
+	// request-header (HTTP 14.1) specifying MIME media type,
+	// application/xrds+xml.
+	resp, err := getter.Get(id, yadisHeaders)
+	if err != nil {
+		return "", "", err
+	}
+
+	defer resp.Body.Close()
+
+	// Section 6.2.5 from Yadis 1.0 spec: Response
+
+	contentType := resp.Header.Get("Content-Type")
+
+	// The response MUST be one of:
+	// (see 6.2.6 for precedence)
+	if l := resp.Header.Get("X-XRDS-Location"); l != "" {
+		// 2. HTTP response-headers that include an X-XRDS-Location
+		// response-header, together with a document
+		return getYadisResourceDescriptor(l, getter)
+	} else if strings.Contains(contentType, "text/html") {
+		// 1. An HTML document with a <head> element that includes a
+		// <meta> element with http-equiv attribute, X-XRDS-Location,
+
+		metaContent, err := findMetaXrdsLocation(resp.Body)
+		if err == nil {
+			return getYadisResourceDescriptor(metaContent, getter)
+		}
+		return "", "", err
+	} else if strings.Contains(contentType, "application/xrds+xml") {
+		// 4. A document of MIME media type, application/xrds+xml.
+		body, err := ioutil.ReadAll(resp.Body)
+		if err == nil {
+			return parseXrds(body)
+		}
+		return "", "", err
+	}
+	// 3. HTTP response-headers only, which MAY include an
+	// X-XRDS-Location response-header, a content-type
+	// response-header specifying MIME media type,
+	// application/xrds+xml, or both.
+	//   (this is handled by one of the 2 previous if statements)
+	return "", "", errors.New("No expected header, or content type")
+}
+
+// Similar as above, but we expect an absolute Yadis document URL.
+func getYadisResourceDescriptor(id string, getter httpGetter) (opEndpoint string, opLocalID string, err error) {
+	resp, err := getter.Get(id, yadisHeaders)
+	if err != nil {
+		return "", "", err
+	}
+	defer resp.Body.Close()
+	// 4. A document of MIME media type, application/xrds+xml.
+	body, err := ioutil.ReadAll(resp.Body)
+	if err == nil {
+		return parseXrds(body)
+	}
+	return "", "", err
+}
+
+// Search for
+// <head>
+//    <meta http-equiv="X-XRDS-Location" content="....">
+func findMetaXrdsLocation(input io.Reader) (location string, err error) {
+	tokenizer := html.NewTokenizer(input)
+	inHead := false
+	for {
+		tt := tokenizer.Next()
+		switch tt {
+		case html.ErrorToken:
+			return "", tokenizer.Err()
+		case html.StartTagToken, html.EndTagToken:
+			tk := tokenizer.Token()
+			if tk.Data == "head" {
+				if tt == html.StartTagToken {
+					inHead = true
+				} else {
+					return "", errors.New("Meta X-XRDS-Location not found")
+				}
+			} else if inHead && tk.Data == "meta" {
+				ok := false
+				content := ""
+				for _, attr := range tk.Attr {
+					if attr.Key == "http-equiv" &&
+						strings.ToLower(attr.Val) == "x-xrds-location" {
+						ok = true
+					} else if attr.Key == "content" {
+						content = attr.Val
+					}
+				}
+				if ok && len(content) > 0 {
+					return content, nil
+				}
+			}
+		}
+	}
+	return "", errors.New("Meta X-XRDS-Location not found")
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 0fd700adc1..91d889c6fa 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -1162,6 +1162,12 @@
 			"path": "golang.org/x/crypto/cast5",
 			"revision": "b8a2a83acfe6e6770b75de42d5ff4c67596675c0",
 			"revisionTime": "2017-01-13T19:21:00Z"
+    },
+    {
+			"checksumSHA1": "pkrINpw0HkmO+18SdtSjje9MB9g=",
+			"path": "github.com/yohcop/openid-go",
+			"revision": "2c050d2dae5345c417db301f11fda6fbf5ad0f0a",
+			"revisionTime": "2016-09-14T08:04:27Z"
 		},
 		{
 			"checksumSHA1": "dwOedwBJ1EIK9+S3t108Bx054Y8=",