github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/api/auth/wrapper.go (about)

     1  package auth
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strings"
     9  
    10  	"github.com/tickoalcantara12/micro/v3/service/api"
    11  	"github.com/tickoalcantara12/micro/v3/service/api/resolver"
    12  	"github.com/tickoalcantara12/micro/v3/service/api/resolver/subdomain"
    13  	"github.com/tickoalcantara12/micro/v3/service/auth"
    14  	"github.com/tickoalcantara12/micro/v3/service/config"
    15  	"github.com/tickoalcantara12/micro/v3/service/logger"
    16  	inauth "github.com/tickoalcantara12/micro/v3/util/auth"
    17  	"github.com/tickoalcantara12/micro/v3/util/ctx"
    18  	"github.com/tickoalcantara12/micro/v3/util/namespace"
    19  )
    20  
    21  // Wrapper wraps a handler and authenticates requests
    22  func Wrapper(r resolver.Resolver, prefix string) api.Wrapper {
    23  	useBlockList := false
    24  	val, err := config.Get("micro.api.blocklist_enabled")
    25  	if err == nil {
    26  		useBlockList = val.Bool(false)
    27  	}
    28  
    29  	return func(h http.Handler) http.Handler {
    30  		return authWrapper{
    31  			handler:       h,
    32  			resolver:      r,
    33  			servicePrefix: prefix,
    34  			useBlockList: useBlockList,
    35  		}
    36  	}
    37  }
    38  
    39  type authWrapper struct {
    40  	handler       http.Handler
    41  	resolver      resolver.Resolver
    42  	servicePrefix string
    43  	useBlockList bool
    44  }
    45  
    46  func (a authWrapper) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    47  	// Determine the name of the service being requested
    48  	endpoint, err := a.resolver.Resolve(req)
    49  	if err == resolver.ErrInvalidPath || err == resolver.ErrNotFound {
    50  		// a file not served by the resolver has been requested (e.g. favicon.ico)
    51  		endpoint = &resolver.Endpoint{Path: req.URL.Path}
    52  	} else if err != nil {
    53  		logger.Error(err)
    54  		http.Error(w, err.Error(), 500)
    55  		return
    56  	} else {
    57  		// set the endpoint in the context so it can be used to resolve
    58  		// the request later
    59  		ctx := context.WithValue(req.Context(), resolver.Endpoint{}, endpoint)
    60  		*req = *req.Clone(ctx)
    61  	}
    62  
    63  	// If an error occured looking up the route, the domain isn't returned. TODO: Find a better way
    64  	// of resolving network for non-standard requests, e.g. "/rpc".
    65  	if r, ok := a.resolver.(*subdomain.Resolver); ok && len(endpoint.Domain) == 0 {
    66  		endpoint.Domain = r.Domain(req)
    67  	}
    68  
    69  	// Set the metadata so we can access it in micro api / web
    70  	req = req.WithContext(ctx.FromRequest(req))
    71  
    72  	// Extract the token from the request
    73  	var token string
    74  	if header := req.Header.Get("Authorization"); len(header) > 0 {
    75  		// Extract the auth token from the request
    76  		if strings.HasPrefix(header, inauth.BearerScheme) {
    77  			token = header[len(inauth.BearerScheme):]
    78  		}
    79  	} else {
    80  		// Get the token out the cookies if not provided in headers
    81  		if c, err := req.Cookie("micro-token"); err == nil && c != nil {
    82  			token = strings.TrimPrefix(c.Value, inauth.TokenCookieName+"=")
    83  			req.Header.Set("Authorization", inauth.BearerScheme+token)
    84  		}
    85  	}
    86  
    87  	// Get the account using the token, some are unauthenticated, so the lack of an
    88  	// account doesn't necessarily mean a forbidden request
    89  	acc, err := auth.Inspect(token)
    90  	if err == nil {
    91  		// inject into the context
    92  		ctx := auth.ContextWithAccount(req.Context(), acc)
    93  		*req = *req.Clone(ctx)
    94  	}
    95  
    96  	// Determine the namespace and set it in the header. If the user passed auth creds
    97  	// on the request, use the namespace that issued the account, otherwise check for
    98  	// the domain of the resolved endpoint.
    99  	ns := req.Header.Get(namespace.NamespaceKey)
   100  	if len(ns) == 0 && acc != nil {
   101  		ns = acc.Issuer
   102  		req.Header.Set(namespace.NamespaceKey, ns)
   103  	} else if len(ns) == 0 {
   104  		ns = endpoint.Domain
   105  		req.Header.Set(namespace.NamespaceKey, ns)
   106  	}
   107  
   108  	// Is this account on the blocklist?
   109  	if acc != nil && a.useBlockList {
   110  		fmt.Println("checking block list")
   111  		if blocked, _ := DefaultBlockList.IsBlocked(req.Context(), acc.ID, acc.Issuer); blocked {
   112  			http.Error(w, "unauthorized request", http.StatusUnauthorized)
   113  			return
   114  		}
   115  	}
   116  
   117  	// Ensure accounts only issued by the namespace are valid.
   118  	if acc != nil && acc.Issuer != ns {
   119  		acc = nil
   120  	}
   121  
   122  	// construct the resource name, e.g. home => foo.api.home
   123  	resName := endpoint.Name
   124  	if len(a.servicePrefix) > 0 {
   125  		resName = a.servicePrefix + "." + resName
   126  	}
   127  
   128  	// determine the resource path. there is an inconsistency in how resolvers
   129  	// use method, some use it as Users.ReadUser (the rpc method), and others
   130  	// use it as the HTTP method, e.g GET. TODO: Refactor this to make it consistent.
   131  	resEndpoint := endpoint.Path
   132  	if len(endpoint.Path) == 0 {
   133  		resEndpoint = endpoint.Method
   134  	}
   135  
   136  	// Options to use when verifying the request
   137  	verifyOpts := []auth.VerifyOption{
   138  		auth.VerifyContext(req.Context()),
   139  		auth.VerifyNamespace(ns),
   140  	}
   141  
   142  	logger.Debugf("Resolving %v %v", resName, resEndpoint)
   143  
   144  	// Perform the verification check to see if the account has access to
   145  	// the resource they're requesting
   146  	res := &auth.Resource{Type: "service", Name: resName, Endpoint: resEndpoint}
   147  	if err := auth.Verify(acc, res, verifyOpts...); err == nil {
   148  		// The account has the necessary permissions to access the resource
   149  		a.handler.ServeHTTP(w, req)
   150  		return
   151  	} else if err != auth.ErrForbidden {
   152  		http.Error(w, err.Error(), http.StatusInternalServerError)
   153  		return
   154  	}
   155  
   156  	// The account is set, but they don't have enough permissions, hence
   157  	// we return a forbidden error.
   158  	if acc != nil {
   159  		http.Error(w, "Forbidden request", http.StatusForbidden)
   160  		return
   161  	}
   162  
   163  	// If there is no auth login url set, 401
   164  	loginURL := auth.DefaultAuth.Options().LoginURL
   165  	if loginURL == "" {
   166  		http.Error(w, "unauthorized request", http.StatusUnauthorized)
   167  		return
   168  	}
   169  
   170  	// this path is only executed where a login URL is specified
   171  
   172  	// get the full request path
   173  	uri := req.URL.Path
   174  	// if the login url has http:// then lets get the entire requested url
   175  	if strings.HasPrefix(loginURL, "https://") || strings.HasPrefix(loginURL, "http://") {
   176  		uri = req.URL.String()
   177  	}
   178  
   179  	// if the login url matches the request then we do nothing
   180  	// its the login page so we want to allow serving it
   181  	if uri == loginURL {
   182  		a.handler.ServeHTTP(w, req)
   183  		return
   184  	}
   185  
   186  	// Redirect to the login path
   187  	params := url.Values{"redirect_to": {req.URL.String()}}
   188  	loginWithRedirect := fmt.Sprintf("%v?%v", loginURL, params.Encode())
   189  	http.Redirect(w, req, loginWithRedirect, http.StatusTemporaryRedirect)
   190  }