├── .github
└── workflows
│ └── go.yaml
├── .travis.yml
├── LICENSE
├── README.md
├── _example
├── index.html
├── login.html
└── server.go
├── discover.go
├── discover_test.go
├── discovery_cache.go
├── discovery_cache_test.go
├── fake_getter_test.go
├── getter.go
├── go.mod
├── go.sum
├── html_discovery.go
├── html_discovery_test.go
├── integration
├── discovery_test.go
└── doc.go
├── nonce_store.go
├── nonce_store_test.go
├── normalizer.go
├── normalizer_test.go
├── openid.go
├── redirect.go
├── redirect_test.go
├── verify.go
├── verify_test.go
├── xrds.go
├── xrds_test.go
├── yadis_discovery.go
└── yadis_discovery_test.go
/.github/workflows/go.yaml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ $default-branch ]
6 | pull_request:
7 | branches: [ $default-branch ]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v3
18 | with:
19 | go-version: 1.19
20 |
21 | - name: Build
22 | run: go build -v ./...
23 |
24 | - name: Test
25 | run: go test -v .
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: go
4 |
5 | go:
6 | - 1.3.x
7 | - 1.4.x
8 | - 1.5.x
9 | - 1.6.x
10 | - 1.7.x
11 | - 1.8.x
12 | - 1.9.x
13 | - 1.10.x
14 | - 1.11.x
15 | - 1.12.x
16 |
17 | env:
18 | - GO111MODULE=on
19 |
20 | # Get deps, build, test, and ensure the code is gofmt'ed.
21 | script:
22 | - go test -v ./...
23 | - diff <(gofmt -d .) <("")
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015 Yohann Coppel
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # openid.go
2 |
3 | This is a consumer (Relying party) implementation of OpenId 2.0,
4 | written in Go.
5 |
6 | go get -u github.com/yohcop/openid-go
7 |
8 | [](https://travis-ci.org/yohcop/openid-go)
9 |
10 | ## Github
11 |
12 | Be awesome! Feel free to clone and use according to the licence.
13 | If you make a useful change that can benefit others, send a
14 | pull request! This ensures that one version has all the good stuff
15 | and doesn't fall behind.
16 |
17 | ## Code example
18 |
19 | See `_example/` for a simple webserver using the openID
20 | implementation. Also, read the comment about the NonceStore towards
21 | the top of that file. The example must be run for the openid-go
22 | directory, like so:
23 |
24 | go run _example/server.go
25 |
26 | ## App Engine
27 |
28 | 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/).
29 |
30 | ```go
31 | oid := openid.NewOpenID(urlfetch.Client(appengine.NewContext(r)))
32 | oid.RedirectURL(...)
33 | oid.Verify(...)
34 | ```
35 |
36 | ## License
37 |
38 | Distributed under the [Apache v2.0 license](http://www.apache.org/licenses/LICENSE-2.0.html).
39 |
40 | ## Libraries
41 |
42 | 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:
43 |
44 | - [Gacnt/myopenid](https://github.com/Gacnt/myopenid) "A Yohcop-Openid Nonce/Discovery storage replacement", using MySQL.
45 | - [Gacnt/sqlxid](https://github.com/Gacnt/sqlxid) "An SQLX Adapter for Nonce / Discovery Cache store"
46 | - [Gacnt/gormid](https://github.com/Gacnt/gormid) "Use GORM (Go Object Relational Mapping) to store OpenID DiscoveryCache / Nonce in a database"
47 | - [hectorj/mysqlOpenID](https://github.com/hectorj/mysqlOpenID) "MySQL OpenID is a package to replace the in memory storage of discoveryCache and nonceStore."
48 |
--------------------------------------------------------------------------------
/_example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go OpenID sample app
4 |
5 |
6 | go OpenID sample app
7 |
8 | {{if .user}}
9 | Welcome {{.user}}.
10 | Devs: set a cookie, or this kind of things to
11 | identify the user across different pages.
12 | {{else}}
13 | Login
14 | {{end}}
15 |
16 |
17 |
--------------------------------------------------------------------------------
/_example/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go OpenID sample app
4 |
11 |
12 |
13 | Login
14 |
15 |
18 |
19 | Sign in with:
20 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/_example/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/yohcop/openid-go"
5 | "html/template"
6 | "log"
7 | "net/http"
8 | )
9 |
10 | const dataDir = "_example/"
11 |
12 | // For the demo, we use in-memory infinite storage nonce and discovery
13 | // cache. In your app, do not use this as it will eat up memory and never
14 | // free it. Use your own implementation, on a better database system.
15 | // If you have multiple servers for example, you may need to share at least
16 | // the nonceStore between them.
17 | var nonceStore = openid.NewSimpleNonceStore()
18 | var discoveryCache = openid.NewSimpleDiscoveryCache()
19 |
20 | func indexHandler(w http.ResponseWriter, r *http.Request) {
21 | p := make(map[string]string)
22 | if t, err := template.ParseFiles(dataDir + "index.html"); err == nil {
23 | t.Execute(w, p)
24 | } else {
25 | log.Print(err)
26 | }
27 | }
28 |
29 | func loginHandler(w http.ResponseWriter, r *http.Request) {
30 | p := make(map[string]string)
31 | if t, err := template.ParseFiles(dataDir + "login.html"); err == nil {
32 | t.Execute(w, p)
33 | } else {
34 | log.Print(err)
35 | }
36 | }
37 |
38 | func discoverHandler(w http.ResponseWriter, r *http.Request) {
39 | if url, err := openid.RedirectURL(r.FormValue("id"),
40 | "http://localhost:8080/openidcallback",
41 | "http://localhost:8080/"); err == nil {
42 | http.Redirect(w, r, url, 303)
43 | } else {
44 | log.Print(err)
45 | }
46 | }
47 |
48 | func callbackHandler(w http.ResponseWriter, r *http.Request) {
49 | fullUrl := "http://localhost:8080" + r.URL.String()
50 | log.Print(fullUrl)
51 | id, err := openid.Verify(
52 | fullUrl,
53 | discoveryCache, nonceStore)
54 | if err == nil {
55 | p := make(map[string]string)
56 | p["user"] = id
57 | if t, err := template.ParseFiles(dataDir + "index.html"); err == nil {
58 | t.Execute(w, p)
59 | } else {
60 | log.Println("WTF")
61 | log.Print(err)
62 | }
63 | } else {
64 | log.Println("WTF2")
65 | log.Print(err)
66 | }
67 | }
68 |
69 | func main() {
70 | http.HandleFunc("/", indexHandler)
71 | http.HandleFunc("/login", loginHandler)
72 | http.HandleFunc("/discover", discoverHandler)
73 | http.HandleFunc("/openidcallback", callbackHandler)
74 | http.ListenAndServe(":8080", nil)
75 | }
76 |
--------------------------------------------------------------------------------
/discover.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | // 7.3.1. Discovered Information
4 | // Upon successful completion of discovery, the Relying Party will
5 | // have one or more sets of the following information (see the
6 | // Terminology section for definitions). If more than one set of the
7 | // following information has been discovered, the precedence rules
8 | // defined in [XRI_Resolution_2.0] are to be applied.
9 | // - OP Endpoint URL
10 | // - Protocol Version
11 | // If the end user did not enter an OP Identifier, the following
12 | // information will also be present:
13 | // - Claimed Identifier
14 | // - OP-Local Identifier
15 | // If the end user entered an OP Identifier, there is no Claimed
16 | // Identifier. For the purposes of making OpenID Authentication
17 | // requests, the value
18 | // "http://specs.openid.net/auth/2.0/identifier_select" MUST be
19 | // used as both the Claimed Identifier and the OP-Local Identifier
20 | // when an OP Identifier is entered.
21 | func Discover(id string) (opEndpoint, opLocalID, claimedID string, err error) {
22 | return defaultInstance.Discover(id)
23 | }
24 |
25 | func (oid *OpenID) Discover(id string) (opEndpoint, opLocalID, claimedID string, err error) {
26 | // From OpenID specs, 7.2: Normalization
27 | if id, err = Normalize(id); err != nil {
28 | return
29 | }
30 |
31 | // From OpenID specs, 7.3: Discovery.
32 |
33 | // If the identifier is an XRI, [XRI_Resolution_2.0] will yield an
34 | // XRDS document that contains the necessary information. It
35 | // should also be noted that Relying Parties can take advantage of
36 | // XRI Proxy Resolvers, such as the one provided by XDI.org at
37 | // http://www.xri.net. This will remove the need for the RPs to
38 | // perform XRI Resolution locally.
39 |
40 | // XRI not supported.
41 |
42 | // If it is a URL, the Yadis protocol [Yadis] SHALL be first
43 | // attempted. If it succeeds, the result is again an XRDS
44 | // document.
45 | if opEndpoint, opLocalID, err = yadisDiscovery(id, oid.urlGetter); err != nil {
46 | // If the Yadis protocol fails and no valid XRDS document is
47 | // retrieved, or no Service Elements are found in the XRDS
48 | // document, the URL is retrieved and HTML-Based discovery SHALL be
49 | // attempted.
50 | opEndpoint, opLocalID, claimedID, err = htmlDiscovery(id, oid.urlGetter)
51 | }
52 |
53 | if err != nil {
54 | return "", "", "", err
55 | }
56 | return
57 | }
58 |
--------------------------------------------------------------------------------
/discover_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestDiscoverWithYadis(t *testing.T) {
8 | // They all redirect to the same XRDS document
9 | expectOpIDErr(t, "example.com/xrds",
10 | "foo", "bar", "", false)
11 | expectOpIDErr(t, "http://example.com/xrds",
12 | "foo", "bar", "", false)
13 | expectOpIDErr(t, "http://example.com/xrds-loc",
14 | "foo", "bar", "", false)
15 | expectOpIDErr(t, "http://example.com/xrds-meta",
16 | "foo", "bar", "", false)
17 | }
18 |
19 | func TestDiscoverWithHtml(t *testing.T) {
20 | // Yadis discovery will fail, and fall back to html.
21 | expectOpIDErr(t, "http://example.com/html",
22 | "example.com/openid", "bar-name", "http://example.com/html",
23 | false)
24 | // The first url redirects to a different URL. The redirected-to
25 | // url should be used as claimedID.
26 | expectOpIDErr(t, "http://example.com/html-redirect",
27 | "example.com/openid", "bar-name", "http://example.com/html",
28 | false)
29 |
30 | expectOpIDErr(t, "http://example.com/html-multi-rel",
31 | "http://www.livejournal.com/openid/server.bml",
32 | "http://exampleuser.livejournal.com/",
33 | "http://example.com/html-multi-rel",
34 | false)
35 | }
36 |
37 | func TestDiscoverBadUrl(t *testing.T) {
38 | expectOpIDErr(t, "http://example.com/404", "", "", "", true)
39 | }
40 |
41 | func expectOpIDErr(t *testing.T, uri, exOpEndpoint, exOpLocalID, exClaimedID string, exErr bool) {
42 | opEndpoint, opLocalID, claimedID, err := testInstance.Discover(uri)
43 | if (err != nil) != exErr {
44 | t.Errorf("Unexpected error: '%s'", err)
45 | } else {
46 | if opEndpoint != exOpEndpoint {
47 | t.Errorf("Extracted Endpoint does not match: Exepect %s, Got %s",
48 | exOpEndpoint, opEndpoint)
49 | }
50 | if opLocalID != exOpLocalID {
51 | t.Errorf("Extracted LocalId does not match: Exepect %s, Got %s",
52 | exOpLocalID, opLocalID)
53 | }
54 | if claimedID != exClaimedID {
55 | t.Errorf("Extracted ClaimedID does not match: Exepect %s, Got %s",
56 | exClaimedID, claimedID)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/discovery_cache.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type DiscoveredInfo interface {
8 | OpEndpoint() string
9 | OpLocalID() string
10 | ClaimedID() string
11 | // ProtocolVersion: it's always openId 2.
12 | }
13 |
14 | type DiscoveryCache interface {
15 | Put(id string, info DiscoveredInfo)
16 | // Return a discovered info, or nil.
17 | Get(id string) DiscoveredInfo
18 | }
19 |
20 | type SimpleDiscoveredInfo struct {
21 | opEndpoint string
22 | opLocalID string
23 | claimedID string
24 | }
25 |
26 | func (s *SimpleDiscoveredInfo) OpEndpoint() string {
27 | return s.opEndpoint
28 | }
29 |
30 | func (s *SimpleDiscoveredInfo) OpLocalID() string {
31 | return s.opLocalID
32 | }
33 |
34 | func (s *SimpleDiscoveredInfo) ClaimedID() string {
35 | return s.claimedID
36 | }
37 |
38 | type SimpleDiscoveryCache struct {
39 | cache map[string]DiscoveredInfo
40 | mutex *sync.Mutex
41 | }
42 |
43 | func NewSimpleDiscoveryCache() *SimpleDiscoveryCache {
44 | return &SimpleDiscoveryCache{cache: map[string]DiscoveredInfo{}, mutex: &sync.Mutex{}}
45 | }
46 |
47 | func (s *SimpleDiscoveryCache) Put(id string, info DiscoveredInfo) {
48 | s.mutex.Lock()
49 | defer s.mutex.Unlock()
50 |
51 | s.cache[id] = info
52 | }
53 |
54 | func (s *SimpleDiscoveryCache) Get(id string) DiscoveredInfo {
55 | s.mutex.Lock()
56 | defer s.mutex.Unlock()
57 |
58 | if info, has := s.cache[id]; has {
59 | return info
60 | }
61 | return nil
62 | }
63 |
64 | func compareDiscoveredInfo(a DiscoveredInfo, opEndpoint, opLocalID, claimedID string) bool {
65 | return a != nil &&
66 | a.OpEndpoint() == opEndpoint &&
67 | a.OpLocalID() == opLocalID &&
68 | a.ClaimedID() == claimedID
69 | }
70 |
--------------------------------------------------------------------------------
/discovery_cache_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestDiscoveryCache(t *testing.T) {
8 | dc := NewSimpleDiscoveryCache()
9 |
10 | // Put some initial values
11 | dc.Put("foo", &SimpleDiscoveredInfo{opEndpoint: "a", opLocalID: "b", claimedID: "c"})
12 |
13 | // Make sure we can retrieve them
14 | if di := dc.Get("foo"); di == nil {
15 | t.Errorf("Expected a result, got nil")
16 | } else if di.OpEndpoint() != "a" || di.OpLocalID() != "b" || di.ClaimedID() != "c" {
17 | t.Errorf("Expected a b c, got %v %v %v", di.OpEndpoint(), di.OpLocalID(), di.ClaimedID())
18 | }
19 |
20 | // Attempt to get a non-existent value
21 | if di := dc.Get("bar"); di != nil {
22 | t.Errorf("Expected nil, got %v", di)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/fake_getter_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "errors"
7 | "net/http"
8 | "net/url"
9 | )
10 |
11 | type fakeGetter struct {
12 | urls map[string]string
13 | redirects map[string]string
14 | }
15 |
16 | var testGetter = &fakeGetter{
17 | make(map[string]string), make(map[string]string)}
18 |
19 | var testInstance = &OpenID{urlGetter: testGetter}
20 |
21 | func (f *fakeGetter) Get(uri string, headers map[string]string) (resp *http.Response, err error) {
22 | key := uri
23 | for k, v := range headers {
24 | key += "#" + k + "#" + v
25 | }
26 |
27 | if doc, ok := f.urls[key]; ok {
28 | request, err := http.NewRequest("GET", uri, nil)
29 | if err != nil {
30 | return nil, err
31 | }
32 |
33 | return http.ReadResponse(bufio.NewReader(
34 | bytes.NewBuffer([]byte(doc))), request)
35 | }
36 | if uri, ok := f.redirects[key]; ok {
37 | return f.Get(uri, headers)
38 | }
39 |
40 | return nil, errors.New("404 not found")
41 | }
42 |
43 | func (f *fakeGetter) Post(uri string, form url.Values) (resp *http.Response, err error) {
44 | return f.Get("POST@"+uri, nil)
45 | }
46 |
47 | func init() {
48 | // Prepare (http#header#header-val --> http response) pairs.
49 |
50 | // === For Yadis discovery ==================================
51 | // Directly reffers a valid XRDS document
52 | testGetter.urls["http://example.com/xrds#Accept#application/xrds+xml"] = `HTTP/1.0 200 OK
53 | Content-Type: application/xrds+xml; charset=UTF-8
54 |
55 |
56 |
58 |
59 |
60 | http://specs.openid.net/auth/2.0/signon
61 | foo
62 | bar
63 |
64 |
65 | `
66 |
67 | // Uses a X-XRDS-Location header to redirect to the valid XRDS document.
68 | testGetter.urls["http://example.com/xrds-loc#Accept#application/xrds+xml"] = `HTTP/1.0 200 OK
69 | X-XRDS-Location: http://example.com/xrds
70 |
71 | nothing interesting here`
72 |
73 | // Html document, with meta tag X-XRDS-Location. Points to the
74 | // previous valid XRDS document.
75 | testGetter.urls["http://example.com/xrds-meta#Accept#application/xrds+xml"] = `HTTP/1.0 200 OK
76 | Content-Type: text/html
77 |
78 |
79 |
80 | `
81 |
82 | // === For HTML discovery ===================================
83 | testGetter.urls["http://example.com/html"] = `HTTP/1.0 200 OK
84 |
85 |
86 |
87 |
88 | `
89 |
90 | testGetter.redirects["http://example.com/html-redirect"] = "http://example.com/html"
91 |
92 | testGetter.urls["http://example.com/html-multi-rel"] = `HTTP/1.0 200 OK
93 | Content-Type: text/html
94 |
95 |
96 |
97 |
99 | `
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/getter.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 | )
7 |
8 | // Interface that simplifies testing.
9 | type httpGetter interface {
10 | Get(uri string, headers map[string]string) (resp *http.Response, err error)
11 | Post(uri string, form url.Values) (resp *http.Response, err error)
12 | }
13 |
14 | type defaultGetter struct {
15 | client *http.Client
16 | }
17 |
18 | func (dg *defaultGetter) Get(uri string, headers map[string]string) (resp *http.Response, err error) {
19 | request, err := http.NewRequest("GET", uri, nil)
20 | if err != nil {
21 | return
22 | }
23 | for h, v := range headers {
24 | request.Header.Add(h, v)
25 | }
26 | return dg.client.Do(request)
27 | }
28 |
29 | func (dg *defaultGetter) Post(uri string, form url.Values) (resp *http.Response, err error) {
30 | return dg.client.PostForm(uri, form)
31 | }
32 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/yohcop/openid-go
2 |
3 | go 1.3
4 |
5 | require golang.org/x/net v0.7.0
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
3 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
4 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
5 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
6 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
7 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
8 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
9 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
10 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
11 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
13 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
14 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
16 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
17 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
18 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
19 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
20 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
21 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
22 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
23 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
24 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
25 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
26 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
27 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
28 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
29 |
--------------------------------------------------------------------------------
/html_discovery.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "strings"
7 |
8 | "golang.org/x/net/html"
9 | )
10 |
11 | func htmlDiscovery(id string, getter httpGetter) (opEndpoint, opLocalID, claimedID string, err error) {
12 | resp, err := getter.Get(id, nil)
13 | if err != nil {
14 | return "", "", "", err
15 | }
16 | opEndpoint, opLocalID, err = findProviderFromHeadLink(resp.Body)
17 | return opEndpoint, opLocalID, resp.Request.URL.String(), err
18 | }
19 |
20 | func findProviderFromHeadLink(input io.Reader) (opEndpoint, opLocalID string, err error) {
21 | tokenizer := html.NewTokenizer(input)
22 | inHead := false
23 | for {
24 | tt := tokenizer.Next()
25 | switch tt {
26 | case html.ErrorToken:
27 | // Even if the document is malformed after we found a
28 | // valid tag, ignore and let's be happy with our
29 | // openid2.provider and potentially openid2.local_id as well.
30 | if len(opEndpoint) > 0 {
31 | return
32 | }
33 | return "", "", tokenizer.Err()
34 | case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
35 | tk := tokenizer.Token()
36 | if tk.Data == "head" {
37 | if tt == html.StartTagToken {
38 | inHead = true
39 | } else {
40 | if len(opEndpoint) > 0 {
41 | return
42 | }
43 | return "", "", errors.New(
44 | "LINK with rel=openid2.provider not found")
45 | }
46 | } else if inHead && tk.Data == "link" {
47 | provider := false
48 | localID := false
49 | href := ""
50 | for _, attr := range tk.Attr {
51 | if attr.Key == "rel" {
52 | // There could be multiple values in the rel= attribute,
53 | // space-separated (it seems to be for OpenID1 vs 2.)
54 | // See example in Appendix A.4. HTML Identifier Markup.
55 | vals := strings.Split(attr.Val, " ")
56 | for _, val := range vals {
57 | if val == "openid2.provider" {
58 | provider = true
59 | break
60 | } else if val == "openid2.local_id" {
61 | localID = true
62 | break
63 | }
64 | }
65 | } else if attr.Key == "href" {
66 | href = attr.Val
67 | }
68 | }
69 | if provider && !localID && len(href) > 0 {
70 | opEndpoint = href
71 | } else if !provider && localID && len(href) > 0 {
72 | opLocalID = href
73 | }
74 | }
75 | }
76 | }
77 | // At this point we should probably have returned either from
78 | // a closing or a tokenizer error (no found).
79 | // But just in case.
80 | if len(opEndpoint) > 0 {
81 | return
82 | }
83 | return "", "", errors.New("LINK rel=openid2.provider not found")
84 | }
85 |
--------------------------------------------------------------------------------
/html_discovery_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestFindEndpointFromLink(t *testing.T) {
9 | searchLink(t, `
10 |
11 |
12 |
13 | `, "example.com/openid", "", false)
14 | searchLink(t, `
15 |
16 |
17 |
18 |
19 |
20 |
21 | `, "foo.com", "bar-name", false)
22 | // Self-closing link
23 | searchLink(t, `
24 |
25 |
26 |
27 |
28 |
29 |
30 | `, "selfclose.com", "selfclose-name", false)
31 | }
32 |
33 | func TestNoEndpointFromLink(t *testing.T) {
34 | searchLink(t, `
35 |
36 |
37 |
38 | `, "", "", true)
39 | // Outside of head.
40 | searchLink(t, `
41 |
42 |
43 |
44 | `, "", "", true)
45 | }
46 |
47 | func searchLink(t *testing.T, doc, opEndpoint, claimedID string, err bool) {
48 | r := bytes.NewReader([]byte(doc))
49 | op, id, e := findProviderFromHeadLink(r)
50 | if (e != nil) != err {
51 | t.Errorf("Unexpected error: '%s'", e)
52 | } else if e == nil {
53 | if op != opEndpoint {
54 | t.Errorf("Found bad endpoint: Expected %s, Got %s",
55 | op, opEndpoint)
56 | }
57 | if id != claimedID {
58 | t.Errorf("Found bad id: Expected %s, Got %s",
59 | id, claimedID)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/integration/discovery_test.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | // These tests fetch real data from google.com and other OpenID
4 | // providers. If they change the files returned, or endpoints, or
5 | // whatever, they will fail. It's ok though, they are full tests.
6 |
7 | import (
8 | . "github.com/yohcop/openid-go"
9 | "testing"
10 | )
11 |
12 | func TestYahoo(t *testing.T) {
13 | expectDiscovery(t, "https://me.yahoo.com",
14 | "https://open.login.yahooapis.com/openid/op/auth",
15 | "",
16 | "")
17 | }
18 |
19 | func TestYohcop(t *testing.T) {
20 | expectDiscovery(t, "http://yohcop.net",
21 | "https://www.google.com/accounts/o8/ud?source=profiles",
22 | "http://www.google.com/profiles/yohcop",
23 | "http://yohcop.net/")
24 | }
25 |
26 | func TestSteam(t *testing.T) {
27 | expectDiscovery(t, "http://steamcommunity.com/openid",
28 | "https://steamcommunity.com/openid/login",
29 | "",
30 | "")
31 | }
32 |
33 | func expectDiscovery(t *testing.T, uri, expectOp, expectLocalId, expectClaimedId string) {
34 | endpoint, localId, claimedId, err := Discover(uri)
35 | if err != nil {
36 | t.Errorf("Discovery failed")
37 | }
38 | if endpoint != expectOp {
39 | t.Errorf("Unexpected endpoint: %s", endpoint)
40 | }
41 | if localId != expectLocalId {
42 | t.Errorf("Unexpected localId: %s", localId)
43 | }
44 | if claimedId != expectClaimedId {
45 | t.Errorf("Unexpected claimedId: %s", claimedId)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/integration/doc.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | // This package only contains a test.
4 |
--------------------------------------------------------------------------------
/nonce_store.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "sync"
8 | "time"
9 | )
10 |
11 | var maxNonceAge = flag.Duration("openid-max-nonce-age",
12 | 60*time.Second,
13 | "Maximum accepted age for openid nonces. The bigger, the more"+
14 | "memory is needed to store used nonces.")
15 |
16 | type NonceStore interface {
17 | // Returns nil if accepted, an error otherwise.
18 | Accept(endpoint, nonce string) error
19 | }
20 |
21 | type Nonce struct {
22 | T time.Time
23 | S string
24 | }
25 |
26 | type SimpleNonceStore struct {
27 | store map[string][]*Nonce
28 | mutex *sync.Mutex
29 | }
30 |
31 | func NewSimpleNonceStore() *SimpleNonceStore {
32 | return &SimpleNonceStore{store: map[string][]*Nonce{}, mutex: &sync.Mutex{}}
33 | }
34 |
35 | func (d *SimpleNonceStore) Accept(endpoint, nonce string) error {
36 | // Value: A string 255 characters or less in length, that MUST be
37 | // unique to this particular successful authentication response.
38 | if len(nonce) < 20 || len(nonce) > 256 {
39 | return errors.New("Invalid nonce")
40 | }
41 |
42 | // The nonce MUST start with the current time on the server, and MAY
43 | // contain additional ASCII characters in the range 33-126 inclusive
44 | // (printable non-whitespace characters), as necessary to make each
45 | // response unique. The date and time MUST be formatted as specified in
46 | // section 5.6 of [RFC3339], with the following restrictions:
47 |
48 | // All times must be in the UTC timezone, indicated with a "Z". No
49 | // fractional seconds are allowed For example:
50 | // 2005-05-15T17:11:51ZUNIQUE
51 | ts, err := time.Parse(time.RFC3339, nonce[0:20])
52 | if err != nil {
53 | return err
54 | }
55 | now := time.Now()
56 | diff := now.Sub(ts)
57 | if diff > *maxNonceAge {
58 | return fmt.Errorf("Nonce too old: %.2fs", diff.Seconds())
59 | }
60 |
61 | s := nonce[20:]
62 |
63 | // Meh.. now we have to use a mutex, to protect that map from
64 | // concurrent access. Could put a go routine in charge of it
65 | // though.
66 | d.mutex.Lock()
67 | defer d.mutex.Unlock()
68 |
69 | if nonces, hasOp := d.store[endpoint]; hasOp {
70 | // Delete old nonces while we are at it.
71 | newNonces := []*Nonce{{ts, s}}
72 | for _, n := range nonces {
73 | if n.T == ts && n.S == s {
74 | // If return early, just ignore the filtered list
75 | // we have been building so far...
76 | return errors.New("Nonce already used")
77 | }
78 | if now.Sub(n.T) < *maxNonceAge {
79 | newNonces = append(newNonces, n)
80 | }
81 | }
82 | d.store[endpoint] = newNonces
83 | } else {
84 | d.store[endpoint] = []*Nonce{{ts, s}}
85 | }
86 | return nil
87 | }
88 |
--------------------------------------------------------------------------------
/nonce_store_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestDefaultNonceStore(t *testing.T) {
9 | *maxNonceAge = 60 * time.Second
10 | now := time.Now().UTC()
11 | // 30 seconds ago
12 | now30s := now.Add(-30 * time.Second)
13 | // 2 minutes ago
14 | now2m := now.Add(-2 * time.Minute)
15 |
16 | now30sStr := now30s.Format(time.RFC3339)
17 | now2mStr := now2m.Format(time.RFC3339)
18 |
19 | ns := NewSimpleNonceStore()
20 | reject(t, ns, "1", "foo") // invalid nonce
21 | reject(t, ns, "1", "fooBarBazLongerThan20Chars") // invalid nonce
22 |
23 | accept(t, ns, "1", now30sStr+"asd")
24 | reject(t, ns, "1", now30sStr+"asd") // same nonce
25 | accept(t, ns, "1", now30sStr+"xxx") // different nonce
26 | reject(t, ns, "1", now30sStr+"xxx") // different nonce again to verify storage of multiple nonces per endpoint
27 | accept(t, ns, "2", now30sStr+"asd") // different endpoint
28 |
29 | reject(t, ns, "1", now2mStr+"old") // too old
30 | reject(t, ns, "3", now2mStr+"old") // too old
31 | }
32 |
33 | func accept(t *testing.T, ns NonceStore, op, nonce string) {
34 | e := ns.Accept(op, nonce)
35 | if e != nil {
36 | t.Errorf("Should accept %s nonce %s", op, nonce)
37 | }
38 | }
39 |
40 | func reject(t *testing.T, ns NonceStore, op, nonce string) {
41 | e := ns.Accept(op, nonce)
42 | if e == nil {
43 | t.Errorf("Should reject %s nonce %s", op, nonce)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/normalizer.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "errors"
5 | "net/url"
6 | "strings"
7 | )
8 |
9 | func Normalize(id string) (string, error) {
10 | id = strings.TrimSpace(id)
11 | if len(id) == 0 {
12 | return "", errors.New("No id provided")
13 | }
14 |
15 | // 7.2 from openID 2.0 spec.
16 |
17 | //If the user's input starts with the "xri://" prefix, it MUST be
18 | //stripped off, so that XRIs are used in the canonical form.
19 | if strings.HasPrefix(id, "xri://") {
20 | id = id[6:]
21 | return id, errors.New("XRI identifiers not supported")
22 | }
23 |
24 | // If the first character of the resulting string is an XRI
25 | // Global Context Symbol ("=", "@", "+", "$", "!") or "(", as
26 | // defined in Section 2.2.1 of [XRI_Syntax_2.0], then the input
27 | // SHOULD be treated as an XRI.
28 | if b := id[0]; b == '=' || b == '@' || b == '+' || b == '$' || b == '!' || b == '(' {
29 | return id, errors.New("XRI identifiers not supported")
30 | }
31 |
32 | // Otherwise, the input SHOULD be treated as an http URL; if it
33 | // does not include a "http" or "https" scheme, the Identifier
34 | // MUST be prefixed with the string "http://". If the URL
35 | // contains a fragment part, it MUST be stripped off together
36 | // with the fragment delimiter character "#". See Section 11.5.2 for
37 | // more information.
38 | if !strings.HasPrefix(id, "http://") && !strings.HasPrefix(id,
39 | "https://") {
40 | id = "http://" + id
41 | }
42 | if fragmentIndex := strings.Index(id, "#"); fragmentIndex != -1 {
43 | id = id[0:fragmentIndex]
44 | }
45 | if u, err := url.ParseRequestURI(id); err != nil {
46 | return "", err
47 | } else {
48 | if u.Host == "" {
49 | return "", errors.New("Invalid address provided as id")
50 | }
51 | if u.Path == "" {
52 | u.Path = "/"
53 | }
54 | id = u.String()
55 | }
56 |
57 | // URL Identifiers MUST then be further normalized by both
58 | // following redirects when retrieving their content and finally
59 | // applying the rules in Section 6 of [RFC3986] to the final
60 | // destination URL. This final URL MUST be noted by the Relying
61 | // Party as the Claimed Identifier and be used when requesting
62 | // authentication.
63 | return id, nil
64 | }
65 |
--------------------------------------------------------------------------------
/normalizer_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestNormalize(t *testing.T) {
8 | // OpenID 2.0 spec Appendix A.1. Normalization
9 | doNormalize(t, "example.com", "http://example.com/", true)
10 | doNormalize(t, "http://example.com", "http://example.com/", true)
11 | doNormalize(t, "https://example.com/", "https://example.com/", true)
12 | doNormalize(t, "http://example.com/user", "http://example.com/user", true)
13 | doNormalize(t, "http://example.com/user/", "http://example.com/user/", true)
14 | doNormalize(t, "http://example.com/", "http://example.com/", true)
15 | doNormalize(t, "=example", "=example", false) // XRI not supported
16 | doNormalize(t, "(=example)", "(=example)", false) // XRI not supported
17 | doNormalize(t, "xri://=example", "=example", false) // XRI not supported
18 |
19 | // Empty
20 | doNormalize(t, "", "", false)
21 | doNormalize(t, " ", "", false)
22 | doNormalize(t, " ", "", false)
23 | doNormalize(t, "xri://", "", false)
24 | doNormalize(t, "http://", "", false)
25 | doNormalize(t, "https://", "", false)
26 |
27 | // Padded with spacing
28 | doNormalize(t, " example.com ", "http://example.com/", true)
29 | doNormalize(t, " http://example.com ", "http://example.com/", true)
30 |
31 | // XRI not supported
32 | doNormalize(t, "xri://asdf", "asdf", false)
33 | doNormalize(t, "=asdf", "=asdf", false)
34 | doNormalize(t, "@asdf", "@asdf", false)
35 |
36 | // HTTP
37 | doNormalize(t, "foo.com", "http://foo.com/", true)
38 | doNormalize(t, "http://foo.com", "http://foo.com/", true)
39 | doNormalize(t, "https://foo.com", "https://foo.com/", true)
40 |
41 | // Fragment need to be removed
42 | doNormalize(t, "http://foo.com#bar", "http://foo.com/", true)
43 | doNormalize(t, "http://foo.com/page#bar", "http://foo.com/page", true)
44 | }
45 |
46 | func doNormalize(t *testing.T, idIn, idOut string, succeed bool) {
47 | if id, err := Normalize(idIn); err != nil && succeed {
48 | t.Errorf("unexpected normalize error: gave %v, expected %v, got %v - %v", idIn, idOut, id, err)
49 | } else if err == nil && !succeed {
50 | t.Errorf("unexpected normalize success: gave %v, expected %v, got %v", idIn, idOut, id)
51 | } else if id != idOut {
52 | t.Errorf("unexpected normalize result: gave %v, expected %v, got %v", idIn, idOut, id)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/openid.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | type OpenID struct {
8 | urlGetter httpGetter
9 | }
10 |
11 | func NewOpenID(client *http.Client) *OpenID {
12 | return &OpenID{urlGetter: &defaultGetter{client: client}}
13 | }
14 |
15 | var defaultInstance = NewOpenID(http.DefaultClient)
16 |
--------------------------------------------------------------------------------
/redirect.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "net/url"
5 | "strings"
6 | )
7 |
8 | func RedirectURL(id, callbackURL, realm string) (string, error) {
9 | return defaultInstance.RedirectURL(id, callbackURL, realm)
10 | }
11 |
12 | func (oid *OpenID) RedirectURL(id, callbackURL, realm string) (string, error) {
13 | opEndpoint, opLocalID, claimedID, err := oid.Discover(id)
14 | if err != nil {
15 | return "", err
16 | }
17 | return BuildRedirectURL(opEndpoint, opLocalID, claimedID, callbackURL, realm)
18 | }
19 |
20 | func BuildRedirectURL(opEndpoint, opLocalID, claimedID, returnTo, realm string) (string, error) {
21 | values := make(url.Values)
22 | values.Add("openid.ns", "http://specs.openid.net/auth/2.0")
23 | values.Add("openid.mode", "checkid_setup")
24 | values.Add("openid.return_to", returnTo)
25 |
26 | // 9.1. Request Parameters
27 | // "openid.claimed_id" and "openid.identity" SHALL be either both present or both absent.
28 | if len(claimedID) > 0 {
29 | values.Add("openid.claimed_id", claimedID)
30 | if len(opLocalID) > 0 {
31 | values.Add("openid.identity", opLocalID)
32 | } else {
33 | // If a different OP-Local Identifier is not specified,
34 | // the claimed identifier MUST be used as the value for openid.identity.
35 | values.Add("openid.identity", claimedID)
36 | }
37 | } else {
38 | // 7.3.1. Discovered Information
39 | // If the end user entered an OP Identifier, there is no Claimed Identifier.
40 | // For the purposes of making OpenID Authentication requests, the value
41 | // "http://specs.openid.net/auth/2.0/identifier_select" MUST be used as both the
42 | // Claimed Identifier and the OP-Local Identifier when an OP Identifier is entered.
43 | values.Add("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select")
44 | values.Add("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select")
45 | }
46 |
47 | if len(realm) > 0 {
48 | values.Add("openid.realm", realm)
49 | }
50 |
51 | if strings.Contains(opEndpoint, "?") {
52 | return opEndpoint + "&" + values.Encode(), nil
53 | }
54 | return opEndpoint + "?" + values.Encode(), nil
55 | }
56 |
--------------------------------------------------------------------------------
/redirect_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 | )
7 |
8 | func TestBuildRedirectUrl(t *testing.T) {
9 | expectURL(t, "https://endpoint/a", "opLocalId", "claimedId", "returnTo", "realm",
10 | "https://endpoint/a?"+
11 | "openid.ns=http://specs.openid.net/auth/2.0"+
12 | "&openid.mode=checkid_setup"+
13 | "&openid.return_to=returnTo"+
14 | "&openid.claimed_id=claimedId"+
15 | "&openid.identity=opLocalId"+
16 | "&openid.realm=realm")
17 | // No realm.
18 | expectURL(t, "https://endpoint/a", "opLocalId", "claimedId", "returnTo", "",
19 | "https://endpoint/a?"+
20 | "openid.ns=http://specs.openid.net/auth/2.0"+
21 | "&openid.mode=checkid_setup"+
22 | "&openid.return_to=returnTo"+
23 | "&openid.claimed_id=claimedId"+
24 | "&openid.identity=opLocalId")
25 | // No realm, no localId
26 | expectURL(t, "https://endpoint/a", "", "claimedId", "returnTo", "",
27 | "https://endpoint/a?"+
28 | "openid.ns=http://specs.openid.net/auth/2.0"+
29 | "&openid.mode=checkid_setup"+
30 | "&openid.return_to=returnTo"+
31 | "&openid.claimed_id=claimedId"+
32 | "&openid.identity=claimedId")
33 | // No realm, no claimedId
34 | expectURL(t, "https://endpoint/a", "opLocalId", "", "returnTo", "",
35 | "https://endpoint/a?"+
36 | "openid.ns=http://specs.openid.net/auth/2.0"+
37 | "&openid.mode=checkid_setup"+
38 | "&openid.return_to=returnTo"+
39 | "&openid.claimed_id="+
40 | "http://specs.openid.net/auth/2.0/identifier_select"+
41 | "&openid.identity="+
42 | "http://specs.openid.net/auth/2.0/identifier_select")
43 | }
44 |
45 | func expectURL(t *testing.T, opEndpoint, opLocalID, claimedID, returnTo, realm, expected string) {
46 | url, err := BuildRedirectURL(opEndpoint, opLocalID, claimedID, returnTo, realm)
47 | if err != nil {
48 | t.Errorf("Unexpected error: %s", err)
49 | }
50 | compareUrls(t, url, expected)
51 | }
52 |
53 | func TestRedirectWithDiscovery(t *testing.T) {
54 | expected := "foo?" +
55 | "openid.ns=http://specs.openid.net/auth/2.0" +
56 | "&openid.mode=checkid_setup" +
57 | "&openid.return_to=mysite/cb" +
58 | "&openid.claimed_id=" +
59 | "http://specs.openid.net/auth/2.0/identifier_select" +
60 | "&openid.identity=" +
61 | "http://specs.openid.net/auth/2.0/identifier_select"
62 |
63 | // They all redirect to the same XRDS document
64 | expectRedirect(t, "http://example.com/xrds",
65 | "mysite/cb", "", expected, false)
66 | expectRedirect(t, "http://example.com/xrds-loc",
67 | "mysite/cb", "", expected, false)
68 | expectRedirect(t, "http://example.com/xrds-meta",
69 | "mysite/cb", "", expected, false)
70 | }
71 |
72 | func expectRedirect(t *testing.T, uri, callback, realm, exRedirect string, exErr bool) {
73 | redirect, err := testInstance.RedirectURL(uri, callback, realm)
74 | if (err != nil) != exErr {
75 | t.Errorf("Unexpected error: '%s'", err)
76 | return
77 | }
78 | compareUrls(t, redirect, exRedirect)
79 | }
80 |
81 | func compareUrls(t *testing.T, url1, expected string) {
82 | p1, err1 := url.Parse(url1)
83 | p2, err2 := url.Parse(expected)
84 | if err1 != nil {
85 | t.Errorf("Url1 non parsable: %s", err1)
86 | return
87 | }
88 | if err2 != nil {
89 | t.Errorf("ExpectedUrl non parsable: %s", err2)
90 | return
91 | }
92 | if p1.Scheme != p2.Scheme ||
93 | p1.Host != p2.Host ||
94 | p1.Path != p2.Path {
95 | t.Errorf("URLs don't match: %s vs %s", url1, expected)
96 | }
97 | q1, _ := url.ParseQuery(p1.RawQuery)
98 | q2, _ := url.ParseQuery(p2.RawQuery)
99 | if err := compareQueryParams(q1, q2); err != nil {
100 | t.Errorf("URLs query params don't match: %s: %s vs %s", err, url1, expected)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/verify.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "net/url"
8 | "strings"
9 | )
10 |
11 | func Verify(uri string, cache DiscoveryCache, nonceStore NonceStore) (id string, err error) {
12 | return defaultInstance.Verify(uri, cache, nonceStore)
13 | }
14 |
15 | func (oid *OpenID) Verify(uri string, cache DiscoveryCache, nonceStore NonceStore) (id string, err error) {
16 | parsedURL, err := url.Parse(uri)
17 | if err != nil {
18 | return "", err
19 | }
20 | values, err := url.ParseQuery(parsedURL.RawQuery)
21 | if err != nil {
22 | return "", err
23 | }
24 |
25 | // 11. Verifying Assertions
26 | // When the Relying Party receives a positive assertion, it MUST
27 | // verify the following before accepting the assertion:
28 |
29 | // - The value of "openid.signed" contains all the required fields.
30 | // (Section 10.1)
31 | if err = verifySignedFields(values); err != nil {
32 | return "", err
33 | }
34 |
35 | // - The signature on the assertion is valid (Section 11.4)
36 | if err = verifySignature(uri, values, oid.urlGetter); err != nil {
37 | return "", err
38 | }
39 |
40 | // - The value of "openid.return_to" matches the URL of the current
41 | // request (Section 11.1)
42 | if err = verifyReturnTo(parsedURL, values); err != nil {
43 | return "", err
44 | }
45 |
46 | // - Discovered information matches the information in the assertion
47 | // (Section 11.2)
48 | if err = oid.verifyDiscovered(parsedURL, values, cache); err != nil {
49 | return "", err
50 | }
51 |
52 | // - An assertion has not yet been accepted from this OP with the
53 | // same value for "openid.response_nonce" (Section 11.3)
54 | if err = verifyNonce(values, nonceStore); err != nil {
55 | return "", err
56 | }
57 |
58 | // If all four of these conditions are met, assertion is now
59 | // verified. If the assertion contained a Claimed Identifier, the
60 | // user is now authenticated with that identifier.
61 | return values.Get("openid.claimed_id"), nil
62 | }
63 |
64 | // 10.1. Positive Assertions
65 | // openid.signed - Comma-separated list of signed fields.
66 | // This entry consists of the fields without the "openid." prefix that the signature covers.
67 | // This list MUST contain at least "op_endpoint", "return_to" "response_nonce" and "assoc_handle",
68 | // and if present in the response, "claimed_id" and "identity".
69 | func verifySignedFields(vals url.Values) error {
70 | ok := map[string]bool{
71 | "op_endpoint": false,
72 | "return_to": false,
73 | "response_nonce": false,
74 | "assoc_handle": false,
75 | "claimed_id": vals.Get("openid.claimed_id") == "",
76 | "identity": vals.Get("openid.identity") == "",
77 | }
78 | signed := strings.Split(vals.Get("openid.signed"), ",")
79 | for _, sf := range signed {
80 | ok[sf] = true
81 | }
82 | for k, v := range ok {
83 | if !v {
84 | return fmt.Errorf("%v must be signed but isn't", k)
85 | }
86 | }
87 | return nil
88 | }
89 |
90 | // 11.1. Verifying the Return URL
91 | // To verify that the "openid.return_to" URL matches the URL that is processing this assertion:
92 | // - The URL scheme, authority, and path MUST be the same between the two
93 | // URLs.
94 | // - Any query parameters that are present in the "openid.return_to" URL
95 | // MUST also be present with the same values in the URL of the HTTP
96 | // request the RP received.
97 | func verifyReturnTo(uri *url.URL, vals url.Values) error {
98 | returnTo := vals.Get("openid.return_to")
99 | rp, err := url.Parse(returnTo)
100 | if err != nil {
101 | return err
102 | }
103 | if uri.Scheme != rp.Scheme ||
104 | uri.Host != rp.Host ||
105 | uri.Path != rp.Path {
106 | return errors.New(
107 | "Scheme, host or path don't match in return_to URL")
108 | }
109 | qp, err := url.ParseQuery(rp.RawQuery)
110 | if err != nil {
111 | return err
112 | }
113 | return compareQueryParams(qp, vals)
114 | }
115 |
116 | // Any parameter in q1 must also be present in q2, and values must match.
117 | func compareQueryParams(q1, q2 url.Values) error {
118 | for k := range q1 {
119 | v1 := q1.Get(k)
120 | v2 := q2.Get(k)
121 | if v1 != v2 {
122 | return fmt.Errorf(
123 | "URLs query params don't match: Param %s different: %s vs %s",
124 | k, v1, v2)
125 | }
126 | }
127 | return nil
128 | }
129 |
130 | func (oid *OpenID) verifyDiscovered(uri *url.URL, vals url.Values, cache DiscoveryCache) error {
131 | version := vals.Get("openid.ns")
132 | if version != "http://specs.openid.net/auth/2.0" {
133 | return errors.New("Bad protocol version")
134 | }
135 |
136 | endpoint := vals.Get("openid.op_endpoint")
137 | if len(endpoint) == 0 {
138 | return errors.New("missing openid.op_endpoint url param")
139 | }
140 | localID := vals.Get("openid.identity")
141 | if len(localID) == 0 {
142 | return errors.New("no localId to verify")
143 | }
144 | claimedID := vals.Get("openid.claimed_id")
145 | if len(claimedID) == 0 {
146 | // If no Claimed Identifier is present in the response, the
147 | // assertion is not about an identifier and the RP MUST NOT use the
148 | // User-supplied Identifier associated with the current OpenID
149 | // authentication transaction to identify the user. Extension
150 | // information in the assertion MAY still be used.
151 | // --- This library does not support this case. So claimed
152 | // identifier must be present.
153 | return errors.New("no claimed_id to verify")
154 | }
155 |
156 | // 11.2. Verifying Discovered Information
157 |
158 | // If the Claimed Identifier in the assertion is a URL and contains a
159 | // fragment, the fragment part and the fragment delimiter character "#"
160 | // MUST NOT be used for the purposes of verifying the discovered
161 | // information.
162 | claimedIDVerify := claimedID
163 | if fragmentIndex := strings.Index(claimedID, "#"); fragmentIndex != -1 {
164 | claimedIDVerify = claimedID[0:fragmentIndex]
165 | }
166 |
167 | // If the Claimed Identifier is included in the assertion, it
168 | // MUST have been discovered by the Relying Party and the
169 | // information in the assertion MUST be present in the
170 | // discovered information. The Claimed Identifier MUST NOT be an
171 | // OP Identifier.
172 | if discovered := cache.Get(claimedIDVerify); discovered != nil &&
173 | discovered.OpEndpoint() == endpoint &&
174 | discovered.OpLocalID() == localID &&
175 | discovered.ClaimedID() == claimedIDVerify {
176 | return nil
177 | }
178 |
179 | // If the Claimed Identifier was not previously discovered by the
180 | // Relying Party (the "openid.identity" in the request was
181 | // "http://specs.openid.net/auth/2.0/identifier_select" or a different
182 | // Identifier, or if the OP is sending an unsolicited positive
183 | // assertion), the Relying Party MUST perform discovery on the Claimed
184 | // Identifier in the response to make sure that the OP is authorized to
185 | // make assertions about the Claimed Identifier.
186 | if ep, _, _, err := oid.Discover(claimedID); err == nil {
187 | if ep == endpoint {
188 | // This claimed ID points to the same endpoint, therefore this
189 | // endpoint is authorized to make assertions about that claimed ID.
190 | // TODO: There may be multiple endpoints found during discovery.
191 | // They should all be checked.
192 | cache.Put(claimedIDVerify, &SimpleDiscoveredInfo{opEndpoint: endpoint, opLocalID: localID, claimedID: claimedIDVerify})
193 | return nil
194 | }
195 | }
196 |
197 | return errors.New("Could not verify the claimed ID")
198 | }
199 |
200 | func verifyNonce(vals url.Values, store NonceStore) error {
201 | nonce := vals.Get("openid.response_nonce")
202 | endpoint := vals.Get("openid.op_endpoint")
203 | return store.Accept(endpoint, nonce)
204 | }
205 |
206 | func verifySignature(uri string, vals url.Values, getter httpGetter) error {
207 | // To have the signature verification performed by the OP, the
208 | // Relying Party sends a direct request to the OP. To verify the
209 | // signature, the OP uses a private association that was generated
210 | // when it issued the positive assertion.
211 |
212 | // 11.4.2.1. Request Parameters
213 | params := make(url.Values)
214 | // openid.mode: Value: "check_authentication"
215 | params.Add("openid.mode", "check_authentication")
216 | // Exact copies of all fields from the authentication response,
217 | // except for "openid.mode".
218 | for k, vs := range vals {
219 | if k == "openid.mode" {
220 | continue
221 | }
222 | for _, v := range vs {
223 | params.Add(k, v)
224 | }
225 | }
226 | resp, err := getter.Post(vals.Get("openid.op_endpoint"), params)
227 | if err != nil {
228 | return err
229 | }
230 | defer resp.Body.Close()
231 | content, err := ioutil.ReadAll(resp.Body)
232 | response := string(content)
233 | lines := strings.Split(response, "\n")
234 |
235 | isValid := false
236 | nsValid := false
237 | for _, l := range lines {
238 | if l == "is_valid:true" {
239 | isValid = true
240 | } else if l == "ns:http://specs.openid.net/auth/2.0" {
241 | nsValid = true
242 | }
243 | }
244 | if isValid && nsValid {
245 | // Yay !
246 | return nil
247 | }
248 |
249 | return errors.New("Could not verify assertion with provider")
250 | }
251 |
--------------------------------------------------------------------------------
/verify_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestVerifyNonce(t *testing.T) {
10 | timeStr := time.Now().UTC().Format(time.RFC3339)
11 | ns := NewSimpleNonceStore()
12 | v := url.Values{}
13 |
14 | // Initial values
15 | v.Set("openid.op_endpoint", "1")
16 | v.Set("openid.response_nonce", timeStr+"foo")
17 | if err := verifyNonce(v, ns); err != nil {
18 | t.Errorf("verifyNonce failed unexpectedly: %v", err)
19 | }
20 |
21 | // Different nonce
22 | v.Set("openid.response_nonce", timeStr+"bar")
23 | if err := verifyNonce(v, ns); err != nil {
24 | t.Errorf("verifyNonce failed unexpectedly: %v", err)
25 | }
26 |
27 | // Different endpoint
28 | v.Set("openid.op_endpoint", "2")
29 | if err := verifyNonce(v, ns); err != nil {
30 | t.Errorf("verifyNonce failed unexpectedly: %v", err)
31 | }
32 | }
33 |
34 | func TestVerifySignedFields(t *testing.T) {
35 | // No claimed_id/identity, properly signed
36 | doVerifySignedFields(t,
37 | url.Values{"openid.signed": []string{"signed,op_endpoint,return_to,response_nonce,assoc_handle"}},
38 | true)
39 |
40 | // Everything properly signed, even empty claimed_id/identity
41 | doVerifySignedFields(t,
42 | url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle"}},
43 | true)
44 |
45 | // With claimed_id/identity, properly signed
46 | doVerifySignedFields(t,
47 | url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle"},
48 | "openid.claimed_id": []string{"foo"},
49 | "openid.identity": []string{"foo"}},
50 | true)
51 |
52 | // With claimed_id/identity, but those two not signed
53 | doVerifySignedFields(t,
54 | url.Values{"openid.signed": []string{"signed,op_endpoint,return_to,response_nonce,assoc_handle"},
55 | "openid.claimed_id": []string{"foo"},
56 | "openid.identity": []string{"foo"}},
57 | false)
58 |
59 | // Missing signature for op_endpoint
60 | doVerifySignedFields(t,
61 | url.Values{"openid.signed": []string{"signed,claimed_id,identity,return_to,response_nonce,assoc_handle"},
62 | "openid.claimed_id": []string{"foo"},
63 | "openid.identity": []string{"foo"}},
64 | false)
65 |
66 | // Missing signature for return_to
67 | doVerifySignedFields(t,
68 | url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,response_nonce,assoc_handle"},
69 | "openid.claimed_id": []string{"foo"},
70 | "openid.identity": []string{"foo"}},
71 | false)
72 |
73 | // Missing signature for response_nonce
74 | doVerifySignedFields(t,
75 | url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,return_to,assoc_handle"},
76 | "openid.claimed_id": []string{"foo"},
77 | "openid.identity": []string{"foo"}},
78 | false)
79 |
80 | // Missing signature for assoc_handle
81 | doVerifySignedFields(t,
82 | url.Values{"openid.signed": []string{"signed,op_endpoint,claimed_id,identity,return_to,response_nonce"},
83 | "openid.claimed_id": []string{"foo"},
84 | "openid.identity": []string{"foo"}},
85 | false)
86 | }
87 |
88 | func doVerifySignedFields(t *testing.T, v url.Values, succeed bool) {
89 | if err := verifySignedFields(v); err == nil && !succeed {
90 | t.Errorf("verifySignedFields succeeded unexpectedly: %v - %v", v, err)
91 | } else if err != nil && succeed {
92 | t.Errorf("verifySignedFields failed unexpectedly: %v - %v", v, err)
93 | }
94 | }
95 |
96 | func TestVerifyDiscovered(t *testing.T) {
97 | dc := NewSimpleDiscoveryCache()
98 | vals := url.Values{"openid.ns": []string{"http://specs.openid.net/auth/2.0"},
99 | "openid.mode": []string{"id_res"},
100 | "openid.op_endpoint": []string{"http://example.com/openid/login"},
101 | "openid.claimed_id": []string{"http://example.com/openid/id/foo"},
102 | "openid.identity": []string{"http://example.com/openid/id/foo"}}
103 |
104 | // Make sure we fail with no discovery handler
105 | if err := testInstance.verifyDiscovered(nil, vals, dc); err == nil {
106 | t.Errorf("verifyDiscovered succeeded unexpectedly with no discovery")
107 | }
108 |
109 | // Add the discovery handler
110 | testGetter.urls["http://example.com/openid/id/foo#Accept#application/xrds+xml"] = `HTTP/1.0 200 OK
111 | Content-Type: application/xrds+xml; charset=UTF-8
112 |
113 |
114 |
115 |
116 |
117 | http://specs.openid.net/auth/2.0/signon
118 | http://example.com/openid/login
119 |
120 |
121 | `
122 |
123 | // Make sure we succeed now
124 | if err := testInstance.verifyDiscovered(nil, vals, dc); err != nil {
125 | t.Errorf("verifyDiscovered failed unexpectedly: %v", err)
126 | }
127 |
128 | // Remove the discovery handler
129 | delete(testGetter.urls, "http://example.com/openid/id/foo#Accept#application/xrds+xml")
130 |
131 | // Make sure we still succeed thanks to the discovery cache
132 | if err := testInstance.verifyDiscovered(nil, vals, dc); err != nil {
133 | t.Errorf("verifyDiscovered failed unexpectedly: %v", err)
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/xrds.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "encoding/xml"
5 | "errors"
6 | "strings"
7 | )
8 |
9 | // TODO: As per 11.2 in openid 2 specs, a service may have multiple
10 | // URIs. We don't care for discovery really, but we do care for
11 | // verification though.
12 | type XrdsIdentifier struct {
13 | Type []string `xml:"Type"`
14 | URI string `xml:"URI"`
15 | LocalID string `xml:"LocalID"`
16 | Priority int `xml:"priority,attr"`
17 | }
18 |
19 | type Xrd struct {
20 | Service []*XrdsIdentifier `xml:"Service"`
21 | }
22 |
23 | type XrdsDocument struct {
24 | XMLName xml.Name `xml:"XRDS"`
25 | Xrd *Xrd `xml:"XRD"`
26 | }
27 |
28 | func parseXrds(input []byte) (opEndpoint, opLocalID string, err error) {
29 | xrdsDoc := &XrdsDocument{}
30 | err = xml.Unmarshal(input, xrdsDoc)
31 | if err != nil {
32 | return
33 | }
34 |
35 | if xrdsDoc.Xrd == nil {
36 | return "", "", errors.New("XRDS document missing XRD tag")
37 | }
38 |
39 | // 7.3.2.2. Extracting Authentication Data
40 | // Once the Relying Party has obtained an XRDS document, it
41 | // MUST first search the document (following the rules
42 | // described in [XRI_Resolution_2.0]) for an OP Identifier
43 | // Element. If none is found, the RP will search for a Claimed
44 | // Identifier Element.
45 | for _, service := range xrdsDoc.Xrd.Service {
46 | // 7.3.2.1.1. OP Identifier Element
47 | // An OP Identifier Element is an element with the
48 | // following information:
49 | // An tag whose text content is
50 | // "http://specs.openid.net/auth/2.0/server".
51 | // An tag whose text content is the OP Endpoint URL
52 | if service.hasType("http://specs.openid.net/auth/2.0/server") {
53 | opEndpoint = strings.TrimSpace(service.URI)
54 | return
55 | }
56 | }
57 | for _, service := range xrdsDoc.Xrd.Service {
58 | // 7.3.2.1.2. Claimed Identifier Element
59 | // A Claimed Identifier Element is an element
60 | // with the following information:
61 | // An tag whose text content is
62 | // "http://specs.openid.net/auth/2.0/signon".
63 | // An tag whose text content is the OP Endpoint
64 | // URL.
65 | // An tag (optional) whose text content is the
66 | // OP-Local Identifier.
67 | if service.hasType("http://specs.openid.net/auth/2.0/signon") {
68 | opEndpoint = strings.TrimSpace(service.URI)
69 | opLocalID = strings.TrimSpace(service.LocalID)
70 | return
71 | }
72 | }
73 | return "", "", errors.New("Could not find a compatible service")
74 | }
75 |
76 | func (xrdsi *XrdsIdentifier) hasType(tpe string) bool {
77 | for _, t := range xrdsi.Type {
78 | if t == tpe {
79 | return true
80 | }
81 | }
82 | return false
83 | }
84 |
--------------------------------------------------------------------------------
/xrds_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestXrds(t *testing.T) {
8 | testExpectOpID(t, []byte(`
9 |
10 |
12 |
13 |
14 | http://openid.net/signon/1.0
15 | http://www.myopenid.com/server
16 | http://smoker.myopenid.com/
17 |
18 |
19 | http://openid.net/signon/1.0
20 | http://www.livejournal.com/openid/server.bml
21 |
22 | http://www.livejournal.com/users/frank/
23 |
24 |
25 |
26 | http://lid.netmesh.org/sso/2.0
27 |
28 |
29 | http://specs.openid.net/auth/2.0/server
30 | foo
31 |
32 |
33 |
34 | `), "foo", "")
35 |
36 | testExpectOpID(t, []byte(`
37 |
38 |
40 |
41 |
42 | http://specs.openid.net/auth/2.0/signon
43 | https://www.exampleprovider.com/endpoint/
44 | https://exampleuser.exampleprovider.com/
45 |
46 |
47 |
48 | `),
49 | "https://www.exampleprovider.com/endpoint/",
50 | "https://exampleuser.exampleprovider.com/")
51 |
52 | // OP Identifier Element has priority over Claimed Identifier Element
53 | testExpectOpID(t, []byte(`
54 |
55 |
57 |
58 |
59 | http://specs.openid.net/auth/2.0/signon
60 | https://www.exampleprovider.com/endpoint-signon/
61 |
62 |
63 | http://specs.openid.net/auth/2.0/server
64 | https://www.exampleprovider.com/endpoint-server/
65 |
66 |
67 |
68 | `),
69 | "https://www.exampleprovider.com/endpoint-server/",
70 | "")
71 | }
72 |
73 | func testExpectOpID(t *testing.T, xrds []byte, op, id string) {
74 | receivedOp, receivedID, err := parseXrds(xrds)
75 | if err != nil {
76 | t.Errorf("Got an error parsing XRDS (%s): %s", string(xrds), err)
77 | } else {
78 | if receivedOp != op {
79 | t.Errorf("Extracted OP does not match: Exepect %s, Got %s",
80 | op, receivedOp)
81 | }
82 | if receivedID != id {
83 | t.Errorf("Extracted ID does not match: Exepect %s, Got %s",
84 | id, receivedID)
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/yadis_discovery.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "io/ioutil"
7 | "strings"
8 |
9 | "golang.org/x/net/html"
10 | )
11 |
12 | var yadisHeaders = map[string]string{
13 | "Accept": "application/xrds+xml"}
14 |
15 | func yadisDiscovery(id string, getter httpGetter) (opEndpoint string, opLocalID string, err error) {
16 | // Section 6.2.4 of Yadis 1.0 specifications.
17 | // The Yadis Protocol is initiated by the Relying Party Agent
18 | // with an initial HTTP request using the Yadis URL.
19 |
20 | // This request MUST be either a GET or a HEAD request.
21 |
22 | // A GET or HEAD request MAY include an HTTP Accept
23 | // request-header (HTTP 14.1) specifying MIME media type,
24 | // application/xrds+xml.
25 | resp, err := getter.Get(id, yadisHeaders)
26 | if err != nil {
27 | return "", "", err
28 | }
29 |
30 | defer resp.Body.Close()
31 |
32 | // Section 6.2.5 from Yadis 1.0 spec: Response
33 |
34 | contentType := resp.Header.Get("Content-Type")
35 |
36 | // The response MUST be one of:
37 | // (see 6.2.6 for precedence)
38 | if l := resp.Header.Get("X-XRDS-Location"); l != "" {
39 | // 2. HTTP response-headers that include an X-XRDS-Location
40 | // response-header, together with a document
41 | return getYadisResourceDescriptor(l, getter)
42 | } else if strings.Contains(contentType, "text/html") {
43 | // 1. An HTML document with a element that includes a
44 | // element with http-equiv attribute, X-XRDS-Location,
45 |
46 | metaContent, err := findMetaXrdsLocation(resp.Body)
47 | if err == nil {
48 | return getYadisResourceDescriptor(metaContent, getter)
49 | }
50 | return "", "", err
51 | } else if strings.Contains(contentType, "application/xrds+xml") {
52 | // 4. A document of MIME media type, application/xrds+xml.
53 | body, err := ioutil.ReadAll(resp.Body)
54 | if err == nil {
55 | return parseXrds(body)
56 | }
57 | return "", "", err
58 | }
59 | // 3. HTTP response-headers only, which MAY include an
60 | // X-XRDS-Location response-header, a content-type
61 | // response-header specifying MIME media type,
62 | // application/xrds+xml, or both.
63 | // (this is handled by one of the 2 previous if statements)
64 | return "", "", errors.New("No expected header, or content type")
65 | }
66 |
67 | // Similar as above, but we expect an absolute Yadis document URL.
68 | func getYadisResourceDescriptor(id string, getter httpGetter) (opEndpoint string, opLocalID string, err error) {
69 | resp, err := getter.Get(id, yadisHeaders)
70 | if err != nil {
71 | return "", "", err
72 | }
73 | defer resp.Body.Close()
74 | // 4. A document of MIME media type, application/xrds+xml.
75 | body, err := ioutil.ReadAll(resp.Body)
76 | if err == nil {
77 | return parseXrds(body)
78 | }
79 | return "", "", err
80 | }
81 |
82 | // Search for
83 | //
84 | //
85 | func findMetaXrdsLocation(input io.Reader) (location string, err error) {
86 | tokenizer := html.NewTokenizer(input)
87 | inHead := false
88 | for {
89 | tt := tokenizer.Next()
90 | switch tt {
91 | case html.ErrorToken:
92 | return "", tokenizer.Err()
93 | case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
94 | tk := tokenizer.Token()
95 | if tk.Data == "head" {
96 | if tt == html.StartTagToken {
97 | inHead = true
98 | } else {
99 | return "", errors.New("Meta X-XRDS-Location not found")
100 | }
101 | } else if inHead && tk.Data == "meta" {
102 | ok := false
103 | content := ""
104 | for _, attr := range tk.Attr {
105 | if attr.Key == "http-equiv" &&
106 | strings.ToLower(attr.Val) == "x-xrds-location" {
107 | ok = true
108 | } else if attr.Key == "content" {
109 | content = attr.Val
110 | }
111 | }
112 | if ok && len(content) > 0 {
113 | return content, nil
114 | }
115 | }
116 | }
117 | }
118 | return "", errors.New("Meta X-XRDS-Location not found")
119 | }
120 |
--------------------------------------------------------------------------------
/yadis_discovery_test.go:
--------------------------------------------------------------------------------
1 | package openid
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestFindMetaXrdsLocation(t *testing.T) {
9 | searchMeta(t, `
10 |
11 |
12 |
13 | `, "foo.com", false)
14 | searchMeta(t, `
15 |
16 |
17 |
18 |
19 | `, "foo.com", false)
20 | }
21 |
22 | func TestMetaXrdsLocationOutsideHead(t *testing.T) {
23 | searchMeta(t, `
24 |
25 |
26 | `, "", true)
27 | searchMeta(t, `
28 |
29 |
30 |
31 | `, "", true)
32 | }
33 |
34 | func TestNoMetaXrdsLocation(t *testing.T) {
35 | searchMeta(t, `
36 |
37 |
38 | `, "", true)
39 | }
40 |
41 | func searchMeta(t *testing.T, doc, loc string, err bool) {
42 | r := bytes.NewReader([]byte(doc))
43 | res, e := findMetaXrdsLocation(r)
44 | if (e != nil) != err {
45 | t.Errorf("Unexpected error: '%s'", e)
46 | } else if e == nil {
47 | if res != loc {
48 | t.Errorf("Found bad location: Expected %s, Got %s", loc, res)
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------