A middleware plugin for securing a Gin application behind Cloudflare Access authentication.
- gin-cloudflare-access
go get github.com/fabiofenoglio/gin-cloudflare-access
package main
import (
"net/http"
gincloudflareaccess "github.com/fabiofenoglio/gin-cloudflare-access"
"github.com/gin-gonic/gin"
)
func main() {
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(&gincloudflareaccess.Config{
TeamDomain: "myorganization",
ValidAudiences: []string{
"123123123123123123123123123123123123123",
},
})
r := gin.Default()
// plug in authenticator at the root level
r.Use(cfAccess.AuthenticationMiddleware())
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// require authenticated users for all routes under /secured
authorized := r.Group("/secured", cfAccess.RequireAuthenticated())
authorized.GET("/hello", func(c *gin.Context) {
principal := gincloudflareaccess.GetPrincipal(c)
c.JSON(http.StatusOK, "hello "+principal.Identity.Name)
})
// run the server and listen on http://localhost:9000
err := r.Run(":9000")
if err != nil {
panic(err)
}
}
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(&gincloudflareaccess.Config{
// ...
})
r := gin.Default()
// plug in authenticator at the root level
r.Use(cfAccess.AuthenticationMiddleware())
// this route will NOT require authentication.
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// this route WILL require authentication
r.GET("/whoami", cfAccess.RequireAuthenticated(), func(c *gin.Context) {
c.String(http.StatusOK, "you are a valid user")
})
// ALL routes under /secured/** will require authentication
authorized := r.Group("/secured", cfAccess.RequireAuthenticated())
authorized.GET("/hello", func(c *gin.Context) {
principal := gincloudflareaccess.GetPrincipal(c)
c.JSON(http.StatusOK, "hello "+principal.Identity.Name)
})
// ...
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(&gincloudflareaccess.Config{
// ...
})
r := gin.Default()
// plug in authenticator at the root level
r.Use(cfAccess.AuthenticationMiddleware())
// ALL routes under /only-administrators/** will be restricted
// to members of [email protected]
//
// You can also use .RequireAllGroups(...) or .RequireAnyGroup(...)
authorized := r.Group("/only-administrators", cfAccess.RequireGroup("[email protected]"))
authorized.GET("/hello", func(c *gin.Context) {
// ...
})
// ...
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(&gincloudflareaccess.Config{
// ...
})
r := gin.Default()
// plug in authenticator at the root level
r.Use(cfAccess.AuthenticationMiddleware())
// ALL routes under /only-fabio/** will be protected by this custom check
authorized := r.Group("/only-fabio", cfAccess.Require(func(c *gin.Context, principal *gincloudflareaccess.CloudflareAccessPrincipal) error {
if principal == nil || principal.Identity.Name != "Fabio" {
return errors.New("you are not my true father!")
}
return nil
}))
authorized.GET("/hello", func(c *gin.Context) {
// ...
})
// ...
r.GET("/hello", func(c *gin.Context) {
principal := gincloudflareaccess.GetPrincipal(c)
c.JSON(http.StatusOK, "hello "+principal.Identity.Name)
})
r.GET("/hello", func(c *gin.Context) {
inGroup := gincloudflareaccess.PrincipalInGroup(c, "[email protected]")
inAllGroups := gincloudflareaccess.PrincipalInAllGroups(c, []string{
"[email protected]",
"[email protected]",
"groupid00X",
})
inAnyGroup := gincloudflareaccess.PrincipalInAnyGroups(c, []string{
"[email protected]",
"[email protected]",
})
// ...
})
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(&gincloudflareaccess.Config{
TeamDomain: "myorganization",
ValidAudiences: []string{
"123123123123123123123123123123123123123",
},
// Whenever a request is blocked because of invalid or missing authentication,
// LDAP group conditions not met or custom checks failing,
// a default error response will be returned in JSON.
//
// You can change the way these errors are handled by providing a ErrorResponseHandler.
// it should call a finalization method such as AbortWithStatusJSON.
//
// The ErrorResponseHandler function will be invoked with the request context,
// the status error (either 401 or 403) and a non-nil error.
ErrorResponseHandler: func(c *gin.Context, status int, err error) {
c.AbortWithStatusJSON(
status,
fmt.Sprintf("customized error response (original error: %v)", err),
)
},
})
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(&gincloudflareaccess.Config{
TeamDomain: "myorganization",
ValidAudiences: []string{
"123123123123123123123123123123123123123",
},
// If for some reason you want to provide the Access header
// under a different header or with a different mechanism,
// you can provide the TokenExtractFunc parameter.
//
// The function should look for an authorization token wherever you need
// in the request, and return it.
// If no token was found you should return an empty string and a nil error.
// The request will be aborted if the function returns a non-nil error.
TokenExtractFunc: func(c *gin.Context) (string, error) {
h := c.Request.Header.Get("X-Custom-Auth-Header")
if h != "" {
return h, nil
}
cookie, err := c.Request.Cookie("X-Auth-Cookie")
if cookie != nil && err != nil {
return cookie.Value, nil
}
return "", nil
},
})
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(&gincloudflareaccess.Config{
TeamDomain: "myorganization",
ValidAudiences: []string{
"123123123123123123123123123123123123123",
},
// By default principals authenticated from a token are cached in memory
// for a short duration.
// You can disable the caching mechanism by providing the DisableCache parameter.
DisableCache: false,
// By default principals authenticated from a token are cached in memory
// for 5 minutes.
// You can change this duration with the CacheTTL parameter.
CacheTTL: 2 * time.Minute,
})
You can provide a custom AuthenticationFunc
if you want to mock authentication for development purposes.
settings := &gincloudflareaccess.Config{
TeamDomain: "myorganization",
ValidAudiences: []string{
"123123123123123123123123123123123123123",
},
}
if (inDevelopment) {
settings.AuthenticationFunc = func(ctx context.Context, _ string) (*CloudflareAccessPrincipal, error) {
return &CloudflareAccessPrincipal{
Identity: &CloudflareIdentity{
Email: "[email protected]",
Name: "some mocked user",
Groups: []CloudflareIdentityGroup{
{
Id: "group0",
Name: "Some Group",
Email: "[email protected]",
},
},
},
Email: "[email protected]",
}, nil
}
}
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(settings)
You might also pass both AuthenticationFunc
and TokenExtractFunc
to have a more dynamic mocking logic:
settings := &gincloudflareaccess.Config{
TeamDomain: "myorganization",
ValidAudiences: []string{
"123123123123123123123123123123123123123",
},
}
if (inDevelopment) {
settings.TokenExtractFunc = func(c *gin.Context) (string, error) {
// the content of X-Mocked-Auth will be passed as 'inputFromHeader' to the AuthenticationFunc
return c.Request.Header.Get("X-Mocked-Auth"), nil
}
settings.AuthenticationFunc = func(ctx context.Context, inputFromHeader string) (*CloudflareAccessPrincipal, error) {
return &CloudflareAccessPrincipal{
Identity: &CloudflareIdentity{
Email: inputFromHeader + "@mock.com",
Name: "user " + inputFromHeader,
Groups: []CloudflareIdentityGroup{
{
Id: "group0",
Name: "Some Group",
Email: "[email protected]",
},
},
},
Email: inputFromHeader + "@mock.com",
}, nil
}
}
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(settings)
package main
import (
"errors"
"fmt"
"net/http"
"time"
gincloudflareaccess "github.com/fabiofenoglio/gin-cloudflare-access"
"github.com/gin-gonic/gin"
)
func main() {
cfAccess := gincloudflareaccess.NewCloudflareAccessMiddleware(&gincloudflareaccess.Config{
// TeamDomain is the name of your team.
//
// it's the third-level domain of your authentication portal,
// for instance if your login page is https://organization.cloudflareaccess.com
// then your TeamDomain is "organization"{
TeamDomain: "organization",
// Every Access Policy created under the Access or Team portal
// will come with a specific Audience Tag.
//
// You should provide at least one audience tag
// but you can support as many policies as you want by providing
// multiple audience tags.
ValidAudiences: []string{
"123123123123123123123123123123123123123",
"456456456456456456456456456456456456456",
},
// By default principals authenticated from a token are cached in memory
// for a short duration.
// You can disable the caching mechanism by providing the DisableCache parameter.
DisableCache: false,
// By default principals authenticated from a token are cached in memory
// for 5 minutes.
// You can change this duration with the CacheTTL parameter.
CacheTTL: 2 * time.Minute,
// If for some reason you want to provide the Access header
// under a different header or with a different mechanism,
// you can provide the TokenExtractFunc parameter.
//
// The function should look for an authorization token wherever you need
// in the request, and return it.
// If no token was found you should return an empty string and a nil error.
// The request will be aborted if the function returns a non-nil error.
TokenExtractFunc: func(c *gin.Context) (string, error) {
h := c.Request.Header.Get("X-Custom-Auth-Header")
if h != "" {
return h, nil
}
cookie, err := c.Request.Cookie("X-Auth-Cookie")
if cookie != nil && err != nil {
return cookie.Value, nil
}
return "", nil
},
// Whenever a request is blocked because of invalid or missing authentication,
// LDAP group conditions not met or custom checks failing,
// a default error response will be returned in JSON.
//
// You can change the way these errors are handled by providing a ErrorResponseHandler.
// it should call a finalization method such as AbortWithStatusJSON.
//
// The ErrorResponseHandler function will be invoked with the request context,
// the status error (either 401 or 403) and a non-nil error.
ErrorResponseHandler: func(c *gin.Context, status int, err error) {
c.AbortWithStatusJSON(
status,
fmt.Sprintf("customized error response (original error: %v)", err),
)
},
})
r := gin.Default()
// plug in authenticator at the root level
// this middleware will read the authorization header or cookies
// and, if provided, will validate and authenticate the user.
//
// invalid credentials and expired tokens will cause an immediate abort.
//
// note that, by itself, this middleware does not prevent
// unauthenticated access nor perform any check on the authentication result
// other than blocking invalid credentials.
// additionals check have to be enabled with the .Require...() middlewares
// that you'll see in the following lines.
r.Use(cfAccess.AuthenticationMiddleware())
// this route will not require authentication
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// let's declare a sample handler to be reused from the following routes
helloHandler := func(c *gin.Context) {
// you can retrieve the principal from the gin.Context using GetPrincipal
// mind that GetPrincipal may return nil for unauthenticated requests
principal := gincloudflareaccess.GetPrincipal(c)
// reply with details about the authenticated principal
c.JSON(http.StatusOK, principal)
}
// require authenticated users for all routes under /secured/**
// by plugging in the RequireAuthenticated middleware
//
// note that as other middlewares, .RequireAuthenticated() can be applied to a single route,
// to a route group or to the whole router
authorized := r.Group("/secured", cfAccess.RequireAuthenticated())
// this routes will require authentication
// (inherited from the 'authorized' route group)
authorized.GET("/some-protected-route", helloHandler)
authorized.GET("/other-protected-route", helloHandler)
// this route will require a custom condition to be evaluated on each request
//
// the .Require() middleware can be used to implements custom checks:
// it receives the request context and the authenticated principals
// and it can return a non-nil error to abort the request.
//
// when the provided function returns an error,
// the default behavior for Forbidden requests executes, so
// if a ErrorResponseHandler has been provided it will be
// invoked with the returned error and a 403 status code.
//
// note that as other middlewares, .Require() can be applied to a single route,
// to a route group or to the whole router
r.GET("/require-custom", cfAccess.Require(func(c *gin.Context, principal *gincloudflareaccess.CloudflareAccessPrincipal) error {
if principal == nil {
return errors.New("auth required")
}
if c.Request.Header.Get("X-Mock-Allow") != principal.Identity.Email {
return errors.New("required custom header not valid")
}
return nil
}), helloHandler)
// this route will require authenticated users belonging to
// a specific LDAP group.
//
// note that as other middlewares, .RequireGroup() can be applied to a single route,
// to a route group or to the whole router
r.GET("/require-group", cfAccess.RequireGroup("[email protected]"), helloHandler)
// this route will require authenticated users belonging to
// everyone of the specified LDAP groups.
//
// note that as other middlewares, .RequireAllGroups() can be applied to a single route,
// to a route group or to the whole router
r.GET("/require-all-groups", cfAccess.RequireAllGroups([]string{
"[email protected]",
"[email protected]",
}), helloHandler)
// this route will require authenticated users belonging to
// at least one of the specified LDAP groups.
//
// note that as other middlewares, .RequireAnyGroup() can be applied to a single route,
// to a route group or to the whole router
r.GET("/require-any-group", cfAccess.RequireAnyGroup([]string{
"[email protected]",
"[email protected]",
}), helloHandler)
// this route will require authentication
r.GET("/auth-demo", cfAccess.RequireAuthenticated(), func(c *gin.Context) {
// you can retrieve the principal with the GetPrincipal method.
// mind that GetPrincipal may return nil for unauthenticated requests
principal := gincloudflareaccess.GetPrincipal(c)
if principal == nil {
panic("didn't expect a nil principal")
}
// you can manually check if the user belongs to
// a/all/any specified LDAP groups with the helper methods:
inGroup := gincloudflareaccess.PrincipalInGroup(c, "[email protected]")
if !inGroup {
panic("go away")
}
})
// run the server and listen on http://localhost:9000
err := r.Run(":9000")
if err != nil {
panic(err)
}
}
{
"token":{
"iss":"https://organization.cloudflareaccess.com",
"aud":[
"456456456456456456456456456456456456456456"
],
"sub":"79ea41a5-d90c-45b0-83b7-98bab753c982",
"exp":"2022-01-23T10:30:51+01:00",
"iat":"2022-01-22T10:30:51+01:00",
"email":"[email protected]",
"identity_nonce":"dskgwjegowegjo",
"country":"IT"
},
"identity":{
"id":"1231231241241212312",
"name":"User Name",
"email":"[email protected]",
"user_uuid":"79ea41a5-d90c-45b0-83b7-98bab753c982",
"account_id":"cee91dbebfad4e93be5df3616215e207",
"ip":"1.2.3.4",
"auth_status":"NONE",
"common_name":"",
"service_token_id":"",
"service_token_status":false,
"is_warp":false,
"is_gateway":false,
"version":0,
"device_sessions":{},
"iat":1642843851,
"idp":{
"id":"891cfb5e-7de3-43e4-9929-4ae34ab6e110",
"type":"google-apps"
},
"geo":{
"country":"IT"
},
"groups":[
{
"id":"f81e269a598341f8807028463abb6eea",
"name":"Administrators",
"email":"[email protected]"
},
{
"id":"865c98799d304008b6258b736321b395",
"name":"Support",
"email":"[email protected]"
}
]
},
"email":"[email protected]"
}