Login via OpenID-2.0 (#618)

tokarchuk/v1.17
Sandro Santilli 8 years ago committed by Kim "BKC" Carlbäcker
parent 0693fbfc00
commit 71d16f69ff
  1. 21
      cmd/web.go
  2. 32
      conf/app.ini
  3. 15
      models/error.go
  4. 2
      models/migrations/migrations.go
  5. 26
      models/migrations/v23.go
  6. 1
      models/models.go
  7. 1
      models/user.go
  8. 117
      models/user_openid.go
  9. 59
      modules/auth/openid/discovery_cache.go
  10. 47
      modules/auth/openid/discovery_cache_test.go
  11. 37
      modules/auth/openid/openid.go
  12. 12
      modules/auth/user_form.go
  13. 45
      modules/auth/user_form_auth_openid.go
  14. 1
      modules/context/context.go
  15. 25
      modules/setting/setting.go
  16. 17
      options/locale/locale_en-US.ini
  17. BIN
      public/img/openid-16x16.png
  18. 10
      routers/user/auth.go
  19. 426
      routers/user/auth_openid.go
  20. 142
      routers/user/setting_openid.go
  21. 46
      templates/user/auth/finalize_openid.tmpl
  22. 7
      templates/user/auth/signin.tmpl
  23. 92
      templates/user/auth/signin_inner.tmpl
  24. 11
      templates/user/auth/signin_navbar.tmpl
  25. 37
      templates/user/auth/signin_openid.tmpl
  26. 45
      templates/user/auth/signup_openid_connect.tmpl
  27. 11
      templates/user/auth/signup_openid_navbar.tmpl
  28. 34
      templates/user/auth/signup_openid_register.tmpl
  29. 7
      templates/user/settings/navbar.tmpl
  30. 57
      templates/user/settings/openid.tmpl
  31. 13
      vendor/github.com/yohcop/openid-go/LICENSE
  32. 38
      vendor/github.com/yohcop/openid-go/README.md
  33. 57
      vendor/github.com/yohcop/openid-go/discover.go
  34. 69
      vendor/github.com/yohcop/openid-go/discovery_cache.go
  35. 31
      vendor/github.com/yohcop/openid-go/getter.go
  36. 77
      vendor/github.com/yohcop/openid-go/html_discovery.go
  37. 87
      vendor/github.com/yohcop/openid-go/nonce_store.go
  38. 64
      vendor/github.com/yohcop/openid-go/normalizer.go
  39. 15
      vendor/github.com/yohcop/openid-go/openid.go
  40. 55
      vendor/github.com/yohcop/openid-go/redirect.go
  41. 250
      vendor/github.com/yohcop/openid-go/verify.go
  42. 83
      vendor/github.com/yohcop/openid-go/xrds.go
  43. 119
      vendor/github.com/yohcop/openid-go/yadis_discovery.go
  44. 6
      vendor/vendor.json

@ -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

@ -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

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)

@ -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
[![Build Status](https://travis-ci.org/yohcop/openid-go.svg?branch=master)](https://travis-ci.org/yohcop/openid-go)
## Github
Be awesome! Feel free to clone and use according to the licence.
If you make a useful change that can benefit others, send a
pull request! This ensures that one version has all the good stuff
and doesn't fall behind.
## Code example
See `_example/` for a simple webserver using the openID
implementation. Also, read the comment about the NonceStore towards
the top of that file. The example must be run for the openid-go
directory, like so:
go run _example/server.go
## App Engine
In order to use this on Google App Engine, you need to create an instance with a custom `*http.Client` provided by [urlfetch](https://cloud.google.com/appengine/docs/go/urlfetch/).
```go
oid := openid.NewOpenID(urlfetch.Client(appengine.NewContext(r)))
oid.RedirectURL(...)
oid.Verify(...)
```
## License
Distributed under the [Apache v2.0 license](http://www.apache.org/licenses/LICENSE-2.0.html).

@ -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=",

Loading…
Cancel
Save