diff --git a/HISTORY.md b/HISTORY.md
index 30ca6fe1..fcccde5f 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -262,7 +262,7 @@ var dirOpts = iris.DirOptions{
- New builtin [requestid](https://github.com/kataras/iris/tree/master/middleware/requestid) middleware.
-- New builtin [JWT](https://github.com/kataras/iris/tree/master/middleware/jwt) middleware based on [square/go-jose](https://github.com/square/go-jose) featured with optional encryption to set claims with sensitive data when necessary.
+- New builtin [JWT](https://github.com/kataras/iris/tree/master/middleware/jwt) middleware based on the fastest JWT implementation; [kataras/jwt](https://github.com/kataras/jwt) featured with optional wire encryption to set claims with sensitive data when necessary.
- New `iris.RouteOverlap` route registration rule. `Party.SetRegisterRule(iris.RouteOverlap)` to allow overlapping across multiple routes for the same request subdomain, method, path. See [1536#issuecomment-643719922](https://github.com/kataras/iris/issues/1536#issuecomment-643719922). This allows two or more **MVC Controllers** to listen on the same path based on one or more registered dependencies (see [_examples/mvc/authenticated-controller](https://github.com/kataras/iris/tree/master/_examples/mvc/authenticated-controller)).
diff --git a/NOTICE b/NOTICE
index 5f0c9def..042230a0 100644
--- a/NOTICE
+++ b/NOTICE
@@ -101,9 +101,9 @@ Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca
toml 3012a1dbe2e4bd1 https://github.com/BurntSushi/toml
391d42b32f0577c
b7bbc7f005
- jose d84c719419c2a90 https://github.com/square/go-jose
- 8d188ea67e09652
- f5c1929ae8
+ jwt 5f34e0a4e28178b https://github.com/kataras/jwt
+ 3781df69552bdc5
+ 481a0d4bef
uuid cb32006e483f2a2 https://github.com/google/uuid
3230e24209cf185
c65b477dbf
diff --git a/_examples/auth/jwt/basic/main.go b/_examples/auth/jwt/basic/main.go
index f4b36699..f37ce681 100644
--- a/_examples/auth/jwt/basic/main.go
+++ b/_examples/auth/jwt/basic/main.go
@@ -4,41 +4,75 @@ import (
"time"
"github.com/kataras/iris/v12"
- "github.com/kataras/iris/v12/middleware/jwt"
+ "github.com/kataras/jwt"
)
+/*
+Learn how to use any JWT 3rd-party package with Iris.
+In this example we use the kataras/jwt one.
+
+Install with:
+ go get -u github.com/kataras/jwt
+
+Documentation:
+ https://github.com/kataras/jwt#table-of-contents
+*/
+
+// Replace with your own key and keep them secret.
+// The "signatureSharedKey" is used for the HMAC(HS256) signature algorithm.
+var signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
+
func main() {
app := iris.New()
- // With AES-GCM (128) encryption:
- // j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
- // Without extra encryption, just the sign key:
- j := jwt.HMAC(15*time.Minute, "secret")
- app.Get("/", generateToken(j))
- app.Get("/protected", j.VerifyMap(), protected)
+ app.Get("/", generateToken)
+ app.Get("/protected", protected)
app.Listen(":8080")
}
-func generateToken(j *jwt.JWT) iris.Handler {
- return func(ctx iris.Context) {
- token, err := j.Token(iris.Map{
- "foo": "bar",
- })
- if err != nil {
- ctx.StopWithStatus(iris.StatusInternalServerError)
- return
- }
+type fooClaims struct {
+ Foo string `json:"foo"`
+}
- ctx.HTML(`Token: ` + token + `
- /secured?token=` + token + ``)
+func generateToken(ctx iris.Context) {
+ claims := fooClaims{
+ Foo: "bar",
}
+
+ // Sign and generate compact form token.
+ token, err := jwt.Sign(jwt.HS256, signatureSharedKey, claims, jwt.MaxAge(10*time.Minute))
+ if err != nil {
+ ctx.StopWithStatus(iris.StatusInternalServerError)
+ return
+ }
+
+ tokenString := string(token) // or jwt.BytesToString
+ ctx.HTML(`Token: ` + tokenString + `
+ /protected?token=` + tokenString + ``)
}
func protected(ctx iris.Context) {
+ // Extract the token, e.g. cookie, Authorization: Bearer $token
+ // or URL query.
+ token := ctx.URLParam("token")
+ // Verify the token.
+ verifiedToken, err := jwt.Verify(jwt.HS256, signatureSharedKey, []byte(token))
+ if err != nil {
+ ctx.StopWithStatus(iris.StatusUnauthorized)
+ return
+ }
+
ctx.Writef("This is an authenticated request.\n\n")
- claims := jwt.Get(ctx).(iris.Map)
+ // Decode the custom claims.
+ var claims fooClaims
+ verifiedToken.Claims(&claims)
- ctx.Writef("foo=%s\n", claims["foo"])
+ // Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp").
+ standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
+ expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
+ timeLeft := standardClaims.Timeleft()
+
+ ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
}
diff --git a/_examples/auth/jwt/middleware/main.go b/_examples/auth/jwt/middleware/main.go
new file mode 100644
index 00000000..948a0c4c
--- /dev/null
+++ b/_examples/auth/jwt/middleware/main.go
@@ -0,0 +1,91 @@
+package main
+
+import (
+ "time"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/middleware/jwt"
+)
+
+var (
+ sigKey = []byte("signature_hmac_secret_shared_key")
+ encKey = []byte("GCM_AES_256_secret_shared_key_32")
+)
+
+type fooClaims struct {
+ Foo string `json:"foo"`
+}
+
+/*
+In this example you will learn the essentials
+of the Iris builtin JWT middleware based on the github.com/kataras/jwt package.
+*/
+
+func main() {
+ app := iris.New()
+
+ signer := jwt.NewSigner(jwt.HS256, sigKey, 10*time.Minute)
+ // Enable payload encryption with:
+ // signer.WithGCM(encKey, nil)
+ app.Get("/", generateToken(signer))
+
+ verifier := jwt.NewVerifier(jwt.HS256, sigKey)
+ // Enable server-side token block feature (even before its expiration time):
+ verifier.WithDefaultBlocklist()
+ // Enable payload decryption with:
+ // verifier.WithGCM(encKey, nil)
+ verifyMiddleware := verifier.Verify(func() interface{} {
+ return new(fooClaims)
+ })
+
+ protectedAPI := app.Party("/protected")
+ // Register the verify middleware to allow access only to authorized clients.
+ protectedAPI.Use(verifyMiddleware)
+ // ^ or UseRouter(verifyMiddleware) to disallow unauthorized http error handlers too.
+
+ protectedAPI.Get("/", protected)
+ // Invalidate the token through server-side, even if it's not expired yet.
+ protectedAPI.Get("/logout", logout)
+
+ // http://localhost:8080
+ // http://localhost:8080/protected?token=$token (or Authorization: Bearer $token)
+ // http://localhost:8080/protected/logout?token=$token
+ // http://localhost:8080/protected?token=$token (401)
+ app.Listen(":8080")
+}
+
+func generateToken(signer *jwt.Signer) iris.Handler {
+ return func(ctx iris.Context) {
+ claims := fooClaims{Foo: "bar"}
+
+ token, err := signer.Sign(claims)
+ if err != nil {
+ ctx.StopWithStatus(iris.StatusInternalServerError)
+ return
+ }
+
+ ctx.Write(token)
+ }
+}
+
+func protected(ctx iris.Context) {
+ // Get the verified and decoded claims.
+ claims := jwt.Get(ctx).(*fooClaims)
+
+ // Optionally, get token information if you want to work with them.
+ // Just an example on how you can retrieve all the standard claims (set by signer's max age, "exp").
+ standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
+ expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
+ timeLeft := standardClaims.Timeleft()
+
+ ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
+}
+
+func logout(ctx iris.Context) {
+ err := ctx.Logout()
+ if err != nil {
+ ctx.WriteString(err.Error())
+ } else {
+ ctx.Writef("token invalidated, a new token is required to access the protected API")
+ }
+}
diff --git a/_examples/auth/jwt/overview/README.md b/_examples/auth/jwt/overview/README.md
deleted file mode 100644
index 1d2e0ae5..00000000
--- a/_examples/auth/jwt/overview/README.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Generate RSA
-
-```sh
-$ openssl genrsa -des3 -out private_rsa.pem 2048
-```
-
-```go
-b, err := ioutil.ReadFile("./private_rsa.pem")
-if err != nil {
- panic(err)
-}
-key := jwt.MustParseRSAPrivateKey(b, []byte("pass"))
-```
-
-OR
-
-```go
-import "crypto/rand"
-import "crypto/rsa"
-
-key, err := rsa.GenerateKey(rand.Reader, 2048)
-```
-
-# Generate Ed25519
-
-```sh
-$ openssl genpkey -algorithm Ed25519 -out private_ed25519.pem
-$ openssl req -x509 -key private_ed25519.pem -out cert_ed25519.pem -days 365
-```
diff --git a/_examples/auth/jwt/overview/main.go b/_examples/auth/jwt/overview/main.go
deleted file mode 100644
index ee2ef42f..00000000
--- a/_examples/auth/jwt/overview/main.go
+++ /dev/null
@@ -1,368 +0,0 @@
-// Package main shows how you can use the Iris unique JWT middleware.
-// The file contains different kind of examples that all do the same job but,
-// depending on your code style and your application's requirements, you may choose one over other.
-package main
-
-import (
- "time"
-
- "github.com/kataras/iris/v12"
- "github.com/kataras/iris/v12/middleware/jwt"
-)
-
-// Claims a custom claims structure.
-type Claims struct {
- // Optionally define JWT's "iss" (Issuer),
- // "sub" (Subject) and "aud" (Audience) for issuer and subject.
- // The JWT's "exp" (expiration) and "iat" (issued at) are automatically
- // set by the middleware.
- Issuer string `json:"iss"`
- Subject string `json:"sub"`
- Audience []string `json:"aud"`
- /*
- Note that the above fields can be also extracted via:
- jwt.GetTokenInfo(ctx).Claims
- But in that example, we just showcase how these info can be embedded
- inside your own Go structure.
- */
-
- // Optionally define a "exp" (Expiry),
- // unlike the rest, this is unset on creation
- // (unless you want to override the middleware's max age option),
- // it's filled automatically by the JWT middleware
- // when the request token is verified.
- // See the POST /user route.
- Expiry *jwt.NumericDate `json:"exp"`
-
- Username string `json:"username"`
-}
-
-func main() {
- // Get keys from system's environment variables
- // JWT_SECRET (for signing and verification) and JWT_SECRET_ENC(for encryption and decryption),
- // or defaults to "secret" and "itsa16bytesecret" respectfully.
- //
- // Use the `jwt.New` instead for more flexibility, if necessary.
- j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
-
- /*
- By default it extracts the token from url parameter "token={token}"
- and the Authorization Bearer {token} header.
- You can also take token from JSON body:
- j.Extractors = append(j.Extractors, jwt.FromJSON)
- */
-
- /* Optionally, enable block list to force-invalidate
- verified tokens even before their expiration time.
- This is useful when the client doesn't clear
- the token on a user logout by itself.
-
- The duration argument clears any expired token on each every tick.
- There is a GC() method that can be manually called to clear expired blocked tokens
- from the memory.
-
- j.Blocklist = jwt.NewBlocklist(30*time.Minute)
- OR NewBlocklistContext(stdContext, 30*time.Minute)
-
-
- To invalidate a verified token just call:
- j.Invalidate(ctx) inside a route handler.
- */
-
- app := iris.New()
- app.Logger().SetLevel("debug")
-
- app.OnErrorCode(iris.StatusUnauthorized, func(ctx iris.Context) {
- // Note that, any error stored by an authentication
- // method in Iris is an iris.ErrPrivate.
- // Available jwt errors:
- // - ErrMissing
- // - ErrMissingKey
- // - ErrExpired
- // - ErrNotValidYet
- // - ErrIssuedInTheFuture
- // - ErrBlocked
- // An iris.ErrPrivate SHOULD never be displayed to the client as it is;
- // because it may contain critical security information about the server.
- //
- // Also keep in mind that JWT middleware logs verification errors to the
- // application's logger ("debug") so, normally you don't have to
- // bother showing the verification error to the browser/client.
- // However, you can retrieve that error and do what ever you feel right:
- if err := ctx.GetErr(); err != nil {
- // If we have an error stored,
- // (JWT middleware stores any verification errors to the Context),
- // set the error as response body,
- // which is the default behavior if that
- // wasn't an authentication error (as explained above)
- ctx.WriteString(err.Error())
- } else {
- // Else, the default behavior when no error was occured;
- // write the status text of the status code:
- ctx.WriteString(iris.StatusText(iris.StatusUnauthorized))
- }
- })
-
- app.Get("/authenticate", func(ctx iris.Context) {
- claims := &Claims{
- Issuer: "server",
- Audience: []string{"user"},
- Username: "kataras",
- }
-
- // WriteToken generates and sends the token to the client.
- // To generate a token use: tok, err := j.Token(claims)
- // then you can write it in any form you'd like.
- // The expiration JWT fields are automatically
- // set by the middleware, that means that your claims value
- // only needs to fill fields that your application specifically requires.
- j.WriteToken(ctx, claims)
- })
-
- // Middleware + type-safe method,
- // useful in 99% of the cases, when your application
- // requires token verification under a whole path prefix, e.g. /protected:
- protectedAPI := app.Party("/protected")
- {
- protectedAPI.Use(j.Verify(func() interface{} {
- // Must return a pointer to a type.
- //
- // The Iris JWT implementation is very sophisticated.
- // We keep our claims in type-safe form.
- // However, you are free to use raw Go maps
- // (map[string]interface{} or iris.Map) too (example later on).
- //
- // Note that you can use the same "j" JWT instance
- // to serve different types of claims on other group of routes,
- // e.g. postRouter.Use(j.Verify(... return new(Post))).
- return new(Claims)
- }))
-
- protectedAPI.Get("/", func(ctx iris.Context) {
- claims := jwt.Get(ctx).(*Claims)
- // All fields parsed from token are set to the claims,
- // including the Expiry (if defined).
- ctx.Writef("Username: %s\nExpires at: %s\nAudience: %s",
- claims.Username, claims.Expiry.Time(), claims.Audience)
- })
- }
-
- // Verify token inside a handler method,
- // useful when you just need to verify a token on a single spot:
- app.Get("/inline", func(ctx iris.Context) {
- var claims Claims
- _, err := j.VerifyToken(ctx, &claims)
- if err != nil {
- ctx.StopWithError(iris.StatusUnauthorized, err)
- return
- }
-
- ctx.Writef("Username: %s\nExpires at: %s\n",
- claims.Username, claims.Expiry.Time())
- })
-
- // Use a common map as claims method,
- // not recommended, as we support typed claims but
- // you can do it:
- app.Get("/map/authenticate", func(ctx iris.Context) {
- claims := map[string]interface{}{ // or iris.Map for shortcut.
- "username": "kataras",
- }
-
- j.WriteToken(ctx, claims)
- })
-
- app.Get("/map/verify/middleware", j.Verify(func() interface{} {
- return &iris.Map{} // or &map[string]interface{}{}
- }), func(ctx iris.Context) {
- claims := jwt.Get(ctx).(iris.Map)
- // The Get method will unwrap the *iris.Map for you,
- // so its values are directly accessible:
- ctx.Writef("Username: %s\nExpires at: %s\n",
- claims["username"], claims["exp"].(*jwt.NumericDate).Time())
- })
-
- app.Get("/map/verify", func(ctx iris.Context) {
- claims := make(iris.Map) // or make(map[string]interface{})
-
- tokenInfo, err := j.VerifyToken(ctx, &claims)
- if err != nil {
- ctx.StopWithError(iris.StatusUnauthorized, err)
- return
- }
-
- ctx.Writef("Username: %s\nExpires at: %s\n",
- claims["username"], tokenInfo.Claims.Expiry.Time()) /* the claims["exp"] is also set. */
- })
-
- // Use the new Context.User() to retrieve the verified client method:
- // 1. Create a go stuct that implements the context.User interface:
- app.Get("/users/authenticate", func(ctx iris.Context) {
- user := &User{Username: "kataras"}
- j.WriteToken(ctx, user)
- })
- usersAPI := app.Party("/users")
- {
- usersAPI.Use(j.Verify(func() interface{} {
- return new(User)
- }))
-
- usersAPI.Get("/", func(ctx iris.Context) {
- user := ctx.User()
- userToken, _ := user.GetToken()
- /*
- You can also cast it to the underline implementation
- and work with its fields:
- expires := user.(*User).Expiry.Time()
- */
- // OR use the GetTokenInfo to get the parsed token information:
- expires := jwt.GetTokenInfo(ctx).Claims.Expiry.Time()
- lifetime := expires.Sub(time.Now()) // remeaning time to be expired.
-
- ctx.Writef("Username: %s\nAuthenticated at: %s\nLifetime: %s\nToken: %s\n",
- user.GetUsername(), user.GetAuthorizedAt(), lifetime, userToken)
- })
- }
-
- // http://localhost:8080/authenticate
- // http://localhost:8080/protected?token={token}
- // http://localhost:8080/inline?token={token}
- //
- // http://localhost:8080/map/authenticate
- // http://localhost:8080/map/verify?token={token}
- // http://localhost:8080/map/verify/middleware?token={token}
- //
- // http://localhost:8080/users/authenticate
- // http://localhost:8080/users?token={token}
- app.Listen(":8080")
-}
-
-// User is a custom implementation of the Iris Context User interface.
-// Optionally, for JWT, you can also implement
-// the SetToken(tok string) and
-// Validate(ctx iris.Context, claims jwt.Claims, e jwt.Expected) error
-// methods to set a token and add custom validation
-// to a User value parsed from a token.
-type User struct {
- iris.User
- Username string `json:"username"`
-
- // Optionally, declare some JWT fields,
- // they are automatically filled by the middleware itself.
- IssuedAt *jwt.NumericDate `json:"iat"`
- Expiry *jwt.NumericDate `json:"exp"`
- Token string `json:"-"`
-}
-
-// GetUsername returns the Username.
-// Look the iris/context.SimpleUser type
-// for all the methods you can implement.
-func (u *User) GetUsername() string {
- return u.Username
-}
-
-// GetAuthorizedAt returns the IssuedAt time.
-// This and the Get/SetToken methods showcase how you can map JWT standard fields
-// to an Iris Context User.
-func (u *User) GetAuthorizedAt() time.Time {
- return u.IssuedAt.Time()
-}
-
-// GetToken is a User interface method.
-func (u *User) GetToken() (string, error) {
- return u.Token, nil
-}
-
-// SetToken is a special jwt.TokenSetter interface which is
-// called automatically when a token is parsed to this User value.
-func (u *User) SetToken(tok string) {
- u.Token = tok
-}
-
-/*
-func default_RSA_Example() {
- j := jwt.RSA(15*time.Minute)
-}
-
-Same as:
-
-func load_File_Or_Generate_RSA_Example() {
- signKey, err := jwt.LoadRSA("jwt_sign.key", 2048)
- if err != nil {
- panic(err)
- }
-
- j, err := jwt.New(15*time.Minute, jwt.RS256, signKey)
- if err != nil {
- panic(err)
- }
-
- encKey, err := jwt.LoadRSA("jwt_enc.key", 2048)
- if err != nil {
- panic(err)
- }
- err = j.WithEncryption(jwt.A128CBCHS256, jwt.RSA15, encKey)
- if err != nil {
- panic(err)
- }
-}
-*/
-
-/*
-func hmac_Example() {
- // hmac
- key := []byte("secret")
- j, err := jwt.New(15*time.Minute, jwt.HS256, key)
- if err != nil {
- panic(err)
- }
-
- // OPTIONAL encryption:
- encryptionKey := []byte("itsa16bytesecret")
- err = j.WithEncryption(jwt.A128GCM, jwt.DIRECT, encryptionKey)
- if err != nil {
- panic(err)
- }
-}
-*/
-
-/*
-func load_From_File_With_Password_Example() {
- b, err := ioutil.ReadFile("./rsa_password_protected.key")
- if err != nil {
- panic(err)
- }
- signKey,err := jwt.ParseRSAPrivateKey(b, []byte("pass"))
- if err != nil {
- panic(err)
- }
-
- j, err := jwt.New(15*time.Minute, jwt.RS256, signKey)
- if err != nil {
- panic(err)
- }
-}
-*/
-
-/*
-func generate_RSA_Example() {
- signKey, err := rsa.GenerateKey(rand.Reader, 4096)
- if err != nil {
- panic(err)
- }
-
- encryptionKey, err := rsa.GenerateKey(rand.Reader, 4096)
- if err != nil {
- panic(err)
- }
-
- j, err := jwt.New(15*time.Minute, jwt.RS512, signKey)
- if err != nil {
- panic(err)
- }
- err = j.WithEncryption(jwt.A128CBCHS256, jwt.RSA15, encryptionKey)
- if err != nil {
- panic(err)
- }
-}
-*/
diff --git a/_examples/auth/jwt/overview/rsa_password_protected.key b/_examples/auth/jwt/overview/rsa_password_protected.key
deleted file mode 100644
index e93fff77..00000000
--- a/_examples/auth/jwt/overview/rsa_password_protected.key
+++ /dev/null
@@ -1,30 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-Proc-Type: 4,ENCRYPTED
-DEK-Info: DES-EDE3-CBC,6B0BC214C94124FE
-
-lAM48DEM/GdCDimr9Vhi+fSHLgduDb0l2BA4uhILgNby51jxY/4X3IqM6f3ImKX7
-cEd9OBug+pwIugB0UW0L0f5Pd59Ovpiaz3xLci1/19ehYnMqsuP3YAnJm40hT5VP
-p0gWRiR415PJ0fPeeJPFx5IsqvkTJ30LWZHUZX4EkdcL5L8PrVbmthGDbLh+OcMc
-LzoP8eTglzlZF03nyvAol6+p2eZtvOJLu8nWG25q17kyBx6kEiCsWFcUBTX9G7sH
-CM3naByDijqZXE/XXtmTMLSRRnlk7Q5WLxClroHlUP9y8BQFMo2TW4Z+vNjHUkc1
-77ghabX1704bAlIE8LLZJKrm/C5+VKyV6117SVG/2bc4036Y5rStXpANbk1j4K0x
-ADvpRhuTpifaogdvJP+8eXBdl841MQMRzWuZHp6UNYYQegoV9C+KHyJx4UPjZyzd
-gblZmKgU+BsX3mV6MLhJtd6dheLZtpBsAlSstJxzmgwqz9faONYEGeItXO+NnxbA
-mxAp/mI+Fz2jfgYlWjwkyPPzD4k/ZMMzB4XLkKKs9XaxUtTomiDkuYZfnABhxt73
-xBy40V1rb/NyeW80pk1zEHM6Iy/48ETSp9U3k9sSOXjMhYbPXgxDtimV8w0qGFAo
-2Tif7ZuaiuC38rOkoHK9C6vy2Dp8lQZ+QBnUKLeFsyhq9CaqSdnyUTMj3oEZXXf+
-TqqeO+PTtl7JaNfGRq6/aMZqxACHkyVUvYvjZzx07CJ2fr+OtNqxallM6Oc/o9NZ
-5u7lpgrYaKM/b67q0d2X/AoxR5zrZuM8eam3acD1PwHFQKbJWuFNmjWtnlZNuR3X
-fZEmxIKwDlup8TxFcqbbZtPHuQA2mTMTqfRkf8oPSO+N6NNaUpb0ignYyA7Eu5GT
-b02d/oNLETMikxUxntMSH7GhuOpfJyELz8krYTttbJ+a93h4wBeYW2+LyAr/cRLB
-mbtKLtaN7f3FaOSnu8e0+zlJ7xglHPXqblRL9q6ZDM5UJtJD4rA7LPZHk/0Y1Kb6
-hBh1qMDu0r3IV4X7MDacvxw7aa7D8TyXJiFSvxykVhds+ndjIe51Ics5908+lev3
-nwE69PLMwyqe2vvE2oDwao4XJuBLCHjcv/VagRSz/XQGMbZqb3L6unyd3UPl8JjP
-ovipNwM4rFnE54uiUUeki7TZGDYO72vQcSaLrmbeAWc2m202+rqLz0WMm6HpPmCv
-IgexpX2MnIeHJ3+BlEjA2u+S6xNSD7qHGk2pb7DD8nRvUdSHAHeaQbrkEfEhhR2Q
-Dw5gdw1JyQ0UKBl5ndn/1Ub2Asl016lZjpqHyMIVS4tFixACDsihEYMmq/zQmTj4
-8oBZTU+fycN/KiGKZBsqxIwgYIeMz/GfvoyN5m57l6fwEZALVpveI1pP4fiZB/Z8
-xLKa5JK6L10lAD1YHWc1dPhamf9Sb3JwN2CFtGvjOJ/YjAZu3jJoxi40DtRkE3Rh
-HI8Cbx1OORzoo0kO0vy42rz5qunYyVmEzPKtOj+YjVEhVJ85yJZ9bTZtuyqMv8mH
-cnwEeIFK8cmm9asbVzQGDwN/UGB4cO3LrMX1RYk4GRttTGlp0729BbmZmu00RnD/
------END RSA PRIVATE KEY-----
diff --git a/_examples/auth/jwt/refresh-token/main.go b/_examples/auth/jwt/refresh-token/main.go
index f48c8231..cccf3266 100644
--- a/_examples/auth/jwt/refresh-token/main.go
+++ b/_examples/auth/jwt/refresh-token/main.go
@@ -1,57 +1,65 @@
package main
import (
+ "fmt"
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
+const (
+ accessTokenMaxAge = 10 * time.Minute
+ refreshTokenMaxAge = time.Hour
+)
+
+var (
+ privateKey, publicKey = jwt.MustLoadRSA("rsa_private_key.pem", "rsa_public_key.pem")
+
+ signer = jwt.NewSigner(jwt.RS256, privateKey, accessTokenMaxAge)
+ verifier = jwt.NewVerifier(jwt.RS256, publicKey)
+)
+
// UserClaims a custom access claims structure.
type UserClaims struct {
- // In order to separate refresh and access tokens on validation level:
- // - Set a different Issuer, with a field of: Issuer string `json:"iss"`
- // - Set the Iris JWT's json tag option "required" on an access token field,
- // e.g. Username string `json:"username,required"`
- // - Let the middleware validate the correct one based on the given MaxAge,
- // which should be different between refresh and max age (refersh should be bigger)
- // by setting the `jwt.ExpectRefreshToken` on Verify/VerifyToken/VerifyTokenString
- // (see `refreshToken` function below)
- ID string `json:"user_id"`
+ ID string `json:"user_id"`
+ // Do: `json:"username,required"` to have this field required
+ // or see the Validate method below instead.
Username string `json:"username"`
}
+// Validate completes the middleware's custom ClaimsValidator.
+// It will not accept a token which its claims missing the username field
+// (useful to not accept refresh tokens generated by the same algorithm).
+func (u *UserClaims) Validate() error {
+ if u.Username == "" {
+ return fmt.Errorf("username field is missing")
+ }
+
+ return nil
+}
+
// For refresh token, we will just use the jwt.Claims
// structure which contains the standard JWT fields.
func main() {
app := iris.New()
+ app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized)
- j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
-
- app.Get("/authenticate", func(ctx iris.Context) {
- generateTokenPair(ctx, j)
- })
-
- app.Get("/refresh", func(ctx iris.Context) {
- refreshToken(ctx, j)
- })
+ app.Get("/authenticate", generateTokenPair)
+ app.Get("/refresh", refreshToken)
protectedAPI := app.Party("/protected")
{
- protectedAPI.Use(j.Verify(func() interface{} {
+ verifyMiddleware := verifier.Verify(func() interface{} {
return new(UserClaims)
- })) // OR j.VerifyToken(ctx, &claims, jwt.MeetRequirements(&UserClaims{}))
+ })
+
+ protectedAPI.Use(verifyMiddleware)
protectedAPI.Get("/", func(ctx iris.Context) {
- // Get token info, even if our UserClaims does not embed those
- // through GetTokenInfo:
- expiresAt := jwt.GetTokenInfo(ctx).Claims.Expiry.Time()
- // Get your custom JWT claims through Get,
- // which is a shortcut of GetTokenInfo(ctx).Value:
claims := jwt.Get(ctx).(*UserClaims)
-
- ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, expiresAt)
+ ctx.Writef("Username: %s\n", claims.Username)
})
}
@@ -59,33 +67,33 @@ func main() {
// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
// http://localhost:8080/protected?token={access_token} (200)
// http://localhost:8080/protected?token={refresh_token} (401)
- // http://localhost:8080/refresh?token={refresh_token}
+ // http://localhost:8080/refresh?refresh_token={refresh_token}
// OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
- // OR http://localhost:8080/refresh (request PLAIN TEXT of {refresh_token}) (200) (response JSON {access_token, refresh_token})
- // http://localhost:8080/refresh?token={access_token} (401)
+ // http://localhost:8080/refresh?refresh_token={access_token} (401)
app.Listen(":8080")
}
-func generateTokenPair(ctx iris.Context, j *jwt.JWT) {
+func generateTokenPair(ctx iris.Context) {
// Simulate a user...
userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
// Map the current user with the refresh token,
// so we make sure, on refresh route, that this refresh token owns
// to that user before re-generate.
- refresh := jwt.Claims{Subject: userID}
+ refreshClaims := jwt.Claims{Subject: userID}
- access := UserClaims{
+ accessClaims := UserClaims{
ID: userID,
Username: "kataras",
}
// Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.
- // Second argument is the refresh claims and,
- // the last one is the access token's claims.
- tokenPair, err := j.TokenPair(1*time.Hour, refresh, access)
+ // First argument is the access claims,
+ // second argument is the refresh claims,
+ // third argument is the refresh max age.
+ tokenPair, err := signer.NewTokenPair(accessClaims, refreshClaims, refreshTokenMaxAge)
if err != nil {
- ctx.Application().Logger().Debugf("token pair: %v", err)
+ ctx.Application().Logger().Errorf("token pair: %v", err)
ctx.StopWithStatus(iris.StatusInternalServerError)
return
}
@@ -95,14 +103,12 @@ func generateTokenPair(ctx iris.Context, j *jwt.JWT) {
ctx.JSON(tokenPair)
}
-func refreshToken(ctx iris.Context, j *jwt.JWT) {
- /*
- We could pass a jwt.Claims pointer as the second argument,
- but we don't have to because the method already returns
- the standard JWT claims information back to us:
- refresh, err := VerifyRefreshToken(ctx, nil)
- */
-
+// There are various methods of refresh token, depending on the application requirements.
+// In this example we will accept a refresh token only, we will verify only a refresh token
+// and we re-generate a whole new pair. An alternative would be to accept a token pair
+// of both access and refresh tokens, verify the refresh, verify the access with a Leeway time
+// and check if its going to expire soon, then generate a single access token.
+func refreshToken(ctx iris.Context) {
// Assuming you have access to the current user, e.g. sessions.
//
// Simulate a database call against our jwt subject
@@ -110,23 +116,46 @@ func refreshToken(ctx iris.Context, j *jwt.JWT) {
// * Note: You can remove the ExpectSubject and do this validation later on by yourself.
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
+ // Get the refresh token from ?refresh_token=$token OR
+ // the request body's JSON{"refresh_token": "$token"}.
+ refreshToken := []byte(ctx.URLParam("refresh_token"))
+ if len(refreshToken) == 0 {
+ // You can read the whole body with ctx.GetBody/ReadBody too.
+ var tokenPair jwt.TokenPair
+ if err := ctx.ReadJSON(&tokenPair); err != nil {
+ ctx.StopWithError(iris.StatusBadRequest, err)
+ return
+ }
+
+ refreshToken = tokenPair.RefreshToken
+ }
+
// Verify the refresh token, which its subject MUST match the "currentUserID".
- _, err := j.VerifyRefreshToken(ctx, nil, jwt.ExpectSubject(currentUserID))
+ _, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})
if err != nil {
- ctx.Application().Logger().Debugf("verify refresh token: %v", err)
+ ctx.Application().Logger().Errorf("verify refresh token: %v", err)
ctx.StatusCode(iris.StatusUnauthorized)
return
}
/* Custom validation checks can be performed after Verify calls too:
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
- userID := refresh.Claims.Subject
+ userID := verifiedToken.StandardClaims.Subject
if userID != currentUserID {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
*/
- // All OK, re-generate the new pair and send to client.
- generateTokenPair(ctx, j)
+ // All OK, re-generate the new pair and send to client,
+ // we could only generate an access token as well.
+ generateTokenPair(ctx)
+}
+
+func handleUnauthorized(ctx iris.Context) {
+ if err := ctx.GetErr(); err != nil {
+ ctx.Application().Logger().Errorf("unauthorized: %v", err)
+ }
+
+ ctx.WriteString("Unauthorized")
}
diff --git a/_examples/auth/jwt/refresh-token/rsa_private_key.pem b/_examples/auth/jwt/refresh-token/rsa_private_key.pem
new file mode 100644
index 00000000..411e3a89
--- /dev/null
+++ b/_examples/auth/jwt/refresh-token/rsa_private_key.pem
@@ -0,0 +1,27 @@
+-----BEGIN PRIVATE KEY-----
+MIIEowIBAAKCAQEArwO0q8WbBvrplz3lTQjsWu66HC7M3mVAjmjLq8Wj/ipqVtiJ
+MrUL9t/0q9PNO/KX9u+HayFNYM4TnYkXVZX3M5E31W8fPPy74D/XpqFwrwT7bAEw
+pT51JJyxkoBAyOh08lmR2EYvpGF7qErra7qbkk4LGFbhoFCXdMLXguT4rPymkzFH
+dQrmGYOBS+v9imSuJddCZpXyv6Ko7AKB4mhzg4RC5RJZO5GEHVUrSMHxZB0syF8c
+U+28iL8A7SlGKTNZPZiHmCQVRqA6WlllL/YV/t6p24kaNZBUp9JGbAzOeKuVUv2u
+vfNKwB/aBwnFKauM9I6RmC4bnI1nGHjETlNNWwIDAQABAoIBAHBPKHmybTGlgpET
+nzo4J7SSzcuYHM/6mdrJVSn9wqcwAN2KR0DK/cqHHTPGz0VRAEPuojAVRtqAZAYM
+G3VIr0HgRrwoextf9BCL549+uhkWUWGVwenIktPT2f/xXaGPyrxazkTDhX8vL3Nn
+4HtZXMweWPBdkJyYGxlKj5Hn7czTpG3VKpvpHeFlY4caF+FT2as1jcQ1MjPnGslH
+Ss+sYPBp/70w2T114Z4wlR4OryI1LeuFeje9obrn0HAmJd0ZKYM21awp/YWJ/y8J
+wIH6XQ4AGR9iTRhuffK1XRM/Iec3K/YhOn4PtKdT7OsIujAKY7A9WcqSFif+/E1g
+jom3eMECgYEAw5Zdqt2uZ19FuDlDTW4Kw8Z2NyXgWp33LkAXG1mJw7bqDhfPeB1c
+xTPs4i4RubGuDusygxZ3GgJAO7tLGzNQfWNoi03mM7Q/BJGkA9VZr+U28zsSRQOQ
++J9xNsdgUMP1js7X/NNM2bxTC8zy9wEsWr9JwNo1C7uHTE9WXAumBI8CgYEA5RKV
+niSbyko36W3Vi0ZnGBrRhy0Eiq85V2mhWzHN+txcv+8aISow2wioTUzrpR0aVZ4j
+v9+siJENlALVzdUFihy0lPxHqLJT746Cixz95WRTLkdHeNllV0DMfOph2x3j1Hjd
+3PgTv+jqb6npY0/2Vb2pp4t/zVikGaObsAalSHUCgYBne8B1bjMfqI3n6gxNBIMX
+kILtrNGmwFuPEgPnyZkVf0sZR8nSwJ5cDJwyE7P3LyZr6E9igllj3nsD35Xef2j/
+3r/qrL2275BEJ5bDHHgGk91eFgwVjcx/b0TkedrhAL2E4LXwpA/OSFEcNkT7IZjJ
+Ltqj+hAE9CSi4HtN2i/tywKBgBotKn28zzSpkIQTMgDNVcCSZ/kbctZqOZI8lty1
+70TIY6znJMQ/bv/ImHrk3FSs47J+9LTbWXrtoHCWdlokCpMCvrv7rDCh2Cea0F4X
+PQg2k67JJGix5vu2guePXQlN/Bfui+PRUWhvtEJ4VxwrKgoYN0fXEA6mH3JymLrf
+t4l1AoGBALk4o9swGjw7MnByYJmOidlJ0p9Wj1BWWJJYoYX2VfjIuvZj6BNxkEb0
+aVmYRC+40e9L1rOyrlyaO/TiQaIPE4ljVs/AmMKGz8sIcVfwdyERH3nDrXxvlAav
+lSvfKoYM3J+5c63CDuU45gztpmavNerzCczqYTLOEMx1eCLHOQlx
+-----END PRIVATE KEY-----
diff --git a/_examples/auth/jwt/refresh-token/rsa_public_key.pem b/_examples/auth/jwt/refresh-token/rsa_public_key.pem
new file mode 100644
index 00000000..99a96176
--- /dev/null
+++ b/_examples/auth/jwt/refresh-token/rsa_public_key.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwO0q8WbBvrplz3lTQjs
+Wu66HC7M3mVAjmjLq8Wj/ipqVtiJMrUL9t/0q9PNO/KX9u+HayFNYM4TnYkXVZX3
+M5E31W8fPPy74D/XpqFwrwT7bAEwpT51JJyxkoBAyOh08lmR2EYvpGF7qErra7qb
+kk4LGFbhoFCXdMLXguT4rPymkzFHdQrmGYOBS+v9imSuJddCZpXyv6Ko7AKB4mhz
+g4RC5RJZO5GEHVUrSMHxZB0syF8cU+28iL8A7SlGKTNZPZiHmCQVRqA6WlllL/YV
+/t6p24kaNZBUp9JGbAzOeKuVUv2uvfNKwB/aBwnFKauM9I6RmC4bnI1nGHjETlNN
+WwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/go.mod b/go.mod
index 6f9f219c..86a975a6 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.15
require (
github.com/BurntSushi/toml v0.3.1
- github.com/CloudyKit/jet/v5 v5.1.0
+ github.com/CloudyKit/jet/v5 v5.1.1
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398
github.com/andybalholm/brotli v1.0.1
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible
@@ -12,7 +12,7 @@ require (
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
github.com/fatih/structs v1.1.0
github.com/flosch/pongo2/v4 v4.0.0
- github.com/go-redis/redis/v8 v8.3.1
+ github.com/go-redis/redis/v8 v8.3.3
github.com/google/uuid v1.1.2
github.com/hashicorp/go-version v1.2.1
github.com/iris-contrib/httpexpect/v2 v2.0.5
@@ -21,24 +21,24 @@ require (
github.com/json-iterator/go v1.1.10
github.com/kataras/blocks v0.0.4
github.com/kataras/golog v0.1.5
+ github.com/kataras/jwt v0.0.4
github.com/kataras/neffos v0.0.16
github.com/kataras/pio v0.0.10
github.com/kataras/sitemap v0.0.5
github.com/kataras/tunnel v0.0.2
- github.com/klauspost/compress v1.11.1
+ github.com/klauspost/compress v1.11.2
github.com/mailru/easyjson v0.7.6
github.com/microcosm-cc/bluemonday v1.0.4
github.com/russross/blackfriday/v2 v2.0.1
github.com/schollz/closestmatch v2.1.0+incompatible
- github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
- github.com/tdewolff/minify/v2 v2.9.9
- github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1
+ github.com/tdewolff/minify/v2 v2.9.10
+ github.com/vmihailenco/msgpack/v5 v5.0.0-beta.9
github.com/yosssi/ace v0.0.5
go.etcd.io/bbolt v1.3.5
- golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
- golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0
- golang.org/x/sys v0.0.0-20201016160150-f659759dc4ca
- golang.org/x/text v0.3.3
+ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
+ golang.org/x/net v0.0.0-20201027133719-8eef5233e2a1
+ golang.org/x/sys v0.0.0-20201028094953-708e7fb298ac
+ golang.org/x/text v0.3.4
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
google.golang.org/protobuf v1.25.0
gopkg.in/ini.v1 v1.62.0
diff --git a/middleware/jwt/alises.go b/middleware/jwt/alises.go
index da34d9c7..df27cb5d 100644
--- a/middleware/jwt/alises.go
+++ b/middleware/jwt/alises.go
@@ -1,91 +1,105 @@
package jwt
import (
- "github.com/square/go-jose/v3"
- "github.com/square/go-jose/v3/json"
- "github.com/square/go-jose/v3/jwt"
+ "github.com/kataras/jwt"
)
+// Type alises for the underline jwt package.
type (
- // Claims represents public claim values (as specified in RFC 7519).
+ // Alg is the signature algorithm interface alias.
+ Alg = jwt.Alg
+ // Claims represents the standard claim values (as specified in RFC 7519).
Claims = jwt.Claims
- // Audience represents the recipients that the token is intended for.
- Audience = jwt.Audience
- // NumericDate represents date and time as the number of seconds since the
- // epoch, including leap seconds. Non-integer values can be represented
- // in the serialized format, but we round to the nearest second.
- NumericDate = jwt.NumericDate
- // Expected defines values used for protected claims validation.
- // If field has zero value then validation is skipped.
+ // Expected is a TokenValidator which performs simple checks
+ // between standard claims values.
+ //
+ // Usage:
+ // expecteed := jwt.Expected{
+ // Issuer: "my-app",
+ // }
+ // verifiedToken, err := verifier.Verify(..., expected)
Expected = jwt.Expected
-)
-var (
- // NewNumericDate constructs NumericDate from time.Time value.
- NewNumericDate = jwt.NewNumericDate
- // Marshal returns the JSON encoding of v.
- Marshal = json.Marshal
- // Unmarshal parses the JSON-encoded data and stores the result
- // in the value pointed to by v.
- Unmarshal = json.Unmarshal
-)
-
-type (
- // KeyAlgorithm represents a key management algorithm.
- KeyAlgorithm = jose.KeyAlgorithm
-
- // SignatureAlgorithm represents a signature (or MAC) algorithm.
- SignatureAlgorithm = jose.SignatureAlgorithm
-
- // ContentEncryption represents a content encryption algorithm.
- ContentEncryption = jose.ContentEncryption
-)
-
-// Key management algorithms.
-const (
- ED25519 = jose.ED25519
- RSA15 = jose.RSA1_5
- RSAOAEP = jose.RSA_OAEP
- RSAOAEP256 = jose.RSA_OAEP_256
- A128KW = jose.A128KW
- A192KW = jose.A192KW
- A256KW = jose.A256KW
- DIRECT = jose.DIRECT
- ECDHES = jose.ECDH_ES
- ECDHESA128KW = jose.ECDH_ES_A128KW
- ECDHESA192KW = jose.ECDH_ES_A192KW
- ECDHESA256KW = jose.ECDH_ES_A256KW
- A128GCMKW = jose.A128GCMKW
- A192GCMKW = jose.A192GCMKW
- A256GCMKW = jose.A256GCMKW
- PBES2HS256A128KW = jose.PBES2_HS256_A128KW
- PBES2HS384A192KW = jose.PBES2_HS384_A192KW
- PBES2HS512A256KW = jose.PBES2_HS512_A256KW
+ // TokenValidator is the token validator interface alias.
+ TokenValidator = jwt.TokenValidator
+ // VerifiedToken is the type alias for the verfieid token type,
+ // the result of the VerifyToken function.
+ VerifiedToken = jwt.VerifiedToken
+ // SignOption used to set signing options at Sign function.
+ SignOption = jwt.SignOption
+ // TokenPair is just a helper structure which holds both access and refresh tokens.
+ TokenPair = jwt.TokenPair
)
// Signature algorithms.
-const (
- EdDSA = jose.EdDSA
- HS256 = jose.HS256
- HS384 = jose.HS384
- HS512 = jose.HS512
- RS256 = jose.RS256
- RS384 = jose.RS384
- RS512 = jose.RS512
- ES256 = jose.ES256
- ES384 = jose.ES384
- ES512 = jose.ES512
- PS256 = jose.PS256
- PS384 = jose.PS384
- PS512 = jose.PS512
+var (
+ EdDSA = jwt.EdDSA
+ HS256 = jwt.HS256
+ HS384 = jwt.HS384
+ HS512 = jwt.HS512
+ RS256 = jwt.RS256
+ RS384 = jwt.RS384
+ RS512 = jwt.RS512
+ ES256 = jwt.ES256
+ ES384 = jwt.ES384
+ ES512 = jwt.ES512
+ PS256 = jwt.PS256
+ PS384 = jwt.PS384
+ PS512 = jwt.PS512
)
-// Content encryption algorithms.
-const (
- A128CBCHS256 = jose.A128CBC_HS256
- A192CBCHS384 = jose.A192CBC_HS384
- A256CBCHS512 = jose.A256CBC_HS512
- A128GCM = jose.A128GCM
- A192GCM = jose.A192GCM
- A256GCM = jose.A256GCM
+// Encryption algorithms.
+var (
+ GCM = jwt.GCM
+ // Helper to generate random key,
+ // can be used to generate hmac signature key and GCM+AES for testing.
+ MustGenerateRandom = jwt.MustGenerateRandom
+)
+
+var (
+ // Leeway adds validation for a leeway expiration time.
+ // If the token was not expired then a comparison between
+ // this "leeway" and the token's "exp" one is expected to pass instead (now+leeway > exp).
+ // Example of use case: disallow tokens that are going to be expired in 3 seconds from now,
+ // this is useful to make sure that the token is valid when the when the user fires a database call for example.
+ // Usage:
+ // verifiedToken, err := verifier.Verify(..., jwt.Leeway(5*time.Second))
+ Leeway = jwt.Leeway
+ // MaxAge is a SignOption to set the expiration "exp", "iat" JWT standard claims.
+ // Can be passed as last input argument of the `Sign` function.
+ //
+ // If maxAge > second then sets expiration to the token.
+ // It's a helper field to set the "exp" and "iat" claim values.
+ // Usage:
+ // signer.Sign(..., jwt.MaxAge(15*time.Minute))
+ MaxAge = jwt.MaxAge
+)
+
+// Shortcuts for Signing and Verifying.
+var (
+ VerifyToken = jwt.Verify
+ VerifyEncryptedToken = jwt.VerifyEncrypted
+ Sign = jwt.Sign
+ SignEncrypted = jwt.SignEncrypted
+)
+
+// Signature algorithm helpers.
+var (
+ MustLoadHMAC = jwt.MustLoadHMAC
+ LoadHMAC = jwt.LoadHMAC
+ MustLoadRSA = jwt.MustLoadRSA
+ LoadPrivateKeyRSA = jwt.LoadPrivateKeyRSA
+ LoadPublicKeyRSA = jwt.LoadPublicKeyRSA
+ ParsePrivateKeyRSA = jwt.ParsePrivateKeyRSA
+ ParsePublicKeyRSA = jwt.ParsePublicKeyRSA
+ MustLoadECDSA = jwt.MustLoadECDSA
+ LoadPrivateKeyECDSA = jwt.LoadPrivateKeyECDSA
+ LoadPublicKeyECDSA = jwt.LoadPublicKeyECDSA
+ ParsePrivateKeyECDSA = jwt.ParsePrivateKeyECDSA
+ ParsePublicKeyECDSA = jwt.ParsePublicKeyECDSA
+ MustLoadEdDSA = jwt.MustLoadEdDSA
+ LoadPrivateKeyEdDSA = jwt.LoadPrivateKeyEdDSA
+ LoadPublicKeyEdDSA = jwt.LoadPublicKeyEdDSA
+ ParsePrivateKeyEdDSA = jwt.ParsePrivateKeyEdDSA
+ ParsePublicKeyEdDSA = jwt.ParsePublicKeyEdDSA
)
diff --git a/middleware/jwt/blocklist.go b/middleware/jwt/blocklist.go
index 377ca027..1c827969 100644
--- a/middleware/jwt/blocklist.go
+++ b/middleware/jwt/blocklist.go
@@ -1,9 +1,7 @@
package jwt
import (
- stdContext "context"
- "sync"
- "time"
+ "github.com/kataras/jwt"
)
// Blocklist should hold and manage invalidated-by-server tokens.
@@ -15,136 +13,14 @@ import (
// e.g. a redis one to keep persistence of invalidated tokens on server restarts.
// and bind to the JWT middleware's Blocklist field.
type Blocklist interface {
- // Set should upsert a token to the storage.
- Set(token string, expiresAt time.Time)
+ jwt.TokenValidator
+
+ // InvalidateToken should invalidate a verified JWT token.
+ InvalidateToken(token []byte, expiry int64)
// Del should remove a token from the storage.
- Del(token string)
+ Del(token []byte)
// Count should return the total amount of tokens stored.
Count() int
// Has should report whether a specific token exists in the storage.
- Has(token string) bool
-}
-
-// blocklist is an in-memory storage of tokens that should be
-// immediately invalidated by the server-side.
-// The most common way to invalidate a token, e.g. on user logout,
-// is to make the client-side remove the token itself.
-// However, if someone else has access to that token,
-// it could be still valid for new requests until its expiration.
-type blocklist struct {
- entries map[string]time.Time // key = token | value = expiration time (to remove expired).
- mu sync.RWMutex
-}
-
-// NewBlocklist returns a new up and running in-memory Token Blocklist.
-// The returned value can be set to the JWT instance's Blocklist field.
-func NewBlocklist(gcEvery time.Duration) Blocklist {
- return NewBlocklistContext(stdContext.Background(), gcEvery)
-}
-
-// NewBlocklistContext same as `NewBlocklist`
-// but it also accepts a standard Go Context for GC cancelation.
-func NewBlocklistContext(ctx stdContext.Context, gcEvery time.Duration) Blocklist {
- b := &blocklist{
- entries: make(map[string]time.Time),
- }
-
- if gcEvery > 0 {
- go b.runGC(ctx, gcEvery)
- }
-
- return b
-}
-
-// Set upserts a given token, with its expiration time,
-// to the block list, so it's immediately invalidated by the server-side.
-func (b *blocklist) Set(token string, expiresAt time.Time) {
- b.mu.Lock()
- b.entries[token] = expiresAt
- b.mu.Unlock()
-}
-
-// Del removes a "token" from the block list.
-func (b *blocklist) Del(token string) {
- b.mu.Lock()
- delete(b.entries, token)
- b.mu.Unlock()
-}
-
-// Count returns the total amount of blocked tokens.
-func (b *blocklist) Count() int {
- b.mu.RLock()
- n := len(b.entries)
- b.mu.RUnlock()
-
- return n
-}
-
-// Has reports whether the given "token" is blocked by the server.
-// This method is called before the token verification,
-// so even if was expired it is removed from the block list.
-func (b *blocklist) Has(token string) bool {
- if token == "" {
- return false
- }
-
- b.mu.RLock()
- _, ok := b.entries[token]
- b.mu.RUnlock()
-
- /* No, the Blocklist will be used after the token is parsed,
- there we can call the Del method if err was ErrExpired.
- if ok {
- // As an extra step, to keep the list size as small as possible,
- // we delete it from list if it's going to be expired
- // ~in the next `blockedExpireLeeway` seconds.~
- // - Let's keep it easier for testing by not setting a leeway.
- // if time.Now().Add(blockedExpireLeeway).After(expiresAt) {
- if time.Now().After(expiresAt) {
- b.Del(token)
- }
- }*/
-
- return ok
-}
-
-// GC iterates over all entries and removes expired tokens.
-// This method is helpful to keep the list size small.
-// Depending on the application, the GC method can be scheduled
-// to called every half or a whole hour.
-// A good value for a GC cron task is the JWT's max age (default).
-func (b *blocklist) GC() int {
- now := time.Now()
- var markedForDeletion []string
-
- b.mu.RLock()
- for token, expiresAt := range b.entries {
- if now.After(expiresAt) {
- markedForDeletion = append(markedForDeletion, token)
- }
- }
- b.mu.RUnlock()
-
- n := len(markedForDeletion)
- if n > 0 {
- for _, token := range markedForDeletion {
- b.Del(token)
- }
- }
-
- return n
-}
-
-func (b *blocklist) runGC(ctx stdContext.Context, every time.Duration) {
- t := time.NewTicker(every)
-
- for {
- select {
- case <-ctx.Done():
- t.Stop()
- return
- case <-t.C:
- b.GC()
- }
- }
+ Has(token []byte) bool
}
diff --git a/middleware/jwt/extractor.go b/middleware/jwt/extractor.go
new file mode 100644
index 00000000..a04dcb98
--- /dev/null
+++ b/middleware/jwt/extractor.go
@@ -0,0 +1,71 @@
+package jwt
+
+import (
+ "strings"
+
+ "github.com/kataras/iris/v12/context"
+)
+
+// TokenExtractor is a function that takes a context as input and returns
+// a token. An empty string should be returned if no token found
+// without additional information.
+type TokenExtractor func(*context.Context) string
+
+// FromHeader is a token extractor.
+// It reads the token from the Authorization request header of form:
+// Authorization: "Bearer {token}".
+func FromHeader(ctx *context.Context) string {
+ authHeader := ctx.GetHeader("Authorization")
+ if authHeader == "" {
+ return ""
+ }
+
+ // pure check: authorization header format must be Bearer {token}
+ authHeaderParts := strings.Split(authHeader, " ")
+ if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
+ return ""
+ }
+
+ return authHeaderParts[1]
+}
+
+// FromQuery is a token extractor.
+// It reads the token from the "token" url query parameter.
+func FromQuery(ctx *context.Context) string {
+ return ctx.URLParam("token")
+}
+
+// FromJSON is a token extractor.
+// Reads a json request body and extracts the json based on the given field.
+// The request content-type should contain the: application/json header value, otherwise
+// this method will not try to read and consume the body.
+func FromJSON(jsonKey string) TokenExtractor {
+ return func(ctx *context.Context) string {
+ if ctx.GetContentTypeRequested() != context.ContentJSONHeaderValue {
+ return ""
+ }
+
+ var m context.Map
+ ctx.RecordRequestBody(true)
+ defer ctx.RecordRequestBody(false)
+ if err := ctx.ReadJSON(&m); err != nil {
+ return ""
+ }
+
+ if m == nil {
+ return ""
+ }
+
+ v, ok := m[jsonKey]
+ if !ok {
+ return ""
+ }
+
+ tok, ok := v.(string)
+ if !ok {
+ return ""
+ }
+
+ return tok
+ }
+}
diff --git a/middleware/jwt/jwt.go b/middleware/jwt/jwt.go
index a2d8172c..e2adc312 100644
--- a/middleware/jwt/jwt.go
+++ b/middleware/jwt/jwt.go
@@ -1,861 +1,7 @@
package jwt
-import (
- "crypto"
- "encoding/json"
- "fmt"
- "os"
- "strings"
- "time"
-
- "github.com/kataras/iris/v12/context"
-
- "github.com/square/go-jose/v3"
- "github.com/square/go-jose/v3/jwt"
-)
+import "github.com/kataras/iris/v12/context"
func init() {
context.SetHandlerName("iris/middleware/jwt.*", "iris.jwt")
}
-
-// TokenExtractor is a function that takes a context as input and returns
-// a token. An empty string should be returned if no token found
-// without additional information.
-type TokenExtractor func(*context.Context) string
-
-// FromHeader is a token extractor.
-// It reads the token from the Authorization request header of form:
-// Authorization: "Bearer {token}".
-func FromHeader(ctx *context.Context) string {
- authHeader := ctx.GetHeader("Authorization")
- if authHeader == "" {
- return ""
- }
-
- // pure check: authorization header format must be Bearer {token}
- authHeaderParts := strings.Split(authHeader, " ")
- if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
- return ""
- }
-
- return authHeaderParts[1]
-}
-
-// FromQuery is a token extractor.
-// It reads the token from the "token" url query parameter.
-func FromQuery(ctx *context.Context) string {
- return ctx.URLParam("token")
-}
-
-// FromJSON is a token extractor.
-// Reads a json request body and extracts the json based on the given field.
-// The request content-type should contain the: application/json header value, otherwise
-// this method will not try to read and consume the body.
-func FromJSON(jsonKey string) TokenExtractor {
- return func(ctx *context.Context) string {
- if ctx.GetContentTypeRequested() != context.ContentJSONHeaderValue {
- return ""
- }
-
- var m context.Map
- ctx.RecordRequestBody(true)
- defer ctx.RecordRequestBody(false)
- if err := ctx.ReadJSON(&m); err != nil {
- return ""
- }
-
- if m == nil {
- return ""
- }
-
- v, ok := m[jsonKey]
- if !ok {
- return ""
- }
-
- tok, ok := v.(string)
- if !ok {
- return ""
- }
-
- return tok
- }
-}
-
-// JWT holds the necessary information the middleware need
-// to sign and verify tokens.
-//
-// The `RSA(privateFile, publicFile, password)` package-level helper function
-// can be used to decode the SignKey and VerifyKey.
-//
-// For an easy use look the `HMAC` package-level function
-// and the its `NewUser` and `VerifyUser` methods.
-type JWT struct {
- // MaxAge is the expiration duration of the generated tokens.
- MaxAge time.Duration
-
- // Extractors are used to extract a raw token string value
- // from the request.
- // Builtin extractors:
- // * FromHeader
- // * FromQuery
- // * FromJSON
- // Defaults to a slice of `FromHeader` and `FromQuery`.
- Extractors []TokenExtractor
-
- // Signer is used to sign the token.
- // It is set on `New` and `Default` package-level functions.
- Signer jose.Signer
- // VerificationKey is used to verify the token (public key).
- VerificationKey interface{}
-
- // Encrypter is used to, optionally, encrypt the token.
- // It is set on `WithEncryption` method.
- Encrypter jose.Encrypter
- // DecriptionKey is used to decrypt the token (private key)
- DecriptionKey interface{}
-
- // Blocklist holds the invalidated-by-server tokens (that are not yet expired).
- // It is not initialized by default.
- // Initialization Usage:
- // j.InitDefaultBlocklist()
- // OR
- // j.Blocklist = jwt.NewBlocklist(gcEveryDuration)
- // Usage:
- // - ctx.Logout()
- // - j.Invalidate(ctx)
- Blocklist Blocklist
-}
-
-type privateKey interface{ Public() crypto.PublicKey }
-
-// New returns a new JWT instance.
-// It accepts a maximum time duration for token expiration
-// and the algorithm among with its key for signing and verification.
-//
-// See `WithEncryption` method to add token encryption too.
-// Use `Token` method to generate a new token string
-// and `VerifyToken` method to decrypt, verify and bind claims of an incoming request token.
-// Token, by default, is extracted by "Authorization: Bearer {token}" request header and
-// url query parameter of "token". Token extractors can be modified through the `Extractors` field.
-//
-// For example, if you want to sign and verify using RSA-256 key:
-// 1. Generate key file, e.g:
-// $ openssl genrsa -des3 -out private.pem 2048
-// 2. Read file contents with io.ReadFile("./private.pem")
-// 3. Pass the []byte result to the `ParseRSAPrivateKey(contents, password)` package-level helper
-// 4. Use the result *rsa.PrivateKey as "key" input parameter of this `New` function.
-//
-// See aliases.go file for available algorithms.
-func New(maxAge time.Duration, alg SignatureAlgorithm, key interface{}) (*JWT, error) {
- sig, err := jose.NewSigner(jose.SigningKey{
- Algorithm: alg,
- Key: key,
- }, (&jose.SignerOptions{}).WithType("JWT"))
-
- if err != nil {
- return nil, err
- }
-
- j := &JWT{
- Signer: sig,
- VerificationKey: key,
- MaxAge: maxAge,
- Extractors: []TokenExtractor{FromHeader, FromQuery},
- }
-
- if s, ok := key.(privateKey); ok {
- j.VerificationKey = s.Public()
- }
-
- return j, nil
-}
-
-// Default key filenames for `RSA`.
-const (
- DefaultSignFilename = "jwt_sign.key"
- DefaultEncFilename = "jwt_enc.key"
-)
-
-// RSA returns a new `JWT` instance.
-// It tries to parse RSA256 keys from "filenames[0]" (defaults to "jwt_sign.key") and
-// "filenames[1]" (defaults to "jwt_enc.key") files or generates and exports new random keys.
-//
-// It panics on errors.
-// Use the `New` package-level function instead for more options.
-func RSA(maxAge time.Duration, filenames ...string) *JWT {
- var (
- signFilename = DefaultSignFilename
- encFilename = DefaultEncFilename
- )
-
- switch len(filenames) {
- case 1:
- signFilename = filenames[0]
- case 2:
- encFilename = filenames[1]
- }
-
- // Do not try to create or load enc key if only sign key already exists.
- withEncryption := true
- if fileExists(signFilename) {
- withEncryption = fileExists(encFilename)
- }
-
- sigKey, err := LoadRSA(signFilename, 2048)
- if err != nil {
- panic(err)
- }
-
- j, err := New(maxAge, RS256, sigKey)
- if err != nil {
- panic(err)
- }
-
- if withEncryption {
- encKey, err := LoadRSA(encFilename, 2048)
- if err != nil {
- panic(err)
- }
- err = j.WithEncryption(A128CBCHS256, RSA15, encKey)
- if err != nil {
- panic(err)
- }
- }
-
- return j
-}
-
-const (
- signEnv = "JWT_SECRET"
- encEnv = "JWT_SECRET_ENC"
-)
-
-func getenv(key string, def string) string {
- v := os.Getenv(key)
- if v == "" {
- return def
- }
-
- return v
-}
-
-// HMAC returns a new `JWT` instance.
-// It tries to read hmac256 secret keys from system environment variables:
-// * JWT_SECRET for signing and verification key and
-// * JWT_SECRET_ENC for encryption and decryption key
-// and defaults them to the given "keys" respectfully.
-//
-// It panics on errors.
-// Use the `New` package-level function instead for more options.
-//
-// Example at:
-// https://github.com/kataras/iris/tree/master/_examples/auth/jwt/overview/main.go
-func HMAC(maxAge time.Duration, keys ...string) *JWT {
- var defaultSignSecret, defaultEncSecret string
-
- switch len(keys) {
- case 1:
- defaultSignSecret = keys[0]
- case 2:
- defaultEncSecret = keys[1]
- }
-
- signSecret := getenv(signEnv, defaultSignSecret)
- encSecret := getenv(encEnv, defaultEncSecret)
-
- j, err := New(maxAge, HS256, []byte(signSecret))
- if err != nil {
- panic(err)
- }
-
- if encSecret != "" {
- err = j.WithEncryption(A128GCM, DIRECT, []byte(encSecret))
- if err != nil {
- panic(err)
- }
- }
-
- return j
-}
-
-// WithEncryption method enables encryption and decryption of the token.
-// It sets an appropriate encrypter(`Encrypter` and the `DecriptionKey` fields) based on the key type.
-func (j *JWT) WithEncryption(contentEncryption ContentEncryption, alg KeyAlgorithm, key interface{}) error {
- var publicKey interface{} = key
- if s, ok := key.(privateKey); ok {
- publicKey = s.Public()
- }
-
- enc, err := jose.NewEncrypter(contentEncryption, jose.Recipient{
- Algorithm: alg,
- Key: publicKey,
- },
- (&jose.EncrypterOptions{}).WithType("JWT").WithContentType("JWT"),
- )
-
- if err != nil {
- return err
- }
-
- j.Encrypter = enc
- j.DecriptionKey = key
- return nil
-}
-
-// InitDefaultBlocklist initializes the Blocklist field with the default in-memory implementation.
-// Should be called on jwt middleware creation-time,
-// after this, the developer can use the Context.Logout method
-// to invalidate a verified token by the server-side.
-func (j *JWT) InitDefaultBlocklist() {
- gcEvery := 30 * time.Minute
- if j.MaxAge > 0 {
- gcEvery = j.MaxAge
- }
- j.Blocklist = NewBlocklist(gcEvery)
-}
-
-// ExpiryMap adds the expiration based on the "maxAge" to the "claims" map.
-// It's called automatically on `Token` method.
-func ExpiryMap(maxAge time.Duration, claims context.Map) {
- now := time.Now()
- if claims["exp"] == nil {
- claims["exp"] = NewNumericDate(now.Add(maxAge))
- }
-
- if claims["iat"] == nil {
- claims["iat"] = NewNumericDate(now)
- }
-}
-
-// Token generates and returns a new token string.
-// See `VerifyToken` too.
-func (j *JWT) Token(claims interface{}) (string, error) {
- return j.token(j.MaxAge, claims)
-}
-
-func (j *JWT) token(maxAge time.Duration, claims interface{}) (string, error) {
- if claims == nil {
- return "", ErrInvalidKey
- }
-
- c, nErr := normalize(claims)
- if nErr != nil {
- return "", nErr
- }
-
- ExpiryMap(maxAge, c)
-
- var (
- token string
- err error
- )
- // jwt.Builder and jwt.NestedBuilder contain same methods but they are not the same.
- //
- // Note that the .Claims method there, converts a Struct to a map under the hoods.
- // That means that we will not have any performance cost
- // if we do it by ourselves and pass always a Map there.
- // That gives us the option to allow user to pass ANY go struct
- // and we can add the "exp", "nbf", "iat" map values by ourselves
- // based on the j.MaxAge.
- // (^ done, see normalize, all methods are
- // changed to accept totally custom types, no need to embed the standard Claims anymore).
- if j.DecriptionKey != nil {
- token, err = jwt.SignedAndEncrypted(j.Signer, j.Encrypter).Claims(c).CompactSerialize()
- } else {
- token, err = jwt.Signed(j.Signer).Claims(c).CompactSerialize()
- // payload, pErr := Marshal(c)
- // if pErr != nil {
- // return "", pErr
- // }
- // sign, sErr := j.Signer.Sign(payload)
- // if sErr != nil {
- // return "", sErr
- // }
-
- // token, err = sign.CompactSerialize()
- }
-
- if err != nil {
- return "", err
- }
-
- return token, nil
-}
-
-// WriteToken is a helper which just generates(calls the `Token` method) and writes
-// a new token to the client in plain text format.
-//
-// Use the `Token` method to get a new generated token raw string value.
-func (j *JWT) WriteToken(ctx *context.Context, claims interface{}) error {
- token, err := j.Token(claims)
- if err != nil {
- ctx.StatusCode(500)
- return err
- }
-
- _, err = ctx.WriteString(token)
- return err
-}
-
-// VerifyToken verifies (and decrypts) the request token,
-// it also validates and binds the parsed token's claims to the "claimsPtr" (destination).
-//
-// The last, variadic, input argument is optionally, if provided then the
-// parsed claims must match the expectations;
-// e.g. Audience, Issuer, ID, Subject.
-// See `ExpectXXX` package-functions for details.
-func (j *JWT) VerifyToken(ctx *context.Context, claimsPtr interface{}, expectations ...Expectation) (*TokenInfo, error) {
- token := j.RequestToken(ctx)
- return j.VerifyTokenString(ctx, token, claimsPtr, expectations...)
-}
-
-// VerifyRefreshToken like the `VerifyToken` but it verifies a refresh token one instead.
-// If the implementation does not fill the application's requirements,
-// you can ignore this method and still use the `VerifyToken` for refresh tokens too.
-//
-// This method adds the ExpectRefreshToken expectation and it
-// tries to read the refresh token from raw body or,
-// if content type was application/json, then it extracts the token
-// from the JSON request body's {"refresh_token": "$token"} key.
-func (j *JWT) VerifyRefreshToken(ctx *context.Context, claimsPtr interface{}, expectations ...Expectation) (*TokenInfo, error) {
- token := j.RequestToken(ctx)
- if token == "" {
- ctx.RecordRequestBody(true)
- defer ctx.RecordRequestBody(false)
-
- var tokenPair TokenPair // read "refresh_token" from JSON.
- if ctx.GetContentTypeRequested() == context.ContentJSONHeaderValue {
- ctx.ReadJSON(&tokenPair) // ignore error.
- token = tokenPair.RefreshToken
- if token == "" {
- return nil, ErrMissing
- }
- } else {
- ctx.ReadBody(&token)
- }
- }
-
- return j.VerifyTokenString(ctx, token, claimsPtr, append(expectations, ExpectRefreshToken)...)
-}
-
-// RequestToken extracts the token from the request.
-func (j *JWT) RequestToken(ctx *context.Context) (token string) {
- for _, extract := range j.Extractors {
- if token = extract(ctx); token != "" {
- break // ok we found it.
- }
- }
-
- return
-}
-
-// TokenSetter is an interface which if implemented
-// the extracted, verified, token is stored to the object.
-type TokenSetter interface {
- SetToken(token string)
-}
-
-// TokenInfo holds the standard token information may required
-// for further actions.
-// This structure is mostly useful when the developer's go structure
-// does not hold the standard jwt fields (e.g. "exp")
-// but want access to the parsed token which contains those fields.
-// Inside the middleware, it is used to invalidate tokens through server-side, see `Invalidate`.
-type TokenInfo struct {
- RequestToken string // The request token.
- Claims Claims // The standard JWT parsed fields from the request Token.
- Value interface{} // The pointer to the end-developer's custom claims structure (see `Get`).
-}
-
-const tokenInfoContextKey = "iris.jwt.token"
-
-// Get returns the verified developer token claims.
-//
-//
-// Usage:
-// j := jwt.New(...)
-// app.Use(j.Verify(func() interface{} { return new(CustomClaims) }))
-// app.Post("/restricted", func(ctx iris.Context){
-// claims := jwt.Get(ctx).(*CustomClaims)
-// [use claims...]
-// })
-//
-// Note that there is one exception, if the value was a pointer
-// to a map[string]interface{}, it returns the map itself so it can be
-// accessible directly without the requirement of unwrapping it, e.g.
-// j.Verify(func() interface{} {
-// return &iris.Map{}
-// }
-// [...]
-// claims := jwt.Get(ctx).(iris.Map)
-func Get(ctx *context.Context) interface{} {
- if tok := GetTokenInfo(ctx); tok != nil {
- switch v := tok.Value.(type) {
- case *context.Map:
- return *v
- case *json.RawMessage:
- // This is useful when we can accept more than one
- // type of JWT token in the same request path,
- // but we also want to keep type safety.
- // Usage:
- // type myClaims struct { Roles []string `json:"roles"`}
- // v := jwt.Get(ctx)
- // var claims myClaims
- // jwt.Unmarshal(v, &claims)
- // [...claims.Roles]
- return *v
- default:
- return v
- }
- }
-
- return nil
-}
-
-// GetTokenInfo returns the verified token's information.
-func GetTokenInfo(ctx *context.Context) *TokenInfo {
- if v := ctx.Values().Get(tokenInfoContextKey); v != nil {
- if t, ok := v.(*TokenInfo); ok {
- return t
- }
- }
-
- return nil
-}
-
-// Invalidate invalidates a verified JWT token.
-// It adds the request token, retrieved by Verify methods, to the block list.
-// Next request will be blocked, even if the token was not yet expired.
-// This method can be used when the client-side does not clear the token
-// on a user logout operation.
-//
-// Note: the Blocklist should be initialized before serve-time: j.InitDefaultBlocklist().
-func (j *JWT) Invalidate(ctx *context.Context) {
- if j.Blocklist == nil {
- ctx.Application().Logger().Debug("jwt.Invalidate: Blocklist is nil")
- return
- }
-
- tokenInfo := GetTokenInfo(ctx)
- if tokenInfo == nil {
- return
- }
-
- j.Blocklist.Set(tokenInfo.RequestToken, tokenInfo.Claims.Expiry.Time())
-}
-
-// VerifyTokenString verifies and unmarshals an extracted request token to "dest" destination.
-// The last variadic input indicates any further validations against the verified token claims.
-// If the given "dest" is a valid context.User then ctx.User() will return it.
-// If the token is missing an `ErrMissing` is returned.
-// If the incoming token was expired an `ErrExpired` is returned.
-// If the incoming token was blocked by the server an `ErrBlocked` is returned.
-func (j *JWT) VerifyTokenString(ctx *context.Context, token string, dest interface{}, expectations ...Expectation) (*TokenInfo, error) {
- if token == "" {
- return nil, ErrMissing
- }
-
- var (
- parsedToken *jwt.JSONWebToken
- err error
- )
-
- if j.DecriptionKey != nil {
- t, cerr := jwt.ParseSignedAndEncrypted(token)
- if cerr != nil {
- return nil, cerr
- }
-
- parsedToken, err = t.Decrypt(j.DecriptionKey)
- } else {
- parsedToken, err = jwt.ParseSigned(token)
- }
- if err != nil {
- return nil, err
- }
-
- var (
- claims Claims
- tokenMaxAger tokenWithMaxAge
- )
-
- var (
- ignoreDest = dest == nil
- ignoreVarClaims bool
- )
- if !ignoreDest { // if dest was not nil, check if the dest is already a standard claims pointer.
- _, ignoreVarClaims = dest.(*Claims)
- }
-
- // Ensure read the standard claims one if dest was Claims or was nil.
- // (it wont break anything if we unmarshal them twice though, we just do it for performance reasons).
- var pointers = []interface{}{&tokenMaxAger}
- if !ignoreDest {
- pointers = append(pointers, dest)
- }
- if !ignoreVarClaims {
- pointers = append(pointers, &claims)
- }
- if err = parsedToken.Claims(j.VerificationKey, pointers...); err != nil {
- return nil, err
- }
-
- // Set the std claims, if missing from receiver so the expectations and validation still work.
- if ignoreVarClaims {
- claims = *dest.(*Claims)
- } else if ignoreDest {
- dest = &claims
- }
-
- expectMaxAge := j.MaxAge
-
- // Build the Expected value.
- expected := Expected{}
- for _, e := range expectations {
- if e != nil {
- // expection can be used as a field validation too (see MeetRequirements).
- if err = e(&expected, dest); err != nil {
- if err == ErrExpectRefreshToken {
- if tokenMaxAger.MaxAge > 0 {
- // If max age exists, grab it and compare it later.
- // Otherwise fire the ErrExpectRefreshToken.
- expectMaxAge = tokenMaxAger.MaxAge
- continue
- }
- }
- return nil, err
- }
- }
- }
-
- gotMaxAge := getMaxAge(claims)
- if !compareMaxAge(expectMaxAge, gotMaxAge) {
- // Additional check to automatically invalidate
- // any previous jwt maxAge setting change.
- // In-short, if the time.Now().Add j.MaxAge
- // does not match the "iat" (issued at) then we invalidate the token.
- return nil, ErrInvalidMaxAge
- }
-
- // For other standard JWT claims fields such as "exp"
- // The developer can just add a field of Expiry *NumericDate `json:"exp"`
- // and will be filled by the parsed token automatically.
- // No need for more interfaces.
-
- err = validateClaims(ctx, dest, claims, expected)
- if err != nil {
- if err == ErrExpired {
- // If token was expired remove it from the block list.
- if j.Blocklist != nil {
- j.Blocklist.Del(token)
- }
- }
-
- return nil, err
- }
-
- if j.Blocklist != nil {
- // If token exists in the block list, then stop here.
- if j.Blocklist.Has(token) {
- return nil, ErrBlocked
- }
- }
-
- if !ignoreDest {
- if ut, ok := dest.(TokenSetter); ok {
- // The u.Token is empty even if we set it and export it on JSON structure.
- // Set it manually.
- ut.SetToken(token)
- }
- }
-
- // Set the information.
- tokenInfo := &TokenInfo{
- RequestToken: token,
- Claims: claims,
- Value: dest,
- }
-
- return tokenInfo, nil
-}
-
-// TokenPair holds the access token and refresh token response.
-type TokenPair struct {
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token"`
-}
-
-type tokenWithMaxAge struct {
- // Useful to separate access from refresh tokens.
- // Can be used to by-pass the internal check of expected
- // MaxAge setting to match the token's received max age too.
- MaxAge time.Duration `json:"tokenMaxAge"`
-}
-
-// TokenPair generates a token pair of access and refresh tokens.
-// The first two arguments required for the refresh token
-// and the last one is the claims for the access token one.
-func (j *JWT) TokenPair(refreshMaxAge time.Duration, refreshClaims interface{}, accessClaims interface{}) (TokenPair, error) {
- if refreshMaxAge <= j.MaxAge {
- return TokenPair{}, fmt.Errorf("refresh max age should be bigger than access token's one[%d - %d]", refreshMaxAge, j.MaxAge)
- }
-
- accessToken, err := j.Token(accessClaims)
- if err != nil {
- return TokenPair{}, err
- }
-
- c, err := normalize(refreshClaims)
- if err != nil {
- return TokenPair{}, err
- }
- if c == nil {
- c = make(context.Map)
- }
- // need to validate against its value instead of the setting's one (see `VerifyTokenString`).
- c["tokenMaxAge"] = refreshMaxAge
-
- refreshToken, err := j.token(refreshMaxAge, c)
- if err != nil {
- return TokenPair{}, nil
- }
-
- pair := TokenPair{
- AccessToken: accessToken,
- RefreshToken: refreshToken,
- }
-
- return pair, nil
-}
-
-// Verify returns a middleware which
-// decrypts an incoming request token to the result of the given "newPtr".
-// It does write a 401 unauthorized status code if verification or decryption failed.
-// It calls the `ctx.Next` on verified requests.
-//
-// Iit unmarshals the token to the specific type returned from the given "newPtr" function.
-// It sets the Context User and User's Token too. So the next handler(s)
-// of the same chain can access the User through a `Context.User()` call.
-//
-// Note unlike `VerifyToken`, this method automatically protects
-// the claims with JSON required tags (see `MeetRequirements` Expection).
-//
-// On verified tokens:
-// - The information can be retrieved through `Get` and `GetTokenInfo` functions.
-// - User is set if the newPtr returns a valid Context User
-// - The Context Logout method is set if Blocklist was initialized
-// Any error is captured to the Context,
-// which can be retrieved by a `ctx.GetErr()` call.
-//
-// See `VerifyJSON` too.
-func (j *JWT) Verify(newPtr func() interface{}, expections ...Expectation) context.Handler {
- if newPtr == nil {
- newPtr = func() interface{} {
- // Return a map here as the default type one,
- // as it does allow .Get callers to access its fields with ease
- // (although, I always recommend using structs for type-safety and
- // also they can accept a required tag option too).
- return &context.Map{}
- }
- }
-
- expections = append(expections, MeetRequirements(newPtr()))
-
- return func(ctx *context.Context) {
- ptr := newPtr()
-
- tokenInfo, err := j.VerifyToken(ctx, ptr, expections...)
- if err != nil {
- ctx.Application().Logger().Debugf("iris.jwt.Verify: %v", err)
- ctx.StopWithError(401, context.PrivateError(err))
- return
- }
-
- if u, ok := ptr.(context.User); ok {
- ctx.SetUser(u)
- }
-
- if j.Blocklist != nil {
- ctx.SetLogoutFunc(j.Invalidate)
- }
-
- ctx.Values().Set(tokenInfoContextKey, tokenInfo)
- ctx.Next()
- }
-}
-
-// VerifyMap is a shortcut of Verify with a function which will bind
-// the claims to a standard Go map[string]interface{}.
-func (j *JWT) VerifyMap(expections ...Expectation) context.Handler {
- return j.Verify(func() interface{} {
- return &context.Map{}
- }, expections...)
-}
-
-// VerifyJSON works like `Verify` but instead it
-// binds its "newPtr" function to return a raw JSON message.
-// It does NOT read the token from JSON by itself,
-// to do that add the `FromJSON` to the Token Extractors.
-// It's used to bind the claims in any value type on the next handler.
-//
-// This allows the caller to bind this JSON message to any Go structure (or map).
-// This is useful when we can accept more than one
-// type of JWT token in the same request path,
-// but we also want to keep type safety.
-// Usage:
-// app.Use(jwt.VerifyJSON())
-// Inside a route Handler:
-// claims := struct { Roles []string `json:"roles"`}{}
-// jwt.ReadJSON(ctx, &claims)
-// ...access to claims.Roles as []string
-func (j *JWT) VerifyJSON(expections ...Expectation) context.Handler {
- return j.Verify(func() interface{} {
- return new(json.RawMessage)
- }, expections...)
-}
-
-// ReadJSON is a helper which binds "claimsPtr" to the
-// raw JSON token claims.
-// Use inside the handlers when `VerifyJSON()` middleware was registered.
-func ReadJSON(ctx *context.Context, claimsPtr interface{}) error {
- v := Get(ctx)
- if v == nil {
- return ErrMissing
- }
- data, ok := v.(json.RawMessage)
- if !ok {
- return ErrMissing
- }
- return Unmarshal(data, claimsPtr)
-}
-
-// NewUser returns a new User based on the given "opts".
-// The caller can modify the User until its `GetToken` is called.
-func (j *JWT) NewUser(opts ...UserOption) *User {
- u := &User{
- j: j,
- SimpleUser: &context.SimpleUser{
- Authorization: "IRIS_JWT_USER", // Used to separate a refresh token with a user/access one too.
- Features: []context.UserFeature{
- context.TokenFeature,
- },
- },
- }
-
- for _, opt := range opts {
- opt(u)
- }
-
- return u
-}
-
-// VerifyUser works like the `Verify` method but instead
-// it unmarshals the token to the specific User type.
-// It sets the Context User too. So the next handler(s)
-// of the same chain can access the User through a `Context.User()` call.
-func (j *JWT) VerifyUser(expectations ...Expectation) context.Handler {
- return j.Verify(func() interface{} {
- return new(User)
- }, expectations...)
-}
diff --git a/middleware/jwt/jwt_test.go b/middleware/jwt/jwt_test.go
index 277ce7bb..b65c02b7 100644
--- a/middleware/jwt/jwt_test.go
+++ b/middleware/jwt/jwt_test.go
@@ -1,8 +1,7 @@
-// Package jwt_test contains simple Iris jwt tests. Most of the jwt functionality is already tested inside the jose package itself.
package jwt_test
import (
- "os"
+ "fmt"
"testing"
"time"
@@ -11,324 +10,56 @@ import (
"github.com/kataras/iris/v12/middleware/jwt"
)
-type userClaims struct {
- // Optionally:
- Issuer string `json:"iss"`
- Subject string `json:"sub"`
- Audience jwt.Audience `json:"aud"`
- //
- Username string `json:"username"`
+var testAlg, testSecret = jwt.HS256, []byte("sercrethatmaycontainch@r$")
+
+type fooClaims struct {
+ Foo string `json:"foo"`
}
-const testMaxAge = 7 * time.Second
-
-// Random RSA verification and encryption.
-func TestRSA(t *testing.T) {
- j := jwt.RSA(testMaxAge)
- t.Cleanup(func() {
- os.Remove(jwt.DefaultSignFilename)
- os.Remove(jwt.DefaultEncFilename)
- })
- testWriteVerifyBlockToken(t, j)
-}
-
-// HMAC verification and encryption.
-func TestHMAC(t *testing.T) {
- j := jwt.HMAC(testMaxAge, "secret", "itsa16bytesecret")
- testWriteVerifyBlockToken(t, j)
-}
-
-func TestNew_HMAC(t *testing.T) {
- j, err := jwt.New(testMaxAge, jwt.HS256, []byte("secret"))
- if err != nil {
- t.Fatal(err)
- }
- err = j.WithEncryption(jwt.A128GCM, jwt.DIRECT, []byte("itsa16bytesecret"))
- if err != nil {
- t.Fatal(err)
- }
-
- testWriteVerifyBlockToken(t, j)
-}
-
-// HMAC verification only (unecrypted).
-func TestVerify(t *testing.T) {
- j, err := jwt.New(testMaxAge, jwt.HS256, []byte("another secret"))
- if err != nil {
- t.Fatal(err)
- }
- testWriteVerifyBlockToken(t, j)
-}
-
-func testWriteVerifyBlockToken(t *testing.T, j *jwt.JWT) {
- t.Helper()
-
- j.InitDefaultBlocklist()
- j.Extractors = append(j.Extractors, jwt.FromJSON("access_token"))
-
- customClaims := &userClaims{
- Issuer: "an-issuer",
- Audience: jwt.Audience{"an-audience"},
- Subject: "user",
- Username: "kataras",
- }
-
+// The actual tests are inside the kataras/jwt repository.
+// This runs simple checks of just the middleware part.
+func TestJWT(t *testing.T) {
app := iris.New()
- app.OnErrorCode(iris.StatusUnauthorized, func(ctx iris.Context) {
- if err := ctx.GetErr(); err != nil {
- // Test accessing the private error and set this as the response body.
- ctx.WriteString(err.Error())
- } else { // Else the default behavior
- ctx.WriteString(iris.StatusText(iris.StatusUnauthorized))
- }
- })
- app.Get("/auth", func(ctx iris.Context) {
- j.WriteToken(ctx, customClaims)
- })
-
- app.Post("/protected", func(ctx iris.Context) {
- var claims userClaims
- _, err := j.VerifyToken(ctx, &claims)
- if err != nil {
- // t.Logf("%s: %v", ctx.Path(), err)
- ctx.StopWithError(iris.StatusUnauthorized, iris.PrivateError(err))
- return
- }
-
- ctx.JSON(claims)
- })
-
- m := app.Party("/middleware")
- m.Use(j.Verify(func() interface{} {
- return new(userClaims)
- }))
- m.Post("/protected", func(ctx iris.Context) {
- claims := jwt.Get(ctx)
- ctx.JSON(claims)
- })
- m.Post("/invalidate", func(ctx iris.Context) {
- ctx.Logout() // OR j.Invalidate(ctx)
- })
-
- e := httptest.New(t, app)
-
- // Get token.
- rawToken := e.GET("/auth").Expect().Status(httptest.StatusOK).Body().Raw()
- if rawToken == "" {
- t.Fatalf("empty token")
- }
-
- restrictedPaths := [...]string{"/protected", "/middleware/protected"}
-
- now := time.Now()
- for _, path := range restrictedPaths {
- // Authorization Header.
- e.POST(path).WithHeader("Authorization", "Bearer "+rawToken).Expect().
- Status(httptest.StatusOK).JSON().Equal(customClaims)
-
- // URL Query.
- e.POST(path).WithQuery("token", rawToken).Expect().
- Status(httptest.StatusOK).JSON().Equal(customClaims)
-
- // JSON Body.
- e.POST(path).WithJSON(iris.Map{"access_token": rawToken}).Expect().
- Status(httptest.StatusOK).JSON().Equal(customClaims)
-
- // Missing "Bearer".
- e.POST(path).WithHeader("Authorization", rawToken).Expect().
- Status(httptest.StatusUnauthorized).Body().Equal("token is missing")
- }
-
- // Invalidate the token.
- e.POST("/middleware/invalidate").WithQuery("token", rawToken).Expect().
- Status(httptest.StatusOK)
- // Token is blocked by server.
- e.POST("/middleware/protected").WithQuery("token", rawToken).Expect().
- Status(httptest.StatusUnauthorized).Body().Equal("token is blocked")
-
- expireRemDur := testMaxAge - time.Since(now)
-
- // Expiration.
- time.Sleep(expireRemDur /* -end */)
- for _, path := range restrictedPaths {
- e.POST(path).WithQuery("token", rawToken).Expect().
- Status(httptest.StatusUnauthorized).Body().Equal("token is expired (exp)")
- }
-}
-
-func TestVerifyMap(t *testing.T) {
- j := jwt.HMAC(testMaxAge, "secret", "itsa16bytesecret")
- expectedClaims := iris.Map{
- "iss": "tester",
- "username": "makis",
- "roles": []string{"admin"},
- }
-
- app := iris.New()
- app.Get("/user/auth", func(ctx iris.Context) {
- err := j.WriteToken(ctx, expectedClaims)
- if err != nil {
- ctx.StopWithError(iris.StatusUnauthorized, err)
- return
- }
-
- if expectedClaims["exp"] == nil || expectedClaims["iat"] == nil {
- ctx.StopWithText(iris.StatusBadRequest,
- "exp or/and iat is nil - this means that the expiry was not set")
- return
- }
- })
-
- userAPI := app.Party("/user")
- userAPI.Post("/", func(ctx iris.Context) {
- var claims iris.Map
- if _, err := j.VerifyToken(ctx, &claims); err != nil {
- ctx.StopWithError(iris.StatusUnauthorized, iris.PrivateError(err))
- return
- }
-
- ctx.JSON(claims)
- })
-
- // Test map + Verify middleware.
- userAPI.Post("/middleware", j.Verify(nil), func(ctx iris.Context) {
- claims := jwt.Get(ctx)
- ctx.JSON(claims)
- })
-
- e := httptest.New(t, app, httptest.LogLevel("error"))
- token := e.GET("/user/auth").Expect().Status(httptest.StatusOK).Body().Raw()
- if token == "" {
- t.Fatalf("empty token")
- }
-
- e.POST("/user").WithHeader("Authorization", "Bearer "+token).Expect().
- Status(httptest.StatusOK).JSON().Equal(expectedClaims)
-
- e.POST("/user/middleware").WithHeader("Authorization", "Bearer "+token).Expect().
- Status(httptest.StatusOK).JSON().Equal(expectedClaims)
-
- e.POST("/user").Expect().Status(httptest.StatusUnauthorized)
-}
-
-type customClaims struct {
- Username string `json:"username"`
- Token string `json:"token"`
-}
-
-func (c *customClaims) SetToken(tok string) {
- c.Token = tok
-}
-
-func TestVerifyStruct(t *testing.T) {
- maxAge := testMaxAge / 2
- j := jwt.HMAC(maxAge, "secret", "itsa16bytesecret")
-
- app := iris.New()
- app.Get("/user/auth", func(ctx iris.Context) {
- err := j.WriteToken(ctx, customClaims{
- Username: "makis",
- })
- if err != nil {
- ctx.StopWithError(iris.StatusUnauthorized, err)
- return
- }
- })
-
- userAPI := app.Party("/user")
- userAPI.Post("/", func(ctx iris.Context) {
- var claims customClaims
- if _, err := j.VerifyToken(ctx, &claims); err != nil {
- ctx.StopWithError(iris.StatusUnauthorized, iris.PrivateError(err))
- return
- }
-
- ctx.JSON(claims)
- })
-
- e := httptest.New(t, app)
- token := e.GET("/user/auth").Expect().Status(httptest.StatusOK).Body().Raw()
- if token == "" {
- t.Fatalf("empty token")
- }
- e.POST("/user").WithHeader("Authorization", "Bearer "+token).Expect().
- Status(httptest.StatusOK).JSON().Object().ContainsMap(iris.Map{
- "username": "makis",
- "token": token, // Test SetToken.
- })
-
- e.POST("/user").Expect().Status(httptest.StatusUnauthorized)
- time.Sleep(maxAge)
- e.POST("/user").WithHeader("Authorization", "Bearer "+token).Expect().Status(httptest.StatusUnauthorized)
-}
-
-func TestVerifyJSON(t *testing.T) {
- j := jwt.HMAC(testMaxAge, "secret", "itsa16bytesecret")
-
- app := iris.New()
- app.Get("/user/auth", func(ctx iris.Context) {
- err := j.WriteToken(ctx, iris.Map{"roles": []string{"admin"}})
- if err != nil {
- ctx.StopWithError(iris.StatusUnauthorized, err)
- return
- }
- })
-
- app.Post("/", j.VerifyJSON(), func(ctx iris.Context) {
- claims := struct {
- Roles []string `json:"roles"`
- }{}
- jwt.ReadJSON(ctx, &claims)
- ctx.JSON(claims)
- })
-
- e := httptest.New(t, app, httptest.LogLevel("error"))
- token := e.GET("/user/auth").Expect().Status(httptest.StatusOK).Body().Raw()
- if token == "" {
- t.Fatalf("empty token")
- }
-
- e.POST("/").WithHeader("Authorization", "Bearer "+token).Expect().
- Status(httptest.StatusOK).JSON().Equal(iris.Map{"roles": []string{"admin"}})
-
- e.POST("/").Expect().Status(httptest.StatusUnauthorized)
-}
-
-func TestVerifyUserAndExpected(t *testing.T) { // Tests the jwt.User struct + context validator + expected.
- maxAge := testMaxAge / 2
- j := jwt.HMAC(maxAge, "secret", "itsa16bytesecret")
- expectedUser := j.NewUser(jwt.Username("makis"), jwt.Roles("admin"), jwt.Fields(iris.Map{
- "custom": true,
- })) // only for the sake of the test, we iniitalize it here.
- expectedUser.Issuer = "tester"
-
- app := iris.New()
- app.Get("/user/auth", func(ctx iris.Context) {
- tok, err := expectedUser.GetToken()
+ signer := jwt.NewSigner(testAlg, testSecret, 3*time.Second)
+ app.Get("/", func(ctx iris.Context) {
+ claims := fooClaims{Foo: "bar"}
+ token, err := signer.Sign(claims)
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
- ctx.WriteString(tok)
+ ctx.Write(token)
})
- userAPI := app.Party("/user")
- userAPI.Use(jwt.WithExpected(jwt.Expected{Issuer: "tester"}, j.VerifyUser()))
- userAPI.Post("/", func(ctx iris.Context) {
- user := ctx.User()
- ctx.JSON(user)
+ verifier := jwt.NewVerifier(testAlg, testSecret)
+ verifier.ErrorHandler = func(ctx iris.Context, err error) { // app.OnErrorCode(401, ...)
+ ctx.StopWithError(iris.StatusUnauthorized, err)
+ }
+ middleware := verifier.Verify(func() interface{} { return new(fooClaims) })
+ app.Get("/protected", middleware, func(ctx iris.Context) {
+ claims := jwt.Get(ctx).(*fooClaims)
+ ctx.WriteString(claims.Foo)
})
e := httptest.New(t, app)
- token := e.GET("/user/auth").Expect().Status(httptest.StatusOK).Body().Raw()
- if token == "" {
- t.Fatalf("empty token")
- }
- e.POST("/user").WithHeader("Authorization", "Bearer "+token).Expect().
- Status(httptest.StatusOK).JSON().Equal(expectedUser)
+ // Get generated token.
+ token := e.GET("/").Expect().Status(iris.StatusOK).Body().Raw()
+ // Test Header.
+ headerValue := fmt.Sprintf("Bearer %s", token)
+ e.GET("/protected").WithHeader("Authorization", headerValue).Expect().
+ Status(iris.StatusOK).Body().Equal("bar")
+ // Test URL query.
+ e.GET("/protected").WithQuery("token", token).Expect().
+ Status(iris.StatusOK).Body().Equal("bar")
- // Test generic client message if we don't manage the private error by ourselves.
- e.POST("/user").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
+ // Test unauthorized.
+ e.GET("/protected").Expect().Status(iris.StatusUnauthorized)
+ e.GET("/protected").WithHeader("Authorization", "missing bearer").Expect().Status(iris.StatusUnauthorized)
+ e.GET("/protected").WithQuery("token", "invalid_token").Expect().Status(iris.StatusUnauthorized)
+ // Test expired (note checks happen based on second round).
+ time.Sleep(5 * time.Second)
+ e.GET("/protected").WithHeader("Authorization", headerValue).Expect().
+ Status(iris.StatusUnauthorized).Body().Equal("token expired")
}
diff --git a/middleware/jwt/rsa_util.go b/middleware/jwt/rsa_util.go
deleted file mode 100644
index f40f69ab..00000000
--- a/middleware/jwt/rsa_util.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package jwt
-
-import (
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "encoding/pem"
- "errors"
- "io/ioutil"
- "os"
-)
-
-// LoadRSA tries to read RSA Private Key from "fname" system file,
-// if does not exist then it generates a new random one based on "bits" (e.g. 2048, 4096)
-// and exports it to a new "fname" file.
-func LoadRSA(fname string, bits int) (key *rsa.PrivateKey, err error) {
- exists := fileExists(fname)
- if exists {
- key, err = importFromFile(fname)
- } else {
- key, err = rsa.GenerateKey(rand.Reader, bits)
- }
-
- if err != nil {
- return
- }
-
- if !exists {
- err = exportToFile(key, fname)
- }
-
- return
-}
-
-func exportToFile(key *rsa.PrivateKey, filename string) error {
- b := x509.MarshalPKCS1PrivateKey(key)
- encoded := pem.EncodeToMemory(
- &pem.Block{
- Type: "RSA PRIVATE KEY",
- Bytes: b,
- },
- )
-
- return ioutil.WriteFile(filename, encoded, 0600)
-}
-
-func importFromFile(filename string) (*rsa.PrivateKey, error) {
- b, err := ioutil.ReadFile(filename)
- if err != nil {
- return nil, err
- }
-
- return ParseRSAPrivateKey(b, nil)
-}
-
-func fileExists(filename string) bool {
- info, err := os.Stat(filename)
- if os.IsNotExist(err) {
- return false
- }
- return !info.IsDir()
-}
-
-var (
- // ErrNotPEM is an error type of the `ParseXXX` function(s) fired
- // when the data are not PEM-encoded.
- ErrNotPEM = errors.New("key must be PEM encoded")
- // ErrInvalidKey is an error type of the `ParseXXX` function(s) fired
- // when the contents are not type of rsa private key.
- ErrInvalidKey = errors.New("key is not of type *rsa.PrivateKey")
-)
-
-// ParseRSAPrivateKey encodes a PEM-encoded PKCS1 or PKCS8 private key protected with a password.
-func ParseRSAPrivateKey(key, password []byte) (*rsa.PrivateKey, error) {
- block, _ := pem.Decode(key)
- if block == nil {
- return nil, ErrNotPEM
- }
-
- var (
- parsedKey interface{}
- err error
- )
-
- var blockDecrypted []byte
- if len(password) > 0 {
- if blockDecrypted, err = x509.DecryptPEMBlock(block, password); err != nil {
- return nil, err
- }
- } else {
- blockDecrypted = block.Bytes
- }
-
- if parsedKey, err = x509.ParsePKCS1PrivateKey(blockDecrypted); err != nil {
- if parsedKey, err = x509.ParsePKCS8PrivateKey(blockDecrypted); err != nil {
- return nil, err
- }
- }
-
- privateKey, ok := parsedKey.(*rsa.PrivateKey)
- if !ok {
- return nil, ErrInvalidKey
- }
-
- return privateKey, nil
-}
diff --git a/middleware/jwt/signer.go b/middleware/jwt/signer.go
new file mode 100644
index 00000000..c2243d0a
--- /dev/null
+++ b/middleware/jwt/signer.go
@@ -0,0 +1,59 @@
+package jwt
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/kataras/jwt"
+)
+
+type Signer struct {
+ Alg Alg
+ Key interface{}
+
+ MaxAge time.Duration
+
+ Encrypt func([]byte) ([]byte, error)
+}
+
+func NewSigner(signatureAlg Alg, signatureKey interface{}, maxAge time.Duration) *Signer {
+ return &Signer{
+ Alg: signatureAlg,
+ Key: signatureKey,
+ MaxAge: maxAge,
+ }
+}
+
+// WithGCM enables AES-GCM payload decryption.
+func (s *Signer) WithGCM(key, additionalData []byte) *Signer {
+ encrypt, _, err := jwt.GCM(key, additionalData)
+ if err != nil {
+ panic(err) // important error before serve, stop everything.
+ }
+
+ s.Encrypt = encrypt
+ return s
+}
+
+func (s *Signer) Sign(claims interface{}, opts ...SignOption) ([]byte, error) {
+ return SignEncrypted(s.Alg, s.Key, s.Encrypt, claims, append([]SignOption{MaxAge(s.MaxAge)}, opts...)...)
+}
+
+func (s *Signer) NewTokenPair(accessClaims interface{}, refreshClaims interface{}, refreshMaxAge time.Duration, accessOpts ...SignOption) (TokenPair, error) {
+ if refreshMaxAge <= s.MaxAge {
+ return TokenPair{}, fmt.Errorf("refresh max age should be bigger than access token's one[%d - %d]", refreshMaxAge, s.MaxAge)
+ }
+
+ accessToken, err := s.Sign(accessClaims, accessOpts...)
+ if err != nil {
+ return TokenPair{}, err
+ }
+
+ refreshToken, err := Sign(s.Alg, s.Key, refreshClaims, MaxAge(refreshMaxAge))
+ if err != nil {
+ return TokenPair{}, err
+ }
+
+ tokenPair := jwt.NewTokenPair(accessToken, refreshToken)
+ return tokenPair, nil
+}
diff --git a/middleware/jwt/user.go b/middleware/jwt/user.go
deleted file mode 100644
index 3d8af27f..00000000
--- a/middleware/jwt/user.go
+++ /dev/null
@@ -1,187 +0,0 @@
-package jwt
-
-import (
- "time"
-
- "github.com/kataras/iris/v12/context"
-)
-
-// User a common User structure for JWT.
-// However, we're not limited to that one;
-// any Go structure can be generated as a JWT token.
-//
-// Look `NewUser` and `VerifyUser` JWT middleware's methods.
-// Use its `GetToken` method to generate the token when
-// the User structure is set.
-type User struct {
- Claims
- // Note: we could use a map too as the Token is generated when GetToken is called.
- *context.SimpleUser
-
- j *JWT
-}
-
-var (
- _ context.FeaturedUser = (*User)(nil)
- _ TokenSetter = (*User)(nil)
- _ ContextValidator = (*User)(nil)
-)
-
-// UserOption sets optional fields for a new User
-// See `NewUser` instance function.
-type UserOption func(*User)
-
-// Username sets the Username and the JWT Claim's Subject
-// to the given "username".
-func Username(username string) UserOption {
- return func(u *User) {
- u.Username = username
- u.Claims.Subject = username
- u.Features = append(u.Features, context.UsernameFeature)
- }
-}
-
-// Email sets the Email field for the User field.
-func Email(email string) UserOption {
- return func(u *User) {
- u.Email = email
- u.Features = append(u.Features, context.EmailFeature)
- }
-}
-
-// Roles upserts to the User's Roles field.
-func Roles(roles ...string) UserOption {
- return func(u *User) {
- u.Roles = roles
- u.Features = append(u.Features, context.RolesFeature)
- }
-}
-
-// MaxAge sets claims expiration and the AuthorizedAt User field.
-func MaxAge(maxAge time.Duration) UserOption {
- return func(u *User) {
- now := time.Now()
- u.Claims.Expiry = NewNumericDate(now.Add(maxAge))
- u.Claims.IssuedAt = NewNumericDate(now)
- u.AuthorizedAt = now
-
- u.Features = append(u.Features, context.AuthorizedAtFeature)
- }
-}
-
-// Fields copies the "fields" to the user's Fields field.
-// This can be used to set custom fields to the User instance.
-func Fields(fields context.Map) UserOption {
- return func(u *User) {
- if len(fields) == 0 {
- return
- }
-
- if u.Fields == nil {
- u.Fields = make(context.Map, len(fields))
- }
-
- for k, v := range fields {
- u.Fields[k] = v
- }
-
- u.Features = append(u.Features, context.FieldsFeature)
- }
-}
-
-// SetToken is called automaticaly on VerifyUser/VerifyObject.
-// It sets the extracted from request, and verified from server raw token.
-func (u *User) SetToken(token string) {
- u.Token = token
-}
-
-// GetToken overrides the SimpleUser's Token
-// and returns the jwt generated token, among with
-// a generator error, if any.
-func (u *User) GetToken() (string, error) {
- if u.Token != "" {
- return u.Token, nil
- }
-
- if u.j != nil { // it's always not nil.
- if u.j.MaxAge > 0 {
- // if the MaxAge option was not manually set, resolve it from the JWT instance.
- MaxAge(u.j.MaxAge)(u)
- }
-
- // we could generate a token here
- // but let's do it on GetToken
- // as the user fields may change
- // by the caller manually until the token
- // sent to the client.
- tok, err := u.j.Token(u)
- if err != nil {
- return "", err
- }
-
- u.Token = tok
- }
-
- if u.Token == "" {
- return "", ErrMissing
- }
-
- return u.Token, nil
-}
-
-// Validate validates the current user's claims against
-// the request. It's called automatically by the JWT instance.
-func (u *User) Validate(ctx *context.Context, claims Claims, e Expected) error {
- err := u.Claims.ValidateWithLeeway(e, 0)
- if err != nil {
- return err
- }
-
- if u.SimpleUser.Authorization != "IRIS_JWT_USER" {
- return ErrInvalidKey
- }
-
- // We could add specific User Expectations (new struct and accept an interface{}),
- // but for the sake of code simplicity we don't, unless is requested, as the caller
- // can validate specific fields by its own at the next step.
- return nil
-}
-
-// UnmarshalJSON implements the json unmarshaler interface.
-func (u *User) UnmarshalJSON(data []byte) error {
- err := Unmarshal(data, &u.Claims)
- if err != nil {
- return err
- }
- simpleUser := new(context.SimpleUser)
- err = Unmarshal(data, simpleUser)
- if err != nil {
- return err
- }
- u.SimpleUser = simpleUser
- return nil
-}
-
-// MarshalJSON implements the json marshaler interface.
-func (u *User) MarshalJSON() ([]byte, error) {
- claimsB, err := Marshal(u.Claims)
- if err != nil {
- return nil, err
- }
-
- userB, err := Marshal(u.SimpleUser)
- if err != nil {
- return nil, err
- }
-
- if len(userB) == 0 {
- return claimsB, nil
- }
-
- claimsB = claimsB[0 : len(claimsB)-1] // remove last '}'
- userB = userB[1:] // remove first '{'
-
- raw := append(claimsB, ',')
- raw = append(raw, userB...)
- return raw, nil
-}
diff --git a/middleware/jwt/validation.go b/middleware/jwt/validation.go
deleted file mode 100644
index 9b5b6964..00000000
--- a/middleware/jwt/validation.go
+++ /dev/null
@@ -1,258 +0,0 @@
-package jwt
-
-import (
- "bytes"
- "errors"
- "reflect"
- "strings"
- "time"
-
- "github.com/kataras/iris/v12/context"
-
- "github.com/square/go-jose/v3/json"
- // Use this package instead of the standard encoding/json
- // to marshal the NumericDate as expected by the implementation (see 'normalize`).
- "github.com/square/go-jose/v3/jwt"
-)
-
-const (
- claimsExpectedContextKey = "iris.jwt.claims.expected"
- needsValidationContextKey = "iris.jwt.claims.unvalidated"
-)
-
-var (
- // ErrMissing when token cannot be extracted from the request (custm error).
- ErrMissing = errors.New("token is missing")
- // ErrMissingKey when token does not contain a required JSON field (custom error).
- ErrMissingKey = errors.New("token is missing a required field")
- // ErrExpired indicates that token is used after expiry time indicated in exp claim.
- ErrExpired = errors.New("token is expired (exp)")
- // ErrNotValidYet indicates that token is used before time indicated in nbf claim.
- ErrNotValidYet = errors.New("token not valid yet (nbf)")
- // ErrIssuedInTheFuture indicates that the iat field is in the future.
- ErrIssuedInTheFuture = errors.New("token issued in the future (iat)")
- // ErrBlocked indicates that the token was not yet expired
- // but was blocked by the server's Blocklist (custom error).
- ErrBlocked = errors.New("token is blocked")
- // ErrInvalidMaxAge indicates that the token is using a different
- // max age than the configurated one ( custom error).
- ErrInvalidMaxAge = errors.New("token contains invalid max age")
- // ErrExpectRefreshToken indicates that the retrieved token
- // was not a refresh token one when `ExpectRefreshToken` is set (custome rror).
- ErrExpectRefreshToken = errors.New("expect refresh token")
-)
-
-// Expectation option to provide
-// an extra layer of token validation, a claims type protection.
-// See `VerifyToken` method.
-type Expectation func(e *Expected, claims interface{}) error
-
-// Expect protects the claims with the expected values.
-func Expect(expected Expected) Expectation {
- return func(e *Expected, _ interface{}) error {
- *e = expected
- return nil
- }
-}
-
-// ExpectID protects the claims with an ID validation.
-func ExpectID(id string) Expectation {
- return func(e *Expected, _ interface{}) error {
- e.ID = id
- return nil
- }
-}
-
-// ExpectIssuer protects the claims with an issuer validation.
-func ExpectIssuer(issuer string) Expectation {
- return func(e *Expected, _ interface{}) error {
- e.Issuer = issuer
- return nil
- }
-}
-
-// ExpectSubject protects the claims with a subject validation.
-func ExpectSubject(sub string) Expectation {
- return func(e *Expected, _ interface{}) error {
- e.Subject = sub
- return nil
- }
-}
-
-// ExpectAudience protects the claims with an audience validation.
-func ExpectAudience(audience ...string) Expectation {
- return func(e *Expected, _ interface{}) error {
- e.Audience = audience
- return nil
- }
-}
-
-// ExpectRefreshToken SHOULD be passed when a token should be verified
-// based on the expiration set by `TokenPair` method instead of the JWT instance's MaxAge setting.
-// Useful to validate Refresh Tokens and invalidate Access ones when refresh API is fired,
-// if that option is missing then refresh tokens are invalidated when an access token was expected.
-//
-// Usage:
-// var refreshClaims jwt.Claims
-// _, err := j.VerifyTokenString(ctx, tokenPair.RefreshToken, &refreshClaims, jwt.ExpectRefreshToken)
-func ExpectRefreshToken(e *Expected, _ interface{}) error { return ErrExpectRefreshToken }
-
-// MeetRequirements protects the custom fields of JWT claims
-// based on the json:required tag; `json:"name,required"`.
-// It accepts the value type.
-//
-// Usage:
-// Verify/VerifyToken(... MeetRequirements(MyUser{}))
-func MeetRequirements(claimsType interface{}) Expectation {
- // pre-calculate if we need to use reflection at serve time to check for required fields,
- // this can work as an alternative of expections for custom non-standard JWT fields.
- requireFieldsIndexes := getRequiredFieldIndexes(claimsType)
-
- return func(e *Expected, claims interface{}) error {
- if len(requireFieldsIndexes) > 0 {
- val := reflect.Indirect(reflect.ValueOf(claims))
- for _, idx := range requireFieldsIndexes {
- field := val.Field(idx)
- if field.IsZero() {
- return ErrMissingKey
- }
- }
- }
-
- return nil
- }
-}
-
-// WithExpected is a middleware wrapper. It wraps a VerifyXXX middleware
-// with expected claims fields protection.
-// Usage:
-// jwt.WithExpected(jwt.Expected{Issuer:"app"}, j.VerifyUser)
-func WithExpected(e Expected, verifyHandler context.Handler) context.Handler {
- return func(ctx *context.Context) {
- ctx.Values().Set(claimsExpectedContextKey, e)
- verifyHandler(ctx)
- }
-}
-
-// ContextValidator validates the object based on the given
-// claims and the expected once. The end-developer
-// can use this method for advanced validations based on the request Context.
-type ContextValidator interface {
- Validate(ctx *context.Context, claims Claims, e Expected) error
-}
-
-func validateClaims(ctx *context.Context, dest interface{}, claims Claims, expected Expected) (err error) {
- // Get any dynamic expectation set by prior middleware.
- // See `WithExpected` middleware.
- if v := ctx.Values().Get(claimsExpectedContextKey); v != nil {
- if e, ok := v.(Expected); ok {
- expected = e
- }
- }
- // Force-set the time, it's important for expiration.
- expected.Time = time.Now()
- switch c := dest.(type) {
- case Claims:
- err = c.ValidateWithLeeway(expected, 0)
- case ContextValidator:
- err = c.Validate(ctx, claims, expected)
- case *context.Map:
- // if the dest is a map then set automatically the expiration settings here,
- // so the caller can work further with it.
- err = claims.ValidateWithLeeway(expected, 0)
- if err == nil {
- (*c)["exp"] = claims.Expiry
- (*c)["iat"] = claims.IssuedAt
- if claims.NotBefore != nil {
- (*c)["nbf"] = claims.NotBefore
- }
- }
- default:
- err = claims.ValidateWithLeeway(expected, 0)
- }
-
- if err != nil {
- switch err {
- case jwt.ErrExpired:
- return ErrExpired
- case jwt.ErrNotValidYet:
- return ErrNotValidYet
- case jwt.ErrIssuedInTheFuture:
- return ErrIssuedInTheFuture
- }
- }
-
- return err
-}
-
-func normalize(i interface{}) (context.Map, error) {
- if m, ok := i.(context.Map); ok {
- return m, nil
- }
-
- m := make(context.Map)
-
- raw, err := json.Marshal(i)
- if err != nil {
- return nil, err
- }
-
- d := json.NewDecoder(bytes.NewReader(raw))
- d.UseNumber()
-
- if err := d.Decode(&m); err != nil {
- return nil, err
- }
-
- return m, nil
-}
-
-func getRequiredFieldIndexes(i interface{}) (v []int) {
- val := reflect.Indirect(reflect.ValueOf(i))
- typ := val.Type()
- if typ.Kind() != reflect.Struct {
- return nil
- }
-
- for i := 0; i < val.NumField(); i++ {
- field := typ.Field(i)
- // Note: for the sake of simplicity we don't lookup for nested objects (FieldByIndex),
- // we could do that as we do in dependency injection feature but unless requirested we don't.
- tag := field.Tag.Get("json")
- if strings.Contains(tag, ",required") {
- v = append(v, i)
- }
- }
-
- return
-}
-
-// getMaxAge returns the result of expiry-issued at.
-// Note that if in JWT MaxAge's was set to a value like: 3.5 seconds
-// this will return 3 on token retreival. Of course this is not a problem
-// in real world apps as they don't invalidate tokens in seconds
-// based on a division result like 2/7.
-func getMaxAge(claims Claims) time.Duration {
- if issuedAt := claims.IssuedAt.Time(); !issuedAt.IsZero() {
- gotMaxAge := claims.Expiry.Time().Sub(issuedAt)
- return gotMaxAge
- }
-
- return 0
-}
-
-func compareMaxAge(expected, got time.Duration) bool {
- if expected == got {
- return true
- }
-
- // got is int64, maybe rounded, but the max age setting is precise, may be a float result
- // e.g. the result of a division 2/7=3.5,
- // try to validate by round of second so similar/or equal max age setting are considered valid.
- min, max := expected-time.Second, expected+time.Second
- if got < min || got > max {
- return false
- }
-
- return true
-}
diff --git a/middleware/jwt/verifier.go b/middleware/jwt/verifier.go
new file mode 100644
index 00000000..7438c921
--- /dev/null
+++ b/middleware/jwt/verifier.go
@@ -0,0 +1,210 @@
+package jwt
+
+import (
+ "reflect"
+ "time"
+
+ "github.com/kataras/iris/v12/context"
+
+ "github.com/kataras/jwt"
+)
+
+const (
+ claimsContextKey = "iris.jwt.claims"
+ verifiedTokenContextKey = "iris.jwt.token"
+)
+
+// Get returns the claims decoded by a verifier.
+func Get(ctx *context.Context) interface{} {
+ if v := ctx.Values().Get(claimsContextKey); v != nil {
+ return v
+ }
+
+ return nil
+}
+
+// GetVerifiedToken returns the verified token structure
+// which holds information about the decoded token
+// and its standard claims.
+func GetVerifiedToken(ctx *context.Context) *VerifiedToken {
+ if v := ctx.Values().Get(verifiedTokenContextKey); v != nil {
+ if tok, ok := v.(*VerifiedToken); ok {
+ return tok
+ }
+ }
+
+ return nil
+}
+
+// Verifier holds common options to verify an incoming token.
+// Its Verify method can be used as a middleware to allow authorized clients to access an API.
+type Verifier struct {
+ Alg Alg
+ Key interface{}
+
+ Decrypt func([]byte) ([]byte, error)
+
+ Extractors []TokenExtractor
+ Blocklist Blocklist
+ Validators []TokenValidator
+
+ ErrorHandler func(ctx *context.Context, err error)
+}
+
+// NewVerifier accepts the algorithm for the token's signature among with its (private) key
+// and optionally some token validators for all verify middlewares that may initialized under this Verifier.
+//
+// See its Verify method.
+func NewVerifier(signatureAlg Alg, signatureKey interface{}, validators ...TokenValidator) *Verifier {
+ return &Verifier{
+ Alg: signatureAlg,
+ Key: signatureKey,
+ Extractors: []TokenExtractor{FromHeader, FromQuery},
+ ErrorHandler: func(ctx *context.Context, err error) {
+ ctx.StopWithError(401, context.PrivateError(err))
+ },
+ Validators: validators,
+ }
+}
+
+// WithGCM enables AES-GCM payload encryption.
+func (v *Verifier) WithGCM(key, additionalData []byte) *Verifier {
+ _, decrypt, err := jwt.GCM(key, additionalData)
+ if err != nil {
+ panic(err) // important error before serve, stop everything.
+ }
+
+ v.Decrypt = decrypt
+ return v
+}
+
+// WithDefaultBlocklist attaches an in-memory blocklist storage
+// to invalidate tokens through server-side.
+// To invalidate a token simply call the Context.Logout method.
+func (v *Verifier) WithDefaultBlocklist() *Verifier {
+ v.Blocklist = jwt.NewBlocklist(30 * time.Minute)
+ return v
+}
+
+func (v *Verifier) invalidate(ctx *context.Context) {
+ if verifiedToken := GetVerifiedToken(ctx); verifiedToken != nil {
+ v.Blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims.Expiry)
+ ctx.Values().Remove(claimsContextKey)
+ ctx.Values().Remove(verifiedTokenContextKey)
+ ctx.SetUser(nil)
+ ctx.SetLogoutFunc(nil)
+ }
+}
+
+// RequestToken extracts the token from the request.
+func (v *Verifier) RequestToken(ctx *context.Context) (token string) {
+ for _, extract := range v.Extractors {
+ if token = extract(ctx); token != "" {
+ break // ok we found it.
+ }
+ }
+
+ return
+}
+
+type (
+ // ClaimsValidator is a special interface which, if the destination claims
+ // implements it then the verifier runs its Validate method before return.
+ ClaimsValidator interface {
+ Validate() error
+ }
+
+ // ClaimsContextValidator same as ClaimsValidator but it accepts
+ // a request context which can be used for further checks before
+ // validating the incoming token's claims.
+ ClaimsContextValidator interface {
+ Validate(*context.Context) error
+ }
+)
+
+// VerifyToken simply verifies the given "token" and validates its standard claims (such as expiration).
+// Returns a structure which holds the token's information. See the Verify method instead.
+func (v *Verifier) VerifyToken(token []byte, validators ...TokenValidator) (*VerifiedToken, error) {
+ return jwt.VerifyEncrypted(v.Alg, v.Key, v.Decrypt, token, validators...)
+}
+
+// Verify is the most important piece of code inside the Verifier.
+// It accepts the "claimsType" function which should return a pointer to a custom structure
+// which the token's decode claims valuee will be binded and validated to.
+// Returns a common Iris handler which can be used as a middleware to protect an API
+// from unauthorized client requests. After this, the route handlers can access the claims
+// through the jwt.Get package-level function.
+//
+// Example Code:
+func (v *Verifier) Verify(claimsType func() interface{}, validators ...TokenValidator) context.Handler {
+ unmarshal := jwt.Unmarshal
+ if claimsType != nil {
+ c := claimsType()
+ if hasRequired(c) {
+ unmarshal = jwt.UnmarshalWithRequired
+ }
+ }
+
+ if v.Blocklist != nil {
+ validators = append([]TokenValidator{v.Blocklist}, append(v.Validators, validators...)...)
+ }
+
+ return func(ctx *context.Context) {
+ token := []byte(v.RequestToken(ctx))
+ verifiedToken, err := v.VerifyToken(token, validators...)
+ if err != nil {
+ v.ErrorHandler(ctx, err)
+ return
+ }
+
+ if claimsType != nil {
+ dest := claimsType()
+ if err = unmarshal(verifiedToken.Payload, dest); err != nil {
+ v.ErrorHandler(ctx, err)
+ return
+ }
+
+ if validator, ok := dest.(ClaimsValidator); ok {
+ if err = validator.Validate(); err != nil {
+ v.ErrorHandler(ctx, err)
+ return
+ }
+ } else if contextValidator, ok := dest.(ClaimsContextValidator); ok {
+ if err = contextValidator.Validate(ctx); err != nil {
+ v.ErrorHandler(ctx, err)
+ return
+ }
+ }
+
+ if u, ok := dest.(context.User); ok {
+ ctx.SetUser(u)
+ }
+
+ ctx.Values().Set(claimsContextKey, dest)
+ }
+
+ if v.Blocklist != nil {
+ ctx.SetLogoutFunc(v.invalidate)
+ }
+
+ ctx.Values().Set(verifiedTokenContextKey, verifiedToken)
+ ctx.Next()
+ }
+}
+
+func hasRequired(i interface{}) bool {
+ val := reflect.Indirect(reflect.ValueOf(i))
+ typ := val.Type()
+ if typ.Kind() != reflect.Struct {
+ return false
+ }
+
+ for i := 0; i < val.NumField(); i++ {
+ field := typ.Field(i)
+ if jwt.HasRequiredJSONTag(field) {
+ return true
+ }
+ }
+
+ return false
+}