Login via OpenID-2.0 (#618)
This commit is contained in:
parent
0693fbfc00
commit
71d16f69ff
44 changed files with 2298 additions and 57 deletions
13
vendor/github.com/yohcop/openid-go/LICENSE
generated
vendored
Normal file
13
vendor/github.com/yohcop/openid-go/LICENSE
generated
vendored
Normal file
|
@ -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.
|
38
vendor/github.com/yohcop/openid-go/README.md
generated
vendored
Normal file
38
vendor/github.com/yohcop/openid-go/README.md
generated
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
# openid.go
|
||||
|
||||
This is a consumer (Relying party) implementation of OpenId 2.0,
|
||||
written in Go.
|
||||
|
||||
go get -u github.com/yohcop/openid-go
|
||||
|
||||
[](https://travis-ci.org/yohcop/openid-go)
|
||||
|
||||
## Github
|
||||
|
||||
Be awesome! Feel free to clone and use according to the licence.
|
||||
If you make a useful change that can benefit others, send a
|
||||
pull request! This ensures that one version has all the good stuff
|
||||
and doesn't fall behind.
|
||||
|
||||
## Code example
|
||||
|
||||
See `_example/` for a simple webserver using the openID
|
||||
implementation. Also, read the comment about the NonceStore towards
|
||||
the top of that file. The example must be run for the openid-go
|
||||
directory, like so:
|
||||
|
||||
go run _example/server.go
|
||||
|
||||
## App Engine
|
||||
|
||||
In order to use this on Google App Engine, you need to create an instance with a custom `*http.Client` provided by [urlfetch](https://cloud.google.com/appengine/docs/go/urlfetch/).
|
||||
|
||||
```go
|
||||
oid := openid.NewOpenID(urlfetch.Client(appengine.NewContext(r)))
|
||||
oid.RedirectURL(...)
|
||||
oid.Verify(...)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the [Apache v2.0 license](http://www.apache.org/licenses/LICENSE-2.0.html).
|
57
vendor/github.com/yohcop/openid-go/discover.go
generated
vendored
Normal file
57
vendor/github.com/yohcop/openid-go/discover.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
69
vendor/github.com/yohcop/openid-go/discovery_cache.go
generated
vendored
Normal file
69
vendor/github.com/yohcop/openid-go/discovery_cache.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
31
vendor/github.com/yohcop/openid-go/getter.go
generated
vendored
Normal file
31
vendor/github.com/yohcop/openid-go/getter.go
generated
vendored
Normal file
|
@ -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)
|
||||
}
|
77
vendor/github.com/yohcop/openid-go/html_discovery.go
generated
vendored
Normal file
77
vendor/github.com/yohcop/openid-go/html_discovery.go
generated
vendored
Normal file
|
@ -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")
|
||||
}
|
87
vendor/github.com/yohcop/openid-go/nonce_store.go
generated
vendored
Normal file
87
vendor/github.com/yohcop/openid-go/nonce_store.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
64
vendor/github.com/yohcop/openid-go/normalizer.go
generated
vendored
Normal file
64
vendor/github.com/yohcop/openid-go/normalizer.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
15
vendor/github.com/yohcop/openid-go/openid.go
generated
vendored
Normal file
15
vendor/github.com/yohcop/openid-go/openid.go
generated
vendored
Normal file
|
@ -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)
|
55
vendor/github.com/yohcop/openid-go/redirect.go
generated
vendored
Normal file
55
vendor/github.com/yohcop/openid-go/redirect.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
250
vendor/github.com/yohcop/openid-go/verify.go
generated
vendored
Normal file
250
vendor/github.com/yohcop/openid-go/verify.go
generated
vendored
Normal file
|
@ -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")
|
||||
}
|
83
vendor/github.com/yohcop/openid-go/xrds.go
generated
vendored
Normal file
83
vendor/github.com/yohcop/openid-go/xrds.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
119
vendor/github.com/yohcop/openid-go/yadis_discovery.go
generated
vendored
Normal file
119
vendor/github.com/yohcop/openid-go/yadis_discovery.go
generated
vendored
Normal file
|
@ -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")
|
||||
}
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
|
@ -1162,6 +1162,12 @@
|
|||
"path": "golang.org/x/crypto/cast5",
|
||||
"revision": "b8a2a83acfe6e6770b75de42d5ff4c67596675c0",
|
||||
"revisionTime": "2017-01-13T19:21:00Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "pkrINpw0HkmO+18SdtSjje9MB9g=",
|
||||
"path": "github.com/yohcop/openid-go",
|
||||
"revision": "2c050d2dae5345c417db301f11fda6fbf5ad0f0a",
|
||||
"revisionTime": "2016-09-14T08:04:27Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "dwOedwBJ1EIK9+S3t108Bx054Y8=",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue