Login via OpenID-2.0 (#618)
parent
0693fbfc00
commit
71d16f69ff
@ -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 |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
|
@ -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) |
||||||
|
} |
||||||
|
|
After Width: | Height: | Size: 230 B |
@ -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 "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" .}} |
{{template "base/footer" .}} |
||||||
|
@ -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" .}} |
@ -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") |
||||||
|
} |
Loading…
Reference in new issue