| @@ -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) | |||
| @@ -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 | |||
| @@ -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) | |||
| } | |||
| @@ -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} | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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") | |||
| } | |||
| } | |||
| @@ -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)"` | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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) | |||
| } | |||
| @@ -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", | |||
| }) | |||
| } | |||
| @@ -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}} | |||
| {{.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> | |||
| <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 .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> | |||
| {{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> | |||
| {{end}} | |||
| </div> | |||
| </div> | |||
| {{end}} | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -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> | |||
| @@ -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" .}} | |||
| @@ -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" .}} | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -26,4 +31,4 @@ | |||
| <a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{AppSubUrl}}/user/settings/delete"> | |||
| {{.i18n.Tr "settings.delete"}} | |||
| </a> | |||
| </div> | |||
| </div> | |||
| @@ -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" .}} | |||
| @@ -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. | |||
| @@ -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). | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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") | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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) | |||
| @@ -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 | |||
| } | |||
| @@ -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") | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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") | |||
| } | |||
| @@ -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=", | |||