#205: Work on supporting OpenID Connect for auth
This commit is contained in:
		
							
								
								
									
										238
									
								
								vendor/github.com/TwiN/g8/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								vendor/github.com/TwiN/g8/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,238 @@ | ||||
| # g8 | ||||
|  | ||||
|   | ||||
| [](https://goreportcard.com/report/github.com/TwiN/g8) | ||||
| [](https://codecov.io/gh/TwiN/g8) | ||||
| [](https://github.com/TwiN/g8) | ||||
| [](https://pkg.go.dev/github.com/TwiN/g8) | ||||
| [](https://github.com/TwiN) | ||||
|  | ||||
| g8, pronounced gate, is a simple Go library for protecting HTTP handlers. | ||||
|  | ||||
| Tired of constantly re-implementing a security layer for each application? Me too, that's why I made g8. | ||||
|  | ||||
|  | ||||
| ## Installation | ||||
| ```console | ||||
| go get -u github.com/TwiN/g8 | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ## Usage | ||||
| Because the entire purpose of g8 is to NOT waste time configuring the layer of security, the primary emphasis is to  | ||||
| keep it as simple as possible. | ||||
|  | ||||
|  | ||||
| ### Simple | ||||
| Just want a simple layer of security without the need for advanced permissions? This configuration is what you're | ||||
| looking for. | ||||
|  | ||||
| ```go | ||||
| authorizationService := g8.NewAuthorizationService().WithToken("mytoken") | ||||
| gate := g8.New().WithAuthorizationService(authorizationService) | ||||
|  | ||||
| router := http.NewServeMux() | ||||
| router.Handle("/unprotected", yourHandler) | ||||
| router.Handle("/protected", gate.Protect(yourHandler)) | ||||
|  | ||||
| http.ListenAndServe(":8080", router) | ||||
| ``` | ||||
|  | ||||
| The endpoint `/protected` is now only accessible if you pass the header `Authorization: Bearer mytoken`. | ||||
|  | ||||
| If you use `http.HandleFunc` instead of `http.Handle`, you may use `gate.ProtectFunc(yourHandler)` instead. | ||||
|  | ||||
| If you're not using the `Authorization` header, you can specify a custom token extractor.  | ||||
| This enables use cases like [Protecting a handler using session cookie](#protecting-a-handler-using-session-cookie) | ||||
|  | ||||
|  | ||||
| ### Advanced permissions | ||||
| If you have tokens with more permissions than others, g8's permission system will make managing authorization a breeze. | ||||
|  | ||||
| Rather than registering tokens, think of it as registering clients, the only difference being that clients may be  | ||||
| configured with permissions while tokens cannot.  | ||||
|  | ||||
| ```go | ||||
| authorizationService := g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken").WithPermission("admin")) | ||||
| gate := g8.New().WithAuthorizationService(authorizationService) | ||||
|  | ||||
| router := http.NewServeMux() | ||||
| router.Handle("/unprotected", yourHandler) | ||||
| router.Handle("/protected-with-admin", gate.ProtectWithPermissions(yourHandler, []string{"admin"})) | ||||
|  | ||||
| http.ListenAndServe(":8080", router) | ||||
| ``` | ||||
|  | ||||
| The endpoint `/protected-with-admin` is now only accessible if you pass the header `Authorization: Bearer mytoken`, | ||||
| because the client with the token `mytoken` has the permission `admin`. Note that the following handler would also be | ||||
| accessible with that token: | ||||
| ```go | ||||
| router.Handle("/protected", gate.Protect(yourHandler)) | ||||
| ``` | ||||
|  | ||||
| To clarify, both clients and tokens have access to handlers that aren't protected with extra permissions, and  | ||||
| essentially, tokens are registered as clients with no extra permissions in the background. | ||||
|  | ||||
| Creating a token like so: | ||||
| ```go | ||||
| authorizationService := g8.NewAuthorizationService().WithToken("mytoken") | ||||
| ``` | ||||
| is the equivalent of creating the following client: | ||||
| ```go | ||||
| authorizationService := g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken")) | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### With client provider | ||||
| A client provider's task is to retrieve a Client from an external source (e.g. a database) when provided with a token. | ||||
| You should use a client provider when you have a lot of tokens and it wouldn't make sense to register all of them using | ||||
| `AuthorizationService`'s `WithToken`/`WithTokens`/`WithClient`/`WithClients`. | ||||
|  | ||||
| Note that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4  | ||||
| aforementioned functions, the client provider will not be used. | ||||
|  | ||||
| ```go | ||||
| clientProvider := g8.NewClientProvider(func(token string) *g8.Client { | ||||
|     // We'll assume that the following function calls your database and returns a struct "User" that  | ||||
|     // has the user's token as well as the permissions granted to said user | ||||
|     user := database.GetUserByToken(token) | ||||
|     if user != nil { | ||||
|         return g8.NewClient(user.Token).WithPermissions(user.Permissions) | ||||
|     } | ||||
|     return nil | ||||
| }) | ||||
| authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider) | ||||
| gate := g8.New().WithAuthorizationService(authorizationService) | ||||
| ``` | ||||
|  | ||||
| You can also configure the client provider to cache the output of the function you provide to retrieve clients by token: | ||||
| ```go | ||||
| clientProvider := g8.NewClientProvider(...).WithCache(ttl, maxSize) | ||||
| ``` | ||||
|  | ||||
| Since g8 leverages [TwiN/gocache](https://github.com/TwiN/gocache), you can also use gocache's  | ||||
| constants for configuring the TTL and the maximum size: | ||||
| - Setting the TTL to `gocache.NoExpiration` (-1) will disable the TTL.  | ||||
| - Setting the maximum size to `gocache.NoMaxSize` (0) will disable the maximum cache size | ||||
|  | ||||
| If you're using a TTL and have a lot of tokens (100k+), you may want to use `clientProvider.StartJanitor()` to allow  | ||||
| the cache to passively delete expired entries. If you have to re-initialize the client provider after the janitor has | ||||
| been started, make sure to stop the janitor first (`clientProvider.StopJanitor()`). This is because the janitor runs on  | ||||
| a separate goroutine, thus, if you were to re-create a client provider and re-assign it, the old client provider would  | ||||
| still exist in memory with the old cache. I'm only specifying this for completeness, because for the overwhelming  | ||||
| majority of people, the gate will be created on application start and never modified again until the application shuts | ||||
| down, in which case, you don't even need to worry about stopping the janitor. | ||||
|  | ||||
| To avoid any misunderstandings, using a client provider is not mandatory. If you only have a few tokens and you can load | ||||
| them on application start, you can just leverage `AuthorizationService`'s `WithToken`/`WithTokens`/`WithClient`/`WithClients`. | ||||
|  | ||||
|  | ||||
| ## AuthorizationService | ||||
| As the previous examples may have hinted, there are several ways to create clients. The one thing they have | ||||
| in common is that they all go through AuthorizationService, which is in charge of both managing clients and determining | ||||
| whether a request should be blocked or allowed through. | ||||
|  | ||||
| | Function           | Description                                                                                                                      |  | ||||
| |:-------------------|:---------------------------------------------------------------------------------------------------------------------------------| | ||||
| | WithToken          | Creates a single static client with no extra permissions                                                                         | | ||||
| | WithTokens         | Creates a slice of static clients with no extra permissions                                                                      | | ||||
| | WithClient         | Creates a single static client                                                                                                   | | ||||
| | WithClients        | Creates a slice of static clients                                                                                                | | ||||
| | WithClientProvider | Creates a client provider which will allow a fallback to a dynamic source (e.g. to a database) when a static client is not found | | ||||
|  | ||||
| Except for `WithClientProvider`, every functions listed above can be called more than once. | ||||
| As a result, you may safely perform actions like this: | ||||
| ```go | ||||
| authorizationService := g8.NewAuthorizationService(). | ||||
|     WithToken("123"). | ||||
|     WithToken("456"). | ||||
|     WithClient(g8.NewClient("789").WithPermission("admin")) | ||||
| gate := g8.New().WithAuthorizationService(authorizationService) | ||||
| ``` | ||||
|  | ||||
| Be aware that g8.Client supports a list of permissions as well. You may call `WithPermission` several times, or call | ||||
| `WithPermissions` with a slice of permissions instead. | ||||
|  | ||||
|  | ||||
| ### Permissions | ||||
| Unlike client permissions, handler permissions are requirements. | ||||
|  | ||||
| A client may have as many permissions as you want, but for said client to have access to a handler protected by | ||||
| permissions, the client must have all permissions defined by said handler in order to have access to it. | ||||
|  | ||||
| In other words, a client with the permissions `create`, `read`, `update` and `delete` would have access to all of these handlers: | ||||
| ```go | ||||
| gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken").WithPermissions([]string{"create", "read", "update", "delete"}))) | ||||
| router := http.NewServeMux() | ||||
| router.Handle("/", gate.Protect(homeHandler)) // equivalent of gate.ProtectWithPermissions(homeHandler, []string{}) | ||||
| router.Handle("/create", gate.ProtectWithPermissions(createHandler, []string{"create"})) | ||||
| router.Handle("/read", gate.ProtectWithPermissions(readHandler, []string{"read"})) | ||||
| router.Handle("/update", gate.ProtectWithPermissions(updateHandler, []string{"update"})) | ||||
| router.Handle("/delete", gate.ProtectWithPermissions(deleteHandler, []string{"delete"})) | ||||
| router.Handle("/crud", gate.ProtectWithPermissions(crudHandler, []string{"create", "read", "update", "delete"})) | ||||
| ``` | ||||
| But it would not have access to the following handler, because while `mytoken` has the `read` permission, it does not  | ||||
| have the `backup` permission: | ||||
| ```go | ||||
| router.Handle("/backup", gate.ProtectWithPermissions(&testHandler{}, []string{"read", "backup"})) | ||||
| ``` | ||||
|  | ||||
| ## Rate limiting | ||||
| To add a rate limit of 100 requests per second: | ||||
| ``` | ||||
| gate := g8.New().WithRateLimit(100) | ||||
| ``` | ||||
|  | ||||
| ## Special use cases | ||||
| ### Protecting a handler using session cookie | ||||
| If you want to only allow authenticated users to access a handler, you can use a custom token extractor function  | ||||
| combined with a client provider. | ||||
|  | ||||
| First, we'll create a function to extract the session ID from the session cookie. While a session ID does not  | ||||
| theoretically refer to a token, g8 uses the term `token` as a blanket term to refer to any string that can be used to | ||||
| identify a client. | ||||
| ```go | ||||
| customTokenExtractorFunc := func(request *http.Request) string { | ||||
|     sessionCookie, err := request.Cookie("session") | ||||
|     if err != nil { | ||||
|         return "" | ||||
|     } | ||||
|     return sessionCookie.Value | ||||
| } | ||||
| ``` | ||||
|  | ||||
| Next, we need to create a client provider that will validate our token, which refers to the session ID in this case. | ||||
| ```go | ||||
| clientProvider := g8.NewClientProvider(func(token string) *g8.Client { | ||||
|     // We'll assume that the following function calls your database and validates whether the session is valid. | ||||
|     isSessionValid := database.CheckIfSessionIsValid(token) | ||||
|     if !isSessionValid { | ||||
|         return nil // Returning nil will cause the gate to return a 401 Unauthorized. | ||||
|     } | ||||
|     // You could also retrieve the user and their permissions if you wanted instead, but for this example, | ||||
|     // all we care about is confirming whether the session is valid or not. | ||||
|     return g8.NewClient(token) | ||||
| }) | ||||
| ``` | ||||
|  | ||||
| Keep in mind that you can get really creative with the client provider above. | ||||
| For instance, you could refresh the session's expiration time, which will allow the user to stay logged in for  | ||||
| as long as they're active. | ||||
|  | ||||
| You're also not limited to using something stateful like the example above. You could use a JWT and have your client | ||||
| provider validate said JWT. | ||||
|  | ||||
| Finally, we can create the authorization service and the gate: | ||||
| ```go | ||||
| authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider) | ||||
| gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc) | ||||
| ``` | ||||
|  | ||||
| If you need to access the token (session ID in this case) from the protected handlers, you can retrieve it from the | ||||
| request context by using the key `g8.TokenContextKey`: | ||||
| ```go | ||||
| http.Handle("/handle", gate.ProtectFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
|     sessionID, _ := r.Context().Value(g8.TokenContextKey).(string) | ||||
|     // ... | ||||
| })) | ||||
| ``` | ||||
		Reference in New Issue
	
	Block a user