From 4a446ac1e227b34263123a41bece6f10292e32f8 Mon Sep 17 00:00:00 2001 From: Makis Maropoulos Date: Wed, 22 Jun 2016 16:01:31 +0300 Subject: [PATCH] Complete the OAuth/OAuth2 'high level' support --- config/iris.go | 5 ++ config/oauth.go | 213 +++++++++++++++++++++++++++++++++++++++++++++ context.go | 23 ++--- context/context.go | 8 +- http.go | 17 ++++ initiatory.go | 50 +++++++++-- iris.go | 24 ++--- logger/logger.go | 1 + 8 files changed, 312 insertions(+), 29 deletions(-) create mode 100644 config/oauth.go diff --git a/config/iris.go b/config/iris.go index a0d4370f..b29c3a06 100644 --- a/config/iris.go +++ b/config/iris.go @@ -104,6 +104,10 @@ type ( // Mail contains the configs for the mail sender service Mail Mail + // OAuth the configs for the gothic oauth/oauth2 authentication for third-party websites + // See https://github.com/iris-contrib/gothic/blob/master/example/main.go + OAuth OAuth + // Server contains the configs for the http server // Server configs are the only one which are setted inside base Iris package (from Listen, ListenTLS, ListenUNIX) NO from users // @@ -147,6 +151,7 @@ func Default() Iris { Render: DefaultRender(), Websocket: DefaultWebsocket(), Mail: DefaultMail(), + OAuth: DefaultOAuth(), Server: DefaultServer(), } } diff --git a/config/oauth.go b/config/oauth.go new file mode 100644 index 00000000..e7ba8471 --- /dev/null +++ b/config/oauth.go @@ -0,0 +1,213 @@ +package config + +import ( + "github.com/imdario/mergo" + "github.com/markbates/goth" + "github.com/markbates/goth/providers/amazon" + "github.com/markbates/goth/providers/bitbucket" + "github.com/markbates/goth/providers/box" + "github.com/markbates/goth/providers/digitalocean" + "github.com/markbates/goth/providers/dropbox" + "github.com/markbates/goth/providers/facebook" + "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/lastfm" + "github.com/markbates/goth/providers/linkedin" + "github.com/markbates/goth/providers/onedrive" + "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/yahoo" + "github.com/markbates/goth/providers/yammer" +) + +const ( + // DefaultAuthPath /auth + DefaultAuthPath = "/auth" +) + +// OAuth the configs for the gothic oauth/oauth2 authentication for third-party websites +// All Key and Secret values are empty by default strings. Non-empty will be registered as Goth Provider automatically, by Iris +// the users can still register their own providers using goth.UseProviders +// contains the providers' keys (& secrets) and the relative auth callback url path(ex: "/auth" will be registered as /auth/:provider/callback) +// +type OAuth struct { + Path string + TwitterKey, TwitterSecret, TwitterName string + FacebookKey, FacebookSecret, FacebookName string + GplusKey, GplusSecret, GplusName string + GithubKey, GithubSecret, GithubName string + SpotifyKey, SpotifySecret, SpotifyName string + LinkedinKey, LinkedinSecret, LinkedinName string + LastfmKey, LastfmSecret, LastfmName string + TwitchKey, TwitchSecret, TwitchName string + DropboxKey, DropboxSecret, DropboxName string + DigitaloceanKey, DigitaloceanSecret, DigitaloceanName string + BitbucketKey, BitbucketSecret, BitbucketName string + InstagramKey, InstagramSecret, InstagramName string + BoxKey, BoxSecret, BoxName string + SalesforceKey, SalesforceSecret, SalesforceName string + AmazonKey, AmazonSecret, AmazonName string + YammerKey, YammerSecret, YammerName string + OneDriveKey, OneDriveSecret, OneDriveName string + YahooKey, YahooSecret, YahooName string + SlackKey, SlackSecret, SlackName string + StripeKey, StripeSecret, StripeName string + WepayKey, WepaySecret, WepayName string + PaypalKey, PaypalSecret, PaypalName string + SteamKey, SteamName string + HerokuKey, HerokuSecret, HerokuName string + UberKey, UberSecret, UberName string + SoundcloudKey, SoundcloudSecret, SoundcloudName string + GitlabKey, GitlabSecret, GitlabName string +} + +// DefaultOAuth returns OAuth config, the fields of the iteral are zero-values ( empty strings) +func DefaultOAuth() OAuth { + return OAuth{ + Path: DefaultAuthPath, + TwitterName: "twitter", + FacebookName: "facebook", + GplusName: "gplus", + GithubName: "github", + SpotifyName: "spotify", + LinkedinName: "linkedin", + LastfmName: "lastfm", + TwitchName: "twitch", + DropboxName: "dropbox", + DigitaloceanName: "digitalocean", + BitbucketName: "bitbucket", + InstagramName: "instagram", + BoxName: "box", + SalesforceName: "salesforce", + AmazonName: "amazon", + YammerName: "yammer", + OneDriveName: "onedrive", + YahooName: "yahoo", + SlackName: "slack", + StripeName: "stripe", + WepayName: "wepay", + PaypalName: "paypal", + SteamName: "steam", + HerokuName: "heroku", + UberName: "uber", + SoundcloudName: "soundcloud", + GitlabName: "gitlab", + } // this will be registered as /auth/:provider in the mux +} + +// MergeSingle merges the default with the given config and returns the result +func (c OAuth) MergeSingle(cfg OAuth) (config OAuth) { + + config = cfg + mergo.Merge(&config, c) + return +} + +// GetAll returns the valid goth providers and the relative url paths (because the goth.Provider doesn't have a public method to get the Auth path...) +// we do the hard-core/hand checking here at the configs. +// +// receives one parameter which is the host from the server,ex: http://localhost:3000, will be used as prefix for the oauth callback +func (c OAuth) GetAll(vhost string) (providers []goth.Provider) { + + getCallbackURL := func(providerName string) string { + return vhost + c.Path + "/" + providerName + "/callback" + } + + //we could use a map but that's easier for the users because of code completion of their IDEs/editors + if c.TwitterKey != "" && c.TwitterSecret != "" { + println(getCallbackURL("twitter")) + providers = append(providers, twitter.New(c.TwitterKey, c.TwitterSecret, getCallbackURL(c.TwitterName))) + } + if c.FacebookKey != "" && c.FacebookSecret != "" { + providers = append(providers, facebook.New(c.FacebookKey, c.FacebookSecret, getCallbackURL(c.FacebookName))) + } + if c.GplusKey != "" && c.GplusSecret != "" { + providers = append(providers, gplus.New(c.GplusKey, c.GplusSecret, getCallbackURL(c.GplusName))) + } + if c.GithubKey != "" && c.GithubSecret != "" { + providers = append(providers, github.New(c.GithubKey, c.GithubSecret, getCallbackURL(c.GithubName))) + } + if c.SpotifyKey != "" && c.SpotifySecret != "" { + providers = append(providers, spotify.New(c.SpotifyKey, c.SpotifySecret, getCallbackURL(c.SpotifyName))) + } + if c.LinkedinKey != "" && c.LinkedinSecret != "" { + providers = append(providers, linkedin.New(c.LinkedinKey, c.LinkedinSecret, getCallbackURL(c.LinkedinName))) + } + if c.LastfmKey != "" && c.LastfmSecret != "" { + providers = append(providers, lastfm.New(c.LastfmKey, c.LastfmSecret, getCallbackURL(c.LastfmName))) + } + if c.TwitchKey != "" && c.TwitchSecret != "" { + providers = append(providers, twitch.New(c.TwitchKey, c.TwitchSecret, getCallbackURL(c.TwitchName))) + } + if c.DropboxKey != "" && c.DropboxSecret != "" { + providers = append(providers, dropbox.New(c.DropboxKey, c.DropboxSecret, getCallbackURL(c.DropboxName))) + } + if c.DigitaloceanKey != "" && c.DigitaloceanSecret != "" { + providers = append(providers, digitalocean.New(c.DigitaloceanKey, c.DigitaloceanSecret, getCallbackURL(c.DigitaloceanName))) + } + if c.BitbucketKey != "" && c.BitbucketSecret != "" { + providers = append(providers, bitbucket.New(c.BitbucketKey, c.BitbucketSecret, getCallbackURL(c.BitbucketName))) + } + if c.InstagramKey != "" && c.InstagramSecret != "" { + providers = append(providers, instagram.New(c.InstagramKey, c.InstagramSecret, getCallbackURL(c.InstagramName))) + } + if c.BoxKey != "" && c.BoxSecret != "" { + providers = append(providers, box.New(c.BoxKey, c.BoxSecret, getCallbackURL(c.BoxName))) + } + if c.SalesforceKey != "" && c.SalesforceSecret != "" { + providers = append(providers, salesforce.New(c.SalesforceKey, c.SalesforceSecret, getCallbackURL(c.SalesforceName))) + } + if c.AmazonKey != "" && c.AmazonSecret != "" { + providers = append(providers, amazon.New(c.AmazonKey, c.AmazonSecret, getCallbackURL(c.AmazonName))) + } + if c.YammerKey != "" && c.YammerSecret != "" { + providers = append(providers, yammer.New(c.YammerKey, c.YammerSecret, getCallbackURL(c.YammerName))) + } + if c.OneDriveKey != "" && c.OneDriveSecret != "" { + providers = append(providers, onedrive.New(c.OneDriveKey, c.OneDriveSecret, getCallbackURL(c.OneDriveName))) + } + if c.YahooKey != "" && c.YahooSecret != "" { + providers = append(providers, yahoo.New(c.YahooKey, c.YahooSecret, getCallbackURL(c.YahooName))) + } + if c.SlackKey != "" && c.SlackSecret != "" { + providers = append(providers, slack.New(c.SlackKey, c.SlackSecret, getCallbackURL(c.SlackName))) + } + if c.StripeKey != "" && c.StripeSecret != "" { + providers = append(providers, stripe.New(c.StripeKey, c.StripeSecret, getCallbackURL(c.StripeName))) + } + if c.WepayKey != "" && c.WepaySecret != "" { + providers = append(providers, wepay.New(c.WepayKey, c.WepaySecret, getCallbackURL(c.WepayName))) + } + if c.PaypalKey != "" && c.PaypalSecret != "" { + providers = append(providers, paypal.New(c.PaypalKey, c.PaypalSecret, getCallbackURL(c.PaypalName))) + } + if c.SteamKey != "" { + providers = append(providers, steam.New(c.SteamKey, getCallbackURL(c.SteamName))) + } + if c.HerokuKey != "" && c.HerokuSecret != "" { + providers = append(providers, heroku.New(c.HerokuKey, c.HerokuSecret, getCallbackURL(c.HerokuName))) + } + if c.UberKey != "" && c.UberSecret != "" { + providers = append(providers, uber.New(c.UberKey, c.UberSecret, getCallbackURL(c.UberName))) + } + if c.SoundcloudKey != "" && c.SoundcloudSecret != "" { + providers = append(providers, soundcloud.New(c.SoundcloudKey, c.SoundcloudSecret, getCallbackURL(c.SoundcloudName))) + } + if c.GitlabKey != "" && c.GitlabSecret != "" { + providers = append(providers, gitlab.New(c.GitlabKey, c.GitlabSecret, getCallbackURL(c.GithubName))) + } + + return +} diff --git a/context.go b/context.go index b5aae14c..54e9f931 100644 --- a/context.go +++ b/context.go @@ -24,7 +24,6 @@ import ( "time" "github.com/iris-contrib/formBinder" - "github.com/iris-contrib/gothic" "github.com/kataras/iris/config" "github.com/kataras/iris/context" "github.com/kataras/iris/errors" @@ -88,6 +87,8 @@ type ( sessionStore store.IStore // pos is the position number of the Context, look .Next to understand pos uint8 + //gothic oauth + oauthUser goth.User } ) @@ -786,13 +787,15 @@ func (ctx *Context) Log(format string, a ...interface{}) { /* Auth */ -// 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 named parameters -// as either "provider" or url query parameter ":provider". -// -// See https://github.com/iris-contrib/gothic/blob/master/example/main.go to see this in action. -func (ctx *Context) CompleteUserAuth() (goth.User, error) { - return gothic.CompleteUserAuth(ctx) +// SetOAuthUser sets the oauth user +// Internal method but exported because useful for advanced use cases +// Iris uses this method to set automatically the authenticated user. +func (ctx *Context) SetOAuthUser(u goth.User) { + ctx.oauthUser = u +} + +// OAuthUser returns the oauthenticated User +// See https://github.com/iris-contrib/gothic/blob/master/exampl/main.go to see this in action. +func (ctx *Context) OAuthUser() goth.User { + return ctx.oauthUser } diff --git a/context/context.go b/context/context.go index 531bdbe3..62403844 100644 --- a/context/context.go +++ b/context/context.go @@ -136,8 +136,12 @@ type ( // IContextAuth handles the authentication/authorization IContextAuth interface { - // CompleteUserAuth + // SetOAuthUser sets the oauth user + // Internal method but exported because useful for advanced use cases + // Iris uses this method to set automatically the authenticated user. + SetOAuthUser(goth.User) + // OAuthUser returns the authenticated User // See https://github.com/iris-contrib/gothic/blob/master/example/main.go to see this in action. - CompleteUserAuth() (goth.User, error) + OAuthUser() goth.User } ) diff --git a/http.go b/http.go index 281dca1e..fbce285b 100644 --- a/http.go +++ b/http.go @@ -286,9 +286,26 @@ func (s *Server) Host() (host string) { // VirtualHost returns the s.Config.ListeningAddr // func (s *Server) VirtualHost() (host string) { + // check the addr if :8080 do it 0.0.0.0:8080 ,we need the hostname for many cases + a := s.Config.ListeningAddr + //check if contains hostname, we need the full host, :8080 should be : 127.0.0.1:8080 + if portIdx := strings.IndexByte(a, ':'); portIdx == 0 { + // then the : is the first letter, so we dont have setted a hostname, lets set it + s.Config.ListeningAddr = config.DefaultServerHostname + a + } return s.Config.ListeningAddr } +// Fullhost returns the scheme+host +func (s *Server) FullHost() string { + scheme := "http://" + // we need to be able to take that before(for testing &debugging) and after server's listen + if s.IsSecure() || (s.Config.CertFile != "" && s.Config.KeyFile != "") { + scheme = "https://" + } + return scheme + s.VirtualHost() +} + // Hostname returns the hostname part only, if host == localhost:8080 it will return the localhost // if server is not listening it returns the config.ListeningAddr's hostname part func (s *Server) Hostname() (hostname string) { diff --git a/initiatory.go b/initiatory.go index f6269e9d..cab6a605 100644 --- a/initiatory.go +++ b/initiatory.go @@ -6,9 +6,11 @@ import ( "sync" "time" + "github.com/iris-contrib/gothic" "github.com/kataras/iris/config" "github.com/kataras/iris/logger" "github.com/kataras/iris/websocket" + "github.com/markbates/goth" "github.com/kataras/iris/mail" "github.com/kataras/iris/render/rest" @@ -68,10 +70,11 @@ const ( // Implements the FrameworkAPI type Framework struct { *muxAPI - rest *rest.Render - templates *template.Template - sessions *sessions.Manager - mailer mail.Service + rest *rest.Render + templates *template.Template + sessions *sessions.Manager + mailer mail.Service + oauthHandlers Middleware // fields which are useful to the user/dev HTTPServer *Server Config *config.Iris @@ -115,13 +118,48 @@ func (s *Framework) initialize() { s.sessions = sessions.New(s.Config.Sessions) } - //set the rest + // set the rest s.rest = rest.New(s.Config.Render.Rest) - //set mail and templates if not already setted + // set mail and templates if not already setted s.prepareMailer() s.prepareTemplates() + // set the oauth providers from the OAuth configuration field + + // the user still can set his/her own provider (using goth.UseProviders), if the configuration for the provider is not exists + // prepare the configs + s.Config.OAuth = config.DefaultOAuth().MergeSingle(s.Config.OAuth) + oauthProviders := s.Config.OAuth.GetAll(s.HTTPServer.FullHost()) + if len(oauthProviders) > 0 { + goth.UseProviders(oauthProviders...) + // set the mux path to handle these providers + s.Get(s.Config.OAuth.Path+"/:provider", func(ctx *Context) { + err := gothic.BeginAuthHandler(ctx) + if err != nil { + s.Logger.Warningf("\n[IRIS: OAUTH] Error:" + err.Error()) + } + }) + + authMiddleware := func(ctx *Context) { + + user, err := gothic.CompleteUserAuth(ctx) + if err != nil { + ctx.EmitError(StatusUnauthorized) + ctx.Log(err.Error()) + return + } + ctx.SetOAuthUser(user) + ctx.Next() + } + + s.oauthHandlers = append([]Handler{HandlerFunc(authMiddleware)}, s.oauthHandlers...) + + s.Handle(MethodGet, s.Config.OAuth.Path+"/:provider/callback", s.oauthHandlers...)("oauth") + } + + // end of auth + // listen to websocket connections websocket.RegisterServer(s, s.Websocket, s.Logger) diff --git a/iris.go b/iris.go index 4bed26be..ff398929 100644 --- a/iris.go +++ b/iris.go @@ -59,7 +59,6 @@ import ( "strings" "time" - "github.com/iris-contrib/gothic" "github.com/kataras/iris/config" "github.com/kataras/iris/context" "github.com/kataras/iris/errors" @@ -97,6 +96,7 @@ type ( MustUseFunc(...HandlerFunc) OnError(int, HandlerFunc) EmitError(int, *Context) + OnUserOAuth(...HandlerFunc) Lookup(string) Route Lookups() []Route Path(string, ...interface{}) string @@ -568,16 +568,18 @@ func (s *Framework) TemplateString(templateFile string, pageContext interface{}, return res } -// BeginAuthHandler is a convienence handler for starting the authentication process. -// It expects to be able to get the name of the provider from the named parameters -// as either "provider" or url query parameter ":provider". -// -// BeginAuthHandler will redirect the user to the appropriate authentication end-point -// for the requested provider. -// -// See https://github.com/iris-contrib/gothic/blob/master/example/main.go to see this in action. -func BeginAuthHandler(ctx *Context) { - gothic.BeginAuthHandler(ctx) +/* Auth */ + +// OnUserOAuth fires the middleware when the user logged in successfully via gothic oauth +// get the user using the context.OAuthUser() +func OnUserOAuth(handlersFn ...HandlerFunc) { + Default.OnUserOAuth(handlersFn...) +} + +// OnUserOAuth fires the middleware when the user logged in successfully via gothic oauth +// get the user using the context.OAuthUser() +func (s *Framework) OnUserOAuth(handlersFn ...HandlerFunc) { + s.oauthHandlers = append(s.oauthHandlers, convertToHandlers(handlersFn)...) } // ------------------------------------------------------------------------------------- diff --git a/logger/logger.go b/logger/logger.go index c8170efb..6336d046 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -31,6 +31,7 @@ func New(c config.Logger) *Logger { color.Output = colorable.NewColorable(c.Out) l := &Logger{&c, color.New(attr(c.ColorBgDefault), attr(c.ColorFgDefault), color.Bold)} + return l }