Merge pull request #52 from creekorful/35-better-acl-for-api
Finalize ACL implementationpull/41/head
commit
09f38acf71
@ -0,0 +1,112 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrInvalidOrMissingAuth is returned if the authorization header is absent or invalid
|
||||
var ErrInvalidOrMissingAuth = &echo.HTTPError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: "Invalid or missing `Authorization` header",
|
||||
}
|
||||
|
||||
// ErrAccessUnauthorized is returned if the token doesn't grant access to the current resource
|
||||
var ErrAccessUnauthorized = &echo.HTTPError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: "Access to the resource is not authorized",
|
||||
}
|
||||
|
||||
// Token is the authentication token used by processes when dialing with the API
|
||||
type Token struct {
|
||||
// Username used for logging purposes
|
||||
Username string `json:"username"`
|
||||
|
||||
// Rights that the token provides
|
||||
// Format is: METHOD - list of paths
|
||||
Rights map[string][]string `json:"rights"`
|
||||
}
|
||||
|
||||
// Middleware is the authentication middleware
|
||||
type Middleware struct {
|
||||
signingKey []byte
|
||||
}
|
||||
|
||||
// NewMiddleware create a new Middleware instance with given secret token signing key
|
||||
func NewMiddleware(signingKey []byte) *Middleware {
|
||||
return &Middleware{signingKey: signingKey}
|
||||
}
|
||||
|
||||
// Middleware return an echo compatible middleware func to use
|
||||
func (m *Middleware) Middleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// Extract authorization header
|
||||
tokenStr := c.Request().Header.Get(echo.HeaderAuthorization)
|
||||
if tokenStr == "" {
|
||||
return ErrInvalidOrMissingAuth
|
||||
}
|
||||
|
||||
tokenStr = strings.TrimPrefix(tokenStr, "Bearer ")
|
||||
|
||||
// Decode the JWT token
|
||||
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
||||
// Validate expected alg
|
||||
if v, ok := t.Method.(*jwt.SigningMethodHMAC); !ok || v.Name != "HS256" {
|
||||
return nil, fmt.Errorf("unexpected signing method: %s", t.Header["alg"])
|
||||
}
|
||||
|
||||
// Return signing secret
|
||||
return m.signingKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
return ErrInvalidOrMissingAuth
|
||||
}
|
||||
|
||||
// From here we have a valid JWT token, extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return fmt.Errorf("error while parsing token claims")
|
||||
}
|
||||
|
||||
rights := map[string][]string{}
|
||||
for method, paths := range claims["rights"].(map[string]interface{}) {
|
||||
for _, path := range paths.([]interface{}) {
|
||||
rights[method] = append(rights[method], path.(string))
|
||||
}
|
||||
}
|
||||
|
||||
t := Token{
|
||||
Username: claims["username"].(string),
|
||||
Rights: rights,
|
||||
}
|
||||
|
||||
// Validate rights
|
||||
paths, contains := t.Rights[c.Request().Method]
|
||||
if !contains {
|
||||
return ErrAccessUnauthorized
|
||||
}
|
||||
|
||||
authorized := false
|
||||
for _, path := range paths {
|
||||
if path == c.Request().URL.Path {
|
||||
authorized = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !authorized {
|
||||
return ErrAccessUnauthorized
|
||||
}
|
||||
|
||||
// Set user context
|
||||
c.Set("username", t.Username)
|
||||
|
||||
// Everything's fine, call next handler ;D
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMiddleware_NoTokenShouldReturnUnauthorized(t *testing.T) {
|
||||
e := echo.New()
|
||||
m := (&Middleware{signingKey: []byte("test")}).Middleware()(okHandler())
|
||||
|
||||
// no token shouldn't be able to access
|
||||
req := httptest.NewRequest(http.MethodGet, "/users", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
if err := m(c); err != ErrInvalidOrMissingAuth {
|
||||
t.Errorf("ErrInvalidOrMissingAuth was expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddleware_InvalidTokenShouldReturnUnauthorized(t *testing.T) {
|
||||
e := echo.New()
|
||||
m := (&Middleware{signingKey: []byte("test")}).Middleware()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/users", nil)
|
||||
req.Header.Add(echo.HeaderAuthorization, "zarBR")
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
if err := m(okHandler())(c); err != ErrInvalidOrMissingAuth {
|
||||
t.Errorf("ErrInvalidOrMissingAuth was expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddleware_BadRightsShouldReturnUnauthorized(t *testing.T) {
|
||||
e := echo.New()
|
||||
m := (&Middleware{signingKey: []byte("test")}).Middleware()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/users", nil)
|
||||
req.Header.Add(echo.HeaderAuthorization, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gRG9lIiwicmlnaHRzIjp7IkdFVCI6WyIvdXNlcnMiXSwiUE9TVCI6WyIvc2VhcmNoIl19fQ.fRx0Q66ZgnY_rKCf-9Vaz6gzGKH_tKSgkVHhoQMtKfM")
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
if err := m(okHandler())(c); err != ErrAccessUnauthorized {
|
||||
t.Errorf("ErrAccessUnauthorized was expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddleware(t *testing.T) {
|
||||
e := echo.New()
|
||||
m := (&Middleware{signingKey: []byte("test")}).Middleware()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/users?id=10", nil)
|
||||
req.Header.Add(echo.HeaderAuthorization, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gRG9lIiwicmlnaHRzIjp7IkdFVCI6WyIvdXNlcnMiXSwiUE9TVCI6WyIvc2VhcmNoIl19fQ.fRx0Q66ZgnY_rKCf-9Vaz6gzGKH_tKSgkVHhoQMtKfM")
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
_ = m(okHandler())(c)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fail()
|
||||
}
|
||||
b, err := ioutil.ReadAll(rec.Body)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
if string(b) != "Hello, John Doe" {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if token := c.Get("username").(string); token != "John Doe" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func okHandler() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if username := c.Get("username").(string); username != "" {
|
||||
return c.String(http.StatusOK, fmt.Sprintf("Hello, %s", username))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue