2020-03-07 14:06:57 +03:00
package auth
import (
2020-04-06 18:01:42 +03:00
"encoding/json"
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-06 18:01:42 +03:00
"github.com/micro/go-micro/v2/metadata"
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-02 19:44:48 +03:00
func CombinedAuthHandler ( namespace string , r resolver . Resolver , h http . Handler ) http . Handler {
2020-04-03 12:08:39 +03:00
if r == nil {
r = path . NewResolver ( )
}
if len ( namespace ) == 0 {
namespace = "go.micro"
}
2020-03-07 14:06:57 +03:00
return authHandler {
2020-04-02 19:44:48 +03:00
handler : h ,
resolver : r ,
auth : auth . DefaultAuth ,
namespace : namespace ,
2020-03-07 14:06:57 +03:00
}
}
type authHandler struct {
2020-04-02 19:44:48 +03:00
handler http . Handler
auth auth . Auth
resolver resolver . Resolver
namespace string
2020-03-07 14:06:57 +03:00
}
func ( h authHandler ) ServeHTTP ( w http . ResponseWriter , req * http . Request ) {
2020-04-02 19:01:06 +03:00
// Determine the namespace
namespace , err := namespaceFromRequest ( req )
if err != nil {
2020-04-03 15:29:48 +03:00
logger . Error ( err )
namespace = auth . DefaultNamespace
2020-04-02 19:01:06 +03:00
}
// Set the namespace in the header
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 )
w . WriteHeader ( http . StatusForbidden )
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-02 19:44:48 +03:00
w . WriteHeader ( http . StatusInternalServerError )
return
2020-04-06 18:01:42 +03:00
} else if err == nil {
// set the endpoint in the context so it can be used to resolve
// the request later
if bytes , err := json . Marshal ( endpoint ) ; err == nil {
ctx := metadata . Set ( req . Context ( ) , "endpoint" , string ( bytes ) )
* req = * req . WithContext ( 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
resName := h . namespace
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 {
w . WriteHeader ( http . StatusForbidden )
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-03-23 19:19:30 +03:00
w . WriteHeader ( http . StatusUnauthorized )
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
func namespaceFromRequest ( req * http . Request ) ( string , error ) {
2020-04-06 18:01:42 +03:00
// 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 ( ) )
2020-04-03 16:09:25 +03:00
// determine the host, e.g. dev.micro.mu:8080
2020-04-03 16:40:24 +03:00
host := req . URL . Hostname ( )
2020-04-03 16:09:25 +03:00
if len ( host ) == 0 {
2020-04-03 16:40:24 +03:00
// fallback to req.Host
host , _ , _ = net . SplitHostPort ( req . Host )
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-02 19:01:06 +03:00
return auth . DefaultNamespace , nil
}
// check for dev enviroment
if host == "localhost" || host == "127.0.0.1" {
return auth . DefaultNamespace , nil
}
// 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
}