pax_global_header 0000666 0000000 0000000 00000000064 13472033373 0014517 g ustar 00root root 0000000 0000000 52 comment=c59699c25602ad4cc7271ad066a5e6d720a5cfe6
openid-go-1.0.0/ 0000775 0000000 0000000 00000000000 13472033373 0013376 5 ustar 00root root 0000000 0000000 openid-go-1.0.0/.travis.yml 0000664 0000000 0000000 00000000407 13472033373 0015510 0 ustar 00root root 0000000 0000000 sudo: false
language: go
go:
- 1.3.x
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
env:
- GO111MODULE=on
# Get deps, build, test, and ensure the code is gofmt'ed.
script:
- go test -v ./...
- diff <(gofmt -d .) <("")
openid-go-1.0.0/LICENSE 0000664 0000000 0000000 00000001052 13472033373 0014401 0 ustar 00root root 0000000 0000000 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.
openid-go-1.0.0/README.md 0000664 0000000 0000000 00000003554 13472033373 0014664 0 ustar 00root root 0000000 0000000 # 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).
## Libraries
Here is a set of libraries I found on GitHub that could make using this library easier depending on your backends. I haven't tested them, this list is for reference only, and in no particular order:
- [Gacnt/myopenid](https://github.com/Gacnt/myopenid) "A Yohcop-Openid Nonce/Discovery storage replacement", using MySQL.
- [Gacnt/sqlxid](https://github.com/Gacnt/sqlxid) "An SQLX Adapter for Nonce / Discovery Cache store"
- [Gacnt/gormid](https://github.com/Gacnt/gormid) "Use GORM (Go Object Relational Mapping) to store OpenID DiscoveryCache / Nonce in a database"
- [hectorj/mysqlOpenID](https://github.com/hectorj/mysqlOpenID) "MySQL OpenID is a package to replace the in memory storage of discoveryCache and nonceStore."
openid-go-1.0.0/_example/ 0000775 0000000 0000000 00000000000 13472033373 0015170 5 ustar 00root root 0000000 0000000 openid-go-1.0.0/_example/index.html 0000664 0000000 0000000 00000000531 13472033373 0017164 0 ustar 00root root 0000000 0000000
go OpenID sample app
go OpenID sample app
{{if .user}}
Welcome {{.user}}.
Devs: set a cookie, or this kind of things to
identify the user across different pages.
openid-go-1.0.0/_example/server.go 0000664 0000000 0000000 00000003621 13472033373 0017027 0 ustar 00root root 0000000 0000000 package main
import (
"github.com/yohcop/openid-go"
"html/template"
"log"
"net/http"
)
const dataDir = "_example/"
// 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 = openid.NewSimpleDiscoveryCache()
func indexHandler(w http.ResponseWriter, r *http.Request) {
p := make(map[string]string)
if t, err := template.ParseFiles(dataDir + "index.html"); err == nil {
t.Execute(w, p)
} else {
log.Print(err)
}
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
p := make(map[string]string)
if t, err := template.ParseFiles(dataDir + "login.html"); err == nil {
t.Execute(w, p)
} else {
log.Print(err)
}
}
func discoverHandler(w http.ResponseWriter, r *http.Request) {
if url, err := openid.RedirectURL(r.FormValue("id"),
"http://localhost:8080/openidcallback",
"http://localhost:8080/"); err == nil {
http.Redirect(w, r, url, 303)
} else {
log.Print(err)
}
}
func callbackHandler(w http.ResponseWriter, r *http.Request) {
fullUrl := "http://localhost:8080" + r.URL.String()
log.Print(fullUrl)
id, err := openid.Verify(
fullUrl,
discoveryCache, nonceStore)
if err == nil {
p := make(map[string]string)
p["user"] = id
if t, err := template.ParseFiles(dataDir + "index.html"); err == nil {
t.Execute(w, p)
} else {
log.Println("WTF")
log.Print(err)
}
} else {
log.Println("WTF2")
log.Print(err)
}
}
func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/discover", discoverHandler)
http.HandleFunc("/openidcallback", callbackHandler)
http.ListenAndServe(":8080", nil)
}
openid-go-1.0.0/discover.go 0000664 0000000 0000000 00000004217 13472033373 0015547 0 ustar 00root root 0000000 0000000 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
}
openid-go-1.0.0/discover_test.go 0000664 0000000 0000000 00000003135 13472033373 0016604 0 ustar 00root root 0000000 0000000 package openid
import (
"testing"
)
func TestDiscoverWithYadis(t *testing.T) {
// They all redirect to the same XRDS document
expectOpIDErr(t, "example.com/xrds",
"foo", "bar", "", false)
expectOpIDErr(t, "http://example.com/xrds",
"foo", "bar", "", false)
expectOpIDErr(t, "http://example.com/xrds-loc",
"foo", "bar", "", false)
expectOpIDErr(t, "http://example.com/xrds-meta",
"foo", "bar", "", false)
}
func TestDiscoverWithHtml(t *testing.T) {
// Yadis discovery will fail, and fall back to html.
expectOpIDErr(t, "http://example.com/html",
"example.com/openid", "bar-name", "http://example.com/html",
false)
// The first url redirects to a different URL. The redirected-to
// url should be used as claimedID.
expectOpIDErr(t, "http://example.com/html-redirect",
"example.com/openid", "bar-name", "http://example.com/html",
false)
}
func TestDiscoverBadUrl(t *testing.T) {
expectOpIDErr(t, "http://example.com/404", "", "", "", true)
}
func expectOpIDErr(t *testing.T, uri, exOpEndpoint, exOpLocalID, exClaimedID string, exErr bool) {
opEndpoint, opLocalID, claimedID, err := testInstance.Discover(uri)
if (err != nil) != exErr {
t.Errorf("Unexpected error: '%s'", err)
} else {
if opEndpoint != exOpEndpoint {
t.Errorf("Extracted Endpoint does not match: Exepect %s, Got %s",
exOpEndpoint, opEndpoint)
}
if opLocalID != exOpLocalID {
t.Errorf("Extracted LocalId does not match: Exepect %s, Got %s",
exOpLocalID, opLocalID)
}
if claimedID != exClaimedID {
t.Errorf("Extracted ClaimedID does not match: Exepect %s, Got %s",
exClaimedID, claimedID)
}
}
}
openid-go-1.0.0/discovery_cache.go 0000664 0000000 0000000 00000002555 13472033373 0017066 0 ustar 00root root 0000000 0000000 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
}
openid-go-1.0.0/discovery_cache_test.go 0000664 0000000 0000000 00000001205 13472033373 0020114 0 ustar 00root root 0000000 0000000 package openid
import (
"testing"
)
func TestDiscoveryCache(t *testing.T) {
dc := NewSimpleDiscoveryCache()
// Put some initial values
dc.Put("foo", &SimpleDiscoveredInfo{opEndpoint: "a", opLocalID: "b", 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() != "a" || di.OpLocalID() != "b" || di.ClaimedID() != "c" {
t.Errorf("Expected a b c, 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)
}
}
openid-go-1.0.0/fake_getter_test.go 0000664 0000000 0000000 00000004647 13472033373 0017257 0 ustar 00root root 0000000 0000000 package openid
import (
"bufio"
"bytes"
"errors"
"net/http"
"net/url"
)
type fakeGetter struct {
urls map[string]string
redirects map[string]string
}
var testGetter = &fakeGetter{
make(map[string]string), make(map[string]string)}
var testInstance = &OpenID{urlGetter: testGetter}
func (f *fakeGetter) Get(uri string, headers map[string]string) (resp *http.Response, err error) {
key := uri
for k, v := range headers {
key += "#" + k + "#" + v
}
if doc, ok := f.urls[key]; ok {
request, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
return http.ReadResponse(bufio.NewReader(
bytes.NewBuffer([]byte(doc))), request)
}
if uri, ok := f.redirects[key]; ok {
return f.Get(uri, headers)
}
return nil, errors.New("404 not found")
}
func (f *fakeGetter) Post(uri string, form url.Values) (resp *http.Response, err error) {
return f.Get("POST@"+uri, nil)
}
func init() {
// Prepare (http#header#header-val --> http response) pairs.
// === For Yadis discovery ==================================
// Directly reffers a valid XRDS document
testGetter.urls["http://example.com/xrds#Accept#application/xrds+xml"] = `HTTP/1.0 200 OK
Content-Type: application/xrds+xml; charset=UTF-8
http://specs.openid.net/auth/2.0/signonfoobar`
// Uses a X-XRDS-Location header to redirect to the valid XRDS document.
testGetter.urls["http://example.com/xrds-loc#Accept#application/xrds+xml"] = `HTTP/1.0 200 OK
X-XRDS-Location: http://example.com/xrds
nothing interesting here`
// Html document, with meta tag X-XRDS-Location. Points to the
// previous valid XRDS document.
testGetter.urls["http://example.com/xrds-meta#Accept#application/xrds+xml"] = `HTTP/1.0 200 OK
Content-Type: text/html
`
// === For HTML discovery ===================================
testGetter.urls["http://example.com/html"] = `HTTP/1.0 200 OK
`
testGetter.redirects["http://example.com/html-redirect"] = "http://example.com/html"
}
openid-go-1.0.0/getter.go 0000664 0000000 0000000 00000001325 13472033373 0015220 0 ustar 00root root 0000000 0000000 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)
}
openid-go-1.0.0/go.mod 0000664 0000000 0000000 00000000150 13472033373 0014500 0 ustar 00root root 0000000 0000000 module github.com/yohcop/openid-go
go 1.3
require golang.org/x/net v0.0.0-20190522155817-f3200d17e092
openid-go-1.0.0/go.sum 0000664 0000000 0000000 00000000770 13472033373 0014535 0 ustar 00root root 0000000 0000000 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
openid-go-1.0.0/html_discovery.go 0000664 0000000 0000000 00000003701 13472033373 0016761 0 ustar 00root root 0000000 0000000 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 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.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 or a tokenizer error (no found).
// But just in case.
if len(opEndpoint) > 0 {
return
}
return "", "", errors.New("LINK rel=openid2.provider not found")
}
openid-go-1.0.0/html_discovery_test.go 0000664 0000000 0000000 00000002744 13472033373 0020026 0 ustar 00root root 0000000 0000000 package openid
import (
"bytes"
"testing"
)
func TestFindEndpointFromLink(t *testing.T) {
searchLink(t, `
`, "example.com/openid", "", false)
searchLink(t, `
`, "foo.com", "bar-name", false)
// Self-closing link
searchLink(t, `
`, "selfclose.com", "selfclose-name", false)
}
func TestNoEndpointFromLink(t *testing.T) {
searchLink(t, `
`, "", "", true)
// Outside of head.
searchLink(t, `
`, "", "", true)
}
func searchLink(t *testing.T, doc, opEndpoint, claimedID string, err bool) {
r := bytes.NewReader([]byte(doc))
op, id, e := findProviderFromHeadLink(r)
if (e != nil) != err {
t.Errorf("Unexpected error: '%s'", e)
} else if e == nil {
if op != opEndpoint {
t.Errorf("Found bad endpoint: Expected %s, Got %s",
op, opEndpoint)
}
if id != claimedID {
t.Errorf("Found bad id: Expected %s, Got %s",
id, claimedID)
}
}
}
openid-go-1.0.0/integration/ 0000775 0000000 0000000 00000000000 13472033373 0015721 5 ustar 00root root 0000000 0000000 openid-go-1.0.0/integration/discovery_test.go 0000664 0000000 0000000 00000002270 13472033373 0021317 0 ustar 00root root 0000000 0000000 package integration
// These tests fetch real data from google.com and other OpenID
// providers. If they change the files returned, or endpoints, or
// whatever, they will fail. It's ok though, they are full tests.
import (
. "github.com/yohcop/openid-go"
"testing"
)
func TestYahoo(t *testing.T) {
expectDiscovery(t, "https://me.yahoo.com",
"https://open.login.yahooapis.com/openid/op/auth",
"",
"")
}
func TestYohcop(t *testing.T) {
expectDiscovery(t, "http://yohcop.net",
"https://www.google.com/accounts/o8/ud?source=profiles",
"http://www.google.com/profiles/yohcop",
"http://yohcop.net/")
}
func TestSteam(t *testing.T) {
expectDiscovery(t, "http://steamcommunity.com/openid",
"https://steamcommunity.com/openid/login",
"",
"")
}
func expectDiscovery(t *testing.T, uri, expectOp, expectLocalId, expectClaimedId string) {
endpoint, localId, claimedId, err := Discover(uri)
if err != nil {
t.Errorf("Discovery failed")
}
if endpoint != expectOp {
t.Errorf("Unexpected endpoint: %s", endpoint)
}
if localId != expectLocalId {
t.Errorf("Unexpected localId: %s", localId)
}
if claimedId != expectClaimedId {
t.Errorf("Unexpected claimedId: %s", claimedId)
}
}
openid-go-1.0.0/integration/doc.go 0000664 0000000 0000000 00000000073 13472033373 0017015 0 ustar 00root root 0000000 0000000 package integration
// This package only contains a test.
openid-go-1.0.0/nonce_store.go 0000664 0000000 0000000 00000004413 13472033373 0016245 0 ustar 00root root 0000000 0000000 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: %.2fs", 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
}
openid-go-1.0.0/nonce_store_test.go 0000664 0000000 0000000 00000002327 13472033373 0017306 0 ustar 00root root 0000000 0000000 package openid
import (
"testing"
"time"
)
func TestDefaultNonceStore(t *testing.T) {
*maxNonceAge = 60 * time.Second
now := time.Now().UTC()
// 30 seconds ago
now30s := now.Add(-30 * time.Second)
// 2 minutes ago
now2m := now.Add(-2 * time.Minute)
now30sStr := now30s.Format(time.RFC3339)
now2mStr := now2m.Format(time.RFC3339)
ns := NewSimpleNonceStore()
reject(t, ns, "1", "foo") // invalid nonce
reject(t, ns, "1", "fooBarBazLongerThan20Chars") // invalid nonce
accept(t, ns, "1", now30sStr+"asd")
reject(t, ns, "1", now30sStr+"asd") // same nonce
accept(t, ns, "1", now30sStr+"xxx") // different nonce
reject(t, ns, "1", now30sStr+"xxx") // different nonce again to verify storage of multiple nonces per endpoint
accept(t, ns, "2", now30sStr+"asd") // different endpoint
reject(t, ns, "1", now2mStr+"old") // too old
reject(t, ns, "3", now2mStr+"old") // too old
}
func accept(t *testing.T, ns NonceStore, op, nonce string) {
e := ns.Accept(op, nonce)
if e != nil {
t.Errorf("Should accept %s nonce %s", op, nonce)
}
}
func reject(t *testing.T, ns NonceStore, op, nonce string) {
e := ns.Accept(op, nonce)
if e == nil {
t.Errorf("Should reject %s nonce %s", op, nonce)
}
}
openid-go-1.0.0/normalizer.go 0000664 0000000 0000000 00000003654 13472033373 0016117 0 ustar 00root root 0000000 0000000 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 == '!' || 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
}
openid-go-1.0.0/normalizer_test.go 0000664 0000000 0000000 00000004100 13472033373 0017141 0 ustar 00root root 0000000 0000000 package openid
import (
"testing"
)
func TestNormalize(t *testing.T) {
// OpenID 2.0 spec Appendix A.1. Normalization
doNormalize(t, "example.com", "http://example.com/", true)
doNormalize(t, "http://example.com", "http://example.com/", true)
doNormalize(t, "https://example.com/", "https://example.com/", true)
doNormalize(t, "http://example.com/user", "http://example.com/user", true)
doNormalize(t, "http://example.com/user/", "http://example.com/user/", true)
doNormalize(t, "http://example.com/", "http://example.com/", true)
doNormalize(t, "=example", "=example", false) // XRI not supported
doNormalize(t, "(=example)", "(=example)", false) // XRI not supported
doNormalize(t, "xri://=example", "=example", false) // XRI not supported
// Empty
doNormalize(t, "", "", false)
doNormalize(t, " ", "", false)
doNormalize(t, " ", "", false)
doNormalize(t, "xri://", "", false)
doNormalize(t, "http://", "", false)
doNormalize(t, "https://", "", false)
// Padded with spacing
doNormalize(t, " example.com ", "http://example.com/", true)
doNormalize(t, " http://example.com ", "http://example.com/", true)
// XRI not supported
doNormalize(t, "xri://asdf", "asdf", false)
doNormalize(t, "=asdf", "=asdf", false)
doNormalize(t, "@asdf", "@asdf", false)
// HTTP
doNormalize(t, "foo.com", "http://foo.com/", true)
doNormalize(t, "http://foo.com", "http://foo.com/", true)
doNormalize(t, "https://foo.com", "https://foo.com/", true)
// Fragment need to be removed
doNormalize(t, "http://foo.com#bar", "http://foo.com/", true)
doNormalize(t, "http://foo.com/page#bar", "http://foo.com/page", true)
}
func doNormalize(t *testing.T, idIn, idOut string, succeed bool) {
if id, err := Normalize(idIn); err != nil && succeed {
t.Errorf("unexpected normalize error: gave %v, expected %v, got %v - %v", idIn, idOut, id, err)
} else if err == nil && !succeed {
t.Errorf("unexpected normalize success: gave %v, expected %v, got %v", idIn, idOut, id)
} else if id != idOut {
t.Errorf("unexpected normalize result: gave %v, expected %v, got %v", idIn, idOut, id)
}
}
openid-go-1.0.0/openid.go 0000664 0000000 0000000 00000000366 13472033373 0015210 0 ustar 00root root 0000000 0000000 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)
openid-go-1.0.0/redirect.go 0000664 0000000 0000000 00000003572 13472033373 0015535 0 ustar 00root root 0000000 0000000 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
}
openid-go-1.0.0/redirect_test.go 0000664 0000000 0000000 00000006245 13472033373 0016574 0 ustar 00root root 0000000 0000000 package openid
import (
"net/url"
"testing"
)
func TestBuildRedirectUrl(t *testing.T) {
expectURL(t, "https://endpoint/a", "opLocalId", "claimedId", "returnTo", "realm",
"https://endpoint/a?"+
"openid.ns=http://specs.openid.net/auth/2.0"+
"&openid.mode=checkid_setup"+
"&openid.return_to=returnTo"+
"&openid.claimed_id=claimedId"+
"&openid.identity=opLocalId"+
"&openid.realm=realm")
// No realm.
expectURL(t, "https://endpoint/a", "opLocalId", "claimedId", "returnTo", "",
"https://endpoint/a?"+
"openid.ns=http://specs.openid.net/auth/2.0"+
"&openid.mode=checkid_setup"+
"&openid.return_to=returnTo"+
"&openid.claimed_id=claimedId"+
"&openid.identity=opLocalId")
// No realm, no localId
expectURL(t, "https://endpoint/a", "", "claimedId", "returnTo", "",
"https://endpoint/a?"+
"openid.ns=http://specs.openid.net/auth/2.0"+
"&openid.mode=checkid_setup"+
"&openid.return_to=returnTo"+
"&openid.claimed_id=claimedId"+
"&openid.identity=claimedId")
// No realm, no claimedId
expectURL(t, "https://endpoint/a", "opLocalId", "", "returnTo", "",
"https://endpoint/a?"+
"openid.ns=http://specs.openid.net/auth/2.0"+
"&openid.mode=checkid_setup"+
"&openid.return_to=returnTo"+
"&openid.claimed_id="+
"http://specs.openid.net/auth/2.0/identifier_select"+
"&openid.identity="+
"http://specs.openid.net/auth/2.0/identifier_select")
}
func expectURL(t *testing.T, opEndpoint, opLocalID, claimedID, returnTo, realm, expected string) {
url, err := BuildRedirectURL(opEndpoint, opLocalID, claimedID, returnTo, realm)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
compareUrls(t, url, expected)
}
func TestRedirectWithDiscovery(t *testing.T) {
expected := "foo?" +
"openid.ns=http://specs.openid.net/auth/2.0" +
"&openid.mode=checkid_setup" +
"&openid.return_to=mysite/cb" +
"&openid.claimed_id=" +
"http://specs.openid.net/auth/2.0/identifier_select" +
"&openid.identity=" +
"http://specs.openid.net/auth/2.0/identifier_select"
// They all redirect to the same XRDS document
expectRedirect(t, "http://example.com/xrds",
"mysite/cb", "", expected, false)
expectRedirect(t, "http://example.com/xrds-loc",
"mysite/cb", "", expected, false)
expectRedirect(t, "http://example.com/xrds-meta",
"mysite/cb", "", expected, false)
}
func expectRedirect(t *testing.T, uri, callback, realm, exRedirect string, exErr bool) {
redirect, err := testInstance.RedirectURL(uri, callback, realm)
if (err != nil) != exErr {
t.Errorf("Unexpected error: '%s'", err)
return
}
compareUrls(t, redirect, exRedirect)
}
func compareUrls(t *testing.T, url1, expected string) {
p1, err1 := url.Parse(url1)
p2, err2 := url.Parse(expected)
if err1 != nil {
t.Errorf("Url1 non parsable: %s", err1)
return
}
if err2 != nil {
t.Errorf("ExpectedUrl non parsable: %s", err2)
return
}
if p1.Scheme != p2.Scheme ||
p1.Host != p2.Host ||
p1.Path != p2.Path {
t.Errorf("URLs don't match: %s vs %s", url1, expected)
}
q1, _ := url.ParseQuery(p1.RawQuery)
q2, _ := url.ParseQuery(p2.RawQuery)
if err := compareQueryParams(q1, q2); err != nil {
t.Errorf("URLs query params don't match: %s: %s vs %s", err, url1, expected)
}
}
openid-go-1.0.0/verify.go 0000664 0000000 0000000 00000017623 13472033373 0015242 0 ustar 00root root 0000000 0000000 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")
}
openid-go-1.0.0/verify_test.go 0000664 0000000 0000000 00000011036 13472033373 0016271 0 ustar 00root root 0000000 0000000 package openid
import (
"net/url"
"testing"
"time"
)
func TestVerifyNonce(t *testing.T) {
timeStr := time.Now().UTC().Format(time.RFC3339)
ns := NewSimpleNonceStore()
v := url.Values{}
// Initial values
v.Set("openid.op_endpoint", "1")
v.Set("openid.response_nonce", timeStr+"foo")
if err := verifyNonce(v, ns); err != nil {
t.Errorf("verifyNonce failed unexpectedly: %v", err)
}
// Different nonce
v.Set("openid.response_nonce", timeStr+"bar")
if err := verifyNonce(v, ns); err != nil {
t.Errorf("verifyNonce failed unexpectedly: %v", err)
}
// Different endpoint
v.Set("openid.op_endpoint", "2")
if err := verifyNonce(v, ns); err != nil {
t.Errorf("verifyNonce failed unexpectedly: %v", err)
}
}
func TestVerifySignedFields(t *testing.T) {
// No claimed_id/identity, properly signed
doVerifySignedFields(t,
url.Values{"openid.signed": []string{"signed,op_endpoint,return_to,response_nonce,assoc_handle"}},
true)
// Everything properly signed, even empty claimed_id/identity
doVerifySignedFields(t,
url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle"}},
true)
// With claimed_id/identity, properly signed
doVerifySignedFields(t,
url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle"},
"openid.claimed_id": []string{"foo"},
"openid.identity": []string{"foo"}},
true)
// With claimed_id/identity, but those two not signed
doVerifySignedFields(t,
url.Values{"openid.signed": []string{"signed,op_endpoint,return_to,response_nonce,assoc_handle"},
"openid.claimed_id": []string{"foo"},
"openid.identity": []string{"foo"}},
false)
// Missing signature for op_endpoint
doVerifySignedFields(t,
url.Values{"openid.signed": []string{"signed,claimed_id,identity,return_to,response_nonce,assoc_handle"},
"openid.claimed_id": []string{"foo"},
"openid.identity": []string{"foo"}},
false)
// Missing signature for return_to
doVerifySignedFields(t,
url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,response_nonce,assoc_handle"},
"openid.claimed_id": []string{"foo"},
"openid.identity": []string{"foo"}},
false)
// Missing signature for response_nonce
doVerifySignedFields(t,
url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,return_to,assoc_handle"},
"openid.claimed_id": []string{"foo"},
"openid.identity": []string{"foo"}},
false)
// Missing signature for assoc_handle
doVerifySignedFields(t,
url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,return_to,response_nonce"},
"openid.claimed_id": []string{"foo"},
"openid.identity": []string{"foo"}},
false)
}
func doVerifySignedFields(t *testing.T, v url.Values, succeed bool) {
if err := verifySignedFields(v); err == nil && !succeed {
t.Errorf("verifySignedFields succeeded unexpectedly: %v - %v", v, err)
} else if err != nil && succeed {
t.Errorf("verifySignedFields failed unexpectedly: %v - %v", v, err)
}
}
func TestVerifyDiscovered(t *testing.T) {
dc := NewSimpleDiscoveryCache()
vals := url.Values{"openid.ns": []string{"http://specs.openid.net/auth/2.0"},
"openid.mode": []string{"id_res"},
"openid.op_endpoint": []string{"http://example.com/openid/login"},
"openid.claimed_id": []string{"http://example.com/openid/id/foo"},
"openid.identity": []string{"http://example.com/openid/id/foo"}}
// Make sure we fail with no discovery handler
if err := testInstance.verifyDiscovered(nil, vals, dc); err == nil {
t.Errorf("verifyDiscovered succeeded unexpectedly with no discovery")
}
// Add the discovery handler
testGetter.urls["http://example.com/openid/id/foo#Accept#application/xrds+xml"] = `HTTP/1.0 200 OK
Content-Type: application/xrds+xml; charset=UTF-8
http://specs.openid.net/auth/2.0/signonhttp://example.com/openid/login`
// Make sure we succeed now
if err := testInstance.verifyDiscovered(nil, vals, dc); err != nil {
t.Errorf("verifyDiscovered failed unexpectedly: %v", err)
}
// Remove the discovery handler
delete(testGetter.urls, "http://example.com/openid/id/foo#Accept#application/xrds+xml")
// Make sure we still succeed thanks to the discovery cache
if err := testInstance.verifyDiscovered(nil, vals, dc); err != nil {
t.Errorf("verifyDiscovered failed unexpectedly: %v", err)
}
}
openid-go-1.0.0/xrds.go 0000664 0000000 0000000 00000004614 13472033373 0014712 0 ustar 00root root 0000000 0000000 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 element with the
// following information:
// An tag whose text content is
// "http://specs.openid.net/auth/2.0/server".
// An 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 element
// with the following information:
// An tag whose text content is
// "http://specs.openid.net/auth/2.0/signon".
// An tag whose text content is the OP Endpoint
// URL.
// An 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
}
openid-go-1.0.0/xrds_test.go 0000664 0000000 0000000 00000005127 13472033373 0015751 0 ustar 00root root 0000000 0000000 package openid
import (
"testing"
)
func TestXrds(t *testing.T) {
testExpectOpID(t, []byte(`
http://openid.net/signon/1.0http://www.myopenid.com/serverhttp://smoker.myopenid.com/http://openid.net/signon/1.0http://www.livejournal.com/openid/server.bml
http://www.livejournal.com/users/frank/
http://lid.netmesh.org/sso/2.0http://specs.openid.net/auth/2.0/serverfoo
`), "foo", "")
testExpectOpID(t, []byte(`
http://specs.openid.net/auth/2.0/signonhttps://www.exampleprovider.com/endpoint/https://exampleuser.exampleprovider.com/
`),
"https://www.exampleprovider.com/endpoint/",
"https://exampleuser.exampleprovider.com/")
// OP Identifier Element has priority over Claimed Identifier Element
testExpectOpID(t, []byte(`
http://specs.openid.net/auth/2.0/signonhttps://www.exampleprovider.com/endpoint-signon/http://specs.openid.net/auth/2.0/serverhttps://www.exampleprovider.com/endpoint-server/
`),
"https://www.exampleprovider.com/endpoint-server/",
"")
}
func testExpectOpID(t *testing.T, xrds []byte, op, id string) {
receivedOp, receivedID, err := parseXrds(xrds)
if err != nil {
t.Errorf("Got an error parsing XRDS (%s): %s", string(xrds), err)
} else {
if receivedOp != op {
t.Errorf("Extracted OP does not match: Exepect %s, Got %s",
op, receivedOp)
}
if receivedID != id {
t.Errorf("Extracted ID does not match: Exepect %s, Got %s",
id, receivedID)
}
}
}
openid-go-1.0.0/yadis_discovery.go 0000664 0000000 0000000 00000006566 13472033373 0017142 0 ustar 00root root 0000000 0000000 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 element that includes a
// 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
//
//
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")
}
openid-go-1.0.0/yadis_discovery_test.go 0000664 0000000 0000000 00000002243 13472033373 0020165 0 ustar 00root root 0000000 0000000 package openid
import (
"bytes"
"testing"
)
func TestFindMetaXrdsLocation(t *testing.T) {
searchMeta(t, `
`, "foo.com", false)
searchMeta(t, `
`, "foo.com", false)
}
func TestMetaXrdsLocationOutsideHead(t *testing.T) {
searchMeta(t, `
`, "", true)
searchMeta(t, `
`, "", true)
}
func TestNoMetaXrdsLocation(t *testing.T) {
searchMeta(t, `
`, "", true)
}
func searchMeta(t *testing.T, doc, loc string, err bool) {
r := bytes.NewReader([]byte(doc))
res, e := findMetaXrdsLocation(r)
if (e != nil) != err {
t.Errorf("Unexpected error: '%s'", e)
} else if e == nil {
if res != loc {
t.Errorf("Found bad location: Expected %s, Got %s", loc, res)
}
}
}