{"id":16513940,"url":"https://github.com/twin/g8","last_synced_at":"2025-03-15T11:33:21.680Z","repository":{"id":45250436,"uuid":"326061680","full_name":"TwiN/g8","owner":"TwiN","description":"⛩️ Go library for protecting your HTTP handlers","archived":false,"fork":false,"pushed_at":"2024-05-04T15:57:36.000Z","size":1008,"stargazers_count":57,"open_issues_count":1,"forks_count":4,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-05-05T09:36:03.576Z","etag":null,"topics":["authorization","bearer","go","golang","hacktoberfest","handler","library","rate-limit","rate-limiting","security","token"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TwiN.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["TwiN"]}},"created_at":"2021-01-01T21:50:32.000Z","updated_at":"2024-05-30T03:16:56.101Z","dependencies_parsed_at":"2024-03-23T16:29:53.741Z","dependency_job_id":"13f1a016-6d8d-40ed-a933-56b5578dd45b","html_url":"https://github.com/TwiN/g8","commit_stats":null,"previous_names":["twinproduction/g8"],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TwiN%2Fg8","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TwiN%2Fg8/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TwiN%2Fg8/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TwiN%2Fg8/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TwiN","download_url":"https://codeload.github.com/TwiN/g8/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243725077,"owners_count":20337660,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authorization","bearer","go","golang","hacktoberfest","handler","library","rate-limit","rate-limiting","security","token"],"created_at":"2024-10-11T16:10:55.444Z","updated_at":"2025-03-15T11:33:21.666Z","avatar_url":"https://github.com/TwiN.png","language":"Go","funding_links":["https://github.com/sponsors/TwiN"],"categories":[],"sub_categories":[],"readme":"# g8\n\n![test](https://github.com/TwiN/g8/actions/workflows/test.yml/badge.svg?branch=master)\n[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/g8)](https://goreportcard.com/report/github.com/TwiN/g8/v3)\n[![codecov](https://codecov.io/gh/TwiN/g8/branch/master/graph/badge.svg)](https://codecov.io/gh/TwiN/g8)\n[![Go version](https://img.shields.io/github/go-mod/go-version/TwiN/g8.svg)](https://github.com/TwiN/g8)\n[![Go Reference](https://pkg.go.dev/badge/github.com/TwiN/g8.svg)](https://pkg.go.dev/github.com/TwiN/g8/v3)\n[![Follow TwiN](https://img.shields.io/github/followers/TwiN?label=Follow\u0026style=social)](https://github.com/TwiN)\n\ng8, pronounced gate, is a simple Go library for protecting HTTP handlers.\n\nTired of constantly re-implementing a security layer for each application? Me too, that's why I made g8.\n\n\n## Installation\n```console\ngo get -u github.com/TwiN/g8/v3\n```\n\n\n## Usage\nBecause the entire purpose of g8 is to NOT waste time configuring the layer of security, the primary emphasis is to \nkeep it as simple as possible.\n\n\n### Simple\nJust want a simple layer of security without the need for advanced permissions? This configuration is what you're\nlooking for.\n\n```go\nauthorizationService := g8.NewAuthorizationService().WithToken(\"mytoken\")\ngate := g8.New().WithAuthorizationService(authorizationService)\n\nrouter := http.NewServeMux()\nrouter.Handle(\"/unprotected\", yourHandler)\nrouter.Handle(\"/protected\", gate.Protect(yourHandler))\n\nhttp.ListenAndServe(\":8080\", router)\n```\n\nThe endpoint `/protected` is now only accessible if you pass the header `Authorization: Bearer mytoken`.\n\nIf you use `http.HandleFunc` instead of `http.Handle`, you may use `gate.ProtectFunc(yourHandler)` instead.\n\nIf you're not using the `Authorization` header, you can specify a custom token extractor. \nThis enables use cases like [Protecting a handler using session cookie](#protecting-a-handler-using-session-cookie)\n\n\n### Advanced permissions\nIf you have tokens with more permissions than others, g8's permission system will make managing authorization a breeze.\n\nRather than registering tokens, think of it as registering clients, the only difference being that clients may be \nconfigured with permissions while tokens cannot. \n\n```go\nauthorizationService := g8.NewAuthorizationService().WithClient(g8.NewClient(\"mytoken\").WithPermission(\"admin\"))\ngate := g8.New().WithAuthorizationService(authorizationService)\n\nrouter := http.NewServeMux()\nrouter.Handle(\"/unprotected\", yourHandler)\nrouter.Handle(\"/protected-with-admin\", gate.ProtectWithPermissions(yourHandler, []string{\"admin\"}))\n\nhttp.ListenAndServe(\":8080\", router)\n```\n\nThe endpoint `/protected-with-admin` is now only accessible if you pass the header `Authorization: Bearer mytoken`,\nbecause the client with the token `mytoken` has the permission `admin`. Note that the following handler would also be\naccessible with that token:\n```go\nrouter.Handle(\"/protected\", gate.Protect(yourHandler))\n```\n\nTo clarify, both clients and tokens have access to handlers that aren't protected with extra permissions, and \nessentially, tokens are registered as clients with no extra permissions in the background.\n\nCreating a token like so:\n```go\nauthorizationService := g8.NewAuthorizationService().WithToken(\"mytoken\")\n```\nis the equivalent of creating the following client:\n```go\nauthorizationService := g8.NewAuthorizationService().WithClient(g8.NewClient(\"mytoken\"))\n```\n\n\n### With client provider\nA client provider's task is to retrieve a Client from an external source (e.g. a database) when provided with a token.\nYou should use a client provider when you have a lot of tokens and it wouldn't make sense to register all of them using\n`AuthorizationService`'s `WithToken`/`WithTokens`/`WithClient`/`WithClients`.\n\nNote that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4 \naforementioned functions, the client provider will not be used.\n\n```go\nclientProvider := g8.NewClientProvider(func(token string) *g8.Client {\n    // We'll assume that the following function calls your database and returns a struct \"User\" that \n    // has the user's token as well as the permissions granted to said user\n    user := database.GetUserByToken(token)\n    if user != nil {\n        return g8.NewClient(user.Token).WithPermissions(user.Permissions)\n    }\n    return nil\n})\nauthorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)\ngate := g8.New().WithAuthorizationService(authorizationService)\n```\n\nYou can also configure the client provider to cache the output of the function you provide to retrieve clients by token:\n```go\nclientProvider := g8.NewClientProvider(...).WithCache(ttl, maxSize)\n```\n\nSince g8 leverages [TwiN/gocache](https://github.com/TwiN/gocache) (unless you're using `WithCustomCache`), \nyou can also use gocache's constants for configuring the TTL and the maximum size:\n- Setting the TTL to `gocache.NoExpiration` (-1) will disable the TTL. \n- Setting the maximum size to `gocache.NoMaxSize` (0) will disable the maximum cache size\n\nTo avoid any misunderstandings, using a client provider is not mandatory. If you only have a few tokens and you can load\nthem on application start, you can just leverage `AuthorizationService`'s `WithToken`/`WithTokens`/`WithClient`/`WithClients`.\n\n\n## AuthorizationService\nAs the previous examples may have hinted, there are several ways to create clients. The one thing they have\nin common is that they all go through AuthorizationService, which is in charge of both managing clients and determining\nwhether a request should be blocked or allowed through.\n\n| Function           | Description                                                                                                                      | \n|:-------------------|:---------------------------------------------------------------------------------------------------------------------------------|\n| WithToken          | Creates a single static client with no extra permissions                                                                         |\n| WithTokens         | Creates a slice of static clients with no extra permissions                                                                      |\n| WithClient         | Creates a single static client                                                                                                   |\n| WithClients        | Creates a slice of static clients                                                                                                |\n| 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 |\n\nExcept for `WithClientProvider`, every functions listed above can be called more than once.\nAs a result, you may safely perform actions like this:\n```go\nauthorizationService := g8.NewAuthorizationService().\n    WithToken(\"123\").\n    WithToken(\"456\").\n    WithClient(g8.NewClient(\"789\").WithPermission(\"admin\"))\ngate := g8.New().WithAuthorizationService(authorizationService)\n```\n\nBe aware that g8.Client supports a list of permissions as well. You may call `WithPermission` several times, or call\n`WithPermissions` with a slice of permissions instead.\n\n\n### Permissions\nUnlike client permissions, handler permissions are requirements.\n\nA client may have as many permissions as you want, but for said client to have access to a handler protected by\npermissions, the client must have all permissions defined by said handler in order to have access to it.\n\nIn other words, a client with the permissions `create`, `read`, `update` and `delete` would have access to all of these handlers:\n```go\ngate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient(\"mytoken\").WithPermissions([]string{\"create\", \"read\", \"update\", \"delete\"})))\nrouter := http.NewServeMux()\nrouter.Handle(\"/\", gate.Protect(homeHandler)) // equivalent of gate.ProtectWithPermissions(homeHandler, []string{})\nrouter.Handle(\"/create\", gate.ProtectWithPermissions(createHandler, []string{\"create\"}))\nrouter.Handle(\"/read\", gate.ProtectWithPermissions(readHandler, []string{\"read\"}))\nrouter.Handle(\"/update\", gate.ProtectWithPermissions(updateHandler, []string{\"update\"}))\nrouter.Handle(\"/delete\", gate.ProtectWithPermissions(deleteHandler, []string{\"delete\"}))\nrouter.Handle(\"/crud\", gate.ProtectWithPermissions(crudHandler, []string{\"create\", \"read\", \"update\", \"delete\"}))\n```\nBut it would not have access to the following handler, because while `mytoken` has the `read` permission, it does not \nhave the `backup` permission:\n```go\nrouter.Handle(\"/backup\", gate.ProtectWithPermissions(\u0026testHandler{}, []string{\"read\", \"backup\"}))\n```\n\nIf you're using an HTTP library that supports middlewares like [mux](https://github.com/gorilla/mux), you can protect \nan entire group of handlers instead using `gate.Protect` or `gate.PermissionMiddleware()`:\n```go\nrouter := mux.NewRouter()\n\nuserRouter := router.PathPrefix(\"/\").Subrouter()\nuserRouter.Use(gate.Protect)\nuserRouter.HandleFunc(\"/api/v1/users/me\", getUserProfile).Methods(\"GET\")\nuserRouter.HandleFunc(\"/api/v1/users/me/friends\", getUserFriends).Methods(\"GET\")\nuserRouter.HandleFunc(\"/api/v1/users/me/email\", updateUserEmail).Methods(\"PATCH\")\n\nadminRouter := router.PathPrefix(\"/\").Subrouter()\nadminRouter.Use(gate.PermissionMiddleware(\"admin\"))\nadminRouter.HandleFunc(\"/api/v1/users/{id}/ban\", banUserByID).Methods(\"POST\")\nadminRouter.HandleFunc(\"/api/v1/users/{id}/delete\", deleteUserByID).Methods(\"DELETE\")\n```\n\n\n## Rate limiting\nTo add a rate limit of 100 requests per second:\n```go\ngate := g8.New().WithRateLimit(100)\n```\n\n\n## Accessing the token from the protected handlers\nIf you need to access the token from the handlers you are protecting with g8, you can retrieve it from the\nrequest context by using the key `g8.TokenContextKey`:\n```go\nhttp.Handle(\"/handle\", gate.ProtectFunc(func(w http.ResponseWriter, r *http.Request) {\n    token, _ := r.Context().Value(g8.TokenContextKey).(string)\n    // ...\n}))\n```\n\n## Examples\n### Protecting a handler using session cookie\nIf you want to only allow authenticated users to access a handler, you can use a custom token extractor function \ncombined with a client provider.\n\nFirst, we'll create a function to extract the session ID from the session cookie. While a session ID does not \ntheoretically refer to a token, g8 uses the term `token` as a blanket term to refer to any string that can be used to\nidentify a client.\n```go\ncustomTokenExtractorFunc := func(request *http.Request) string {\n    sessionCookie, err := request.Cookie(\"session\")\n    if err != nil {\n        return \"\"\n    }\n    return sessionCookie.Value\n}\n```\n\nNext, we need to create a client provider that will validate our token, which refers to the session ID in this case.\n```go\nclientProvider := g8.NewClientProvider(func(token string) *g8.Client {\n    // We'll assume that the following function calls your database and validates whether the session is valid.\n    isSessionValid := database.CheckIfSessionIsValid(token)\n    if !isSessionValid {\n        return nil // Returning nil will cause the gate to return a 401 Unauthorized.\n    }\n    // You could also retrieve the user and their permissions if you wanted instead, but for this example,\n    // all we care about is confirming whether the session is valid or not.\n    return g8.NewClient(token)\n})\n```\n\nKeep in mind that you can get really creative with the client provider above.\nFor instance, you could refresh the session's expiration time, which will allow the user to stay logged in for \nas long as they're active.\n\nYou're also not limited to using something stateful like the example above. You could use a JWT and have your client\nprovider validate said JWT.\n\nFinally, we can create the authorization service and the gate:\n```go\nauthorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)\ngate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)\n```\n\nIf you need to access the token (session ID in this case) from the protected handlers, you can retrieve it from the\nrequest context by using the key `g8.TokenContextKey`:\n```go\nhttp.Handle(\"/handle\", gate.ProtectFunc(func(w http.ResponseWriter, r *http.Request) {\n    sessionID, _ := r.Context().Value(g8.TokenContextKey).(string)\n    // ...\n}))\n```\n\n### Using a custom header\nThe logic is the same as the example above:\n```go\ncustomTokenExtractorFunc := func(request *http.Request) string {\n    return request.Header.Get(\"X-API-Token\")\n}\n\nclientProvider := g8.NewClientProvider(func(token string) *g8.Client {\n    // We'll assume that the following function calls your database and returns a struct \"User\" that \n    // has the user's token as well as the permissions granted to said user\n    user := database.GetUserByToken(token)\n    if user != nil {\n        return g8.NewClient(user.Token).WithPermissions(user.Permissions)\n    }\n    return nil\n})\nauthorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)\ngate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)\n```\n\n### Using a custom cache\n\n```go\npackage main\n\nimport (\n    g8 \"github.com/TwiN/g8/v3\"\n)\n\ntype customCache struct {\n    entries map[string]any\n    sync.Mutex\n}\n\nfunc (c *customCache) Get(key string) (value any, exists bool) {\n    return nil, false\n}\n\nfunc (c *customCache) Set(key string, value any) {\n    // ...\n}\n\n// To verify the implementation\nvar _ g8.Cache = (*customCache)(nil)\n\nfunc main() {\n    getClientByTokenFunc := func(token string) *g8.Client {\n        // We'll assume that the following function calls your database and returns a struct \"User\" that\n        // has the user's token as well as the permissions granted to said user\n        user := database.GetUserByToken(token)\n        if user != nil {\n            return g8.NewClient(user.Token).WithPermissions(user.Permissions).WithData(user.Data)\n        }\n        return nil\n    }\n    // Create the provider with the custom cache\n    provider := g8.NewClientProvider(getClientByTokenFunc).WithCustomCache(\u0026customCache{})\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwin%2Fg8","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftwin%2Fg8","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwin%2Fg8/lists"}