micro/api/server/auth/auth.go

184 lines
5.6 KiB
Go
Raw Normal View History

package auth
import (
2020-04-06 18:10:08 +03:00
"context"
"fmt"
"net"
"net/http"
"net/url"
"strings"
2020-04-02 19:44:48 +03:00
"github.com/micro/go-micro/v2/api/resolver"
2020-04-03 12:08:39 +03:00
"github.com/micro/go-micro/v2/api/resolver/path"
"github.com/micro/go-micro/v2/auth"
"github.com/micro/go-micro/v2/logger"
2020-04-07 12:28:39 +03:00
"golang.org/x/net/publicsuffix"
)
// CombinedAuthHandler wraps a server and authenticates requests
2020-04-07 11:40:40 +03:00
func CombinedAuthHandler(prefix, namespace string, r resolver.Resolver, h http.Handler) http.Handler {
2020-04-03 12:08:39 +03:00
if r == nil {
r = path.NewResolver()
}
return authHandler{
2020-04-07 11:40:40 +03:00
handler: h,
resolver: r,
auth: auth.DefaultAuth,
servicePrefix: prefix,
namespace: namespace,
}
}
type authHandler struct {
2020-04-07 11:40:40 +03:00
handler http.Handler
auth auth.Auth
resolver resolver.Resolver
namespace string
servicePrefix string
}
func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
2020-04-07 11:40:40 +03:00
// Determine the namespace and set it in the header
2020-04-07 12:28:39 +03:00
namespace := h.NamespaceFromRequest(req)
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.Warnf("Cross namespace request forbidden: account %v (%v) requested access to %v in the %v namespace", acc.ID, acc.Namespace, req.URL.Path, namespace)
2020-04-07 02:19:49 +03:00
http.Error(w, "Forbidden namespace", 403)
}
2020-04-02 19:44:48 +03:00
// Determine the name of the service being requested
endpoint, err := h.resolver.Resolve(req)
2020-04-03 11:18:30 +03:00
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 {
2020-04-06 18:01:42 +03:00
logger.Error(err)
2020-04-07 02:19:49 +03:00
http.Error(w, err.Error(), 500)
2020-04-02 19:44:48 +03:00
return
2020-04-07 02:19:49 +03:00
} else {
2020-04-06 18:01:42 +03:00
// set the endpoint in the context so it can be used to resolve
// the request later
2020-04-06 18:10:08 +03:00
ctx := context.WithValue(req.Context(), resolver.Endpoint{}, endpoint)
2020-04-07 02:29:35 +03:00
*req = *req.Clone(ctx)
2020-04-02 19:44:48 +03:00
}
2020-04-03 11:18:30 +03:00
// construct the resource name, e.g. home => go.micro.web.home
2020-04-07 11:40:40 +03:00
resName := h.servicePrefix
2020-04-03 11:18:30 +03:00
if len(endpoint.Name) > 0 {
resName = resName + "." + endpoint.Name
}
2020-04-02 19:44:48 +03:00
2020-04-03 11:45:39 +03:00
// 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
}
2020-04-02 19:44:48 +03:00
// 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}
2020-04-03 11:45:39 +03:00
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 {
2020-04-07 02:19:49 +03:00
http.Error(w, "Forbidden request", 403)
return
}
// If there is no auth login url set, 401
loginURL := h.auth.Options().LoginURL
if loginURL == "" {
2020-04-07 02:19:49 +03:00
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)
}
2020-04-07 12:28:39 +03:00
func (h authHandler) NamespaceFromRequest(req *http.Request) string {
2020-04-07 11:40:40 +03:00
// check to see what the provided namespace is, we only do
// domain mapping if the namespace is set to 'domain'
if h.namespace != "domain" {
return h.namespace
}
2020-04-06 18:01:42 +03:00
2020-04-03 16:09:25 +03:00
// determine the host, e.g. dev.micro.mu:8080
2020-04-07 11:40:40 +03:00
var host string
if h, _, err := net.SplitHostPort(req.Host); err == nil {
host = h // host does contain a port
2020-04-07 12:08:06 +03:00
} else if strings.Contains(err.Error(), "missing port in address") {
2020-04-07 11:40:40 +03:00
host = req.Host // host does not contain a port
}
// check for an ip address
2020-04-03 16:09:25 +03:00
if net.ParseIP(host) != nil {
2020-04-07 11:40:40 +03:00
return auth.DefaultNamespace
}
// check for dev enviroment
if host == "localhost" || host == "127.0.0.1" {
2020-04-07 11:40:40 +03:00
return auth.DefaultNamespace
}
2020-04-07 12:28:39 +03:00
// extract the top level domain plus one (e.g. 'myapp.com')
domain, err := publicsuffix.EffectiveTLDPlusOne(host)
if err != nil {
logger.Debugf("Unable to extract domain from %v", host)
2020-04-07 11:40:40 +03:00
return auth.DefaultNamespace
}
2020-04-07 12:34:26 +03:00
// check to see if the domain matches the host of micr.mu, in
// these casees we return the default namespace
if domain == host || domain == "micro.mu" {
2020-04-07 12:28:39 +03:00
return auth.DefaultNamespace
}
// remove the domain from the host, leaving the subdomain
subdomain := strings.TrimSuffix(host, "."+domain)
2020-04-07 11:40:40 +03:00
// return the reversed subdomain as the namespace
2020-04-07 12:28:39 +03:00
comps := strings.Split(subdomain, ".")
for i := len(comps)/2 - 1; i >= 0; i-- {
opp := len(comps) - 1 - i
comps[i], comps[opp] = comps[opp], comps[i]
}
2020-04-07 12:28:39 +03:00
return strings.Join(comps, ".")
}