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 }