Skip to content

Commit 71d16f6

Browse files
strkbkcsoft
authored andcommitted
Login via OpenID-2.0 (#618)
1 parent 0693fbf commit 71d16f6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2294
-53
lines changed

cmd/web.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,19 @@ func runWeb(ctx *cli.Context) error {
200200
m.Group("/user", func() {
201201
m.Get("/login", user.SignIn)
202202
m.Post("/login", bindIgnErr(auth.SignInForm{}), user.SignInPost)
203+
if setting.EnableOpenIDSignIn {
204+
m.Combo("/login/openid").
205+
Get(user.SignInOpenID).
206+
Post(bindIgnErr(auth.SignInOpenIDForm{}), user.SignInOpenIDPost)
207+
m.Group("/openid", func() {
208+
m.Combo("/connect").
209+
Get(user.ConnectOpenID).
210+
Post(bindIgnErr(auth.ConnectOpenIDForm{}), user.ConnectOpenIDPost)
211+
m.Combo("/register").
212+
Get(user.RegisterOpenID).
213+
Post(bindIgnErr(auth.SignUpOpenIDForm{}), user.RegisterOpenIDPost)
214+
})
215+
}
203216
m.Get("/sign_up", user.SignUp)
204217
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
205218
m.Get("/reset_password", user.ResetPasswd)
@@ -230,6 +243,14 @@ func runWeb(ctx *cli.Context) error {
230243
m.Post("/email/delete", user.DeleteEmail)
231244
m.Get("/password", user.SettingsPassword)
232245
m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost)
246+
if setting.EnableOpenIDSignIn {
247+
m.Group("/openid", func() {
248+
m.Combo("").Get(user.SettingsOpenID).
249+
Post(bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost)
250+
m.Post("/delete", user.DeleteOpenID)
251+
})
252+
}
253+
233254
m.Combo("/ssh").Get(user.SettingsSSHKeys).
234255
Post(bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost)
235256
m.Post("/ssh/delete", user.DeleteSSHKey)

conf/app.ini

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,38 @@ MIN_PASSWORD_LENGTH = 6
182182
; True when users are allowed to import local server paths
183183
IMPORT_LOCAL_PATHS = false
184184

185+
[openid]
186+
;
187+
; OpenID is an open standard and decentralized authentication protocol.
188+
; Your identity is the address of a webpage you provide, which describes
189+
; how to prove you are in control of that page.
190+
;
191+
; For more info: https://en.wikipedia.org/wiki/OpenID
192+
;
193+
; Current implementation supports OpenID-2.0
194+
;
195+
; Tested to work providers at the time of writing:
196+
; - Any GNUSocial node (your.hostname.tld/username)
197+
; - Any SimpleID provider (http://simpleid.koinic.net)
198+
; - http://openid.org.cn/
199+
; - openid.stackexchange.com
200+
; - login.launchpad.net
201+
;
202+
; Whether to allow signin in via OpenID
203+
ENABLE_OPENID_SIGNIN = true
204+
; Whether to allow registering via OpenID
205+
ENABLE_OPENID_SIGNUP = true
206+
; Allowed URI patterns (POSIX regexp).
207+
; Space separated.
208+
; Only these would be allowed if non-blank.
209+
; Example value: trusted.domain.org trusted.domain.net
210+
WHITELISTED_URIS =
211+
; Forbidden URI patterns (POSIX regexp).
212+
; Space sepaated.
213+
; Only used if WHITELISTED_URIS is blank.
214+
; Example value: loadaverage.org/badguy stackexchange.com/.*spammer
215+
BLACKLISTED_URIS =
216+
185217
[service]
186218
ACTIVE_CODE_LIVE_MINUTES = 180
187219
RESET_PASSWD_CODE_LIVE_MINUTES = 180

models/error.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ func (err ErrEmailAlreadyUsed) Error() string {
9393
return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
9494
}
9595

96+
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
97+
type ErrOpenIDAlreadyUsed struct {
98+
OpenID string
99+
}
100+
101+
// IsErrOpenIDAlreadyUsed checks if an error is a ErrOpenIDAlreadyUsed.
102+
func IsErrOpenIDAlreadyUsed(err error) bool {
103+
_, ok := err.(ErrOpenIDAlreadyUsed)
104+
return ok
105+
}
106+
107+
func (err ErrOpenIDAlreadyUsed) Error() string {
108+
return fmt.Sprintf("OpenID has been used [oid: %s]", err.OpenID)
109+
}
110+
96111
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
97112
type ErrUserOwnRepos struct {
98113
UID int64

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ var migrations = []Migration{
9494
NewMigration("rewrite authorized_keys file via new format", useNewPublickeyFormat),
9595
// v22 -> v23
9696
NewMigration("generate and migrate wiki Git hooks", generateAndMigrateUncycloGitHooks),
97+
// v23 -> v24
98+
NewMigration("add user openid table", addUserOpenID),
9799
}
98100

99101
// Migrate database to current version

models/migrations/v23.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2017 Gitea. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/go-xorm/xorm"
11+
)
12+
13+
// UserOpenID is the list of all OpenID identities of a user.
14+
type UserOpenID struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
UID int64 `xorm:"INDEX NOT NULL"`
17+
URI string `xorm:"UNIQUE NOT NULL"`
18+
}
19+
20+
21+
func addUserOpenID(x *xorm.Engine) error {
22+
if err := x.Sync2(new(UserOpenID)); err != nil {
23+
return fmt.Errorf("Sync2: %v", err)
24+
}
25+
return nil
26+
}

models/models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func init() {
116116
new(RepoRedirect),
117117
new(ExternalLoginUser),
118118
new(ProtectedBranch),
119+
new(UserOpenID),
119120
)
120121

121122
gonicNames := []string{"SSL", "UID"}

models/user.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,7 @@ func deleteUser(e *xorm.Session, u *User) error {
964964
&Action{UserID: u.ID},
965965
&IssueUser{UID: u.ID},
966966
&EmailAddress{UID: u.ID},
967+
&UserOpenID{UID: u.ID},
967968
); err != nil {
968969
return fmt.Errorf("deleteBeans: %v", err)
969970
}

models/user_openid.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"errors"
9+
10+
"code.gitea.io/gitea/modules/auth/openid"
11+
"code.gitea.io/gitea/modules/log"
12+
)
13+
14+
var (
15+
// ErrOpenIDNotExist openid is not known
16+
ErrOpenIDNotExist = errors.New("OpenID is unknown")
17+
)
18+
19+
// UserOpenID is the list of all OpenID identities of a user.
20+
type UserOpenID struct {
21+
ID int64 `xorm:"pk autoincr"`
22+
UID int64 `xorm:"INDEX NOT NULL"`
23+
URI string `xorm:"UNIQUE NOT NULL"`
24+
}
25+
26+
// GetUserOpenIDs returns all openid addresses that belongs to given user.
27+
func GetUserOpenIDs(uid int64) ([]*UserOpenID, error) {
28+
openids := make([]*UserOpenID, 0, 5)
29+
if err := x.
30+
Where("uid=?", uid).
31+
Find(&openids); err != nil {
32+
return nil, err
33+
}
34+
35+
return openids, nil
36+
}
37+
38+
func isOpenIDUsed(e Engine, uri string) (bool, error) {
39+
if len(uri) == 0 {
40+
return true, nil
41+
}
42+
43+
return e.Get(&UserOpenID{URI: uri})
44+
}
45+
46+
// IsOpenIDUsed returns true if the openid has been used.
47+
func IsOpenIDUsed(openid string) (bool, error) {
48+
return isOpenIDUsed(x, openid)
49+
}
50+
51+
// NOTE: make sure openid.URI is normalized already
52+
func addUserOpenID(e Engine, openid *UserOpenID) error {
53+
used, err := isOpenIDUsed(e, openid.URI)
54+
if err != nil {
55+
return err
56+
} else if used {
57+
return ErrOpenIDAlreadyUsed{openid.URI}
58+
}
59+
60+
_, err = e.Insert(openid)
61+
return err
62+
}
63+
64+
// AddUserOpenID adds an pre-verified/normalized OpenID URI to given user.
65+
func AddUserOpenID(openid *UserOpenID) error {
66+
return addUserOpenID(x, openid)
67+
}
68+
69+
// DeleteUserOpenID deletes an openid address of given user.
70+
func DeleteUserOpenID(openid *UserOpenID) (err error) {
71+
var deleted int64
72+
// ask to check UID
73+
var address = UserOpenID{
74+
UID: openid.UID,
75+
}
76+
if openid.ID > 0 {
77+
deleted, err = x.Id(openid.ID).Delete(&address)
78+
} else {
79+
deleted, err = x.
80+
Where("openid=?", openid.URI).
81+
Delete(&address)
82+
}
83+
84+
if err != nil {
85+
return err
86+
} else if deleted != 1 {
87+
return ErrOpenIDNotExist
88+
}
89+
return nil
90+
}
91+
92+
// GetUserByOpenID returns the user object by given OpenID if exists.
93+
func GetUserByOpenID(uri string) (*User, error) {
94+
if len(uri) == 0 {
95+
return nil, ErrUserNotExist{0, uri, 0}
96+
}
97+
98+
uri, err := openid.Normalize(uri)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
log.Trace("Normalized OpenID URI: " + uri)
104+
105+
// Otherwise, check in openid table
106+
oid := &UserOpenID{URI: uri}
107+
has, err := x.Get(oid)
108+
if err != nil {
109+
return nil, err
110+
}
111+
if has {
112+
return GetUserByID(oid.UID)
113+
}
114+
115+
return nil, ErrUserNotExist{0, uri, 0}
116+
}
117+
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package openid
6+
7+
import (
8+
"sync"
9+
"time"
10+
11+
"github.com/yohcop/openid-go"
12+
)
13+
14+
type timedDiscoveredInfo struct {
15+
info openid.DiscoveredInfo
16+
time time.Time
17+
}
18+
19+
type timedDiscoveryCache struct {
20+
cache map[string]timedDiscoveredInfo
21+
ttl time.Duration
22+
mutex *sync.Mutex
23+
}
24+
25+
func newTimedDiscoveryCache(ttl time.Duration) *timedDiscoveryCache {
26+
return &timedDiscoveryCache{cache: map[string]timedDiscoveredInfo{}, ttl: ttl, mutex: &sync.Mutex{}}
27+
}
28+
29+
func (s *timedDiscoveryCache) Put(id string, info openid.DiscoveredInfo) {
30+
s.mutex.Lock()
31+
defer s.mutex.Unlock()
32+
33+
s.cache[id] = timedDiscoveredInfo{info: info, time: time.Now()}
34+
}
35+
36+
// Delete timed-out cache entries
37+
func (s *timedDiscoveryCache) cleanTimedOut() {
38+
now := time.Now()
39+
for k, e := range s.cache {
40+
diff := now.Sub(e.time)
41+
if diff > s.ttl {
42+
delete(s.cache, k)
43+
}
44+
}
45+
}
46+
47+
func (s *timedDiscoveryCache) Get(id string) openid.DiscoveredInfo {
48+
s.mutex.Lock()
49+
defer s.mutex.Unlock()
50+
51+
// Delete old cached while we are at it.
52+
s.cleanTimedOut()
53+
54+
if info, has := s.cache[id]; has {
55+
return info.info
56+
}
57+
return nil
58+
}
59+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package openid
6+
7+
import (
8+
"testing"
9+
"time"
10+
)
11+
12+
type testDiscoveredInfo struct {}
13+
func (s *testDiscoveredInfo) ClaimedID() string {
14+
return "claimedID"
15+
}
16+
func (s *testDiscoveredInfo) OpEndpoint() string {
17+
return "opEndpoint"
18+
}
19+
func (s *testDiscoveredInfo) OpLocalID() string {
20+
return "opLocalID"
21+
}
22+
23+
func TestTimedDiscoveryCache(t *testing.T) {
24+
dc := newTimedDiscoveryCache(1*time.Second)
25+
26+
// Put some initial values
27+
dc.Put("foo", &testDiscoveredInfo{}) //openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"})
28+
29+
// Make sure we can retrieve them
30+
if di := dc.Get("foo"); di == nil {
31+
t.Errorf("Expected a result, got nil")
32+
} else if di.OpEndpoint() != "opEndpoint" || di.OpLocalID() != "opLocalID" || di.ClaimedID() != "claimedID" {
33+
t.Errorf("Expected opEndpoint opLocalID claimedID, got %v %v %v", di.OpEndpoint(), di.OpLocalID(), di.ClaimedID())
34+
}
35+
36+
// Attempt to get a non-existent value
37+
if di := dc.Get("bar"); di != nil {
38+
t.Errorf("Expected nil, got %v", di)
39+
}
40+
41+
// Sleep one second and try retrive again
42+
time.Sleep(1 * time.Second)
43+
44+
if di := dc.Get("foo"); di != nil {
45+
t.Errorf("Expected a nil, got a result")
46+
}
47+
}

0 commit comments

Comments
 (0)