Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login via OpenID-2.0 #618

Merged
merged 16 commits into from
Mar 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this line, add:

m.Combo("").Get(user.SettingsOpenID).
	Post(bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost)
m.Post("/delete", user.DeleteOpenID)

And remove the Group below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are under /user/settings, not just /user

})
}
m.Get("/sign_up", user.SignUp)
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
Expand Down Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions conf/app.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why separated on space? go-ini separates arrays on comma

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces are guaranteed not to be valid characters in URIs, while commas could be, making it harder to encode in the patterns

; Only these would be allowed if non-blank.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe give an example of blocking a domain?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with b7237b4 (more specific than just a domain)

; 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
Expand Down
15 changes: 15 additions & 0 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions models/migrations/v23.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func init() {
new(RepoRedirect),
new(ExternalLoginUser),
new(ProtectedBranch),
new(UserOpenID),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
1 change: 1 addition & 0 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
117 changes: 117 additions & 0 deletions models/user_openid.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You openid.Normalize here, yet in AddUserOpenIDs you normalize in another way, why?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddUserOpenIDs has gone, check addUserOpenID, which does the same normalization (good question though why I don't use Normalize there...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm debugging what seems to be an error due to this, stay tuned (registering a user by logging in as "https://launchpad.net/~strk" and then logging in via "login.launchpad.net" resulting in a mismatch, despite the verification step gives the same resulting ID uri)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, I'm registering lowercased "https://login.launchpad.net/+id/lcm3qwk" while Normalize keps case and thus gets me "https://login.launchpad.net/+id/lcm3qwk". The OpenID specs don't say anything about case so maybe we should keep it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with 2ba2d6a7303f1e1cbc26dbc2f5aad938f4afa37e (the addUser function assumes incoming item is pre-normalized as it returns from a verification step)

if err != nil {
return nil, err
}

log.Trace("Normalized OpenID URI: " + uri)

// Otherwise, check in openid table
oid := &UserOpenID{URI: uri}
has, err := x.Get(oid)
if err != nil {
return nil, err
}
if has {
return GetUserByID(oid.UID)
}

return nil, ErrUserNotExist{0, uri, 0}
}

59 changes: 59 additions & 0 deletions modules/auth/openid/discovery_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package openid

import (
"sync"
"time"

"github.com/yohcop/openid-go"
)

type timedDiscoveredInfo struct {
info openid.DiscoveredInfo
time time.Time
}

type timedDiscoveryCache struct {
cache map[string]timedDiscoveredInfo
ttl time.Duration
mutex *sync.Mutex
}

func newTimedDiscoveryCache(ttl time.Duration) *timedDiscoveryCache {
return &timedDiscoveryCache{cache: map[string]timedDiscoveredInfo{}, ttl: ttl, mutex: &sync.Mutex{}}
}

func (s *timedDiscoveryCache) Put(id string, info openid.DiscoveredInfo) {
s.mutex.Lock()
defer s.mutex.Unlock()

s.cache[id] = timedDiscoveredInfo{info: info, time: time.Now()}
}

// Delete timed-out cache entries
func (s *timedDiscoveryCache) cleanTimedOut() {
now := time.Now()
for k, e := range s.cache {
diff := now.Sub(e.time)
if diff > s.ttl {
delete(s.cache, k)
}
}
}

func (s *timedDiscoveryCache) Get(id string) openid.DiscoveredInfo {
s.mutex.Lock()
defer s.mutex.Unlock()

// Delete old cached while we are at it.
s.cleanTimedOut()

if info, has := s.cache[id]; has {
return info.info
}
return nil
}

47 changes: 47 additions & 0 deletions modules/auth/openid/discovery_cache_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading