├── .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 | --------------------------------------------------------------------------------