2020-03-07 14:06:57 +03:00
package auth
import (
2020-04-06 18:10:08 +03:00
"context"
2020-03-17 23:04:16 +03:00
"fmt"
2020-04-02 19:01:06 +03:00
"net"
2020-03-07 14:06:57 +03:00
"net/http"
2020-03-17 23:04:16 +03:00
"net/url"
2020-03-07 14:06:57 +03:00
"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"
2020-03-07 14:06:57 +03:00
"github.com/micro/go-micro/v2/auth"
2020-04-02 19:01:06 +03:00
"github.com/micro/go-micro/v2/logger"
2020-03-07 14:06:57 +03:00
)
// 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 ( )
}
2020-03-07 14:06:57 +03:00
return authHandler {
2020-04-07 11:40:40 +03:00
handler : h ,
resolver : r ,
auth : auth . DefaultAuth ,
servicePrefix : prefix ,
namespace : namespace ,
2020-03-07 14:06:57 +03:00
}
}
type authHandler struct {
2020-04-07 11:40:40 +03:00
handler http . Handler
auth auth . Auth
resolver resolver . Resolver
namespace string
servicePrefix string
2020-03-07 14:06:57 +03:00
}
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
namespace := h . namespaceFromRequest ( req )
2020-04-02 19:01:06 +03:00
req . Header . Set ( auth . NamespaceKey , namespace )
2020-03-17 22:24:10 +03:00
// 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
2020-03-25 14:20:53 +03:00
if strings . HasPrefix ( header , auth . BearerScheme ) {
token = header [ len ( auth . BearerScheme ) : ]
2020-03-17 22:24:10 +03:00
}
} else {
// Get the token out the cookies if not provided in headers
if c , err := req . Cookie ( "micro-token" ) ; err == nil && c != nil {
2020-03-23 19:19:30 +03:00
token = strings . TrimPrefix ( c . Value , auth . TokenCookieName + "=" )
2020-03-25 14:20:53 +03:00
req . Header . Set ( "Authorization" , auth . BearerScheme + token )
2020-03-17 22:24:10 +03:00
}
}
2020-03-07 14:06:57 +03:00
2020-03-23 19:19:30 +03:00
// 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 {
2020-04-02 19:01:06 +03:00
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-03-07 14:06:57 +03:00
}
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
2020-04-03 16:19:03 +03:00
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
2020-03-23 19:19:30 +03:00
h . handler . ServeHTTP ( w , req )
return
2020-03-07 14:06:57 +03:00
}
2020-04-02 19:01:06 +03:00
// The account is set, but they don't have enough permissions, hence
// we return a forbidden error.
2020-03-23 19:19:30 +03:00
if len ( acc . ID ) > 0 {
2020-04-07 02:19:49 +03:00
http . Error ( w , "Forbidden request" , 403 )
2020-03-07 14:06:57 +03:00
return
}
// If there is no auth login url set, 401
2020-03-23 19:19:30 +03:00
loginURL := h . auth . Options ( ) . LoginURL
2020-03-07 14:06:57 +03:00
if loginURL == "" {
2020-04-07 02:19:49 +03:00
http . Error ( w , "unauthorized request" , 401 )
2020-03-16 13:30:56 +03:00
return
2020-03-07 14:06:57 +03:00
}
// Redirect to the login path
2020-03-17 23:04:16 +03:00
params := url . Values { "redirect_to" : { req . URL . Path } }
loginWithRedirect := fmt . Sprintf ( "%v?%v" , loginURL , params . Encode ( ) )
http . Redirect ( w , req , loginWithRedirect , http . StatusTemporaryRedirect )
2020-03-07 14:06:57 +03:00
}
2020-04-02 19:01:06 +03:00
2020-04-07 11:40:40 +03:00
func ( h authHandler ) namespaceFromRequest ( req * http . Request ) string {
// 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 the micro.mu domain
if strings . HasSuffix ( host , "micro.mu" ) {
return auth . DefaultNamespace
2020-04-03 16:09:25 +03:00
}
2020-04-02 19:01:06 +03:00
// 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
2020-04-02 19:01:06 +03:00
}
// 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-02 19:01:06 +03:00
}
2020-04-07 02:19:49 +03:00
// TODO: this logic needs to be replaced with usage of publicsuffix
2020-04-02 19:01:06 +03:00
// if host is not a subdomain, deturn default namespace
comps := strings . Split ( host , "." )
2020-04-07 11:40:40 +03:00
if len ( comps ) < 3 {
return auth . DefaultNamespace
2020-04-02 19:01:06 +03:00
}
2020-04-07 11:40:40 +03:00
// return the reversed subdomain as the namespace
nComps := comps [ 0 : len ( comps ) - 2 ]
for i := len ( nComps ) / 2 - 1 ; i >= 0 ; i -- {
opp := len ( nComps ) - 1 - i
nComps [ i ] , nComps [ opp ] = nComps [ opp ] , nComps [ i ]
2020-04-02 19:01:06 +03:00
}
2020-04-07 11:40:40 +03:00
return strings . Join ( nComps , "." )
2020-04-02 19:01:06 +03:00
}