jwt

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 4, 2023 License: Apache-2.0 Imports: 11 Imported by: 1

README

Hertz-JWT(This is a community driven project)

This is a middleware for Hertz framework.

It uses jwt-go to provide a jwt authentication middleware. It provides additional handler functions to provide the login api that will generate the token and an additional refresh handler that can be used to refresh tokens.

This repo is forked from gin-jwt and adapted to Hertz.

Security Issue

Simple HS256 JWT token brute force cracker. Effective only to crack JWT tokens with weak secrets. Recommendation: Use strong long secrets or RS256 tokens. See the jwt-cracker repository.

Usage

Download and install:

export GO111MODULE=on
go get github.com/hertz-contrib/jwt

Import it in your code:

import "github.com/hertz-contrib/jwt"

Example

Please see the example file and you can use ExtractClaims to fetch user data.

package main

import (
   "context"
   "log"
   "time"

   "github.com/cloudwego/hertz/pkg/app"
   "github.com/cloudwego/hertz/pkg/app/server"
   "github.com/hertz-contrib/jwt"
)

type login struct {
   Username string `form:"username,required" json:"username,required"`
   Password string `form:"password,required" json:"password,required"`
}

var identityKey = "id"

func PingHandler(c context.Context, ctx *app.RequestContext) {
   ctx.JSON(200, map[string]string{
      "ping": "pong",
   })
}

// User demo
type User struct {
   UserName  string
   FirstName string
   LastName  string
}

func main() {
   h := server.Default()

   // the jwt middleware
   authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
      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(ctx context.Context, c *app.RequestContext) interface{} {
         claims := jwt.ExtractClaims(ctx, c)
         return &User{
            UserName: claims[identityKey].(string),
         }
      },
      Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
         var loginVals login
         if err := c.BindAndValidate(&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:  "Hertz",
               FirstName: "CloudWeGo",
            }, nil
         }

         return nil, jwt.ErrFailedAuthentication
      },
      Authorizator: func(data interface{}, ctx context.Context, c *app.RequestContext) bool {
         if v, ok := data.(*User); ok && v.UserName == "admin" {
            return true
         }

         return false
      },
      Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
         c.JSON(code, map[string]interface{}{
            "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". If you want empty value, use WithoutDefaultTokenHeadName.
      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())
   }

   // When you use jwt.New(), the function is already automatically called for checking,
   // which means you don't need to call it again.
   errInit := authMiddleware.MiddlewareInit()

   if errInit != nil {
      log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
   }

   h.POST("/login", authMiddleware.LoginHandler)

   h.NoRoute(authMiddleware.MiddlewareFunc(), func(ctx context.Context, c *app.RequestContext) {
      claims := jwt.ExtractClaims(ctx, c)
      log.Printf("NoRoute claims: %#v\n", claims)
      c.JSON(404, map[string]string{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
   })

   auth := h.Group("/auth")
   // Refresh time can be longer than token timeout
   auth.GET("/refresh_token", authMiddleware.RefreshHandler)
   auth.Use(authMiddleware.MiddlewareFunc())
   {
      auth.GET("/ping", PingHandler)
   }

   h.Spin()
}

Demo

Please run example/basic/main.go file and listen 8888 port.

go run example/basic/main.go

Download and install httpie CLI HTTP client.

Login API
http -v --json POST localhost:8888/login username=admin password=admin

Output

POST /login HTTP/1.1
Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 42
Content-Type: application/json
Host: localhost:8888
User-Agent: HTTPie/3.2.1

{
    "password": "admin",
    "username": "admin"
}


HTTP/1.1 200 OK
Content-Length: 212
Content-Type: application/json; charset=utf-8
Date: Sun, 05 Jun 2022 04:49:20 GMT
Server: hertz

{
    "code": 200,
    "expire": "2022-06-05T13:49:20+08:00",
    "token": "**"
}
Refresh token API
http -v -f GET localhost:8888/auth/refresh_token "Authorization:Bearer xxxxxxxxx"  "Content-Type: application/json"

Output

GET /auth/refresh_token HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Bearer **
Connection: keep-alive
Content-Type: application/json
Host: localhost:8888
User-Agent: HTTPie/3.2.1



HTTP/1.1 200 OK
Content-Length: 212
Content-Type: application/json; charset=utf-8
Date: Sun, 05 Jun 2022 04:50:40 GMT
Server: hertz

{
    "code": 200,
    "expire": "2022-06-05T13:50:41+08:00",
    "token": "**"
}

Hello world

Please login as admin and password as admin

http -f GET localhost:8888/auth/ping "Authorization:Bearer xxxxxxxxx"  "Content-Type: application/json"

Response message 200 OK:

HTTP/1.1 200 OK
Content-Length: 15
Content-Type: application/json; charset=utf-8
Date: Sun, 05 Jun 2022 04:53:59 GMT
Server: hertz

{
    "ping": "pong"
}

Authorization

Please login as test and password as test

http -f GET localhost:8888/auth/ping "Authorization:Bearer xxxxxxxxx"  "Content-Type: application/json"

Response message 403 Forbidden:

HTTP/1.1 403 Forbidden
Content-Length: 74
Content-Type: application/json; charset=utf-8
Date: Sun, 05 Jun 2022 04:57:06 GMT
Server: hertz
Www-Authenticate: JWT realm=test zone

{
    "code": 403,
    "message": "you don't have permission to access this resource"
}

Use these options for setting the JWT in a cookie. See the Mozilla documentation for more information on these options.

  SendCookie:       true,
  SecureCookie:     false, //non HTTPS dev environments
  CookieHTTPOnly:   true,  // JS can't modify
  CookieDomain:     "localhost:8888",
  CookieName:       "token", // default jwt
  TokenLookup:      "cookie:token",
  CookieSameSite:   http.SameSiteDefaultMode, //SameSiteDefaultMode, SameSiteLaxMode, SameSiteStrictMode, SameSiteNoneMode
Login request flow (using the LoginHandler)
  1. PROVIDED: LoginHandler

This is a provided function to be called on any login endpoint, which will trigger the flow described below.

  1. REQUIRED: Authenticator This function should verify the user credentials given the hertz context (i.e. password matches hashed password for a given user email, and any other authentication logic). Then the authenticator should return a struct or map that contains the user data that will be embedded in the jwt token. This might be something like an account id, role, is_verified, etc. After having successfully authenticated, the data returned from the authenticator is passed in as a parameter into the PayloadFunc, which is used to embed the user identifiers mentioned above into the jwt token. If an error is returned, the Unauthorized function is used (explained below).

  2. OPTIONAL: PayloadFunc

This function is called after having successfully authenticated (logged in). It should take whatever was returned from Authenticator and convert it into MapClaims (i.e. map[string]interface{}). A typical use case of this function is for when Authenticator returns a struct which holds the user identifiers, and that struct needs to be converted into a map. MapClaims should include one element that is [IdentityKey (default is "identity"): some_user_identity]. The elements of MapClaims returned in PayloadFunc will be embedded within the jwt token (as token claims). When users pass in their token on subsequent requests, you can get these claims back by using ExtractClaims.

  1. OPTIONAL: LoginResponse

After having successfully authenticated with Authenticator, created the jwt token using the identifiers from map returned from PayloadFunc, and set it as a cookie if SendCookie is enabled, this function is called. It is used to handle any post-login logic. This might look something like using the hertz context to return a JSON of the token back to the user.

Subsequent requests on endpoints requiring jwt token (using MiddlewareFunc).
  1. PROVIDED: MiddlewareFunc

This is hertz middleware that should be used within any endpoints that require the jwt token to be present. This middleware will parse the request headers for the token if it exists, and check that the jwt token is valid (not expired, correct signature). Then it will call IdentityHandler followed by Authorizator. If Authorizator passes and all of the previous token validity checks passed, the middleware will continue the request. If any of these checks fail, the Unauthorized function is used (explained below).

  1. OPTIONAL: IdentityHandler

The default of this function is likely sufficient for your needs. The purpose of this function is to fetch the user identity from claims embedded within the jwt token, and pass this identity value to Authorizator. This function assumes [IdentityKey: some_user_identity] is one of the attributes embedded within the claims of the jwt token (determined by PayloadFunc).

  1. OPTIONAL: Authorizator

Given the user identity value (data parameter) and the hertz context, this function should check if the user is authorized to be reaching this endpoint (on the endpoints where the MiddlewareFunc applies). This function should likely use ExtractClaims to check if the user has the sufficient permissions to reach this endpoint, as opposed to hitting the database on every request. This function should return true if the user is authorized to continue through with the request, or false if they are not authorized (where Unauthorized will be called).

Logout Request flow (using LogoutHandler)
  1. PROVIDED: LogoutHandler

This is a provided function to be called on any logout endpoint, which will clear any cookies if SendCookie is set, and then call LogoutResponse.

  1. OPTIONAL: LogoutResponse

This should likely just return back to the user the http status code, if logout was successful or not.

Refresh Request flow (using RefreshHandler)
  1. PROVIDED: RefreshHandler:

This is a provided function to be called on any refresh token endpoint. If the token passed in is was issued within the MaxRefreshTime time frame, then this handler will create/set a new token similar to the LoginHandler, and pass this token into RefreshResponse

  1. OPTIONAL: RefreshResponse:

This should likely return a JSON of the token back to the user, similar to LoginResponse

Failures with logging in, bad tokens, or lacking privileges
  1. OPTIONAL Unauthorized:

On any error logging in, authorizing the user, or when there was no token or a invalid token passed in with the request, the following will happen. The hertz context will be aborted depending on DisabledAbort, then HTTPStatusMessageFunc is called which by default converts the error into a string. Finally the Unauthorized function will be called. This function should likely return a JSON containing the http error code and error message to the user.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrMissingSecretKey indicates Secret key is required
	ErrMissingSecretKey = errors.New("secret key is required")

	// ErrForbidden when HTTP status 403 is given
	ErrForbidden = errors.New("you don't have permission to access this resource")

	// ErrMissingAuthenticatorFunc indicates Authenticator is required
	ErrMissingAuthenticatorFunc = errors.New("HertzJWTMiddleware.Authenticator func is undefined")

	// ErrMissingLoginValues indicates a user tried to authenticate without username or password
	ErrMissingLoginValues = errors.New("missing Username or Password")

	// ErrFailedAuthentication indicates authentication failed, could be faulty username or password
	ErrFailedAuthentication = errors.New("incorrect Username or Password")

	// ErrFailedTokenCreation indicates JWT Token failed to create, reason unknown
	ErrFailedTokenCreation = errors.New("failed to create JWT Token")

	// ErrExpiredToken indicates JWT token has expired. Can't refresh.
	ErrExpiredToken = errors.New("token is expired") // in practice, this is generated from the jwt library not by us

	// ErrEmptyAuthHeader can be thrown if authing with a HTTP header, the Auth header needs to be set
	ErrEmptyAuthHeader = errors.New("auth header is empty")

	// ErrMissingExpField missing exp field in token
	ErrMissingExpField = errors.New("missing exp field")

	// ErrWrongFormatOfExp field must be float64 format
	ErrWrongFormatOfExp = errors.New("exp must be float64 format")

	// ErrInvalidAuthHeader indicates auth header is invalid, could for example have the wrong Realm name
	ErrInvalidAuthHeader = errors.New("auth header is invalid")

	// ErrEmptyQueryToken can be thrown if authing with URL Query, the query token variable is empty
	ErrEmptyQueryToken = errors.New("query token is empty")

	// ErrEmptyCookieToken can be thrown if authing with a cookie, the token cookie is empty
	ErrEmptyCookieToken = errors.New("cookie token is empty")

	// ErrEmptyParamToken can be thrown if authing with parameter in path, the parameter in path is empty
	ErrEmptyParamToken = errors.New("parameter token is empty")

	// ErrEmptyFormToken can be thrown if authing with post form, the form token is empty
	ErrEmptyFormToken = errors.New("form token is empty")

	// ErrInvalidSigningAlgorithm indicates signing algorithm is invalid, needs to be HS256, HS384, HS512, RS256, RS384 or RS512
	ErrInvalidSigningAlgorithm = errors.New("invalid signing algorithm")

	// ErrNoPrivKeyFile indicates that the given private key is unreadable
	ErrNoPrivKeyFile = errors.New("private key file unreadable")

	// ErrNoPubKeyFile indicates that the given public key is unreadable
	ErrNoPubKeyFile = errors.New("public key file unreadable")

	// ErrInvalidPrivKey indicates that the given private key is invalid
	ErrInvalidPrivKey = errors.New("private key invalid")

	// ErrInvalidPubKey indicates the the given public key is invalid
	ErrInvalidPubKey = errors.New("public key invalid")

	// IdentityKey default identity key
	IdentityKey = "identity"
)

Functions

func GetToken

func GetToken(ctx context.Context, c *app.RequestContext) string

GetToken help to get the JWT token string

Types

type HandlerBoolFunc

type HandlerBoolFunc func(c context.Context, ctx *app.RequestContext) bool

type HertzJWTMiddleware

type HertzJWTMiddleware struct {
	// Realm name to display to the user. Required.
	Realm string

	// signing algorithm - possible values are HS256, HS384, HS512, RS256, RS384 or RS512
	// Optional, default is HS256.
	SigningAlgorithm string

	// Secret key used for signing. Required.
	Key []byte

	// Callback to retrieve key used for signing. Setting KeyFunc will bypass
	// all other key settings
	KeyFunc func(token *jwt.Token) (interface{}, error)

	// Duration that a jwt token is valid. Optional, defaults to one hour.
	Timeout time.Duration

	// This field allows clients to refresh their token until MaxRefresh has passed.
	// Note that clients can refresh their token in the last moment of MaxRefresh.
	// This means that the maximum validity timespan for a token is TokenTime + MaxRefresh.
	// Optional, defaults to 0 meaning not refreshable.
	MaxRefresh time.Duration

	// Callback function that should perform the authentication of the user based on login info.
	// Must return user data as user identifier, it will be stored in Claim Array. Required.
	// Check error (e) to determine the appropriate error message.
	Authenticator func(ctx context.Context, c *app.RequestContext) (interface{}, error)

	// Callback function that should perform the authorization of the authenticated user. Called
	// only after an authentication success. Must return true on success, false on failure.
	// Optional, default to success.
	Authorizator func(data interface{}, ctx context.Context, c *app.RequestContext) bool

	// Callback function that will be called during login.
	// Using this function it is possible to add additional payload data to the webtoken.
	// The data is then made available during requests via c.Get("JWT_PAYLOAD").
	// Note that the payload is not encrypted.
	// The attributes mentioned on jwt.io can't be used as keys for the map.
	// Optional, by default no additional data will be set.
	PayloadFunc func(data interface{}) MapClaims

	// User can define own Unauthorized func.
	Unauthorized func(ctx context.Context, c *app.RequestContext, code int, message string)

	// User can define own LoginResponse func.
	LoginResponse func(ctx context.Context, c *app.RequestContext, code int, message string, time time.Time)

	// User can define own LogoutResponse func.
	LogoutResponse func(ctx context.Context, c *app.RequestContext, code int)

	// User can define own RefreshResponse func.
	RefreshResponse func(ctx context.Context, c *app.RequestContext, code int, message string, time time.Time)

	// Set the identity handler function
	IdentityHandler func(ctx context.Context, c *app.RequestContext) interface{}

	// Set the identity key
	IdentityKey string

	// 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>"
	// - "form:<name>"
	TokenLookup string

	// TokenHeadName is a string in the header. Default value is "Bearer"
	TokenHeadName string

	// WithoutDefaultTokenHeadName allow set empty TokenHeadName
	WithoutDefaultTokenHeadName bool

	// 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 func() time.Time

	// HTTP Status messages for when something in the JWT middleware fails.
	// Check error (e) to determine the appropriate error message.
	HTTPStatusMessageFunc func(e error, ctx context.Context, c *app.RequestContext) string

	// Private key file for asymmetric algorithms
	PrivKeyFile string

	// Private Key bytes for asymmetric algorithms
	//
	// Note: PrivKeyFile takes precedence over PrivKeyBytes if both are set
	PrivKeyBytes []byte

	// Public key file for asymmetric algorithms
	PubKeyFile string

	// Private key passphrase
	PrivateKeyPassphrase string

	// Public key bytes for asymmetric algorithms.
	//
	// Note: PubKeyFile takes precedence over PubKeyBytes if both are set
	PubKeyBytes []byte

	// Optionally return the token as a cookie
	SendCookie bool

	// Duration that a cookie is valid. Optional, by default equals to Timeout value.
	CookieMaxAge time.Duration

	// Allow insecure cookies for development over http
	SecureCookie bool

	// Allow cookies to be accessed client side for development
	CookieHTTPOnly bool

	// Allow cookie domain change for development
	CookieDomain string

	// SendAuthorization allow return authorization header for every request
	SendAuthorization bool

	// Disable abort() of context.
	DisabledAbort bool

	// CookieName allow cookie name change for development
	CookieName string

	// CookieSameSite allow use protocol.CookieSameSite cookie param
	CookieSameSite protocol.CookieSameSite

	// ParseOptions allow to modify jwt's parser methods
	ParseOptions []jwt.ParserOption
	// contains filtered or unexported fields
}

HertzJWTMiddleware provides a Json-Web-Token authentication implementation. On failure, a 401 HTTP response is returned. On success, the wrapped middleware is called, and the userID is made available as c.Get("userID").(string). Users can get a token by posting a json request to LoginHandler. The token then needs to be passed in the Authentication header. Example: Authorization:Bearer XXX_TOKEN_XXX

func New

New for check error with HertzJWTMiddleware

func (*HertzJWTMiddleware) CheckIfTokenExpire

func (mw *HertzJWTMiddleware) CheckIfTokenExpire(ctx context.Context, c *app.RequestContext) (jwt.MapClaims, error)

CheckIfTokenExpire check if token expire

func (*HertzJWTMiddleware) GetClaimsFromJWT

func (mw *HertzJWTMiddleware) GetClaimsFromJWT(ctx context.Context, c *app.RequestContext) (MapClaims, error)

GetClaimsFromJWT get claims from JWT token

func (*HertzJWTMiddleware) LoginHandler

func (mw *HertzJWTMiddleware) LoginHandler(ctx context.Context, c *app.RequestContext)

LoginHandler can be used by clients to get a jwt token. Payload needs to be json in the form of {"username": "USERNAME", "password": "PASSWORD"}. Reply will be of the form {"token": "TOKEN"}.

func (*HertzJWTMiddleware) LogoutHandler

func (mw *HertzJWTMiddleware) LogoutHandler(ctx context.Context, c *app.RequestContext)

LogoutHandler can be used by clients to remove the jwt cookie (if set)

func (*HertzJWTMiddleware) MiddlewareFunc

func (mw *HertzJWTMiddleware) MiddlewareFunc() app.HandlerFunc

MiddlewareFunc makes HertzJWTMiddleware implement the Middleware interface.

func (*HertzJWTMiddleware) MiddlewareFuncOptional

func (mw *HertzJWTMiddleware) MiddlewareFuncOptional(fn HandlerBoolFunc) app.HandlerFunc

MiddlewareFuncOptional makes HertzJWTMiddleware implement the Middleware interface.

func (*HertzJWTMiddleware) MiddlewareInit

func (mw *HertzJWTMiddleware) MiddlewareInit() error

MiddlewareInit initialize jwt configs.

func (*HertzJWTMiddleware) ParseToken

func (mw *HertzJWTMiddleware) ParseToken(ctx context.Context, c *app.RequestContext) (*jwt.Token, error)

ParseToken parse jwt token from hertz context

func (*HertzJWTMiddleware) ParseTokenString

func (mw *HertzJWTMiddleware) ParseTokenString(token string) (*jwt.Token, error)

ParseTokenString parse jwt token string

func (*HertzJWTMiddleware) RefreshHandler

func (mw *HertzJWTMiddleware) RefreshHandler(ctx context.Context, c *app.RequestContext)

RefreshHandler can be used to refresh a token. The token still needs to be valid on refresh. Shall be put under an endpoint that is using the HertzJWTMiddleware. Reply will be of the form {"token": "TOKEN"}.

func (*HertzJWTMiddleware) RefreshToken

func (mw *HertzJWTMiddleware) RefreshToken(ctx context.Context, c *app.RequestContext) (string, time.Time, error)

RefreshToken refresh token and check if token is expired

func (*HertzJWTMiddleware) TokenGenerator

func (mw *HertzJWTMiddleware) TokenGenerator(data interface{}) (string, time.Time, error)

TokenGenerator method that clients can use to get a jwt token.

type MapClaims

type MapClaims map[string]interface{}

MapClaims type that uses the map[string]interface{} for JSON decoding This is the default claims type if you don't supply one

func ExtractClaims

func ExtractClaims(ctx context.Context, c *app.RequestContext) MapClaims

ExtractClaims help to extract the JWT claims

func ExtractClaimsFromToken

func ExtractClaimsFromToken(token *jwt.Token) MapClaims

ExtractClaimsFromToken help to extract the JWT claims from token

Directories

Path Synopsis
example

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL