Complete the OAuth/OAuth2 'high level' support

This commit is contained in:
Makis Maropoulos 2016-06-22 16:01:31 +03:00
parent 56f78567a2
commit 4a446ac1e2
8 changed files with 312 additions and 29 deletions

View File

@ -104,6 +104,10 @@ type (
// Mail contains the configs for the mail sender service // Mail contains the configs for the mail sender service
Mail Mail 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 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 // 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(), Render: DefaultRender(),
Websocket: DefaultWebsocket(), Websocket: DefaultWebsocket(),
Mail: DefaultMail(), Mail: DefaultMail(),
OAuth: DefaultOAuth(),
Server: DefaultServer(), Server: DefaultServer(),
} }
} }

213
config/oauth.go Normal file
View File

@ -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
}

View File

@ -24,7 +24,6 @@ import (
"time" "time"
"github.com/iris-contrib/formBinder" "github.com/iris-contrib/formBinder"
"github.com/iris-contrib/gothic"
"github.com/kataras/iris/config" "github.com/kataras/iris/config"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/errors" "github.com/kataras/iris/errors"
@ -88,6 +87,8 @@ type (
sessionStore store.IStore sessionStore store.IStore
// pos is the position number of the Context, look .Next to understand // pos is the position number of the Context, look .Next to understand
pos uint8 pos uint8
//gothic oauth
oauthUser goth.User
} }
) )
@ -786,13 +787,15 @@ func (ctx *Context) Log(format string, a ...interface{}) {
/* Auth */ /* Auth */
// CompleteUserAuth does what it says on the tin. It completes the authentication // SetOAuthUser sets the oauth user
// process and fetches all of the basic information about the user from the provider. // Internal method but exported because useful for advanced use cases
// // Iris uses this method to set automatically the authenticated user.
// It expects to be able to get the name of the provider from the named parameters func (ctx *Context) SetOAuthUser(u goth.User) {
// as either "provider" or url query parameter ":provider". ctx.oauthUser = u
// }
// See https://github.com/iris-contrib/gothic/blob/master/example/main.go to see this in action.
func (ctx *Context) CompleteUserAuth() (goth.User, error) { // OAuthUser returns the oauthenticated User
return gothic.CompleteUserAuth(ctx) // 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
} }

View File

@ -136,8 +136,12 @@ type (
// IContextAuth handles the authentication/authorization // IContextAuth handles the authentication/authorization
IContextAuth interface { 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. // See https://github.com/iris-contrib/gothic/blob/master/example/main.go to see this in action.
CompleteUserAuth() (goth.User, error) OAuthUser() goth.User
} }
) )

17
http.go
View File

@ -286,9 +286,26 @@ func (s *Server) Host() (host string) {
// VirtualHost returns the s.Config.ListeningAddr // VirtualHost returns the s.Config.ListeningAddr
// //
func (s *Server) VirtualHost() (host string) { 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 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 // 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 // if server is not listening it returns the config.ListeningAddr's hostname part
func (s *Server) Hostname() (hostname string) { func (s *Server) Hostname() (hostname string) {

View File

@ -6,9 +6,11 @@ import (
"sync" "sync"
"time" "time"
"github.com/iris-contrib/gothic"
"github.com/kataras/iris/config" "github.com/kataras/iris/config"
"github.com/kataras/iris/logger" "github.com/kataras/iris/logger"
"github.com/kataras/iris/websocket" "github.com/kataras/iris/websocket"
"github.com/markbates/goth"
"github.com/kataras/iris/mail" "github.com/kataras/iris/mail"
"github.com/kataras/iris/render/rest" "github.com/kataras/iris/render/rest"
@ -68,10 +70,11 @@ const (
// Implements the FrameworkAPI // Implements the FrameworkAPI
type Framework struct { type Framework struct {
*muxAPI *muxAPI
rest *rest.Render rest *rest.Render
templates *template.Template templates *template.Template
sessions *sessions.Manager sessions *sessions.Manager
mailer mail.Service mailer mail.Service
oauthHandlers Middleware
// fields which are useful to the user/dev // fields which are useful to the user/dev
HTTPServer *Server HTTPServer *Server
Config *config.Iris Config *config.Iris
@ -115,13 +118,48 @@ func (s *Framework) initialize() {
s.sessions = sessions.New(s.Config.Sessions) s.sessions = sessions.New(s.Config.Sessions)
} }
//set the rest // set the rest
s.rest = rest.New(s.Config.Render.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.prepareMailer()
s.prepareTemplates() 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 // listen to websocket connections
websocket.RegisterServer(s, s.Websocket, s.Logger) websocket.RegisterServer(s, s.Websocket, s.Logger)

24
iris.go
View File

@ -59,7 +59,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/iris-contrib/gothic"
"github.com/kataras/iris/config" "github.com/kataras/iris/config"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/errors" "github.com/kataras/iris/errors"
@ -97,6 +96,7 @@ type (
MustUseFunc(...HandlerFunc) MustUseFunc(...HandlerFunc)
OnError(int, HandlerFunc) OnError(int, HandlerFunc)
EmitError(int, *Context) EmitError(int, *Context)
OnUserOAuth(...HandlerFunc)
Lookup(string) Route Lookup(string) Route
Lookups() []Route Lookups() []Route
Path(string, ...interface{}) string Path(string, ...interface{}) string
@ -568,16 +568,18 @@ func (s *Framework) TemplateString(templateFile string, pageContext interface{},
return res return res
} }
// BeginAuthHandler is a convienence handler for starting the authentication process. /* Auth */
// It expects to be able to get the name of the provider from the named parameters
// as either "provider" or url query parameter ":provider". // OnUserOAuth fires the middleware when the user logged in successfully via gothic oauth
// // get the user using the context.OAuthUser()
// BeginAuthHandler will redirect the user to the appropriate authentication end-point func OnUserOAuth(handlersFn ...HandlerFunc) {
// for the requested provider. Default.OnUserOAuth(handlersFn...)
// }
// See https://github.com/iris-contrib/gothic/blob/master/example/main.go to see this in action.
func BeginAuthHandler(ctx *Context) { // OnUserOAuth fires the middleware when the user logged in successfully via gothic oauth
gothic.BeginAuthHandler(ctx) // get the user using the context.OAuthUser()
func (s *Framework) OnUserOAuth(handlersFn ...HandlerFunc) {
s.oauthHandlers = append(s.oauthHandlers, convertToHandlers(handlersFn)...)
} }
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------

View File

@ -31,6 +31,7 @@ func New(c config.Logger) *Logger {
color.Output = colorable.NewColorable(c.Out) color.Output = colorable.NewColorable(c.Out)
l := &Logger{&c, color.New(attr(c.ColorBgDefault), attr(c.ColorFgDefault), color.Bold)} l := &Logger{&c, color.New(attr(c.ColorBgDefault), attr(c.ColorFgDefault), color.Bold)}
return l return l
} }