From 9a7a65f05ecacd89ad901424ad03635ce899b651 Mon Sep 17 00:00:00 2001 From: ben-toogood Date: Sat, 7 Mar 2020 11:06:57 +0000 Subject: [PATCH] Auth Provider (#1309) * auth provider mock interface * Auth Provider Options * Implement API Server Auth Package * Add weh utils * Add Login URL * Auth Provider Options * Add auth provider scope and setting token in cookie * Remove auth_login_url flag Co-authored-by: Asim Aslam Co-authored-by: Ben Toogood --- api/server/auth/auth.go | 70 ++++++++++++++++++++++++++++++++++++ api/server/http/http.go | 3 ++ auth/auth.go | 5 ++- auth/options.go | 20 +++++++++++ auth/provider/basic/basic.go | 34 ++++++++++++++++++ auth/provider/oauth/oauth.go | 42 ++++++++++++++++++++++ auth/provider/options.go | 47 ++++++++++++++++++++++++ auth/provider/provider.go | 28 +++++++++++++++ config/cmd/cmd.go | 67 ++++++++++++++++++++++++++++++++++ util/http/http.go | 35 ++++++++++++++++++ 10 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 api/server/auth/auth.go create mode 100644 auth/provider/basic/basic.go create mode 100644 auth/provider/oauth/oauth.go create mode 100644 auth/provider/options.go create mode 100644 auth/provider/provider.go diff --git a/api/server/auth/auth.go b/api/server/auth/auth.go new file mode 100644 index 00000000..84a36607 --- /dev/null +++ b/api/server/auth/auth.go @@ -0,0 +1,70 @@ +package auth + +import ( + "net/http" + "strings" + + "github.com/micro/go-micro/v2/auth" + "github.com/micro/go-micro/v2/metadata" +) + +// CombinedAuthHandler wraps a server and authenticates requests +func CombinedAuthHandler(h http.Handler) http.Handler { + return authHandler{ + handler: h, + auth: auth.DefaultAuth, + } +} + +type authHandler struct { + handler http.Handler + auth auth.Auth +} + +const ( + // BearerScheme is the prefix in the auth header + BearerScheme = "Bearer " +) + +func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + loginURL := h.auth.Options().LoginURL + + // Return if the user disabled auth on this endpoint + excludes := h.auth.Options().Exclude + if len(loginURL) > 0 { + excludes = append(excludes, loginURL) + } + for _, e := range excludes { + if e == req.URL.Path { + h.handler.ServeHTTP(w, req) + return + } + } + + var token string + if header, ok := metadata.Get(req.Context(), "Authorization"); ok { + // Extract the auth token from the request + if strings.HasPrefix(header, BearerScheme) { + token = header[len(BearerScheme):] + } + } else { + // Get the token out the cookies if not provided in headers + if c, err := req.Cookie(auth.CookieName); err != nil && c != nil { + token = c.Value + } + } + + // If the token is valid, allow the request + if _, err := h.auth.Verify(token); err == nil { + h.handler.ServeHTTP(w, req) + return + } + + // If there is no auth login url set, 401 + if loginURL == "" { + w.WriteHeader(401) + } + + // Redirect to the login path + http.Redirect(w, req, loginURL, http.StatusTemporaryRedirect) +} diff --git a/api/server/http/http.go b/api/server/http/http.go index 214f203e..1fed42b1 100644 --- a/api/server/http/http.go +++ b/api/server/http/http.go @@ -8,6 +8,8 @@ import ( "os" "sync" + "github.com/micro/go-micro/v2/api/server/auth" + "github.com/gorilla/handlers" "github.com/micro/go-micro/v2/api/server" "github.com/micro/go-micro/v2/api/server/cors" @@ -47,6 +49,7 @@ func (s *httpServer) Init(opts ...server.Option) error { func (s *httpServer) Handle(path string, handler http.Handler) { h := handlers.CombinedLoggingHandler(os.Stdout, handler) + h = auth.CombinedAuthHandler(handler) if s.opts.EnableCORS { h = cors.CombinedCORSHandler(h) diff --git a/auth/auth.go b/auth/auth.go index 06ef3b5d..36ee3f4c 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -44,7 +44,7 @@ type Role struct { // Account provided by an auth provider type Account struct { - // ID of the account (UUID or email) + // ID of the account (UUIDV4, email or username) Id string `json:"id"` // Token used to authenticate Token string `json:"token"` @@ -62,6 +62,9 @@ const ( // MetadataKey is the key used when storing the account // in metadata MetadataKey = "auth-account" + // CookieName is the name of the cookie which stores the + // auth token + CookieName = "micro-token" ) // AccountFromContext gets the account from the context, which diff --git a/auth/options.go b/auth/options.go index 059c3931..4f49d819 100644 --- a/auth/options.go +++ b/auth/options.go @@ -1,5 +1,7 @@ package auth +import "github.com/micro/go-micro/v2/auth/provider" + type Options struct { // Token is an auth token Token string @@ -9,6 +11,10 @@ type Options struct { PrivateKey string // Endpoints to exclude Exclude []string + // Provider is an auth provider + Provider provider.Provider + // LoginURL is the relative url path where a user can login + LoginURL string } type Option func(o *Options) @@ -41,6 +47,20 @@ func Token(t string) Option { } } +// Provider set the auth provider +func Provider(p provider.Provider) Option { + return func(o *Options) { + o.Provider = p + } +} + +// LoginURL sets the auth LoginURL +func LoginURL(url string) Option { + return func(o *Options) { + o.LoginURL = url + } +} + type GenerateOptions struct { // Metadata associated with the account Metadata map[string]string diff --git a/auth/provider/basic/basic.go b/auth/provider/basic/basic.go new file mode 100644 index 00000000..413053ef --- /dev/null +++ b/auth/provider/basic/basic.go @@ -0,0 +1,34 @@ +package basic + +import ( + "github.com/micro/go-micro/v2/auth/provider" +) + +// NewProvider returns an initialised basic provider +func NewProvider(opts ...provider.Option) provider.Provider { + var options provider.Options + for _, o := range opts { + o(&options) + } + return &basic{options} +} + +type basic struct { + opts provider.Options +} + +func (b *basic) String() string { + return "basic" +} + +func (b *basic) Options() provider.Options { + return b.opts +} + +func (b *basic) Endpoint() string { + return "" +} + +func (b *basic) Redirect() string { + return "" +} diff --git a/auth/provider/oauth/oauth.go b/auth/provider/oauth/oauth.go new file mode 100644 index 00000000..f43c45f2 --- /dev/null +++ b/auth/provider/oauth/oauth.go @@ -0,0 +1,42 @@ +package oauth + +import ( + "fmt" + + "github.com/micro/go-micro/v2/auth/provider" +) + +// NewProvider returns an initialised oauth provider +func NewProvider(opts ...provider.Option) provider.Provider { + var options provider.Options + for _, o := range opts { + o(&options) + } + return &oauth{options} +} + +type oauth struct { + opts provider.Options +} + +func (o *oauth) String() string { + return "oauth" +} + +func (o *oauth) Options() provider.Options { + return o.opts +} + +func (o *oauth) Endpoint() string { + s := fmt.Sprintf("%v?client_id=%v", o.opts.Endpoint, o.opts.ClientID) + + if scope := o.opts.Scope; len(scope) > 0 { + s = fmt.Sprintf("%v&scope=%v", s, scope) + } + + return s +} + +func (o *oauth) Redirect() string { + return o.opts.Redirect +} diff --git a/auth/provider/options.go b/auth/provider/options.go new file mode 100644 index 00000000..930df479 --- /dev/null +++ b/auth/provider/options.go @@ -0,0 +1,47 @@ +package provider + +// Option returns a function which sets an option +type Option func(*Options) + +// Options a provider can have +type Options struct { + // ClientID is the application's ID. + ClientID string + // ClientSecret is the application's secret. + ClientSecret string + // Endpoint for the provider + Endpoint string + // Redirect url incase of UI + Redirect string + // Scope of the oauth request + Scope string +} + +// Credentials is an option which sets the client id and secret +func Credentials(id, secret string) Option { + return func(o *Options) { + o.ClientID = id + o.ClientSecret = secret + } +} + +// Endpoint sets the endpoint option +func Endpoint(e string) Option { + return func(o *Options) { + o.Endpoint = e + } +} + +// Redirect sets the Redirect option +func Redirect(r string) Option { + return func(o *Options) { + o.Redirect = r + } +} + +// Scope sets the oauth scope +func Scope(s string) Option { + return func(o *Options) { + o.Scope = s + } +} diff --git a/auth/provider/provider.go b/auth/provider/provider.go new file mode 100644 index 00000000..86a4504d --- /dev/null +++ b/auth/provider/provider.go @@ -0,0 +1,28 @@ +// Package provider is an external auth provider e.g oauth +package provider + +import ( + "time" +) + +// Provider is an auth provider +type Provider interface { + // String returns the name of the provider + String() string + // Options returns the options of a provider + Options() Options + // Endpoint for the provider + Endpoint() string + // Redirect url incase of UI + Redirect() string +} + +// Grant is a granted authorisation +type Grant struct { + // token for reuse + Token string + // Expiry of the token + Expiry time.Time + // Scopes associated with grant + Scopes []string +} diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index fbb253dc..9e1d0039 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "github.com/micro/go-micro/v2/auth/provider" + "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/broker" "github.com/micro/go-micro/v2/client" @@ -70,6 +72,10 @@ import ( jwtAuth "github.com/micro/go-micro/v2/auth/jwt" sAuth "github.com/micro/go-micro/v2/auth/service" storeAuth "github.com/micro/go-micro/v2/auth/store" + + // auth providers + "github.com/micro/go-micro/v2/auth/provider/basic" + "github.com/micro/go-micro/v2/auth/provider/oauth" ) type Cmd interface { @@ -269,6 +275,36 @@ var ( EnvVars: []string{"MICRO_AUTH_EXCLUDE"}, Usage: "Comma-separated list of endpoints excluded from authentication, e.g. Users.ListUsers", }, + &cli.StringFlag{ + Name: "auth_provider", + EnvVars: []string{"MICRO_AUTH_PROVIDER"}, + Usage: "Auth provider used to login user", + }, + &cli.StringFlag{ + Name: "auth_provider_client_id", + EnvVars: []string{"MICRO_AUTH_PROVIDER_CLIENT_ID"}, + Usage: "The client id to be used for oauth", + }, + &cli.StringFlag{ + Name: "auth_provider_client_secret", + EnvVars: []string{"MICRO_AUTH_PROVIDER_CLIENT_SECRET"}, + Usage: "The client secret to be used for oauth", + }, + &cli.StringFlag{ + Name: "auth_provider_endpoint", + EnvVars: []string{"MICRO_AUTH_PROVIDER_ENDPOINT"}, + Usage: "The enpoint to be used for oauth", + }, + &cli.StringFlag{ + Name: "auth_provider_redirect", + EnvVars: []string{"MICRO_AUTH_PROVIDER_REDIRECT"}, + Usage: "The redirect to be used for oauth", + }, + &cli.StringFlag{ + Name: "auth_provider_scope", + EnvVars: []string{"MICRO_AUTH_PROVIDER_SCOPE"}, + Usage: "The scope to be used for oauth", + }, } DefaultBrokers = map[string]func(...broker.Option) broker.Broker{ @@ -328,6 +364,11 @@ var ( "jwt": jwtAuth.NewAuth, } + DefaultAuthProviders = map[string]func(...provider.Option) provider.Provider{ + "oauth": oauth.NewProvider, + "basic": basic.NewProvider, + } + DefaultProfiles = map[string]func(...profile.Option) profile.Profile{ "http": http.NewProfile, "pprof": pprof.NewProfile, @@ -627,6 +668,32 @@ func (c *cmd) Before(ctx *cli.Context) error { authOpts = append(authOpts, auth.Exclude(ctx.StringSlice("auth_exclude")...)) } + if name := ctx.String("auth_provider"); len(name) > 0 { + p, ok := DefaultAuthProviders[name] + if !ok { + return fmt.Errorf("AuthProvider %s not found", name) + } + + var provOpts []provider.Option + + clientID := ctx.String("auth_provider_client_id") + clientSecret := ctx.String("auth_provider_client_secret") + if len(clientID) > 0 || len(clientSecret) > 0 { + provOpts = append(provOpts, provider.Credentials(clientID, clientSecret)) + } + if e := ctx.String("auth_provider_endpoint"); len(e) > 0 { + provOpts = append(provOpts, provider.Endpoint(e)) + } + if r := ctx.String("auth_provider_redirect"); len(r) > 0 { + provOpts = append(provOpts, provider.Redirect(r)) + } + if s := ctx.String("auth_provider_scope"); len(s) > 0 { + provOpts = append(provOpts, provider.Scope(s)) + } + + authOpts = append(authOpts, auth.Provider(p(provOpts...))) + } + if len(authOpts) > 0 { if err := (*c.opts.Auth).Init(authOpts...); err != nil { log.Fatalf("Error configuring auth: %v", err) diff --git a/util/http/http.go b/util/http/http.go index 055374ed..3b743c2a 100644 --- a/util/http/http.go +++ b/util/http/http.go @@ -1,12 +1,47 @@ package http import ( + "encoding/json" + "fmt" + "log" "net/http" "github.com/micro/go-micro/v2/client/selector" "github.com/micro/go-micro/v2/registry" ) +// Write sets the status and body on a http ResponseWriter +func Write(w http.ResponseWriter, contentType string, status int, body string) { + w.Header().Set("Content-Length", fmt.Sprintf("%v", len(body))) + w.Header().Set("Content-Type", contentType) + w.WriteHeader(status) + fmt.Fprintf(w, `%v`, body) +} + +// WriteBadRequestError sets a 400 status code +func WriteBadRequestError(w http.ResponseWriter, err error) { + rawBody, err := json.Marshal(map[string]string{ + "error": err.Error(), + }) + if err != nil { + WriteInternalServerError(w, err) + return + } + Write(w, "application/json", 400, string(rawBody)) +} + +// WriteInternalServerError sets a 500 status code +func WriteInternalServerError(w http.ResponseWriter, err error) { + rawBody, err := json.Marshal(map[string]string{ + "error": err.Error(), + }) + if err != nil { + log.Println(err) + return + } + Write(w, "application/json", 500, string(rawBody)) +} + func NewRoundTripper(opts ...Option) http.RoundTripper { options := Options{ Registry: registry.DefaultRegistry,