Login via OpenID-2.0 (#618)
This commit is contained in:
		
							parent
							
								
									0693fbfc00
								
							
						
					
					
						commit
						71d16f69ff
					
				
					 44 changed files with 2298 additions and 57 deletions
				
			
		
							
								
								
									
										21
									
								
								cmd/web.go
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								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) | ||||
|  |  | |||
							
								
								
									
										32
									
								
								conf/app.ini
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								conf/app.ini
									
									
									
									
										vendored
									
									
								
							|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
							
								
								
									
										26
									
								
								models/migrations/v23.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								models/migrations/v23.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
|  | @ -116,6 +116,7 @@ func init() { | |||
| 		new(RepoRedirect), | ||||
| 		new(ExternalLoginUser), | ||||
| 		new(ProtectedBranch), | ||||
| 		new(UserOpenID), | ||||
| 	) | ||||
| 
 | ||||
| 	gonicNames := []string{"SSL", "UID"} | ||||
|  |  | |||
|  | @ -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) | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										117
									
								
								models/user_openid.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								models/user_openid.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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} | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										59
									
								
								modules/auth/openid/discovery_cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								modules/auth/openid/discovery_cache.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										47
									
								
								modules/auth/openid/discovery_cache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								modules/auth/openid/discovery_cache_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										37
									
								
								modules/auth/openid/openid.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								modules/auth/openid/openid.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||
| } | ||||
| 
 | ||||
|  | @ -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)"` | ||||
|  |  | |||
							
								
								
									
										45
									
								
								modules/auth/user_form_auth_openid.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								modules/auth/user_form_auth_openid.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||
| } | ||||
| 
 | ||||
|  | @ -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) | ||||
| 	} | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								public/img/openid-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/img/openid-16x16.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 230 B | 
|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										426
									
								
								routers/user/auth_openid.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										426
									
								
								routers/user/auth_openid.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||
| } | ||||
							
								
								
									
										142
									
								
								routers/user/setting_openid.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								routers/user/setting_openid.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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", | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										46
									
								
								templates/user/auth/finalize_openid.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								templates/user/auth/finalize_openid.tmpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" .}} | ||||
|  | @ -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" .}} | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										11
									
								
								templates/user/auth/signin_navbar.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								templates/user/auth/signin_navbar.tmpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										37
									
								
								templates/user/auth/signin_openid.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								templates/user/auth/signin_openid.tmpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" .}} | ||||
							
								
								
									
										45
									
								
								templates/user/auth/signup_openid_connect.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								templates/user/auth/signup_openid_connect.tmpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" .}} | ||||
							
								
								
									
										11
									
								
								templates/user/auth/signup_openid_navbar.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								templates/user/auth/signup_openid_navbar.tmpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
| 
 | ||||
							
								
								
									
										34
									
								
								templates/user/auth/signup_openid_register.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								templates/user/auth/signup_openid_register.tmpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" .}} | ||||
|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										57
									
								
								templates/user/settings/openid.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								templates/user/settings/openid.tmpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" .}} | ||||
							
								
								
									
										13
									
								
								vendor/github.com/yohcop/openid-go/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								vendor/github.com/yohcop/openid-go/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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. | ||||
							
								
								
									
										38
									
								
								vendor/github.com/yohcop/openid-go/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								vendor/github.com/yohcop/openid-go/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| 
 | ||||
| [](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). | ||||
							
								
								
									
										57
									
								
								vendor/github.com/yohcop/openid-go/discover.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								vendor/github.com/yohcop/openid-go/discover.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
							
								
								
									
										69
									
								
								vendor/github.com/yohcop/openid-go/discovery_cache.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								vendor/github.com/yohcop/openid-go/discovery_cache.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
							
								
								
									
										31
									
								
								vendor/github.com/yohcop/openid-go/getter.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								vendor/github.com/yohcop/openid-go/getter.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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) | ||||
| } | ||||
							
								
								
									
										77
									
								
								vendor/github.com/yohcop/openid-go/html_discovery.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								vendor/github.com/yohcop/openid-go/html_discovery.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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") | ||||
| } | ||||
							
								
								
									
										87
									
								
								vendor/github.com/yohcop/openid-go/nonce_store.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								vendor/github.com/yohcop/openid-go/nonce_store.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
							
								
								
									
										64
									
								
								vendor/github.com/yohcop/openid-go/normalizer.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								vendor/github.com/yohcop/openid-go/normalizer.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
							
								
								
									
										15
									
								
								vendor/github.com/yohcop/openid-go/openid.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								vendor/github.com/yohcop/openid-go/openid.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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) | ||||
							
								
								
									
										55
									
								
								vendor/github.com/yohcop/openid-go/redirect.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								vendor/github.com/yohcop/openid-go/redirect.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
							
								
								
									
										250
									
								
								vendor/github.com/yohcop/openid-go/verify.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								vendor/github.com/yohcop/openid-go/verify.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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") | ||||
| } | ||||
							
								
								
									
										83
									
								
								vendor/github.com/yohcop/openid-go/xrds.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								vendor/github.com/yohcop/openid-go/xrds.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
							
								
								
									
										119
									
								
								vendor/github.com/yohcop/openid-go/yadis_discovery.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								vendor/github.com/yohcop/openid-go/yadis_discovery.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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") | ||||
| } | ||||
							
								
								
									
										6
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							|  | @ -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=", | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue