Echo is a high performance, extensible, minimalist Go web framework.
This post explains how to protect Echo endpoints with access token issued by Crossid OAuth2 auth server.
Init an Echo app
mkdir myapp && cd myapp
go mod init myapp
go get github.com/labstack/echo/v4
go get github.com/MicahParks/keyfunc
Lets create server.go with few routes that require different access level.
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// accessible anonymously.
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Public!")
})
// renders the authenticated user.
e.GET("/whoami", func(c echo.Context) error {
return c.String(http.StatusOK, "Whoami!")
})
// accessible only by users that assigned to the "admin" scope.
e.GET("/admin", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin!")
})
e.Logger.Fatal(e.Start(":1323"))
}
We defined three routes:
route | access level |
---|---|
/ | accessible by everyone including anonymous visitors. |
/whoami | accessible by any authenticated user |
/admin | accessible by an authenticated user that is assigned to the admin scope. |
Start server
go run server.go
At this point, all routes are accessible by everyone:
curl http://localhost:1323/
curl http://localhost:1323/whoami
curl http://localhost:1323/admin
Auth middleware
Echo has its own JWT middleware to protect routes with JWTs.
Lets create a JWT Middleware instance:
func authmw() echo.MiddlewareFunc {
// jwksURL is the URL of the public JSON Web Tokens that sign our tokens.
jwksURL := "https://<tenant>.crossid.io/oauth2/.well-known/jwks.json"
jwks, err := keyfunc.Get(jwksURL, keyfunc.Options{
RefreshErrorHandler: func(err error) {
panic(fmt.Sprintf("There was an error with the jwt.KeyFunc\nError:%s\n", err.Error()))
},
})
if err != nil {
panic(fmt.Sprintf("Failed to create JWKs from resource at the given URL.\nError:%s\n", err.Error()))
}
// initialize JWT middleware instance
return middleware.JWTWithConfig(middleware.JWTConfig{
// skip public endpoints
Skipper: func(context echo.Context) bool {
return context.Path() == "/"
},
KeyFunc: func(token *jwt.Token) (interface{}, error) {
// a hack to convert jwt -> v4 tokens
t, _, err := new(jwtv4.Parser).ParseUnverified(token.Raw, jwtv4.MapClaims{})
if err != nil {
return nil, err
}
return jwks.KeyFunc(t)
},
})
}
- line 3 - We must provide a valid well known JWKs endpoint where all public keys used to sign tokens are exposed.
- line 26 - is the actual function that performs the access token verification against the authorization server keys.
Use MW in Echo
Simple tell Echo to use the jwt middleware before all routes.
func main() {
e := echo.New()
e.Use(authmw())
// ...
}
Protecting admin
To protect admin route with specific scopes, replace the admin handler with:
e.GET("/admin", func(c echo.Context) error {
// the authenticated token is store by the middleware as "user"
tok := c.Get("user").(*jwt.Token)
// access token scopes
scps := tok.Claims.(jwt.MapClaims)["scp"].([]interface{})
allowed := false
for _, scp := range scps {
if scp == "admin" {
allowed = true
break
}
}
if !allowed {
return c.JSON(http.StatusForbidden, map[string]string{"message": "Insufficient privileges"})
}
return c.String(http.StatusOK, "Admin!")
})
Whoami endpoint
Lets implement the whoami route to print the verified token's claims:
e.GET("/whoami", func(c echo.Context) error {
cl := c.Get("user").(*jwt.Token).Claims.(jwt.MapClaims)
return c.JSONPretty(http.StatusOK, cl, " ")
})
Recap
We have seen how simple is to start an Echo server with various access levels route protection.
What's next?
- See Get Started guide how to use Crossid, you can start for free.
- For other frameworks see Languages.
Full sample available at here.