From 5e00c50c3751c8963d8ef73758cca20afce4c08d Mon Sep 17 00:00:00 2001 From: kataras Date: Wed, 7 Jun 2017 22:14:13 +0300 Subject: [PATCH] Add OAuth2 integration example, have fun! https://github.com/iris-contrib/community-board/issues/4 Former-commit-id: e4f9dfbdfe16c9ca1ad84ac5a844a5d060fb3b5e --- README.md | 2 +- _examples/README.md | 1 + _examples/intermediate/oauth2/main.go | 409 ++++++++++++++++++ .../intermediate/oauth2/templates/index.html | 3 + .../intermediate/oauth2/templates/user.html | 11 + .../sessions/securecookie/main.go | 11 +- 6 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 _examples/intermediate/oauth2/main.go create mode 100644 _examples/intermediate/oauth2/templates/index.html create mode 100644 _examples/intermediate/oauth2/templates/user.html diff --git a/README.md b/README.md index e364286f..db179b40 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ $ rizla main.go
Legends -I'm sorry for taking this personally but I really need to thanks each one of them because they stood up [♡](https://github.com/kataras/iris#support) for me when others trying to "bullying" my personality in order to deflame Iris. +I'm sorry for taking this personally but I really need to thank each one of them because they stood up [♡](https://github.com/kataras/iris#support) for me when others tried to "bullying" my personality in order to deflame Iris. All of us should read and respect the official [golang](https://golang.org/conduct) and [iris](https://github.com/iris-contrib/community-board/blob/master/CODE-OF-CONDUCT.md) community **Code of Conduct**. This type of commitment and communication is the way of making Go great. diff --git a/_examples/README.md b/_examples/README.md index f7c34b02..b3ea04de 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -44,6 +44,7 @@ It doesn't contains "best ways" neither explains all its features. It's just a s * [Request Logger](beginner/request-logger/main.go) * [Basic Authentication](beginner/basicauth/main.go) * [Level: Intermediate](intermediate) + * [OAUth2](intermediate/oauth2/main.go) * [Transactions](intermediate/transactions/main.go) * [HTTP Testing](intermediate/httptest/main_test.go) * [Watch & Compile Typescript source files](intermediate/typescript/main.go) diff --git a/_examples/intermediate/oauth2/main.go b/_examples/intermediate/oauth2/main.go new file mode 100644 index 00000000..f592f8f2 --- /dev/null +++ b/_examples/intermediate/oauth2/main.go @@ -0,0 +1,409 @@ +package main + +// Any OAuth2 (even the pure golang/x/net/oauth2) package +// can be used with Iris but at this example we will see the markbates' goth: +// +// $ go get github.com/markbates/goth/... +// +// This OAuth2 example works with sessions, so we will need +// to attach a session manager. +// Optionally: for even more secure session values, +// developers can use any third-party package to add a custom cookie encoder/decoder. +// At this example we will use the gorilla's securecookie: +// +// $ go get github.com/gorilla/securecookie +// Example of securecookie can be found at "sessions/securecookie" example folder. + +// Notes: +// The whole example is converted by markbates/goth/example/main.go. +// It's tested with my own TWITTER application and it worked, even for localhost. +// I guess that everything else works as expected, all bugs reported by goth library's community +// are fixed in the time I wrote that example, have fun! +import ( + "errors" + "os" + "sort" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/view" + + "github.com/gorilla/securecookie" // optionally, used for session's encoder/decoder + + "github.com/markbates/goth" + "github.com/markbates/goth/providers/amazon" + "github.com/markbates/goth/providers/auth0" + "github.com/markbates/goth/providers/bitbucket" + "github.com/markbates/goth/providers/box" + "github.com/markbates/goth/providers/dailymotion" + "github.com/markbates/goth/providers/deezer" + "github.com/markbates/goth/providers/digitalocean" + "github.com/markbates/goth/providers/discord" + "github.com/markbates/goth/providers/dropbox" + "github.com/markbates/goth/providers/facebook" + "github.com/markbates/goth/providers/fitbit" + "github.com/markbates/goth/providers/github" + "github.com/markbates/goth/providers/gitlab" + "github.com/markbates/goth/providers/gplus" + "github.com/markbates/goth/providers/heroku" + "github.com/markbates/goth/providers/instagram" + "github.com/markbates/goth/providers/intercom" + "github.com/markbates/goth/providers/lastfm" + "github.com/markbates/goth/providers/linkedin" + "github.com/markbates/goth/providers/meetup" + "github.com/markbates/goth/providers/onedrive" + "github.com/markbates/goth/providers/openidConnect" + "github.com/markbates/goth/providers/paypal" + "github.com/markbates/goth/providers/salesforce" + "github.com/markbates/goth/providers/slack" + "github.com/markbates/goth/providers/soundcloud" + "github.com/markbates/goth/providers/spotify" + "github.com/markbates/goth/providers/steam" + "github.com/markbates/goth/providers/stripe" + "github.com/markbates/goth/providers/twitch" + "github.com/markbates/goth/providers/twitter" + "github.com/markbates/goth/providers/uber" + "github.com/markbates/goth/providers/wepay" + "github.com/markbates/goth/providers/xero" + "github.com/markbates/goth/providers/yahoo" + "github.com/markbates/goth/providers/yammer" +) + +// These are some function helpers that you may use if you want + +// GetProviderName is a function used to get the name of a provider +// for a given request. By default, this provider is fetched from +// the URL query string. If you provide it in a different way, +// assign your own function to this variable that returns the provider +// name for your request. +var GetProviderName = func(ctx context.Context) (string, error) { + // try to get it from the url param "provider" + if p := ctx.URLParam("provider"); p != "" { + return p, nil + } + + // try to get it from the url PATH parameter "{provider} or :provider or {provider:string} or {provider:alphabetical}" + if p := ctx.Params().Get("provider"); p != "" { + return p, nil + } + + // try to get it from context's per-request storage + if p := ctx.Values().GetString("provider"); p != "" { + return p, nil + } + // if not found then return an empty string with the corresponding error + return "", errors.New("you must select a provider") +} + +/* +BeginAuthHandler is a convienence handler for starting the authentication process. +It expects to be able to get the name of the provider from the query parameters +as either "provider" or ":provider". + +BeginAuthHandler will redirect the user to the appropriate authentication end-point +for the requested provider. + +See https://github.com/markbates/goth/examples/main.go to see this in action. +*/ +func BeginAuthHandler(ctx context.Context) { + url, err := GetAuthURL(ctx) + if err != nil { + ctx.StatusCode(iris.StatusBadRequest) + ctx.Writef("%v", err) + return + } + + ctx.Redirect(url, iris.StatusTemporaryRedirect) +} + +/* +GetAuthURL starts the authentication process with the requested provided. +It will return a URL that should be used to send users to. + +It expects to be able to get the name of the provider from the query parameters +as either "provider" or ":provider" or from the context's value of "provider" key. + +I would recommend using the BeginAuthHandler instead of doing all of these steps +yourself, but that's entirely up to you. +*/ +func GetAuthURL(ctx context.Context) (string, error) { + providerName, err := GetProviderName(ctx) + if err != nil { + return "", err + } + + provider, err := goth.GetProvider(providerName) + if err != nil { + return "", err + } + sess, err := provider.BeginAuth(SetState(ctx)) + if err != nil { + return "", err + } + + url, err := sess.GetAuthURL() + if err != nil { + return "", err + } + + ctx.Session().Set(providerName, sess.Marshal()) + return url, nil +} + +// SetState sets the state string associated with the given request. +// If no state string is associated with the request, one will be generated. +// This state is sent to the provider and can be retrieved during the +// callback. +var SetState = func(ctx context.Context) string { + state := ctx.URLParam("state") + if len(state) > 0 { + return state + } + + return "state" + +} + +// GetState gets the state returned by the provider during the callback. +// This is used to prevent CSRF attacks, see +// http://tools.ietf.org/html/rfc6749#section-10.12 +var GetState = func(ctx context.Context) string { + return ctx.URLParam("state") +} + +/* +CompleteUserAuth does what it says on the tin. It completes the authentication +process and fetches all of the basic information about the user from the provider. + +It expects to be able to get the name of the provider from the query parameters +as either "provider" or ":provider". + +See https://github.com/markbates/goth/examples/main.go to see this in action. +*/ +var CompleteUserAuth = func(ctx context.Context) (goth.User, error) { + providerName, err := GetProviderName(ctx) + if err != nil { + return goth.User{}, err + } + + provider, err := goth.GetProvider(providerName) + if err != nil { + return goth.User{}, err + } + + value := ctx.Session().GetString(providerName) + if value == "" { + return goth.User{}, errors.New("session value for " + providerName + " not found") + } + + sess, err := provider.UnmarshalSession(value) + if err != nil { + return goth.User{}, err + } + + user, err := provider.FetchUser(sess) + if err == nil { + // user can be found with existing session data + return user, err + } + + // get new token and retry fetch + _, err = sess.Authorize(provider, ctx.Request().URL.Query()) + if err != nil { + return goth.User{}, err + } + + ctx.Session().Set(providerName, sess.Marshal()) + return provider.FetchUser(sess) +} + +// Logout invalidates a user session. +func Logout(ctx context.Context) error { + providerName, err := GetProviderName(ctx) + if err != nil { + return err + } + + ctx.Session().Delete(providerName) + return nil +} + +// End of the "some function helpers". + +func main() { + goth.UseProviders( + twitter.New(os.Getenv("TWITTER_KEY"), os.Getenv("TWITTER_SECRET"), "http://localhost:3000/auth/twitter/callback"), + // If you'd like to use authenticate instead of authorize in Twitter provider, use this instead. + // twitter.NewAuthenticate(os.Getenv("TWITTER_KEY"), os.Getenv("TWITTER_SECRET"), "http://localhost:3000/auth/twitter/callback"), + + facebook.New(os.Getenv("FACEBOOK_KEY"), os.Getenv("FACEBOOK_SECRET"), "http://localhost:3000/auth/facebook/callback"), + fitbit.New(os.Getenv("FITBIT_KEY"), os.Getenv("FITBIT_SECRET"), "http://localhost:3000/auth/fitbit/callback"), + gplus.New(os.Getenv("GPLUS_KEY"), os.Getenv("GPLUS_SECRET"), "http://localhost:3000/auth/gplus/callback"), + github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), "http://localhost:3000/auth/github/callback"), + spotify.New(os.Getenv("SPOTIFY_KEY"), os.Getenv("SPOTIFY_SECRET"), "http://localhost:3000/auth/spotify/callback"), + linkedin.New(os.Getenv("LINKEDIN_KEY"), os.Getenv("LINKEDIN_SECRET"), "http://localhost:3000/auth/linkedin/callback"), + lastfm.New(os.Getenv("LASTFM_KEY"), os.Getenv("LASTFM_SECRET"), "http://localhost:3000/auth/lastfm/callback"), + twitch.New(os.Getenv("TWITCH_KEY"), os.Getenv("TWITCH_SECRET"), "http://localhost:3000/auth/twitch/callback"), + dropbox.New(os.Getenv("DROPBOX_KEY"), os.Getenv("DROPBOX_SECRET"), "http://localhost:3000/auth/dropbox/callback"), + digitalocean.New(os.Getenv("DIGITALOCEAN_KEY"), os.Getenv("DIGITALOCEAN_SECRET"), "http://localhost:3000/auth/digitalocean/callback", "read"), + bitbucket.New(os.Getenv("BITBUCKET_KEY"), os.Getenv("BITBUCKET_SECRET"), "http://localhost:3000/auth/bitbucket/callback"), + instagram.New(os.Getenv("INSTAGRAM_KEY"), os.Getenv("INSTAGRAM_SECRET"), "http://localhost:3000/auth/instagram/callback"), + intercom.New(os.Getenv("INTERCOM_KEY"), os.Getenv("INTERCOM_SECRET"), "http://localhost:3000/auth/intercom/callback"), + box.New(os.Getenv("BOX_KEY"), os.Getenv("BOX_SECRET"), "http://localhost:3000/auth/box/callback"), + salesforce.New(os.Getenv("SALESFORCE_KEY"), os.Getenv("SALESFORCE_SECRET"), "http://localhost:3000/auth/salesforce/callback"), + amazon.New(os.Getenv("AMAZON_KEY"), os.Getenv("AMAZON_SECRET"), "http://localhost:3000/auth/amazon/callback"), + yammer.New(os.Getenv("YAMMER_KEY"), os.Getenv("YAMMER_SECRET"), "http://localhost:3000/auth/yammer/callback"), + onedrive.New(os.Getenv("ONEDRIVE_KEY"), os.Getenv("ONEDRIVE_SECRET"), "http://localhost:3000/auth/onedrive/callback"), + + //Pointed localhost.com to http://localhost:3000/auth/yahoo/callback through proxy as yahoo + // does not allow to put custom ports in redirection uri + yahoo.New(os.Getenv("YAHOO_KEY"), os.Getenv("YAHOO_SECRET"), "http://localhost.com"), + slack.New(os.Getenv("SLACK_KEY"), os.Getenv("SLACK_SECRET"), "http://localhost:3000/auth/slack/callback"), + stripe.New(os.Getenv("STRIPE_KEY"), os.Getenv("STRIPE_SECRET"), "http://localhost:3000/auth/stripe/callback"), + wepay.New(os.Getenv("WEPAY_KEY"), os.Getenv("WEPAY_SECRET"), "http://localhost:3000/auth/wepay/callback", "view_user"), + //By default paypal production auth urls will be used, please set PAYPAL_ENV=sandbox as environment variable for testing + //in sandbox environment + paypal.New(os.Getenv("PAYPAL_KEY"), os.Getenv("PAYPAL_SECRET"), "http://localhost:3000/auth/paypal/callback"), + steam.New(os.Getenv("STEAM_KEY"), "http://localhost:3000/auth/steam/callback"), + heroku.New(os.Getenv("HEROKU_KEY"), os.Getenv("HEROKU_SECRET"), "http://localhost:3000/auth/heroku/callback"), + uber.New(os.Getenv("UBER_KEY"), os.Getenv("UBER_SECRET"), "http://localhost:3000/auth/uber/callback"), + soundcloud.New(os.Getenv("SOUNDCLOUD_KEY"), os.Getenv("SOUNDCLOUD_SECRET"), "http://localhost:3000/auth/soundcloud/callback"), + gitlab.New(os.Getenv("GITLAB_KEY"), os.Getenv("GITLAB_SECRET"), "http://localhost:3000/auth/gitlab/callback"), + dailymotion.New(os.Getenv("DAILYMOTION_KEY"), os.Getenv("DAILYMOTION_SECRET"), "http://localhost:3000/auth/dailymotion/callback", "email"), + deezer.New(os.Getenv("DEEZER_KEY"), os.Getenv("DEEZER_SECRET"), "http://localhost:3000/auth/deezer/callback", "email"), + discord.New(os.Getenv("DISCORD_KEY"), os.Getenv("DISCORD_SECRET"), "http://localhost:3000/auth/discord/callback", discord.ScopeIdentify, discord.ScopeEmail), + meetup.New(os.Getenv("MEETUP_KEY"), os.Getenv("MEETUP_SECRET"), "http://localhost:3000/auth/meetup/callback"), + + //Auth0 allocates domain per customer, a domain must be provided for auth0 to work + auth0.New(os.Getenv("AUTH0_KEY"), os.Getenv("AUTH0_SECRET"), "http://localhost:3000/auth/auth0/callback", os.Getenv("AUTH0_DOMAIN")), + xero.New(os.Getenv("XERO_KEY"), os.Getenv("XERO_SECRET"), "http://localhost:3000/auth/xero/callback"), + ) + + // OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html) + // because the OpenID Connect provider initialize it self in the New(), it can return an error which should be handled or ignored + // ignore the error for now + openidConnect, _ := openidConnect.New(os.Getenv("OPENID_CONNECT_KEY"), os.Getenv("OPENID_CONNECT_SECRET"), "http://localhost:3000/auth/openid-connect/callback", os.Getenv("OPENID_CONNECT_DISCOVERY_URL")) + if openidConnect != nil { + goth.UseProviders(openidConnect) + } + + m := make(map[string]string) + m["amazon"] = "Amazon" + m["bitbucket"] = "Bitbucket" + m["box"] = "Box" + m["dailymotion"] = "Dailymotion" + m["deezer"] = "Deezer" + m["digitalocean"] = "Digital Ocean" + m["discord"] = "Discord" + m["dropbox"] = "Dropbox" + m["facebook"] = "Facebook" + m["fitbit"] = "Fitbit" + m["github"] = "Github" + m["gitlab"] = "Gitlab" + m["soundcloud"] = "SoundCloud" + m["spotify"] = "Spotify" + m["steam"] = "Steam" + m["stripe"] = "Stripe" + m["twitch"] = "Twitch" + m["uber"] = "Uber" + m["wepay"] = "Wepay" + m["yahoo"] = "Yahoo" + m["yammer"] = "Yammer" + m["gplus"] = "Google Plus" + m["heroku"] = "Heroku" + m["instagram"] = "Instagram" + m["intercom"] = "Intercom" + m["lastfm"] = "Last FM" + m["linkedin"] = "Linkedin" + m["onedrive"] = "Onedrive" + m["paypal"] = "Paypal" + m["twitter"] = "Twitter" + m["salesforce"] = "Salesforce" + m["slack"] = "Slack" + m["meetup"] = "Meetup.com" + m["auth0"] = "Auth0" + m["openid-connect"] = "OpenID Connect" + m["xero"] = "Xero" + + var keys []string + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + providerIndex := &ProviderIndex{Providers: keys, ProvidersMap: m} + + // create our app, + // set a view + // set sessions + // and setup the router for the showcase + app := iris.New() + + // attach and build our templates + app.AttachView(view.HTML("./templates", ".html")) + + // attach a session manager + cookieName := "mycustomsessionid" + // AES only supports key sizes of 16, 24 or 32 bytes. + // You either need to provide exactly that amount or you derive the key from what you type in. + hashKey := []byte("the-big-and-secret-fash-key-here") + blockKey := []byte("lot-secret-of-characters-big-too") + secureCookie := securecookie.New(hashKey, blockKey) + sessManager := sessions.New(sessions.Config{ + Cookie: cookieName, + Encode: secureCookie.Encode, + Decode: secureCookie.Decode, + }) + app.AttachSessionManager(sessManager) + + // start of the router + + app.Get("/auth/{provider}/callback", func(ctx context.Context) { + + user, err := CompleteUserAuth(ctx) + if err != nil { + ctx.StatusCode(iris.StatusInternalServerError) + ctx.Writef("%v", err) + return + } + ctx.ViewData("", user) + if err := ctx.View("user.html"); err != nil { + ctx.Writef("%v", err) + } + }) + + app.Get("/logout/{provider}", func(ctx context.Context) { + Logout(ctx) + ctx.Redirect("/", iris.StatusTemporaryRedirect) + }) + + app.Get("/auth/{provider}", func(ctx context.Context) { + // try to get the user without re-authenticating + if gothUser, err := CompleteUserAuth(ctx); err == nil { + ctx.ViewData("", gothUser) + if err := ctx.View("user.html"); err != nil { + ctx.Writef("%v", err) + } + } else { + BeginAuthHandler(ctx) + } + }) + + app.Get("/", func(ctx context.Context) { + + ctx.ViewData("", providerIndex) + + if err := ctx.View("index.html"); err != nil { + ctx.Writef("%v", err) + } + }) + + // http://localhost:3000 + app.Run(iris.Addr("localhost:3000")) +} + +type ProviderIndex struct { + Providers []string + ProvidersMap map[string]string +} diff --git a/_examples/intermediate/oauth2/templates/index.html b/_examples/intermediate/oauth2/templates/index.html new file mode 100644 index 00000000..53652804 --- /dev/null +++ b/_examples/intermediate/oauth2/templates/index.html @@ -0,0 +1,3 @@ +{{range $key,$value:=.Providers}} +

Log in with {{index $.ProvidersMap $value}}

+{{end}} \ No newline at end of file diff --git a/_examples/intermediate/oauth2/templates/user.html b/_examples/intermediate/oauth2/templates/user.html new file mode 100644 index 00000000..10ab7c5c --- /dev/null +++ b/_examples/intermediate/oauth2/templates/user.html @@ -0,0 +1,11 @@ +

logout

+

Name: {{.Name}} [{{.LastName}}, {{.FirstName}}]

+

Email: {{.Email}}

+

NickName: {{.NickName}}

+

Location: {{.Location}}

+

AvatarURL: {{.AvatarURL}}

+

Description: {{.Description}}

+

UserID: {{.UserID}}

+

AccessToken: {{.AccessToken}}

+

ExpiresAt: {{.ExpiresAt}}

+

RefreshToken: {{.RefreshToken}}

\ No newline at end of file diff --git a/_examples/intermediate/sessions/securecookie/main.go b/_examples/intermediate/sessions/securecookie/main.go index c3eb55dc..38822633 100644 --- a/_examples/intermediate/sessions/securecookie/main.go +++ b/_examples/intermediate/sessions/securecookie/main.go @@ -1,13 +1,16 @@ package main -import ( - // developers can use any library to add a custom cookie encoder/decoder. - // At this example we use the gorilla's securecookie library: - "github.com/gorilla/securecookie" +// developers can use any library to add a custom cookie encoder/decoder. +// At this example we use the gorilla's securecookie package: +// $ go get github.com/gorilla/securecookie +// $ go run main.go +import ( "github.com/kataras/iris" "github.com/kataras/iris/context" "github.com/kataras/iris/sessions" + + "github.com/gorilla/securecookie" ) func main() {