Implementing ROLE with Gin-JWT - go

I'm working with framework GIN and Gin-JWT in Golang.
So far so good, I was able to authorize and authenticate my REST API with JWT following the example in Gin-JWT package.
I'm trying now to implement some kind of Role in my API.
The flow would be:
Login and auth
Create the JWT with inside the userID and the RoleID
When I call a REST API I confront the role associated to the API with the RoleID in JWT to authorized
So far I have this in my main:
jwtAfp := InitJwtMiddleware(db)
afb := r.Group("api/v1/afb")
afb.Use(jwtAfp.MiddlewareFunc())
afb.GET("/ping", afbController.Ping)
and this for the InitJwtMiddleware using Gin-JWT
func InitJwtMiddleware(db *gorm.DB) *jwt.GinJWTMiddleware {
return &jwt.GinJWTMiddleware{
Realm: "afb",
Key: []byte("secret pwd"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*model.User); ok {
return jwt.MapClaims{
"afb": v.ID,
}
}
return jwt.MapClaims{}
},
Authenticator: func(c *gin.Context) (interface{}, error) {
var loginVals login
if err := c.Bind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
email := loginVals.Username
password := loginVals.Password
var u model.User
db.Where("email = ?", email).First(&u)
if service.CheckPasswordHash(password, u.Password) {
return &u, nil
}
return nil, jwt.ErrFailedAuthentication
},
Authorizator: func(data interface{}, c *gin.Context) bool {
claims := jwt.ExtractClaims(c)
v, ok := data.(float64)
if ok && v == claims["afb"] {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
TokenHeadName: "Bearer",
TimeFunc: time.Now,
}
}
I would like to add the checking on the Role in the Authorizator section but I'm struggling on how i can do this.
I come up with passing in the InitJwtMiddleware(db) function also the role, this will work but I don't like the idea to "instaziate" a GinJWTMiddleware for each ROLE/API. Or if I could know inside the middleware which function (controller) will be called later I can then figure out if authorize or not. But even this solutin sound awkward to me. I think there will be a most elegant solution, any ideas?

You can try this:
https://github.com/kyfk/gin-jwt
It's the simplest auth[orization/entication] library.
The VerifyPerm function could be helpful for role management.
There's a complete example
func main() {
auth, err := jwt.New(jwt.Auth{
SecretKey: []byte("must change here"),
// Authenticator authenticates a request and return jwt.MapClaims
// that contains a user information of the request.
Authenticator: func(c *gin.Context) (jwt.MapClaims, error) {
var loginForm LoginForm
if err := c.ShouldBind(&loginForm); err != nil {
return nil, jwt.ErrorAuthenticationFailed
}
u, ok := authenticate(req.Username, req.Password)
if ok {
return nil, jwt.ErrorAuthenticationFailed
}
return jwt.MapClaims{
"username": u.Username,
"role": u.Role,
}, nil
},
// UserFetcher takes a jwt.MapClaims and return a user object.
UserFetcher: func(c *gin.Context, claims jwt.MapClaims) (interface{}, error) {
username, ok := claims["username"].(string)
if !ok {
return nil, nil
}
return findByUsername(username)
},
})
// some lines
e.Use(jwt.ErrorHandler)
// issue authorization token
e.POST("/login", auth.AuthenticateHandler)
// refresh token expiration
e.POST("/auth/refresh_token", auth.RefreshHandler)
// role management
e.GET("/operator/hello", Operator(auth), SayHello) // this is only for Operator
e.GET("/admin/hello", Admin(auth), SayHello) // this is only for Admin
}
func Operator(m jwt.Auth) gin.HandlerFunc {
return m.VerifyPerm(func(claims jwt.MapClaims) bool {
return role(claims).IsOperator()
})
}
func Admin(m jwt.Auth) gin.HandlerFunc {
return m.VerifyPerm(func(claims jwt.MapClaims) bool {
return role(claims).IsAdmin()
})
}

Related

testify using real function result as mocked function argument

How can I test this function
I need to make sure mocked FindByID function return value is a user with created UUID.
func (s *Service) Register(newUser domain.User) (domain.User, error) {
newUser.ID = uuid.New()
s.repo.Create(newUser)
createdUser, err := s.FindByID(newUser.ID)
if err != nil {
return domain.User{}, err
}
return createdUser, nil
}
Currently test function
t.Run("When Register called, it should return createdUser", func(t *testing.T) {
repo := new(mocks.UserRepository)
s := service.NewService(repo)
user := domain.User{Name: "TestName", Email: "TestEmail"}
repo.On("Create", mock.Anything)
// I need to make sure FindByID function return value is a user with created UUID
repo.On("FindByID", mock.Anything).Return(user, nil)
createdUser, err := s.Register(user)
assert.NoError(t, err)
assert.IsType(t, createdUser.ID, uuid.UUID{})
})
As one of options is creating mock manually (if it was generated) to this specific case.
For example (testify-mock):
type repoMock struct {
mock.Mock
cache map[uuid.UUID]domain.User
}
func newRepoMock() *repoMock {
cache := make(map[uuid.UUID]domain.User)
return &repoMock{cache: cache}
}
func (m *repoMock) Create(user domain.User) {
m.Called(user)
m.cache[user.ID] = user
}
func (m *repoMock) FindByID(uuid uuid.UUID) (domain.User, error) {
args := m.Called(s)
res := args[0]
if res == nil {
return m.cache[uuid], args.Error(1)
}
return res.(domain.User), args.Error(1)
}
PLAYGROUND

How to retrieve the authenticated user in Golang

func Login(c echo.Context) error {
user := &users.User{}
if err := c.Bind(&user); err != nil {
return err
}
return token.SigIn(c, user.Email, user.Password)
}
This is my Login function that retrieve the token when the user send the requests.
the Signin func that handle the token
func SigIn(c echo.Context, email, password string) error {
user := users.User{}
db := database.SetUp()
if err := db.Where("email = ?", email).First(&user).Error; gorm.IsRecordNotFoundError(err) {
restErr := errors.NewBadRequestError("Invalid credentials")
return c.JSON(http.StatusBadRequest, restErr)
}
if user.VerifyPassword(password) != nil {
restErr := errors.NewUnauthorizedError("Couldn't log you in with these credentials")
return c.JSON(http.StatusUnauthorized, restErr)
}
//user is successfull
return CreateToken(c)
}
the CreateToken func is as follow
type TokenJWT struct {
Token string `json:"token"`
}
func CreateToken(c echo.Context) error {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["authorized"] = true
claims["name"] = "Pascal Gaetan"
claims["exp"] = time.Now().Add(time.Hour * 1).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("my_secret_key"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, TokenJWT{
Token: t,
})
}
when everyhting is succesfull, i would like to get the authenticated user through an URL /api/me that calls a Me function
Let me split your question into two parts: the first one is how to easily encode and decode user in or from JWT token and the second part is how to write a generic code which can retrieve user from everywhere.
From your example I mentioned that you created a MapClaims but to reduce parsing complexity it will be better to create a token using a custom claims type. If you are using dgrijalva/jwt-go, then according to documentation you can do something like that
type UserClaims struct {
Name string `json:"name"`
jwt.StandardClaims
}
// encode it as before, but with your created type
t := jwt.New(signer)
userClaims := &UserClaims{Name: "Burmese"}
t.Claims = userClaims
tokenString, err = t.SignedString(]byte("my_secret_key"))
then you can parse your user in your router/framework middleware with
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("my_secret_key"), nil
})
if claims, ok := token.Claims.(*UserClaims); ok && token.Valid {
fmt.Printf("%v %v", claims.Name, claims.StandardClaims.ExpiresAt)
} else {
fmt.Println(err)
}
This example was adopted from an official documentation here
Now you know how to parse authenticated user struct with ease and the next logic move is to wrap it into your middleware. Whether there are a lot of implementation details like you can retrieve JWT from cookie, header or query, also defining some ordering on them, the gist the following: you should have wrapped abovementioned code into your middleware and after parsing the struct you can pass it via your request context. I don't use echo and other frameworks, but for pure net/http you can pass your parsed struct from middleware with
context.WithValue(ctx, UserCtxKey, claims)
Hope it helps!
This is a fairly common design pattern to create an authenticated client and then call various action methods on it. You could do something like the following:
type Client struct {
... // other members
token string // unexported unless there is a special reason to do otherwise
}
func NewClient(c echo.Context, email, password string) (*Client, error) {
user := users.User{}
cl := Client{}
... // your original method
cl.token = token
return &cl, nil
}
func (c *Client) DoSomething(...) ... { ... }

How to add json data as parameter to c.Body.Request?

I have a function named LoginUser(c *gin.Context) with argument c *gin.Context. I want to call LoginUser from another function CreateBlogsWithUser. But LoginUser requires username and password. I tried to pass in c.Request.Body but it is not working.
func (server *Server) CreateBlogsWithUser() {
resp := httptest.NewRecorder()
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(resp)
c.Request.Header.Add("Content-Type","application/json")
c.Request.Body.Add("uname","test") //this line is not working
c.Request.Body.Add("password","test#123") //this line is not working
LoginUser(c)
}
func LoginUser(c *gin.Context) {
requestData := models.CCPADefaultData{}
err := json.NewDecoder(c.Request.Body).Decode(&requestData)
if err != nil {
errList["Invalid_body"] = "Missing request parameters."
c.JSON(http.StatusUnprocessableEntity, gin.H{
"status": http.StatusUnprocessableEntity,
"error": "Always come in this if condition.",
})
return
}
//XXXXXXXXX
//Code
}
I tried many things but none is working for me.
Please help how to pass parameter if argument is c *gin.Context

Angular 8 CORS when connecting to Go Gin

I built a backend with Golang's Gin framework and the JWT middleware for it. This is the official example from the readme, which I used:
main.go
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
type login struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
var identityKey = "id"
func helloHandler(c *gin.Context) {
claims := jwt.ExtractClaims(c)
user, _ := c.Get(identityKey)
c.JSON(200, gin.H{
"userID": claims[identityKey],
"userName": user.(*User).UserName,
"text": "Hello World.",
})
}
// User demo
type User struct {
UserName string
FirstName string
LastName string
}
func main() {
port := os.Getenv("PORT")
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
if port == "" {
port = "8000"
}
// the jwt middleware
authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
Realm: "test zone",
Key: []byte("secret key"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
IdentityKey: identityKey,
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*User); ok {
return jwt.MapClaims{
identityKey: v.UserName,
}
}
return jwt.MapClaims{}
},
IdentityHandler: func(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
return &User{
UserName: claims[identityKey].(string),
}
},
Authenticator: func(c *gin.Context) (interface{}, error) {
var loginVals login
if err := c.ShouldBind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
password := loginVals.Password
if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
return &User{
UserName: userID,
LastName: "Bo-Yi",
FirstName: "Wu",
}, nil
}
return nil, jwt.ErrFailedAuthentication
},
Authorizator: func(data interface{}, c *gin.Context) bool {
if v, ok := data.(*User); ok && v.UserName == "admin" {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "cookie:<name>"
// - "param:<name>"
TokenLookup: "header: Authorization, query: token, cookie: jwt",
// TokenLookup: "query:token",
// TokenLookup: "cookie:token",
// TokenHeadName is a string in the header. Default value is "Bearer"
TokenHeadName: "Bearer",
// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
TimeFunc: time.Now,
})
if err != nil {
log.Fatal("JWT Error:" + err.Error())
}
r.POST("/login", authMiddleware.LoginHandler)
r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
claims := jwt.ExtractClaims(c)
log.Printf("NoRoute claims: %#v\n", claims)
c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
})
auth := r.Group("/auth")
// Refresh time can be longer than token timeout
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
auth.Use(authMiddleware.MiddlewareFunc())
{
auth.GET("/hello", helloHandler)
}
if err := http.ListenAndServe(":"+port, r); err != nil {
log.Fatal(err)
}
}
My auth service in Angular 8 looks like this:
auth.service
headers = new HttpHeaders({ "Content-Type": "application/json" });
login(username: string, password: string): Promise<any> {
const url: string = `${this.BASE_URL}` + "/login";
const request = this.http
.post(
url,
JSON.stringify({ username: username, password: password }),
{ headers: this.headers }
)
.toPromise();
return request;
}
But this gives me an error message in Chrome:
Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/login' from origin 'http://localhost:4200' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
In the console Gin returns status code 204 though.
I thought this was a CORS issue, so I implemented Gin's CORS middleware:
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:8000"},
AllowMethods: []string{"PUT", "PATCH"},
AllowHeaders: []string{"Origin"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool {
return origin == "https://github.com"
},
MaxAge: 12 * time.Hour,
}))
Unfortunately it still didn't work. Also not if I added the POST method to the allowed methods:
AllowMethods: []string{"PUT", "PATCH", "POST"}
My last try was to use proxy, as described in the Angular documentation:
proxy.conf.json
{
"/api": {
"target": "http://localhost:8000",
"secure": false
}
}
angular.json
"options": {
"browserTarget": "your-application-name:build",
"proxyConfig": "src/proxy.conf.json"
}
I re-started with ng serve, but the error still appears (I changed the urls to /api/login in the auth.service and main.go according to the configuration in the proxy file).
What am I doing wrong here?
Change AllowHeaders: []string{"Origin"} to AllowHeaders: []string{"content-type"};

Fasthttp + fasthttprouter, trying to write middleware

I'm currently trying to write some middleware to work with fasthttp and fasthttprouter. And I'm stuck.
func jwt(h fasthttprouter.Handle) fasthttprouter.Handle {
myfunc := func(ctx *fasthttp.RequestCtx, _ fasthttprouter.Params) {
fmt.Println(string(ctx.Request.Header.Cookie("Authorization")))
}
return myfunc
}
How do I run the actual handler now? I feel like i'm missing something very simple.
I've read through this blog post: Middleware in Golang. But i'm lost.
Any ideas?
Regards
for example, let us create a middleware function that will handle CORS using:
github.com/buaazp/fasthttprouter and github.com/valyala/fasthttp
var (
corsAllowHeaders = "authorization"
corsAllowMethods = "HEAD,GET,POST,PUT,DELETE,OPTIONS"
corsAllowOrigin = "*"
corsAllowCredentials = "true"
)
func CORS(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
ctx.Response.Header.Set("Access-Control-Allow-Credentials", corsAllowCredentials)
ctx.Response.Header.Set("Access-Control-Allow-Headers", corsAllowHeaders)
ctx.Response.Header.Set("Access-Control-Allow-Methods", corsAllowMethods)
ctx.Response.Header.Set("Access-Control-Allow-Origin", corsAllowOrigin)
next(ctx)
}
}
Now we chain this middleware function on our Index handler and register it on the router.
func Index(ctx *fasthttp.RequestCtx) {
fmt.Fprint(ctx, "some-api")
}
func main() {
router := fasthttprouter.New()
router.GET("/", Index)
if err := fasthttp.ListenAndServe(":8181", CORS(router.Handler)); err != nil {
log.Fatalf("Error in ListenAndServe: %s", err)
}
}
Example of auth middleware for fasthttp & fasthttprouter (new versions)
type Middleware func(h fasthttp.RequestHandler) fasthttp.RequestHandler
type AuthFunc func(ctx *fasthttp.RequestCtx) bool
func NewAuthMiddleware(authFunc AuthFunc) Middleware {
return func(h fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
result, err: = authFunc(ctx)
if result {
h(ctx)
} else {
ctx.Response.SetStatusCode(fasthttp.StatusUnauthorized)
}
}
}
}
func AuthCheck(ctx *fasthttp.RequestCtx)(bool, error) {
return false; // for example ;)
}
// router
authMiddleware: = middleware.NewAuthMiddleware(security.AuthCheck)
...
router.GET("/protected", authMiddleware(handlers.ProtectedHandler))

Resources