├── .gitignore
├── README.md
├── authy.go
├── authy_test.go
├── config.go
├── logo.png
├── martini
├── example_test.go
└── martini.go
├── mock_test.go
├── oauth2
└── oauth2.go
├── provider
├── default.go
└── provider.go
└── session.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Authy
2 | =====
3 |
4 | Authy is a go library that acts as an oauth authentication middleware for [net/http](http://golang.org/pkg/net/http),
5 | it aims to provide drop-in support for most OAuth 1 (not implemented yet) and 2 providers. It is inspired from node.js
6 | libraries such as [grant](https://github.com/simov/grant) or [everyauth](https://github.com/bnoguchi/everyauth).
7 |
8 | The current OAuth implementation is kinda rough and basic but should do the trick.
9 |
10 | Providers
11 | ---------
12 |
13 | The current list of providers is a verbatim adaptation of the one provided by [grant](https://github.com/simov/grant).
14 |
15 | **Provider** | **API documentation**
16 | ----------------|------------------------------------------------------------------------------------------------------
17 | `500px` | [https://developers.500px.com/](https://developers.500px.com/)
18 | `amazon` | [http://login.amazon.com/documentation](http://login.amazon.com/documentation)
19 | `angellist` | [https://angel.co/api](https://angel.co/api)
20 | `appnet` | [https://developers.app.net/reference/resources/](https://developers.app.net/reference/resources/)
21 | `asana` | [http://developer.asana.com/documentation/](http://developer.asana.com/documentation/)
22 | `assembla` | [http://api-doc.assembla.com/](http://api-doc.assembla.com/)
23 | `basecamp` | [https://github.com/basecamp/bcx-api/](https://github.com/basecamp/bcx-api/)
24 | `bitbucket` | [https://confluence.atlassian.com/display/BITBUCKET](https://confluence.atlassian.com/display/BITBUCKET)
25 | `bitly` | [http://dev.bitly.com](http://dev.bitly.com)
26 | `box` | [https://developers.box.com/](https://developers.box.com/)
27 | `buffer` | [http://dev.buffer.com](http://dev.buffer.com)
28 | `cheddar` | [https://cheddarapp.com/developer/](https://cheddarapp.com/developer/)
29 | `coinbase` | [https://www.coinbase.com/docs/api/overview](https://www.coinbase.com/docs/api/overview)
30 | `dailymile` | [http://www.dailymile.com/api/documentation](http://www.dailymile.com/api/documentation)
31 | `dailymotion` | [https://developer.dailymotion.com/documentation#graph-api](https://developer.dailymotion.com/documentation#graph-api)
32 | `deezer` | [http://developers.deezer.com/](http://developers.deezer.com/)
33 | `deviantart` | [https://www.deviantart.com/developers/](https://www.deviantart.com/developers/)
34 | `digitalocean` | [https://developers.digitalocean.com/](https://developers.digitalocean.com/)
35 | `disqus` | [https://disqus.com/api/docs/](https://disqus.com/api/docs/)
36 | `dropbox` | [https://www.dropbox.com/developers](https://www.dropbox.com/developers)
37 | `edmodo` | [https://developers.edmodo.com/](https://developers.edmodo.com/)
38 | `elance` | [https://www.elance.com/q/api2](https://www.elance.com/q/api2)
39 | `eventbrite` | [http://developer.eventbrite.com/](http://developer.eventbrite.com/)
40 | `evernote` | [https://dev.evernote.com/doc/](https://dev.evernote.com/doc/)
41 | `everyplay` | [https://developers.everyplay.com/](https://developers.everyplay.com/)
42 | `eyeem` | [https://www.eyeem.com/developers](https://www.eyeem.com/developers)
43 | `facebook` | [https://developers.facebook.com](https://developers.facebook.com)
44 | `feedly` | [https://developer.feedly.com/](https://developer.feedly.com/)
45 | `fitbit` | [http://dev.fitbit.com/](http://dev.fitbit.com/)
46 | `flattr` | [http://developers.flattr.net/](http://developers.flattr.net/)
47 | `flickr` | [https://www.flickr.com/services/api/](https://www.flickr.com/services/api/)
48 | `flowdock` | [https://www.flowdock.com/api](https://www.flowdock.com/api)
49 | `foursquare` | [https://developer.foursquare.com/](https://developer.foursquare.com/)
50 | `geeklist` | [http://hackers.geekli.st/](http://hackers.geekli.st/)
51 | `getpocket` | [http://getpocket.com/developer/](http://getpocket.com/developer/)
52 | `github` | [http://developer.github.com](http://developer.github.com)
53 | `gitter` | [https://developer.gitter.im/docs/welcome](https://developer.gitter.im/docs/welcome)
54 | `goodreads` | [https://www.goodreads.com/api](https://www.goodreads.com/api)
55 | `google` | [https://developers.google.com/](https://developers.google.com/)
56 | `harvest` | [https://github.com/harvesthq/api](https://github.com/harvesthq/api)
57 | `heroku` | [https://devcenter.heroku.com/categories/platform-api](https://devcenter.heroku.com/categories/platform-api)
58 | `imgur` | [https://api.imgur.com/](https://api.imgur.com/)
59 | `instagram` | [http://instagram.com/developer](http://instagram.com/developer)
60 | `jawbone` | [https://jawbone.com/up/developer/](https://jawbone.com/up/developer/)
61 | `linkedin` | [http://developer.linkedin.com](http://developer.linkedin.com)
62 | `live` | [http://msdn.microsoft.com/en-us/library/dn783283.aspx](http://msdn.microsoft.com/en-us/library/dn783283.aspx)
63 | `mailchimp` | [http://apidocs.mailchimp.com/](http://apidocs.mailchimp.com/)
64 | `meetup` | [http://www.meetup.com/meetup_api/](http://www.meetup.com/meetup_api/)
65 | `mixcloud` | [http://www.mixcloud.com/developers/](http://www.mixcloud.com/developers/)
66 | `odesk` | [https://developers.odesk.com](https://developers.odesk.com)
67 | `openstreetmap` | [http://wiki.openstreetmap.org/wiki/API_v0.6](http://wiki.openstreetmap.org/wiki/API_v0.6)
68 | `paypal` | [https://developer.paypal.com/docs/](https://developer.paypal.com/docs/)
69 | `podio` | [https://developers.podio.com/](https://developers.podio.com/)
70 | `rdio` | [http://www.rdio.com/developers/](http://www.rdio.com/developers/)
71 | `redbooth` | [https://redbooth.com/api/](https://redbooth.com/api/)
72 | `reddit` | [http://www.reddit.com/dev/api](http://www.reddit.com/dev/api)
73 | `runkeeper` | [http://developer.runkeeper.com/healthgraph/overview](http://developer.runkeeper.com/healthgraph/overview)
74 | `salesforce` | [https://www.salesforce.com/us/developer/docs/api_rest](https://www.salesforce.com/us/developer/docs/api_rest)
75 | `shopify` | [http://docs.shopify.com/api](http://docs.shopify.com/api)
76 | `skyrock` | [http://www.skyrock.com/developer/documentation/](http://www.skyrock.com/developer/documentation/)
77 | `slack` | [https://api.slack.com/](https://api.slack.com/)
78 | `slice` | [https://developer.slice.com/](https://developer.slice.com/)
79 | `soundcloud` | [http://developers.soundcloud.com](http://developers.soundcloud.com)
80 | `spotify` | [https://developer.spotify.com](https://developer.spotify.com)
81 | `stackexchange` | [https://api.stackexchange.com](https://api.stackexchange.com)
82 | `stocktwits` | [http://stocktwits.com/developers](http://stocktwits.com/developers)
83 | `strava` | [http://strava.github.io/api/](http://strava.github.io/api/)
84 | `stripe` | [https://stripe.com/docs](https://stripe.com/docs)
85 | `traxo` | [https://developer.traxo.com/](https://developer.traxo.com/)
86 | `trello` | [https://trello.com/docs/](https://trello.com/docs/)
87 | `tripit` | [https://www.tripit.com/developer](https://www.tripit.com/developer)
88 | `tumblr` | [http://www.tumblr.com/docs/en/api/v2](http://www.tumblr.com/docs/en/api/v2)
89 | `twitch` | [https://github.com/justintv/twitch-api](https://github.com/justintv/twitch-api)
90 | `twitter` | [https://dev.twitter.com](https://dev.twitter.com)
91 | `uber` | [https://developer.uber.com/v1/api-reference/](https://developer.uber.com/v1/api-reference/)
92 | `vimeo` | [https://developer.vimeo.com/](https://developer.vimeo.com/)
93 | `vk` | [http://vk.com/dev](http://vk.com/dev)
94 | `withings` | [http://oauth.withings.com/api](http://oauth.withings.com/api)
95 | `wordpress` | [https://developer.wordpress.com/docs/api/](https://developer.wordpress.com/docs/api/)
96 | `xing` | [https://dev.xing.com/docs](https://dev.xing.com/docs)
97 | `yahoo` | [https://developer.yahoo.com/](https://developer.yahoo.com/)
98 | `yammer` | [https://developer.yammer.com/](https://developer.yammer.com/)
99 | `yandex` | [http://api.yandex.com/](http://api.yandex.com/)
100 | `zendesk` | [https://developer.zendesk.com/rest_api/docs/core/introduction](https://developer.zendesk.com/rest_api/docs/core/introduction)
101 |
102 | Usage
103 | -----
104 |
105 | With [martini](https://github.com/go-martini/martini):
106 |
107 | `server.go`
108 | ```go
109 | package main
110 |
111 | import (
112 | "encoding/json"
113 | "github.com/go-martini/martini"
114 | "github.com/gophergala/authy/martini"
115 | "github.com/martini-contrib/render"
116 | "github.com/martini-contrib/sessions"
117 | "os"
118 | )
119 |
120 | type Config struct {
121 | Secret string `json:"secret"`
122 | Authy authy.Config `json:"authy"`
123 | }
124 |
125 | func readConfig() (Config, error) {
126 | f, err := os.Open("config.json")
127 | if err != nil {
128 | return Config{}, err
129 | }
130 |
131 | decoder := json.NewDecoder(f)
132 |
133 | var config Config
134 | decoder.Decode(&config)
135 |
136 | return config, nil
137 | }
138 |
139 | func main() {
140 | // read app config (and authy config)
141 | config, err := readConfig()
142 | if err != nil {
143 | panic(err)
144 | }
145 |
146 | // setup Martini
147 | m := martini.Classic()
148 | m.Use(sessions.Sessions("authy", sessions.NewCookieStore([]byte(config.Secret))))
149 | // register our middleware
150 | m.Use(authy.Authy(config.Authy))
151 | m.Use(render.Renderer())
152 |
153 | // see the LoginRequired middleware, automatically redirect to the login page if necessary
154 | m.Get("/generic_callback", authy.LoginRequired(), func(token authy.Token, r render.Render) {
155 | r.HTML(200, "callback", token)
156 | })
157 |
158 | m.Run()
159 | }
160 | ```
161 |
162 | `templates/callback.tmpl`
163 | ```html
164 |
165 |
166 | {{.Value}} ({{.Scope}})
167 |
168 |
169 | ```
170 |
171 | `config.json`
172 | ```json
173 | {
174 | "authy": {
175 | "login_page": "/login",
176 | "callback": "/generic_callback",
177 | "providers": {
178 | "github": {
179 | "key": "my-app-key",
180 | "secret": "my-app-secret",
181 | "scope": ["repo", "user:email"]
182 | }
183 | }
184 | }
185 | }
186 | ```
--------------------------------------------------------------------------------
/authy.go:
--------------------------------------------------------------------------------
1 | // Package authy implements the base methods for implementing oauth providers
2 | // It is recommended instead to use one of the middlewares already provided by the package
3 | //
4 | // Middlewares:
5 | //
6 | // * Martini: https://github.com/gophergala/authy/martini
7 | //
8 | // For a full guide visit https://github.com/gophergala/authy
9 | package authy
10 |
11 | import (
12 | "errors"
13 | "fmt"
14 | "github.com/gophergala/authy/oauth2"
15 | "github.com/gophergala/authy/provider"
16 | "net/http"
17 | )
18 |
19 | // Authy represents the current configuration and cached provider data
20 | type Authy struct {
21 | config Config
22 | providers map[string]provider.ProviderConfig
23 | }
24 |
25 | // Token is returned on a successful auth
26 | type Token struct {
27 | // The provider on which that token can be used
28 | Provider string
29 | // The actual value of the token
30 | Value string
31 | // The scopes returned by the provider, some providers may allow the user to change the scope of an auth request
32 | // Make sure to check the available scopes before doing queries on their webservices
33 | Scope []string
34 | }
35 |
36 | // Parse the configuration and build the list of providers, return an Authy instance
37 | func NewAuthy(config Config) (Authy, error) {
38 | var availableProviders = map[string]provider.ProviderConfig{}
39 |
40 | // load all providers
41 | for providerName, providerConfig := range config.Providers {
42 | providerData, err := provider.GetProvider(providerName)
43 | if err != nil {
44 | return Authy{}, err
45 | }
46 | providerConfig.Provider = providerData
47 | availableProviders[providerName] = providerConfig
48 | }
49 |
50 | return Authy{
51 | config: config,
52 | providers: availableProviders,
53 | }, nil
54 | }
55 |
56 | // Generate a CSRF token and store it in the provided session object, return the authorisation URL
57 | // It should be noted that the session object should prevent the user from seeing the sum generated
58 | func (a Authy) Authorize(providerName string, session Session, r *http.Request) (string, error) {
59 | providerConfig, ok := a.providers[providerName]
60 | if ok != true {
61 | return "", errors.New(fmt.Sprintf("unknown provider %s", providerName))
62 | }
63 |
64 | if providerConfig.Provider.OAuth == 2 {
65 | state, err := oauth2.NewState()
66 | if err != nil {
67 | return "", err
68 | }
69 |
70 | // save authentication state in session
71 | session.Set("authy."+providerName+".state", state)
72 | providerConfig.State = state
73 |
74 | // generate authorisation URL
75 | redirectUrl, err := oauth2.AuthorizeURL(providerConfig, r)
76 |
77 | if err != nil {
78 | return "", err
79 | }
80 |
81 | return redirectUrl, nil
82 | }
83 |
84 | return "", errors.New("Not Implemented")
85 | }
86 |
87 | // Check the CSRF token then query the distant provider for an access token using the code that was provided by the
88 | // authorization API
89 | func (a Authy) Access(providerName string, session Session, r *http.Request) (Token, string, error) {
90 | providerConfig, ok := a.providers[providerName]
91 | if ok != true {
92 | return Token{}, "", errors.New(fmt.Sprintf("unknown provider %s", providerName))
93 | }
94 |
95 | if providerConfig.Provider.OAuth == 2 {
96 | // check the state parameter against CSRF
97 | state := session.Get("authy." + providerName + ".state")
98 | if state == nil {
99 | return Token{}, "", errors.New("state token is not set in session, possible CSRF")
100 | }
101 |
102 | stateParam := r.URL.Query().Get("state")
103 | if stateParam != state.(string) {
104 | return Token{}, "", errors.New("invalid state param provided, possible CSRF")
105 | }
106 | // we don't need it anymore
107 | session.Delete("authy." + providerName + ".state")
108 |
109 | code := r.URL.Query().Get("code")
110 | if code == "" {
111 | return Token{}, "", errors.New("code was not found in the query parameters")
112 | }
113 |
114 | // retrieve access token from provider
115 | token, err := oauth2.GetAccessToken(providerConfig, r)
116 | if err != nil {
117 | return Token{}, "", err
118 | }
119 |
120 | // provide the proper callback URL
121 | redirectUrl := a.config.Callback
122 | if providerConfig.Callback != "" {
123 | redirectUrl = providerConfig.Callback
124 | }
125 |
126 | // return the token
127 | return Token{
128 | Value: token.AccessToken,
129 | Scope: token.Scope,
130 | }, redirectUrl, nil
131 | }
132 |
133 | return Token{}, "", errors.New("Not Implemented")
134 | }
135 |
--------------------------------------------------------------------------------
/authy_test.go:
--------------------------------------------------------------------------------
1 | package authy_test
2 |
3 | import (
4 | "github.com/gophergala/authy"
5 | "github.com/gophergala/authy/provider"
6 | . "github.com/smartystreets/goconvey/convey"
7 | "net/url"
8 | "testing"
9 | )
10 |
11 | var config = authy.Config{
12 | PathLogin: "/login",
13 | Callback: "/login/success",
14 | Providers: map[string]provider.ProviderConfig{
15 | "github": provider.ProviderConfig{
16 | Key: "my-key",
17 | Secret: "my-secret",
18 | Scope: []string{"repo", "user:mail"},
19 | },
20 | },
21 | }
22 |
23 | var badConfig = authy.Config{
24 | Providers: map[string]provider.ProviderConfig{
25 | "invalid": provider.ProviderConfig{
26 | Key: "my-key",
27 | },
28 | },
29 | }
30 |
31 | func TestAuthy(t *testing.T) {
32 | Convey("Invalid configuration", t, func() {
33 | _, err := authy.NewAuthy(badConfig)
34 | So(err, ShouldNotEqual, nil)
35 | })
36 |
37 | Convey("Instanciate Authy", t, func() {
38 | a, err := authy.NewAuthy(config)
39 | So(err, ShouldEqual, nil)
40 |
41 | // create a fake session
42 | session := &FakeSession{
43 | items: map[interface{}]interface{}{},
44 | }
45 |
46 | // and a fake oauth
47 | server := FakeOAuthServer()
48 |
49 | // and even a fake github
50 | github, _ := provider.GetProvider("github")
51 | github.AuthorizeURL = server.URL + "/oauth2"
52 | github.AccessURL = server.URL + "/oauth2"
53 | provider.RegisterProvider(github)
54 |
55 | Convey("Try to get url for an invalid provider", func() {
56 | _, err := a.Authorize("bitbucket", session, FakeHttpRequest("http://localhost:2000/authy/bitbucket"))
57 | So(err, ShouldNotEqual, nil)
58 | })
59 |
60 | Convey("Get authorization url for GitHub", func() {
61 | _, err := a.Authorize("github", session, FakeHttpRequest("http://localhost:2000/authy/github"))
62 | So(err, ShouldEqual, nil)
63 |
64 | Convey("Get access token from an invalid provider", func() {
65 | _, _, err := a.Access("someone", session, FakeHttpRequest("http://localhost:2000/"))
66 | So(err, ShouldNotEqual, nil)
67 | })
68 |
69 | Convey("Get access token from GitHub", func() {
70 | _, _, err := a.Access("github", session, FakeHttpRequest("http://localhost:2000/authy/github/callback?code=foo&state="+url.QueryEscape(session.Get("authy.github.state").(string))))
71 | So(err, ShouldEqual, nil)
72 |
73 | Convey("State was deleted so second call should fail", func() {
74 | _, _, err := a.Access("github", session, FakeHttpRequest("http://localhost:2000/"))
75 | So(err, ShouldNotEqual, nil)
76 | server.Close()
77 | })
78 | })
79 |
80 | })
81 |
82 | })
83 | }
84 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package authy
2 |
3 | import (
4 | "github.com/gophergala/authy/provider"
5 | )
6 |
7 | // Configuration for authy, is already mapped for being parsed by encoding/json
8 | //
9 | // Example JSON file:
10 | //
11 | // {
12 | // "callback": "/login/success",
13 | // "providers": {
14 | // "github": {
15 | // "key": "be148a4abf2796b3a8e1",
16 | // "secret": "1bbf884bbf79ef21fef03410389eb451300abd84",
17 | // "scope": ["repo", "email"]
18 | // }
19 | // }
20 | // }
21 | type Config struct {
22 | // Where to redirect the user for login if supported by the middleware (defaults to /login)
23 | PathLogin string `json:"login"`
24 | // Which base route to use (defaults to /authy)
25 | BasePath string `json:"base_path"`
26 | // Where the user is redirected by default after a successful auth
27 | Callback string `json:"callback"`
28 | // A list of providers
29 | Providers map[string]provider.ProviderConfig `json:"providers"`
30 | }
31 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gophergala/authy/a3b9cd5ebf7cac83d28d5a326d6e28257946b9f8/logo.png
--------------------------------------------------------------------------------
/martini/example_test.go:
--------------------------------------------------------------------------------
1 | package authy_test
2 |
3 | import (
4 | "github.com/go-martini/martini"
5 | "github.com/gophergala/authy/martini"
6 | "github.com/gophergala/authy/provider"
7 | "github.com/martini-contrib/render"
8 | "github.com/martini-contrib/sessions"
9 | )
10 |
11 | var config = authy.Config{
12 | PathLogin: "/login",
13 | Callback: "/login/success",
14 | Providers: map[string]provider.ProviderConfig{
15 | "github": provider.ProviderConfig{
16 | Key: "my-key",
17 | Secret: "my-secret",
18 | Scope: []string{"repo", "user:mail"},
19 | },
20 | },
21 | }
22 |
23 | func ExampleAuthy() {
24 | m := martini.Classic()
25 |
26 | // the session need to be set for the CSRF token system to work
27 | m.Use(sessions.Sessions("authy", sessions.NewCookieStore([]byte("no one will guess this passphrase"))))
28 | m.Use(authy.Authy(config))
29 | }
30 |
31 | func ExampleLoginRequired() {
32 | m := martini.Classic()
33 |
34 | // the session need to be set for the CSRF token system to work
35 | m.Use(sessions.Sessions("authy", sessions.NewCookieStore([]byte("no one will guess this passphrase"))))
36 | m.Use(authy.Authy(config))
37 |
38 | // use authy.LoginRequired to redirect the user to the login page if not logged in
39 | m.Get("/profile", authy.LoginRequired(), func(token authy.Token, r render.Render) {
40 | r.HTML(200, "callback", token)
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/martini/martini.go:
--------------------------------------------------------------------------------
1 | // Implements several middlewares for using Authy with Martini
2 | package authy
3 |
4 | import (
5 | "github.com/go-martini/martini"
6 | "github.com/gophergala/authy"
7 | "github.com/martini-contrib/sessions"
8 | "net/http"
9 | "net/url"
10 | "regexp"
11 | "strings"
12 | )
13 |
14 | type Config authy.Config
15 | type Token authy.Token
16 |
17 | // Takes an Authy config and returns a middleware to use with martini
18 | // See examples below
19 | func Authy(config Config) martini.Handler {
20 | baseRoute := "/authy"
21 | if config.BasePath != "" {
22 | baseRoute = config.BasePath
23 | }
24 |
25 | // should be moved in the authy package
26 | if config.PathLogin == "" {
27 | config.PathLogin = "/login"
28 | }
29 |
30 | authRoute := regexp.MustCompile("^" + baseRoute + "/([^/#?]+)")
31 | callbackRoute := regexp.MustCompile("^" + baseRoute + "/([^/]+)/callback")
32 | authy, err := authy.NewAuthy(authy.Config(config))
33 |
34 | // due to the way middleware are used, it's the cleanest? way to deal with this?
35 | if err != nil {
36 | panic(err)
37 | }
38 |
39 | return func(s sessions.Session, c martini.Context, w http.ResponseWriter, r *http.Request) {
40 | c.Map(config)
41 |
42 | // if we are already logged, ignore login route matching
43 | if tokenValue := s.Get("authy.token.value"); tokenValue != nil {
44 | c.Map(Token{
45 | Provider: s.Get("authy.provider").(string),
46 | Value: tokenValue.(string),
47 | Scope: strings.Split(s.Get("authy.token.scope").(string), ","),
48 | })
49 | return
50 | }
51 |
52 | matches := authRoute.FindStringSubmatch(r.URL.Path)
53 | if len(matches) > 0 && matches[0] == r.URL.Path {
54 | redirectUrl, err := authy.Authorize(matches[1], s, r)
55 | if err != nil {
56 | panic(err)
57 | }
58 |
59 | // redirect user to oauth website
60 | http.Redirect(w, r, redirectUrl, http.StatusFound)
61 | return
62 | }
63 |
64 | matches = callbackRoute.FindStringSubmatch(r.URL.Path)
65 | if len(matches) > 0 && matches[0] == r.URL.Path {
66 | token, redirectUrl, err := authy.Access(matches[1], s, r)
67 | if err != nil {
68 | panic(err)
69 | }
70 |
71 | // save token in session
72 | s.Set("authy.provider", matches[1])
73 | s.Set("authy.token.value", token.Value)
74 | s.Set("authy.token.scope", strings.Join(token.Scope, ","))
75 |
76 | http.Redirect(w, r, redirectUrl, http.StatusFound)
77 | return
78 | }
79 | }
80 | }
81 |
82 | // Use this middleware on the routes where you need the user to be logged in
83 | func LoginRequired() martini.Handler {
84 | return func(config Config, s sessions.Session, w http.ResponseWriter, r *http.Request) {
85 | if tokenValue := s.Get("authy.token.value"); tokenValue == nil {
86 | next := url.QueryEscape(r.URL.RequestURI())
87 | http.Redirect(w, r, config.PathLogin+"?next="+next, http.StatusFound)
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/mock_test.go:
--------------------------------------------------------------------------------
1 | package authy_test
2 |
3 | import (
4 | "github.com/gorilla/mux"
5 | "net/http"
6 | "net/http/httptest"
7 | "net/url"
8 | )
9 |
10 | // a fake session object
11 | type FakeSession struct {
12 | items map[interface{}]interface{}
13 | }
14 |
15 | func (f *FakeSession) Get(key interface{}) interface{} {
16 | return f.items[key]
17 | }
18 |
19 | func (f *FakeSession) Set(key interface{}, val interface{}) {
20 | f.items[key] = val
21 | }
22 |
23 | func (f *FakeSession) Delete(key interface{}) {
24 | delete(f.items, key)
25 | }
26 |
27 | // generate a fake http request
28 | func FakeHttpRequest(requestUrl string) *http.Request {
29 | parsedUrl, _ := url.Parse(requestUrl)
30 | return &http.Request{
31 | URL: parsedUrl,
32 | }
33 | }
34 |
35 | // fake oauth2 service
36 | func FakeOAuthServer() (s *httptest.Server) {
37 | r := mux.NewRouter()
38 |
39 | r.HandleFunc("/oauth2", func(rw http.ResponseWriter, r *http.Request) {
40 | values := url.Values{}
41 | values.Set("access_token", "fakeaccesstoken")
42 | values.Set("scope", r.URL.Query().Get("scope"))
43 | values.Set("token_type", "example")
44 | rw.Write([]byte(values.Encode()))
45 | })
46 |
47 | s = httptest.NewServer(r)
48 | return
49 | }
50 |
--------------------------------------------------------------------------------
/oauth2/oauth2.go:
--------------------------------------------------------------------------------
1 | // This package partially implements OAuth2 for Authy
2 | package oauth2
3 |
4 | // see http://tools.ietf.org/html/rfc6749
5 |
6 | import (
7 | "crypto/rand"
8 | "encoding/hex"
9 | "errors"
10 | "fmt"
11 | "github.com/google/go-querystring/query"
12 | "github.com/gophergala/authy/provider"
13 | "io/ioutil"
14 | "net/http"
15 | "net/url"
16 | "strings"
17 | )
18 |
19 | type authorizationRequest struct {
20 | ClientId string `url:"client_id"`
21 | ResponseType string `url:"response_type"`
22 | RedirectURI string `url:"redirect_uri,omitempty"`
23 | Scope string `url:"scope,omitempty"`
24 | State string `url:"state,omitempty"`
25 | }
26 |
27 | type accessTokenRequest struct {
28 | ClientId string `url:"client_id"`
29 | ClientSecret string `url:"client_secret"`
30 | GrantType string `url:"grant_type"`
31 | Code string `url:"code"`
32 | RedirectURI string `url:"redirect_uri,omitempty"`
33 | }
34 |
35 | type Token struct {
36 | AccessToken string
37 | Scope []string
38 | Type string
39 | }
40 |
41 | // standard oauth2 error (http://tools.ietf.org/html/rfc6749#section-5.2)
42 | type Error struct {
43 | Code string
44 | Description string
45 | URI string
46 | }
47 |
48 | func (err Error) Error() string {
49 | msg := err.Code
50 | if err.Description != "" {
51 | msg += ": " + err.Description
52 | }
53 | if err.URI != "" {
54 | msg += " (see " + err.URI + ")"
55 | }
56 | return msg
57 | }
58 |
59 | func genCallbackURL(config provider.ProviderConfig, r *http.Request) string {
60 | var redirectURI = url.URL{
61 | Host: r.Host,
62 | Path: r.URL.Path + "/callback",
63 | }
64 |
65 | if _, ok := r.Header["X-HTTPS"]; r.TLS != nil || ok == true {
66 | redirectURI.Scheme = "https"
67 | } else {
68 | redirectURI.Scheme = "http"
69 | }
70 |
71 | return redirectURI.String()
72 | }
73 |
74 | // create a new random token for the CSRF check
75 | func NewState() (string, error) {
76 | rawState := make([]byte, 16)
77 | _, err := rand.Read(rawState)
78 | if err != nil {
79 | return "", err
80 | }
81 |
82 | return hex.EncodeToString(rawState), nil
83 | }
84 |
85 | // Generates the proper authorization URL for the given service
86 | func AuthorizeURL(config provider.ProviderConfig, r *http.Request) (dest string, err error) {
87 | // subdomain support
88 | baseUrl := config.Provider.AuthorizeURL
89 | if config.Provider.Subdomain == true {
90 | if config.Subdomain == "" {
91 | err = errors.New(fmt.Sprintf("provider %s expects the config to contain your subdomain", config.Provider.Name))
92 | return
93 | }
94 | baseUrl = strings.Replace(baseUrl, "[subdomain]", config.Subdomain, -1)
95 | }
96 |
97 | authUrl, err := url.Parse(baseUrl)
98 | if err != nil {
99 | return
100 | }
101 |
102 | values, err := query.Values(authorizationRequest{
103 | ClientId: config.Key,
104 | ResponseType: "code",
105 | RedirectURI: genCallbackURL(config, r),
106 | Scope: strings.Join(config.Scope, config.Provider.ScopeDelimiter),
107 | State: config.State,
108 | })
109 |
110 | // custom parameters
111 | if len(config.CustomParameters) > 0 {
112 | for _, name := range config.Provider.CustomParameters {
113 | if value, ok := config.CustomParameters[name]; ok == true {
114 | values.Set(name, value)
115 | }
116 | }
117 | }
118 |
119 | if err != nil {
120 | return
121 | }
122 |
123 | authUrl.RawQuery = values.Encode()
124 | dest = authUrl.String()
125 | return
126 | }
127 |
128 | // Query the remote service for an access token
129 | func GetAccessToken(config provider.ProviderConfig, r *http.Request) (token Token, err error) {
130 | queryValues, err := query.Values(accessTokenRequest{
131 | ClientId: config.Key,
132 | ClientSecret: config.Secret,
133 | Code: r.URL.Query().Get("code"),
134 | GrantType: "authorization_code",
135 | RedirectURI: genCallbackURL(config, r),
136 | })
137 |
138 | if err != nil {
139 | return
140 | }
141 |
142 | resp, err := http.PostForm(config.Provider.AccessURL, queryValues)
143 | if err != nil {
144 | return
145 | }
146 | defer resp.Body.Close()
147 |
148 | body, err := ioutil.ReadAll(resp.Body)
149 | if err != nil {
150 | return
151 | }
152 |
153 | values, err := url.ParseQuery(string(body))
154 | if err != nil {
155 | return
156 | }
157 |
158 | if _, ok := values["error"]; ok == true {
159 | err = Error{
160 | Code: values["error"][0],
161 | Description: values["error_description"][0],
162 | URI: values["error_uri"][0],
163 | }
164 | return
165 | }
166 |
167 | // everything went A-OK!
168 | token.AccessToken = values["access_token"][0]
169 | token.Type = values["token_type"][0]
170 |
171 | // TODO: maybe store the scope with the state token and set it there if not returned
172 | // by the service
173 | if scope, ok := values["scope"]; ok == true {
174 | token.Scope = strings.Split(scope[0], config.Provider.ScopeDelimiter)
175 | }
176 |
177 | return
178 | }
179 |
--------------------------------------------------------------------------------
/provider/default.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | var defaultProviders = map[string]Provider{
4 | "500px": Provider{
5 | Name: "500px",
6 | RequestURL: "https://api.500px.com/v1/oauth/request_token",
7 | AuthorizeURL: "https://api.500px.com/v1/oauth/authorize",
8 | AccessURL: "https://api.500px.com/v1/oauth/access_token",
9 | OAuth: 1,
10 | },
11 | "amazon": Provider{
12 | Name: "amazon",
13 | AuthorizeURL: "https://www.amazon.com/ap/oa",
14 | AccessURL: "https://api.amazon.com/auth/o2/token",
15 | OAuth: 2,
16 | ScopeDelimiter: " ",
17 | },
18 | "angellist": Provider{
19 | Name: "angellist",
20 | AuthorizeURL: "https://angel.co/api/oauth/authorize",
21 | AccessURL: "https://angel.co/api/oauth/token",
22 | OAuth: 2,
23 | ScopeDelimiter: " ",
24 | },
25 | "appnet": Provider{
26 | Name: "appnet",
27 | AuthorizeURL: "https://account.app.net/oauth/authenticate",
28 | AccessURL: "https://account.app.net/oauth/access_token",
29 | OAuth: 2,
30 | ScopeDelimiter: " ",
31 | },
32 | "asana": Provider{
33 | Name: "asana",
34 | AuthorizeURL: "https://app.asana.com/-/oauth_authorize",
35 | AccessURL: "https://app.asana.com/-/oauth_token",
36 | OAuth: 2,
37 | },
38 | "assembla": Provider{
39 | Name: "assembla",
40 | AuthorizeURL: "https://api.assembla.com/authorization",
41 | AccessURL: "https://_client_id:_client_secret@api.assembla.com/token",
42 | OAuth: 2,
43 | },
44 | "basecamp": Provider{
45 | Name: "basecamp",
46 | AuthorizeURL: "https://launchpad.37signals.com/authorization/new",
47 | AccessURL: "https://launchpad.37signals.com/authorization/token",
48 | OAuth: 2,
49 | },
50 | "bitbucket": Provider{
51 | Name: "bitbucket",
52 | RequestURL: "https://bitbucket.org/api/1.0/oauth/request_token",
53 | AuthorizeURL: "https://bitbucket.org/api/1.0/oauth/authenticate",
54 | AccessURL: "https://bitbucket.org/api/1.0/oauth/access_token",
55 | OAuth: 1,
56 | },
57 | "bitly": Provider{
58 | Name: "bitly",
59 | AuthorizeURL: "https://bitly.com/oauth/authorize",
60 | AccessURL: "https://api-ssl.bitly.com/oauth/access_token",
61 | OAuth: 2,
62 | },
63 | "box": Provider{
64 | Name: "box",
65 | AuthorizeURL: "https://api.box.com/oauth2/authorize",
66 | AccessURL: "https://api.box.com/oauth2/token",
67 | OAuth: 2,
68 | },
69 | "buffer": Provider{
70 | Name: "buffer",
71 | AuthorizeURL: "https://bufferapp.com/oauth2/authorize",
72 | AccessURL: "https://api.bufferapp.com/1/oauth2/token.json",
73 | OAuth: 2,
74 | },
75 | "cheddar": Provider{
76 | Name: "cheddar",
77 | AuthorizeURL: "https://api.cheddarapp.com/oauth/authorize",
78 | AccessURL: "https://api.cheddarapp.com/oauth/token",
79 | OAuth: 2,
80 | },
81 | "coinbase": Provider{
82 | Name: "coinbase",
83 | AuthorizeURL: "https://www.coinbase.com/oauth/authorize",
84 | AccessURL: "https://www.coinbase.com/oauth/token",
85 | OAuth: 2,
86 | ScopeDelimiter: " ",
87 | CustomParameters: []string{"meta"},
88 | },
89 | "dailymile": Provider{
90 | Name: "dailymile",
91 | AuthorizeURL: "https://api.dailymile.com/oauth/authorize",
92 | AccessURL: "https://api.dailymile.com/oauth/token",
93 | OAuth: 2,
94 | },
95 | "dailymotion": Provider{
96 | Name: "dailymotion",
97 | AuthorizeURL: "https://www.dailymotion.com/oauth/authorize",
98 | AccessURL: "https://api.dailymotion.com/oauth/token",
99 | OAuth: 2,
100 | },
101 | "deezer": Provider{
102 | Name: "deezer",
103 | AuthorizeURL: "https://connect.deezer.com/oauth/auth.php",
104 | AccessURL: "https://connect.deezer.com/oauth/access_token.php",
105 | OAuth: 2,
106 | },
107 | "deviantart": Provider{
108 | Name: "deviantart",
109 | AuthorizeURL: "https://www.deviantart.com/oauth2/authorize",
110 | AccessURL: "https://www.deviantart.com/oauth2/token",
111 | OAuth: 2,
112 | ScopeDelimiter: " ",
113 | },
114 | "digitalocean": Provider{
115 | Name: "digitalocean",
116 | AuthorizeURL: "https://cloud.digitalocean.com/v1/oauth/authorize",
117 | AccessURL: "https://cloud.digitalocean.com/v1/oauth/token",
118 | OAuth: 2,
119 | ScopeDelimiter: " ",
120 | },
121 | "disqus": Provider{
122 | Name: "disqus",
123 | AuthorizeURL: "https://disqus.com/api/oauth/2.0/authorize/",
124 | AccessURL: "https://disqus.com/api/oauth/2.0/access_token/",
125 | OAuth: 2,
126 | },
127 | "dropbox": Provider{
128 | Name: "dropbox",
129 | AuthorizeURL: "https://www.dropbox.com/1/oauth2/authorize",
130 | AccessURL: "https://api.dropbox.com/1/oauth2/token",
131 | OAuth: 2,
132 | },
133 | "edmodo": Provider{
134 | Name: "edmodo",
135 | AuthorizeURL: "https://api.edmodo.com/oauth/authorize",
136 | AccessURL: "https://api.edmodo.com/oauth/token",
137 | OAuth: 2,
138 | ScopeDelimiter: " ",
139 | },
140 | "elance": Provider{
141 | Name: "elance",
142 | AuthorizeURL: "https://api.elance.com/api2/oauth/authorize",
143 | AccessURL: "https://api.elance.com/api2/oauth/token",
144 | OAuth: 2,
145 | },
146 | "eventbrite": Provider{
147 | Name: "eventbrite",
148 | AuthorizeURL: "https://www.eventbrite.com/oauth/authorize",
149 | AccessURL: "https://www.eventbrite.com/oauth/token",
150 | OAuth: 2,
151 | },
152 | "evernote": Provider{
153 | Name: "evernote",
154 | RequestURL: "https://www.evernote.com/oauth",
155 | AuthorizeURL: "https://www.evernote.com/OAuth.action",
156 | AccessURL: "https://www.evernote.com/oauth",
157 | OAuth: 1,
158 | },
159 | "everyplay": Provider{
160 | Name: "everyplay",
161 | AuthorizeURL: "https://everyplay.com/connect",
162 | AccessURL: "https://api.everyplay.com/oauth2/access_token",
163 | OAuth: 2,
164 | },
165 | "eyeem": Provider{
166 | Name: "eyeem",
167 | AuthorizeURL: "http://www.eyeem.com/oauth/authorize",
168 | AccessURL: "http://api.eyeem.com/v2/oauth/token",
169 | OAuth: 2,
170 | },
171 | "facebook": Provider{
172 | Name: "facebook",
173 | AuthorizeURL: "https://www.facebook.com/dialog/oauth",
174 | AccessURL: "https://graph.facebook.com/oauth/access_token",
175 | OAuth: 2,
176 | },
177 | "feedly": Provider{
178 | Name: "feedly",
179 | AuthorizeURL: "https://cloud.feedly.com/v3/auth/auth",
180 | AccessURL: "http://cloud.feedly.com/v3/auth/token",
181 | OAuth: 2,
182 | },
183 | "fitbit": Provider{
184 | Name: "fitbit",
185 | RequestURL: "https://api.fitbit.com/oauth/request_token",
186 | AuthorizeURL: "https://www.fitbit.com/oauth/authorize",
187 | AccessURL: "https://api.fitbit.com/oauth/access_token",
188 | OAuth: 1,
189 | },
190 | "flattr": Provider{
191 | Name: "flattr",
192 | AuthorizeURL: "https://flattr.com/oauth/authorize",
193 | AccessURL: "https://flattr.com/oauth/token",
194 | OAuth: 2,
195 | ScopeDelimiter: " ",
196 | },
197 | "flickr": Provider{
198 | Name: "flickr",
199 | RequestURL: "https://www.flickr.com/services/oauth/request_token",
200 | AuthorizeURL: "https://www.flickr.com/services/oauth/authorize",
201 | AccessURL: "https://www.flickr.com/services/oauth/access_token",
202 | OAuth: 1,
203 | CustomParameters: []string{"perms"},
204 | },
205 | "flowdock": Provider{
206 | Name: "flowdock",
207 | AuthorizeURL: "https://api.flowdock.com/oauth/authorize",
208 | AccessURL: "https://api.flowdock.com/oauth/token",
209 | OAuth: 2,
210 | ScopeDelimiter: " ",
211 | },
212 | "foursquare": Provider{
213 | Name: "foursquare",
214 | AuthorizeURL: "https://foursquare.com/oauth2/authenticate",
215 | AccessURL: "https://foursquare.com/oauth2/access_token",
216 | OAuth: 2,
217 | },
218 | "freshbooks": Provider{
219 | Name: "freshbooks",
220 | RequestURL: "https://[subdomain].freshbooks.com/oauth/oauth_request.php",
221 | AuthorizeURL: "https://[subdomain].freshbooks.com/oauth/oauth_authorize.php",
222 | AccessURL: "https://[subdomain].freshbooks.com/oauth/oauth_access.php",
223 | OAuth: 1,
224 | Subdomain: true,
225 | },
226 | "geeklist": Provider{
227 | Name: "geeklist",
228 | RequestURL: "http://api.geekli.st/v1/oauth/request_token",
229 | AuthorizeURL: "https://geekli.st/oauth/authorize",
230 | AccessURL: "http://api.geekli.st/v1/oauth/access_token",
231 | OAuth: 1,
232 | },
233 | "getpocket": Provider{
234 | Name: "getpocket",
235 | RequestURL: "https://getpocket.com/v3/oauth/request",
236 | AuthorizeURL: "https://getpocket.com/auth/authorize",
237 | AccessURL: "https://getpocket.com/v3/oauth/authorize",
238 | },
239 | "github": Provider{
240 | Name: "github",
241 | AuthorizeURL: "https://github.com/login/oauth/authorize",
242 | AccessURL: "https://github.com/login/oauth/access_token",
243 | OAuth: 2,
244 | },
245 | "gitter": Provider{
246 | Name: "gitter",
247 | AuthorizeURL: "https://gitter.im/login/oauth/authorize",
248 | AccessURL: "https://gitter.im/login/oauth/token",
249 | OAuth: 2,
250 | },
251 | "goodreads": Provider{
252 | Name: "goodreads",
253 | RequestURL: "http://www.goodreads.com/oauth/request_token",
254 | AuthorizeURL: "http://www.goodreads.com/oauth/authorize",
255 | AccessURL: "http://www.goodreads.com/oauth/access_token",
256 | OAuth: 1,
257 | },
258 | "google": Provider{
259 | Name: "google",
260 | AuthorizeURL: "https://accounts.google.com/o/oauth2/auth",
261 | AccessURL: "https://accounts.google.com/o/oauth2/token",
262 | OAuth: 2,
263 | ScopeDelimiter: " ",
264 | CustomParameters: []string{"access_type"},
265 | },
266 | "harvest": Provider{
267 | Name: "harvest",
268 | AuthorizeURL: "https://api.harvestapp.com/oauth2/authorize",
269 | AccessURL: "https://api.harvestapp.com/oauth2/token",
270 | OAuth: 2,
271 | },
272 | "heroku": Provider{
273 | Name: "heroku",
274 | AuthorizeURL: "https://id.heroku.com/oauth/authorize",
275 | AccessURL: "https://id.heroku.com/oauth/token",
276 | OAuth: 2,
277 | },
278 | "imgur": Provider{
279 | Name: "imgur",
280 | AuthorizeURL: "https://api.imgur.com/oauth2/authorize",
281 | AccessURL: "https://api.imgur.com/oauth2/token",
282 | OAuth: 2,
283 | },
284 | "instagram": Provider{
285 | Name: "instagram",
286 | AuthorizeURL: "https://api.instagram.com/oauth/authorize",
287 | AccessURL: "https://api.instagram.com/oauth/access_token",
288 | OAuth: 2,
289 | ScopeDelimiter: " ",
290 | },
291 | "jawbone": Provider{
292 | Name: "jawbone",
293 | AuthorizeURL: "https://jawbone.com/auth/oauth2/auth",
294 | AccessURL: "https://jawbone.com/auth/oauth2/token",
295 | OAuth: 2,
296 | ScopeDelimiter: " ",
297 | },
298 | "linkedin": Provider{
299 | Name: "linkedin",
300 | RequestURL: "https://api.linkedin.com/uas/oauth/requestToken",
301 | AuthorizeURL: "https://www.linkedin.com/uas/oauth/authenticate",
302 | AccessURL: "https://api.linkedin.com/uas/oauth/accessToken",
303 | OAuth: 1,
304 | },
305 | "linkedin2": Provider{
306 | Name: "linkedin2",
307 | AuthorizeURL: "https://www.linkedin.com/uas/oauth2/authorization",
308 | AccessURL: "https://www.linkedin.com/uas/oauth2/accessToken",
309 | OAuth: 2,
310 | ScopeDelimiter: " ",
311 | },
312 | "live": Provider{
313 | Name: "live",
314 | AuthorizeURL: "https://login.live.com/oauth20_authorize.srf",
315 | AccessURL: "https://login.live.com/oauth20_token.srf",
316 | OAuth: 2,
317 | },
318 | "mailchimp": Provider{
319 | Name: "mailchimp",
320 | AuthorizeURL: "https://login.mailchimp.com/oauth2/authorize",
321 | AccessURL: "https://login.mailchimp.com/oauth2/token",
322 | OAuth: 2,
323 | },
324 | "meetup": Provider{
325 | Name: "meetup",
326 | AuthorizeURL: "https://secure.meetup.com/oauth2/authorize",
327 | AccessURL: "https://secure.meetup.com/oauth2/access",
328 | OAuth: 2,
329 | ScopeDelimiter: " ",
330 | },
331 | "mixcloud": Provider{
332 | Name: "mixcloud",
333 | AuthorizeURL: "https://www.mixcloud.com/oauth/authorize",
334 | AccessURL: "https://www.mixcloud.com/oauth/access_token",
335 | OAuth: 2,
336 | },
337 | "odesk": Provider{
338 | Name: "odesk",
339 | RequestURL: "https://www.odesk.com/api/auth/v1/oauth/token/request",
340 | AuthorizeURL: "https://www.odesk.com/services/api/auth",
341 | AccessURL: "https://www.odesk.com/api/auth/v1/oauth/token/access",
342 | OAuth: 1,
343 | },
344 | "openstreetmap": Provider{
345 | Name: "openstreetmap",
346 | RequestURL: "http://www.openstreetmap.org/oauth/request_token",
347 | AuthorizeURL: "http://www.openstreetmap.org/oauth/authorize",
348 | AccessURL: "http://www.openstreetmap.org/oauth/access_token",
349 | OAuth: 1,
350 | },
351 | "paypal": Provider{
352 | Name: "paypal",
353 | AuthorizeURL: "https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize",
354 | AccessURL: "https://api.paypal.com/v1/identity/openidconnect/tokenservice",
355 | OAuth: 2,
356 | ScopeDelimiter: " ",
357 | },
358 | "podio": Provider{
359 | Name: "podio",
360 | AuthorizeURL: "https://podio.com/oauth/authorize",
361 | AccessURL: "https://podio.com/oauth/token",
362 | OAuth: 2,
363 | },
364 | "rdio": Provider{
365 | Name: "rdio",
366 | RequestURL: "http://api.rdio.com/oauth/request_token",
367 | AuthorizeURL: "https://www.rdio.com/oauth/authorize",
368 | AccessURL: "http://api.rdio.com/oauth/access_token",
369 | OAuth: 1,
370 | },
371 | "redbooth": Provider{
372 | Name: "redbooth",
373 | AuthorizeURL: "https://redbooth.com/oauth2/authorize",
374 | AccessURL: "https://redbooth.com/oauth2/token",
375 | OAuth: 2,
376 | },
377 | "reddit": Provider{
378 | Name: "reddit",
379 | AuthorizeURL: "https://ssl.reddit.com/api/v1/authorize",
380 | AccessURL: "https://ssl.reddit.com/api/v1/access_token",
381 | OAuth: 2,
382 | CustomParameters: []string{"duration"},
383 | },
384 | "runkeeper": Provider{
385 | Name: "runkeeper",
386 | AuthorizeURL: "https://runkeeper.com/apps/authorize",
387 | AccessURL: "https://runkeeper.com/apps/token",
388 | OAuth: 2,
389 | },
390 | "salesforce": Provider{
391 | Name: "salesforce",
392 | AuthorizeURL: "https://login.salesforce.com/services/oauth2/authorize",
393 | AccessURL: "https://login.salesforce.com/services/oauth2/token",
394 | OAuth: 2,
395 | ScopeDelimiter: " ",
396 | },
397 | "shopify": Provider{
398 | Name: "shopify",
399 | AuthorizeURL: "https://[subdomain].myshopify.com/admin/oauth/authorize",
400 | AccessURL: "https://[subdomain].myshopify.com/admin/oauth/access_token",
401 | OAuth: 2,
402 | Subdomain: true,
403 | },
404 | "skyrock": Provider{
405 | Name: "skyrock",
406 | RequestURL: "https://api.skyrock.com/v2/oauth/initiate",
407 | AuthorizeURL: "https://api.skyrock.com/v2/oauth/authorize",
408 | AccessURL: "https://api.skyrock.com/v2/oauth/token",
409 | OAuth: 1,
410 | },
411 | "slack": Provider{
412 | Name: "slack",
413 | AuthorizeURL: "https://slack.com/oauth/authorize",
414 | AccessURL: "https://slack.com/api/oauth.access",
415 | OAuth: 2,
416 | },
417 | "slice": Provider{
418 | Name: "slice",
419 | AuthorizeURL: "https://api.slice.com/oauth/authorize",
420 | AccessURL: "https://api.slice.com/oauth/token",
421 | OAuth: 2,
422 | },
423 | "soundcloud": Provider{
424 | Name: "soundcloud",
425 | AuthorizeURL: "https://soundcloud.com/connect",
426 | AccessURL: "https://api.soundcloud.com/oauth2/token",
427 | OAuth: 2,
428 | },
429 | "spotify": Provider{
430 | Name: "spotify",
431 | AuthorizeURL: "https://accounts.spotify.com/authorize",
432 | AccessURL: "https://accounts.spotify.com/api/token",
433 | OAuth: 2,
434 | ScopeDelimiter: " ",
435 | CustomParameters: []string{"show_dialog"},
436 | },
437 | "stackexchange": Provider{
438 | Name: "stackexchange",
439 | AuthorizeURL: "https://stackexchange.com/oauth",
440 | AccessURL: "https://stackexchange.com/oauth/access_token",
441 | OAuth: 2,
442 | },
443 | "stocktwits": Provider{
444 | Name: "stocktwits",
445 | AuthorizeURL: "https://api.stocktwits.com/api/2/oauth/authorize",
446 | AccessURL: "https://api.stocktwits.com/api/2/oauth/token",
447 | OAuth: 2,
448 | },
449 | "strava": Provider{
450 | Name: "strava",
451 | AuthorizeURL: "https://www.strava.com/oauth/authorize",
452 | AccessURL: "https://www.strava.com/oauth/token",
453 | OAuth: 2,
454 | },
455 | "stripe": Provider{
456 | Name: "stripe",
457 | AuthorizeURL: "https://connect.stripe.com/oauth/authorize",
458 | AccessURL: "https://connect.stripe.com/oauth/token",
459 | OAuth: 2,
460 | },
461 | "traxo": Provider{
462 | Name: "traxo",
463 | AuthorizeURL: "https://www.traxo.com/oauth/authenticate",
464 | AccessURL: "https://www.traxo.com/oauth/token",
465 | OAuth: 2,
466 | },
467 | "trello": Provider{
468 | Name: "trello",
469 | RequestURL: "https://trello.com/1/OAuthGetRequestToken",
470 | AuthorizeURL: "https://trello.com/1/OAuthAuthorizeToken",
471 | AccessURL: "https://trello.com/1/OAuthGetAccessToken",
472 | OAuth: 1,
473 | CustomParameters: []string{"scope", "expiration"},
474 | },
475 | "tripit": Provider{
476 | Name: "tripit",
477 | RequestURL: "https://api.tripit.com/oauth/request_token",
478 | AuthorizeURL: "https://www.tripit.com/oauth/authorize",
479 | AccessURL: "https://api.tripit.com/oauth/access_token",
480 | OAuth: 1,
481 | },
482 | "tumblr": Provider{
483 | Name: "tumblr",
484 | RequestURL: "http://www.tumblr.com/oauth/request_token",
485 | AuthorizeURL: "http://www.tumblr.com/oauth/authorize",
486 | AccessURL: "http://www.tumblr.com/oauth/access_token",
487 | OAuth: 1,
488 | },
489 | "twitch": Provider{
490 | Name: "twitch",
491 | AuthorizeURL: "https://api.twitch.tv/kraken/oauth2/authorize",
492 | AccessURL: "https://api.twitch.tv/kraken/oauth2/token",
493 | OAuth: 2,
494 | ScopeDelimiter: " ",
495 | },
496 | "twitter": Provider{
497 | Name: "twitter",
498 | RequestURL: "https://api.twitter.com/oauth/request_token",
499 | AuthorizeURL: "https://api.twitter.com/oauth/authenticate",
500 | AccessURL: "https://api.twitter.com/oauth/access_token",
501 | OAuth: 1,
502 | },
503 | "uber": Provider{
504 | Name: "uber",
505 | AuthorizeURL: "https://login.uber.com/oauth/authorize",
506 | AccessURL: "https://login.uber.com/oauth/token",
507 | OAuth: 2,
508 | },
509 | "vimeo": Provider{
510 | Name: "vimeo",
511 | AuthorizeURL: "https://api.vimeo.com/oauth/authorize",
512 | AccessURL: "https://api.vimeo.com/oauth/access_token",
513 | OAuth: 2,
514 | ScopeDelimiter: " ",
515 | },
516 | "vk": Provider{
517 | Name: "vk",
518 | AuthorizeURL: "https://oauth.vk.com/authorize",
519 | AccessURL: "https://oauth.vk.com/access_token",
520 | OAuth: 2,
521 | },
522 | "withings": Provider{
523 | Name: "withings",
524 | RequestURL: "https://oauth.withings.com/account/request_token",
525 | AuthorizeURL: "https://oauth.withings.com/account/authorize",
526 | AccessURL: "https://oauth.withings.com/account/access_token",
527 | OAuth: 1,
528 | },
529 | "wordpress": Provider{
530 | Name: "wordpress",
531 | AuthorizeURL: "https://public-api.wordpress.com/oauth2/authorize",
532 | AccessURL: "https://public-api.wordpress.com/oauth2/token",
533 | OAuth: 2,
534 | CustomParameters: []string{"blog"},
535 | },
536 | "xing": Provider{
537 | Name: "xing",
538 | RequestURL: "https://api.xing.com/v1/request_token",
539 | AuthorizeURL: "https://api.xing.com/v1/authorize",
540 | AccessURL: "https://api.xing.com/v1/access_token",
541 | OAuth: 1,
542 | },
543 | "yahoo": Provider{
544 | Name: "yahoo",
545 | RequestURL: "https://api.login.yahoo.com/oauth/v2/get_request_token",
546 | AuthorizeURL: "https://api.login.yahoo.com/oauth/v2/request_auth",
547 | AccessURL: "https://api.login.yahoo.com/oauth/v2/get_token",
548 | OAuth: 1,
549 | },
550 | "yammer": Provider{
551 | Name: "yammer",
552 | AuthorizeURL: "https://www.yammer.com/dialog/oauth",
553 | AccessURL: "https://www.yammer.com/oauth2/access_token.json",
554 | OAuth: 2,
555 | },
556 | "yandex": Provider{
557 | Name: "yandex",
558 | AuthorizeURL: "https://oauth.yandex.ru/authorize",
559 | AccessURL: "https://oauth.yandex.ru/token",
560 | OAuth: 2,
561 | },
562 | "zendesk": Provider{
563 | Name: "zendesk",
564 | AuthorizeURL: "https://[subdomain].zendesk.com/oauth/authorizations/new",
565 | AccessURL: "https://[subdomain].zendesk.com/oauth/tokens",
566 | OAuth: 2,
567 | ScopeDelimiter: " ",
568 | Subdomain: true,
569 | },
570 | }
571 |
--------------------------------------------------------------------------------
/provider/provider.go:
--------------------------------------------------------------------------------
1 | // The provider package contains the list of service providers and their base configuration
2 | package provider
3 |
4 | import (
5 | "errors"
6 | "fmt"
7 | )
8 |
9 | // Contains implementation details to be used by Authy
10 | type Provider struct {
11 | Name string
12 | RequestURL string
13 | AuthorizeURL string
14 | AccessURL string
15 | OAuth int
16 | ScopeDelimiter string
17 | Subdomain bool
18 | CustomParameters []string
19 | }
20 |
21 | // Those keys are imported from your config, set the proper ones based on your provider's oauth information
22 | type ProviderConfig struct {
23 | Provider Provider `json:"-"`
24 | Key string `json:"key"`
25 | Secret string `json:"secret"`
26 | Scope []string `json:"scope"`
27 | State string `json:"state"`
28 | Callback string `json:"callback"`
29 | Subdomain string `json:"subdomain"`
30 | CustomParameters map[string]string `json:"custom_parameters"`
31 | }
32 |
33 | var customProviders = map[string]Provider{}
34 |
35 | // Get a provider by name
36 | func GetProvider(name string) (Provider, error) {
37 | provider, ok := customProviders[name]
38 | if ok != true {
39 | provider, ok = defaultProviders[name]
40 | }
41 |
42 | if ok != true {
43 | return Provider{}, errors.New(fmt.Sprintf("unknown provider: %s", name))
44 | }
45 |
46 | if provider.ScopeDelimiter == "" {
47 | provider.ScopeDelimiter = ","
48 | }
49 | return provider, nil
50 | }
51 |
52 | // Register a custom provider, takes precedence on default providers
53 | func RegisterProvider(provider Provider) error {
54 | if provider.Name == "" {
55 | return errors.New("custom provider's name cannot be empty")
56 | }
57 |
58 | customProviders[provider.Name] = provider
59 | return nil
60 | }
61 |
--------------------------------------------------------------------------------
/session.go:
--------------------------------------------------------------------------------
1 | package authy
2 |
3 | // The session object is used to store the CSRF token used by OAuth2
4 | type Session interface {
5 | // Get a key from the session
6 | Get(key interface{}) interface{}
7 | // Set a key in the session
8 | Set(key interface{}, value interface{})
9 | // Unset a key from the session
10 | Delete(key interface{})
11 | }
12 |
--------------------------------------------------------------------------------