diff --git a/api/server/auth/auth.go b/api/server/auth/auth.go deleted file mode 100644 index 530e6403..00000000 --- a/api/server/auth/auth.go +++ /dev/null @@ -1,181 +0,0 @@ -package auth - -import ( - "context" - "fmt" - "net" - "net/http" - "net/url" - "strings" - - "github.com/micro/go-micro/v2/api/resolver" - "github.com/micro/go-micro/v2/api/resolver/path" - "github.com/micro/go-micro/v2/auth" - "github.com/micro/go-micro/v2/logger" -) - -// CombinedAuthHandler wraps a server and authenticates requests -func CombinedAuthHandler(namespace string, r resolver.Resolver, h http.Handler) http.Handler { - if r == nil { - r = path.NewResolver() - } - if len(namespace) == 0 { - namespace = "go.micro" - } - - return authHandler{ - handler: h, - resolver: r, - auth: auth.DefaultAuth, - namespace: namespace, - } -} - -type authHandler struct { - handler http.Handler - auth auth.Auth - resolver resolver.Resolver - namespace string -} - -func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - // Determine the namespace - namespace, err := namespaceFromRequest(req) - if err != nil { - logger.Error(err) - namespace = auth.DefaultNamespace - } - - // Set the namespace in the header - req.Header.Set(auth.NamespaceKey, namespace) - - // Extract the token from the request - var token string - if header := req.Header.Get("Authorization"); len(header) > 0 { - // Extract the auth token from the request - if strings.HasPrefix(header, auth.BearerScheme) { - token = header[len(auth.BearerScheme):] - } - } else { - // Get the token out the cookies if not provided in headers - if c, err := req.Cookie("micro-token"); err == nil && c != nil { - token = strings.TrimPrefix(c.Value, auth.TokenCookieName+"=") - req.Header.Set("Authorization", auth.BearerScheme+token) - } - } - - // Get the account using the token, fallback to a blank account - // since some endpoints can be unauthenticated, so the lack of an - // account doesn't necesserially mean a forbidden request - acc, err := h.auth.Inspect(token) - if err != nil { - acc = &auth.Account{Namespace: namespace} - } - - // Check the accounts namespace matches the namespace we're operating - // within. If not forbid the request and log the occurance. - if acc.Namespace != namespace { - logger.Debugf("Cross namespace request warning: account %v (%v) requested access to %v in the %v namespace", acc.ID, acc.Namespace, req.URL.Path, namespace) - // http.Error(w, "Forbidden namespace", 403) - } - - // Determine the name of the service being requested - endpoint, err := h.resolver.Resolve(req) - if err == resolver.ErrInvalidPath || err == resolver.ErrNotFound { - // a file not served by the resolver has been requested (e.g. favicon.ico) - endpoint = &resolver.Endpoint{Path: req.URL.Path} - } else if err != nil { - logger.Error(err) - http.Error(w, err.Error(), 500) - return - } else { - // set the endpoint in the context so it can be used to resolve - // the request later - ctx := context.WithValue(req.Context(), resolver.Endpoint{}, endpoint) - *req = *req.Clone(ctx) - } - - // construct the resource name, e.g. home => go.micro.web.home - resName := h.namespace - if len(endpoint.Name) > 0 { - resName = resName + "." + endpoint.Name - } - - // determine the resource path. there is an inconsistency in how resolvers - // use method, some use it as Users.ReadUser (the rpc method), and others - // use it as the HTTP method, e.g GET. TODO: Refactor this to make it consistent. - resEndpoint := endpoint.Path - if len(endpoint.Path) == 0 { - resEndpoint = endpoint.Method - } - - // Perform the verification check to see if the account has access to - // the resource they're requesting - res := &auth.Resource{Type: "service", Name: resName, Endpoint: resEndpoint, Namespace: namespace} - if err := h.auth.Verify(acc, res); err == nil { - // The account has the necessary permissions to access the resource - h.handler.ServeHTTP(w, req) - return - } - - // The account is set, but they don't have enough permissions, hence - // we return a forbidden error. - if len(acc.ID) > 0 { - http.Error(w, "Forbidden request", 403) - return - } - - // If there is no auth login url set, 401 - loginURL := h.auth.Options().LoginURL - if loginURL == "" { - http.Error(w, "unauthorized request", 401) - return - } - - // Redirect to the login path - params := url.Values{"redirect_to": {req.URL.Path}} - loginWithRedirect := fmt.Sprintf("%v?%v", loginURL, params.Encode()) - http.Redirect(w, req, loginWithRedirect, http.StatusTemporaryRedirect) -} - -func namespaceFromRequest(req *http.Request) (string, error) { - // needed to tmp debug host in prod. will be removed. - logger.Infof("Host is '%v'; URL Host is '%v'; URL Hostname is '%v'", req.Host, req.URL.Host, req.URL.Hostname()) - - // determine the host, e.g. dev.micro.mu:8080 - host := req.URL.Hostname() - if len(host) == 0 { - // fallback to req.Host - var err error - host, _, err = net.SplitHostPort(req.Host) - if err != nil && strings.Contains(err.Error(), "missing port in address") { - host = req.Host - } - } - - // check for an ip address - if net.ParseIP(host) != nil { - return auth.DefaultNamespace, nil - } - - // check for dev enviroment - if host == "localhost" || host == "127.0.0.1" { - return auth.DefaultNamespace, nil - } - - // TODO: this logic needs to be replaced with usage of publicsuffix - // if host is not a subdomain, deturn default namespace - comps := strings.Split(host, ".") - if len(comps) != 3 { - return auth.DefaultNamespace, nil - } - - // check for the micro.mu domain - domain := fmt.Sprintf("%v.%v", comps[1], comps[2]) - if domain == "micro.mu" { - return auth.DefaultNamespace, nil - } - - // return the subdomain as the host - return comps[0], nil -} diff --git a/api/server/http/http.go b/api/server/http/http.go index 2599d2db..6615232d 100644 --- a/api/server/http/http.go +++ b/api/server/http/http.go @@ -10,7 +10,6 @@ import ( "github.com/gorilla/handlers" "github.com/micro/go-micro/v2/api/server" - "github.com/micro/go-micro/v2/api/server/auth" "github.com/micro/go-micro/v2/api/server/cors" "github.com/micro/go-micro/v2/logger" ) @@ -53,7 +52,11 @@ 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(s.opts.Namespace, s.opts.Resolver, handler) + + // apply the wrappers, e.g. auth + for _, wrapper := range s.opts.Wrappers { + h = wrapper(h) + } if s.opts.EnableCORS { h = cors.CombinedCORSHandler(h) diff --git a/api/server/options.go b/api/server/options.go index 5d167ced..6d87f543 100644 --- a/api/server/options.go +++ b/api/server/options.go @@ -2,6 +2,7 @@ package server import ( "crypto/tls" + "net/http" "github.com/micro/go-micro/v2/api/resolver" "github.com/micro/go-micro/v2/api/server/acme" @@ -16,8 +17,16 @@ type Options struct { EnableTLS bool ACMEHosts []string TLSConfig *tls.Config - Namespace string Resolver resolver.Resolver + Wrappers []Wrapper +} + +type Wrapper func(h http.Handler) http.Handler + +func WrapHandler(w Wrapper) Option { + return func(o *Options) { + o.Wrappers = append(o.Wrappers, w) + } } func EnableCORS(b bool) Option { @@ -56,12 +65,6 @@ func TLSConfig(t *tls.Config) Option { } } -func Namespace(n string) Option { - return func(o *Options) { - o.Namespace = n - } -} - func Resolver(r resolver.Resolver) Option { return func(o *Options) { o.Resolver = r diff --git a/auth/auth.go b/auth/auth.go index 5f954776..e34051fa 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -89,7 +89,7 @@ type Token struct { const ( // DefaultNamespace used for auth - DefaultNamespace = "micro" + DefaultNamespace = "go.micro" // NamespaceKey is the key used when storing the namespace in metadata NamespaceKey = "Micro-Namespace" // MetadataKey is the key used when storing the account in metadata diff --git a/auth/options.go b/auth/options.go index 929cf674..d3c0f4ea 100644 --- a/auth/options.go +++ b/auth/options.go @@ -8,6 +8,8 @@ import ( ) type Options struct { + // Namespace the service belongs to + Namespace string // ID is the services auth ID ID string // Secret is used to authenticate the service @@ -28,6 +30,13 @@ type Options struct { type Option func(o *Options) +// Namespace the service belongs to +func Namespace(n string) Option { + return func(o *Options) { + o.Namespace = n + } +} + // Store to back auth func Store(s store.Store) Option { return func(o *Options) { diff --git a/auth/service/service.go b/auth/service/service.go index 57d6082e..cb1740c7 100644 --- a/auth/service/service.go +++ b/auth/service/service.go @@ -158,6 +158,11 @@ func (s *svc) Revoke(role string, res *auth.Resource) error { // Verify an account has access to a resource func (s *svc) Verify(acc *auth.Account, res *auth.Resource) error { + // set the namespace on the resource + if len(res.Namespace) == 0 { + res.Namespace = s.Options().Namespace + } + queries := [][]string{ {res.Namespace, res.Type, res.Name, res.Endpoint}, // check for specific role, e.g. service.foo.ListFoo:admin (role is checked in accessForRule) {res.Namespace, res.Type, res.Name, "*"}, // check for wildcard endpoint, e.g. service.foo* @@ -205,16 +210,15 @@ func (s *svc) Verify(acc *auth.Account, res *auth.Resource) error { func (s *svc) Inspect(token string) (*auth.Account, error) { // try to decode JWT locally and fall back to srv if an error occurs if len(strings.Split(token, ".")) == 3 && s.jwt != nil { - if acc, err := s.jwt.Inspect(token); err == nil { - return acc, nil - } + return s.jwt.Inspect(token) } + // the token is not a JWT or we do not have the keys to decode it, + // fall back to the auth service rsp, err := s.auth.Inspect(context.TODO(), &pb.InspectRequest{Token: token}) if err != nil { return nil, err } - return serializeAccount(rsp.Account), nil } diff --git a/go.sum b/go.sum index 74822cf3..6011d507 100644 --- a/go.sum +++ b/go.sum @@ -546,6 +546,7 @@ golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/util/wrapper/wrapper.go b/util/wrapper/wrapper.go index 505772b6..13ae5531 100644 --- a/util/wrapper/wrapper.go +++ b/util/wrapper/wrapper.go @@ -9,7 +9,6 @@ import ( "github.com/micro/go-micro/v2/debug/stats" "github.com/micro/go-micro/v2/debug/trace" "github.com/micro/go-micro/v2/errors" - "github.com/micro/go-micro/v2/logger" "github.com/micro/go-micro/v2/metadata" "github.com/micro/go-micro/v2/server" ) @@ -145,11 +144,6 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper { return h(ctx, req, rsp) } - // Check for auth service endpoints which should be excluded from auth - if strings.HasPrefix(req.Endpoint(), "Auth.") { - return h(ctx, req, rsp) - } - // Extract the token if present. Note: if noop is being used // then the token can be blank without erroring var token string @@ -162,33 +156,17 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper { token = header[len(auth.BearerScheme):] } - // Get the namespace for the request - namespace, ok := metadata.Get(ctx, auth.NamespaceKey) - if !ok { - logger.Debugf("Missing request namespace") - namespace = auth.DefaultNamespace - } - // Inspect the token and get the account account, err := a.Inspect(token) if err != nil { - account = &auth.Account{Namespace: namespace} - } - - // Check the accounts namespace matches the namespace we're operating - // within. If not forbid the request and log the occurance. - if account.Namespace != namespace { - logger.Debugf("Cross namespace request forbidden: account %v (%v) requested access to %v %v in the %v namespace", - account.ID, account.Namespace, req.Service(), req.Endpoint(), namespace) - // return errors.Forbidden(req.Service(), "cross namespace request") + account = &auth.Account{} } // construct the resource res := &auth.Resource{ - Type: "service", - Name: req.Service(), - Endpoint: req.Endpoint(), - Namespace: namespace, + Type: "service", + Name: req.Service(), + Endpoint: req.Endpoint(), } // Verify the caller has access to the resource