diff --git a/HISTORY.md b/HISTORY.md
index 95ad69f33..fcccde5f7 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -29,7 +29,7 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements
- A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below.
-- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/main.go) client credentials.
+- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/main.go) or [jwt](https://github.com/kataras/iris/blob/master/_examples/auth/jwt/overview/main.go) client credentials.
- Add the ability to [share functions](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) between handlers chain and add an [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-services) on sharing Go structures (aka services).
- Add the new `Party.UseOnce` method to the `*Route`
@@ -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)).
@@ -310,13 +310,14 @@ var dirOpts = iris.DirOptions{
## New Context Methods
+- `Context.ReadURL(ptr interface{}) error` shortcut of `ReadParams` and `ReadQuery`. Binds URL dynamic path parameters and URL query parameters to the given "ptr" pointer of a struct value.
- `Context.SetUser(User)` and `Context.User() User` to store and retrieve an authenticated client. Read more [here](https://github.com/iris-contrib/middleware/issues/63).
- `Context.SetLogoutFunc(fn interface{}, persistenceArgs ...interface{})` and `Logout(args ...interface{}) error` methods to allow different kind of auth middlewares to be able to set a "logout" a user/client feature with a single function, the route handler may not be aware of the implementation of the authentication used.
- `Context.SetFunc(name string, fn interface{}, persistenceArgs ...interface{})` and `Context.CallFunc(name string, args ...interface{}) ([]reflect.Value, error)` to allow middlewares to share functions dynamically when the type of the function is not predictable, see the [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) for more.
- `Context.TextYAML(interface{}) error` same as `Context.YAML` but with set the Content-Type to `text/yaml` instead (Google Chrome renders it as text).
- `Context.IsDebug() bool` reports whether the application is running under debug/development mode. It is a shortcut of Application.Logger().Level >= golog.DebugLevel.
-- `Context.IsRecovered() bool` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/master/middleware/recover). Also the `iris.IsErrPrivate` function and `iris.ErrPrivate` interface have been introduced.
-- `Context.RecordBody()` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once.
+- `Context.IsRecovered() bool` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/master/middleware/recover). Also the `Context.GetErrPublic() (bool, error)`, `Context.SetErrPrivate(err error)` methods and `iris.ErrPrivate` interface have been introduced.
+- `Context.RecordRequestBody(bool)` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once.
- `Context.IsRecordingBody() bool` reports whether the request body can be readen multiple times.
- `Context.ReadHeaders(ptr interface{}) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-headers/main.go).
- `Context.ReadParams(ptr interface{}) error` binds dynamic path parameters to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-params/main.go).
@@ -490,6 +491,7 @@ Prior to this version the `iris.Context` was the only one dependency that has be
| [net.IP](https://golang.org/pkg/net/#IP) | `net.ParseIP(ctx.RemoteAddr())` |
| [mvc.Code](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Code) | `ctx.GetStatusCode() int` |
| [mvc.Err](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Err) | `ctx.GetErr() error` |
+| [iris/context.User](https://pkg.go.dev/github.com/kataras/iris/v12/context?tab=doc#User) | `ctx.User()` |
| `string`, | |
| `int, int8, int16, int32, int64`, | |
| `uint, uint8, uint16, uint32, uint64`, | |
diff --git a/NOTICE b/NOTICE
index 5f0c9defb..042230a09 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/README.md b/_examples/README.md
index ee830ce6e..f744c8d13 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -168,8 +168,9 @@
* [Bind Form](request-body/read-form/main.go)
* [Checkboxes](request-body/read-form/checkboxes/main.go)
* [Bind Query](request-body/read-query/main.go)
- * [Bind Headers](request-body/read-headers/main.go)
* [Bind Params](request-body/read-params/main.go)
+ * [Bind URL](request-body/read-url/main.go)
+ * [Bind Headers](request-body/read-headers/main.go)
* [Bind Body](request-body/read-body/main.go)
* [Bind Custom per type](request-body/read-custom-per-type/main.go)
* [Bind Custom via Unmarshaler](request-body/read-custom-via-unmarshaler/main.go)
@@ -197,8 +198,12 @@
* Authentication, Authorization & Bot Detection
* [Basic Authentication](auth/basicauth/main.go)
* [CORS](auth/cors)
- * [JWT](auth/jwt/main.go)
+ * JSON Web Tokens
+ * [Basic](auth/jwt/basic/main.go)
+ * [Middleware](auth/jwt/midleware/main.go)
+ * [Blocklist](auth/jwt/blocklist/main.go)
* [Refresh Token](auth/jwt/refresh-token/main.go)
+ * [Tutorial](auth/jwt/tutorial)
* [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go)
* [OAUth2](auth/goth/main.go)
* [Manage Permissions](auth/permissions/main.go)
@@ -218,6 +223,7 @@
* [Badger](sessions/database/badger/main.go)
* [BoltDB](sessions/database/boltdb/main.go)
* [Redis](sessions/database/redis/main.go)
+ * [View Data](sessions/viewdata)
* Websocket
* [Gorilla FileWatch (3rd-party)](websocket/gorilla-filewatch/main.go)
* [Basic](websocket/basic)
diff --git a/_examples/auth/basicauth/main.go b/_examples/auth/basicauth/main.go
index 80ecae42c..0df1a5e55 100644
--- a/_examples/auth/basicauth/main.go
+++ b/_examples/auth/basicauth/main.go
@@ -56,7 +56,9 @@ func h(ctx iris.Context) {
// makes sure for that, otherwise this handler will not be executed.
// OR:
user := ctx.User()
- ctx.Writef("%s %s:%s", ctx.Path(), user.GetUsername(), user.GetPassword())
+ username, _ := user.GetUsername()
+ password, _ := user.GetPassword
+ ctx.Writef("%s %s:%s", ctx.Path(), username, password)
}
func logout(ctx iris.Context) {
diff --git a/_examples/auth/jwt/README.md b/_examples/auth/jwt/README.md
deleted file mode 100644
index 1d2e0ae50..000000000
--- a/_examples/auth/jwt/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/basic/main.go b/_examples/auth/jwt/basic/main.go
new file mode 100644
index 000000000..f37ce681f
--- /dev/null
+++ b/_examples/auth/jwt/basic/main.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+ "time"
+
+ "github.com/kataras/iris/v12"
+ "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()
+
+ app.Get("/", generateToken)
+ app.Get("/protected", protected)
+
+ app.Listen(":8080")
+}
+
+type fooClaims struct {
+ Foo string `json:"foo"`
+}
+
+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")
+
+ // Decode the custom claims.
+ var claims fooClaims
+ verifiedToken.Claims(&claims)
+
+ // 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/blocklist/main.go b/_examples/auth/jwt/blocklist/main.go
new file mode 100644
index 000000000..8f3362c8f
--- /dev/null
+++ b/_examples/auth/jwt/blocklist/main.go
@@ -0,0 +1,101 @@
+package main
+
+import (
+ "time"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/middleware/jwt"
+ "github.com/kataras/iris/v12/middleware/jwt/blocklist/redis"
+
+ // Optionally to set token identifier.
+ "github.com/google/uuid"
+)
+
+var (
+ signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
+
+ signer = jwt.NewSigner(jwt.HS256, signatureSharedKey, 15*time.Minute)
+ verifier = jwt.NewVerifier(jwt.HS256, signatureSharedKey)
+)
+
+type userClaims struct {
+ Username string `json:"username"`
+}
+
+func main() {
+ app := iris.New()
+
+ // IMPORTANT
+ //
+ // To use the in-memory blocklist just:
+ // verifier.WithDefaultBlocklist()
+ // To use a persistence blocklist, e.g. redis,
+ // start your redis-server and:
+ blocklist := redis.NewBlocklist()
+ // To configure single client or a cluster one:
+ // blocklist.ClientOptions.Addr = "127.0.0.1:6379"
+ // blocklist.ClusterOptions.Addrs = []string{...}
+ // To set a prefix for jwt ids:
+ // blocklist.Prefix = "myapp-"
+ //
+ // To manually connect and check its error before continue:
+ // err := blocklist.Connect()
+ // By default the verifier will try to connect, if failed then it will throw http error.
+ //
+ // And then register it:
+ verifier.Blocklist = blocklist
+ verifyMiddleware := verifier.Verify(func() interface{} {
+ return new(userClaims)
+ })
+
+ app.Get("/", authenticate)
+
+ protectedAPI := app.Party("/protected", verifyMiddleware)
+ protectedAPI.Get("/", protected)
+ protectedAPI.Get("/logout", logout)
+
+ // http://localhost:8080
+ // http://localhost:8080/protected?token=$token
+ // http://localhost:8080/logout?token=$token
+ // http://localhost:8080/protected?token=$token (401)
+ app.Listen(":8080")
+}
+
+func authenticate(ctx iris.Context) {
+ claims := userClaims{
+ Username: "kataras",
+ }
+
+ // Generate JWT ID.
+ random, err := uuid.NewRandom()
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+ id := random.String()
+
+ // Set the ID with the jwt.ID.
+ token, err := signer.Sign(claims, jwt.ID(id))
+
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.Write(token)
+}
+
+func protected(ctx iris.Context) {
+ claims := jwt.Get(ctx).(*userClaims)
+
+ // To the standard claims, e.g. the generated ID:
+ // jwt.GetVerifiedToken(ctx).StandardClaims.ID
+
+ ctx.WriteString(claims.Username)
+}
+
+func logout(ctx iris.Context) {
+ ctx.Logout()
+
+ ctx.Redirect("/", iris.StatusTemporaryRedirect)
+}
diff --git a/_examples/auth/jwt/main.go b/_examples/auth/jwt/main.go
deleted file mode 100644
index 2c31091fb..000000000
--- a/_examples/auth/jwt/main.go
+++ /dev/null
@@ -1,159 +0,0 @@
-package main
-
-import (
- "time"
-
- "github.com/kataras/iris/v12"
- "github.com/kataras/iris/v12/middleware/jwt"
-)
-
-// UserClaims a custom claims structure. You can just use jwt.Claims too.
-type UserClaims struct {
- jwt.Claims
- Username string
-}
-
-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)
-
- app := iris.New()
- app.Logger().SetLevel("debug")
-
- app.Get("/authenticate", func(ctx iris.Context) {
- standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
- // NOTE: if custom claims then the `j.Expiry(claims)` (or jwt.Expiry(duration, claims))
- // MUST be called in order to set the expiration time.
- customClaims := UserClaims{
- Claims: j.Expiry(standardClaims),
- Username: "kataras",
- }
-
- j.WriteToken(ctx, customClaims)
- })
-
- userRouter := app.Party("/user")
- {
- // userRouter.Use(j.Verify)
- // userRouter.Get("/", func(ctx iris.Context) {
- // var claims UserClaims
- // if err := jwt.ReadClaims(ctx, &claims); err != nil {
- // // Validation-only errors, the rest are already
- // // checked on `j.Verify` middleware.
- // ctx.StopWithStatus(iris.StatusUnauthorized)
- // return
- // }
- //
- // ctx.Writef("Claims: %#+v\n", claims)
- // })
- //
- // OR:
- userRouter.Get("/", func(ctx iris.Context) {
- var claims UserClaims
- if err := j.VerifyToken(ctx, &claims); err != nil {
- ctx.StopWithStatus(iris.StatusUnauthorized)
- return
- }
-
- ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, claims.Expiry.Time())
- })
- }
-
- app.Listen(":8080")
-}
-
-/*
-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/middleware/main.go b/_examples/auth/jwt/middleware/main.go
new file mode 100644
index 000000000..6af83e642
--- /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.WithEncryption(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.WithDecryption(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/refresh-token/main.go b/_examples/auth/jwt/refresh-token/main.go
index 9e3be184c..1e6ca5879 100644
--- a/_examples/auth/jwt/refresh-token/main.go
+++ b/_examples/auth/jwt/refresh-token/main.go
@@ -1,139 +1,183 @@
package main
import (
+ "fmt"
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
-// UserClaims a custom claims structure. You can just use jwt.Claims too.
+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 {
- jwt.Claims
- Username string
+ 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"`
}
-// TokenPair holds the access token and refresh token response.
-type TokenPair struct {
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token"`
+// GetID implements the partial context user's ID interface.
+// Note that if claims were a map then the claims value converted to UserClaims
+// and no need to implement any method.
+//
+// This is useful when multiple auth methods are used (e.g. basic auth, jwt)
+// but they all share a couple of methods.
+func (u *UserClaims) GetID() string {
+ return u.ID
}
-func main() {
- app := iris.New()
+// GetUsername implements the partial context user's Username interface.
+func (u *UserClaims) GetUsername() string {
+ return u.Username
+}
- // Access token, short-live.
- accessJWT := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
- // Refresh token, long-live. Important: Give different secret keys(!)
- refreshJWT := jwt.HMAC(1*time.Hour, "other secret", "other16bytesecre")
- // On refresh token, we extract it only from a request body
- // of JSON, e.g. {"refresh_token": $token }.
- // You can also do it manually in the handler level though.
- refreshJWT.Extractors = []jwt.TokenExtractor{
- jwt.FromJSON("refresh_token"),
+// 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")
}
- // Generate access and refresh tokens and send to the client.
- app.Get("/authenticate", func(ctx iris.Context) {
- tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
- if err != nil {
- ctx.StopWithStatus(iris.StatusInternalServerError)
- return
- }
+ return nil
+}
- ctx.JSON(tokenPair)
- })
-
- app.Get("/refresh", func(ctx iris.Context) {
- // Manual (if jwt.FromJSON missing):
- // var payload = struct {
- // RefreshToken string `json:"refresh_token"`
- // }{}
- //
- // err := ctx.ReadJSON(&payload)
- // if err != nil {
- // ctx.StatusCode(iris.StatusBadRequest)
- // return
- // }
- //
- // j.VerifyTokenString(ctx, payload.RefreshToken, &claims)
-
- var claims jwt.Claims
- if err := refreshJWT.VerifyToken(ctx, &claims); err != nil {
- ctx.Application().Logger().Warnf("verify refresh token: %v", err)
- ctx.StopWithStatus(iris.StatusUnauthorized)
- return
- }
+// For refresh token, we will just use the jwt.Claims
+// structure which contains the standard JWT fields.
- userID := claims.Subject
- if userID == "" {
- ctx.StopWithStatus(iris.StatusUnauthorized)
- return
- }
+func main() {
+ app := iris.New()
+ app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized)
+
+ app.Get("/authenticate", generateTokenPair)
+ app.Get("/refresh", refreshToken)
+
+ protectedAPI := app.Party("/protected")
+ {
+ verifyMiddleware := verifier.Verify(func() interface{} {
+ return new(UserClaims)
+ })
+
+ protectedAPI.Use(verifyMiddleware)
+
+ protectedAPI.Get("/", func(ctx iris.Context) {
+ // Access the claims through: jwt.Get:
+ // claims := jwt.Get(ctx).(*UserClaims)
+ // ctx.Writef("Username: %s\n", claims.Username)
+ //
+ // OR through context's user (if at least one method was implement by our UserClaims):
+ user := ctx.User()
+ id, _ := user.GetID()
+ username, _ := user.GetUsername()
+ ctx.Writef("ID: %s\nUsername: %s\n", id, username)
+ })
+ }
- // Simulate a database call against our jwt subject.
- if userID != "53afcf05-38a3-43c3-82af-8bbbe0e4a149" {
- ctx.StopWithStatus(iris.StatusUnauthorized)
- return
- }
+ // http://localhost:8080/protected (401)
+ // 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?refresh_token={refresh_token}
+ // OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
+ // http://localhost:8080/refresh?refresh_token={access_token} (401)
+ app.Listen(":8080")
+}
- // All OK, re-generate the new pair and send to client.
- tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
- if err != nil {
- ctx.StopWithStatus(iris.StatusInternalServerError)
- return
- }
+func generateTokenPair(ctx iris.Context) {
+ // Simulate a user...
+ userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
- ctx.JSON(tokenPair)
- })
+ // 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.
+ refreshClaims := jwt.Claims{Subject: userID}
- app.Get("/", func(ctx iris.Context) {
- var claims UserClaims
- if err := accessJWT.VerifyToken(ctx, &claims); err != nil {
- ctx.StopWithStatus(iris.StatusUnauthorized)
- return
- }
+ accessClaims := UserClaims{
+ ID: userID,
+ Username: "kataras",
+ }
- ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, claims.Expiry.Time())
- })
+ // Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.
+ // 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().Errorf("token pair: %v", err)
+ ctx.StopWithStatus(iris.StatusInternalServerError)
+ return
+ }
- // http://localhost:8080 (401)
- // http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
- // http://localhost:8080?token={access_token} (200)
- // http://localhost:8080?token={refresh_token} (401)
- // http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
- app.Listen(":8080")
+ // Send the generated token pair to the client.
+ // The tokenPair looks like: {"access_token": $token, "refresh_token": $token}
+ ctx.JSON(tokenPair)
}
-func generateTokenPair(accessJWT, refreshJWT *jwt.JWT) (TokenPair, error) {
- standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
+// 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
+ // to make sure that this refresh token is a pair generated by this user.
+ // * 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
+ }
- customClaims := UserClaims{
- Claims: accessJWT.Expiry(standardClaims),
- Username: "kataras",
+ refreshToken = tokenPair.RefreshToken
}
- accessToken, err := accessJWT.Token(customClaims)
+ // Verify the refresh token, which its subject MUST match the "currentUserID".
+ _, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})
if err != nil {
- return TokenPair{}, err
+ ctx.Application().Logger().Errorf("verify refresh token: %v", err)
+ ctx.StatusCode(iris.StatusUnauthorized)
+ return
}
- // At refresh tokens you don't need any custom claims.
- refreshClaims := refreshJWT.Expiry(jwt.Claims{
- ID: "refresh_kataras",
- // For example, the User ID,
- // this is necessary to check against the database
- // if the user still exist or has credentials to access our page.
- Subject: "53afcf05-38a3-43c3-82af-8bbbe0e4a149",
- })
+ /* Custom validation checks can be performed after Verify calls too:
+ currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
+ userID := verifiedToken.StandardClaims.Subject
+ if userID != currentUserID {
+ ctx.StopWithStatus(iris.StatusUnauthorized)
+ return
+ }
+ */
- refreshToken, err := refreshJWT.Token(refreshClaims)
- if err != nil {
- return TokenPair{}, err
+ // 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)
}
- return TokenPair{
- AccessToken: accessToken,
- RefreshToken: refreshToken,
- }, nil
+ 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 000000000..411e3a89e
--- /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 000000000..99a96176a
--- /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/_examples/auth/jwt/rsa_password_protected.key b/_examples/auth/jwt/rsa_password_protected.key
deleted file mode 100644
index e93fff776..000000000
--- a/_examples/auth/jwt/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/tutorial/README.md b/_examples/auth/jwt/tutorial/README.md
new file mode 100644
index 000000000..5a52c56b9
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/README.md
@@ -0,0 +1,62 @@
+# Iris JWT Tutorial
+
+This example show how to use JWT with domain-driven design pattern with Iris. There is also a simple Go client which describes how you can use Go to authorize a user and use the server's API.
+
+## Run the server
+
+```sh
+$ go run main.go
+```
+
+## Authenticate, get the token
+
+```sh
+$ curl --location --request POST 'http://localhost:8080/signin' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--data-urlencode 'username=admin' \
+--data-urlencode 'password=admin'
+
+> $token
+```
+
+## Get all TODOs for this User
+
+```sh
+$ curl --location --request GET 'http://localhost:8080/todos' \
+--header 'Authorization: Bearer $token'
+
+> $todos
+```
+
+## Get a specific User's TODO
+
+```sh
+$ curl --location --request GET 'http://localhost:8080/todos/$id' \
+--header 'Authorization: Bearer $token'
+
+> $todo
+```
+
+## Get all TODOs for all Users (admin role)
+
+```sh
+$ curl --location --request GET 'http://localhost:8080/admin/todos' \
+--header 'Authorization: Bearer $token'
+
+> $todos
+```
+
+## Create a new TODO
+
+```sh
+$ curl --location --request POST 'http://localhost:8080/todos' \
+--header 'Authorization: Bearer $token' \
+--header 'Content-Type: application/json' \
+--data-raw '{
+ "title": "test titlte",
+ "body": "test body"
+}'
+
+> Status Created
+> $todo
+```
diff --git a/_examples/auth/jwt/tutorial/api/auth.go b/_examples/auth/jwt/tutorial/api/auth.go
new file mode 100644
index 000000000..2e540e099
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/api/auth.go
@@ -0,0 +1,140 @@
+package api
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "myapp/domain/model"
+ "myapp/domain/repository"
+ "myapp/util"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/middleware/jwt"
+)
+
+const defaultSecretKey = "sercrethatmaycontainch@r$32chars"
+
+func getSecretKey() string {
+ secret := os.Getenv(util.AppName + "_SECRET")
+ if secret == "" {
+ return defaultSecretKey
+ }
+
+ return secret
+}
+
+// UserClaims represents the user token claims.
+type UserClaims struct {
+ UserID string `json:"user_id"`
+ Roles []model.Role `json:"roles"`
+}
+
+// Validate implements the custom struct claims validator,
+// this is totally optionally and maybe unnecessary but good to know how.
+func (u *UserClaims) Validate() error {
+ if u.UserID == "" {
+ return fmt.Errorf("%w: %s", jwt.ErrMissingKey, "user_id")
+ }
+
+ return nil
+}
+
+// Verify allows only authorized clients.
+func Verify() iris.Handler {
+ secret := getSecretKey()
+
+ verifier := jwt.NewVerifier(jwt.HS256, []byte(secret), jwt.Expected{Issuer: util.AppName})
+ verifier.Extractors = []jwt.TokenExtractor{jwt.FromHeader} // extract token only from Authorization: Bearer $token
+ return verifier.Verify(func() interface{} {
+ return new(UserClaims)
+ })
+}
+
+// AllowAdmin allows only authorized clients with "admin" access role.
+// Should be registered after Verify.
+func AllowAdmin(ctx iris.Context) {
+ if !IsAdmin(ctx) {
+ ctx.StopWithText(iris.StatusForbidden, "admin access required")
+ return
+ }
+
+ ctx.Next()
+}
+
+// SignIn accepts the user form data and returns a token to authorize a client.
+func SignIn(repo repository.UserRepository) iris.Handler {
+ secret := getSecretKey()
+ signer := jwt.NewSigner(jwt.HS256, []byte(secret), 15*time.Minute)
+
+ return func(ctx iris.Context) {
+ /*
+ type LoginForm struct {
+ Username string `form:"username"`
+ Password string `form:"password"`
+ }
+ and ctx.ReadForm OR use the ctx.FormValue(s) method.
+ */
+
+ var (
+ username = ctx.FormValue("username")
+ password = ctx.FormValue("password")
+ )
+
+ user, ok := repo.GetByUsernameAndPassword(username, password)
+ if !ok {
+ ctx.StopWithText(iris.StatusBadRequest, "wrong username or password")
+ return
+ }
+
+ claims := UserClaims{
+ UserID: user.ID,
+ Roles: user.Roles,
+ }
+
+ // Optionally, generate a JWT ID.
+ jti, err := util.GenerateUUID()
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ token, err := signer.Sign(claims, jwt.Claims{
+ ID: jti,
+ Issuer: util.AppName,
+ })
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.Write(token)
+ }
+}
+
+// SignOut invalidates a user from server-side using the jwt Blocklist.
+func SignOut(ctx iris.Context) {
+ ctx.Logout() // this is automatically binded to a function which invalidates the current request token by the JWT Verifier above.
+}
+
+// GetClaims returns the current authorized client claims.
+func GetClaims(ctx iris.Context) *UserClaims {
+ claims := jwt.Get(ctx).(*UserClaims)
+ return claims
+}
+
+// GetUserID returns the current authorized client's user id extracted from claims.
+func GetUserID(ctx iris.Context) string {
+ return GetClaims(ctx).UserID
+}
+
+// IsAdmin reports whether the current client has admin access.
+func IsAdmin(ctx iris.Context) bool {
+ for _, role := range GetClaims(ctx).Roles {
+ if role == model.Admin {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/_examples/auth/jwt/tutorial/api/todo.go b/_examples/auth/jwt/tutorial/api/todo.go
new file mode 100644
index 000000000..317494e85
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/api/todo.go
@@ -0,0 +1,119 @@
+package api
+
+import (
+ "errors"
+ "myapp/domain/repository"
+
+ "github.com/kataras/iris/v12"
+)
+
+// TodoRequest represents a Todo HTTP request.
+type TodoRequest struct {
+ Title string `json:"title" form:"title" url:"title"`
+ Body string `json:"body" form:"body" url:"body"`
+}
+
+// CreateTodo handles the creation of a Todo entry.
+func CreateTodo(repo repository.TodoRepository) iris.Handler {
+ return func(ctx iris.Context) {
+ var req TodoRequest
+ err := ctx.ReadBody(&req) // will bind the "req" to a JSON, form or url query request data.
+ if err != nil {
+ ctx.StopWithError(iris.StatusBadRequest, err)
+ return
+ }
+
+ userID := GetUserID(ctx)
+ todo, err := repo.Create(userID, req.Title, req.Body)
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.StatusCode(iris.StatusCreated)
+ ctx.JSON(todo)
+ }
+}
+
+// GetTodo lists all users todos.
+// Parameter: {id}.
+func GetTodo(repo repository.TodoRepository) iris.Handler {
+ return func(ctx iris.Context) {
+ id := ctx.Params().Get("id")
+ userID := GetUserID(ctx)
+
+ todo, err := repo.GetByID(id)
+ if err != nil {
+ code := iris.StatusInternalServerError
+ if errors.Is(err, repository.ErrNotFound) {
+ code = iris.StatusNotFound
+ }
+
+ ctx.StopWithError(code, err)
+ return
+ }
+
+ if !IsAdmin(ctx) { // admin can access any user's todos.
+ if todo.UserID != userID {
+ ctx.StopWithStatus(iris.StatusForbidden)
+ return
+ }
+ }
+
+ ctx.JSON(todo)
+ }
+}
+
+// ListTodos lists todos of the current user.
+func ListTodos(repo repository.TodoRepository) iris.Handler {
+ return func(ctx iris.Context) {
+ userID := GetUserID(ctx)
+ todos, err := repo.GetAllByUser(userID)
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ // if len(todos) == 0 {
+ // ctx.StopWithError(iris.StatusNotFound, fmt.Errorf("no entries found"))
+ // return
+ // }
+ // Or let the client decide what to do on empty list.
+ ctx.JSON(todos)
+ }
+}
+
+// ListAllTodos lists all users todos.
+// Access: admin.
+// Middleware: AllowAdmin.
+func ListAllTodos(repo repository.TodoRepository) iris.Handler {
+ return func(ctx iris.Context) {
+ todos, err := repo.GetAll()
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.JSON(todos)
+ }
+}
+
+/* Leave as exercise: use filtering instead...
+
+// ListTodosByUser lists all todos by a specific user.
+// Access: admin.
+// Middleware: AllowAdmin.
+// Parameter: {id}.
+func ListTodosByUser(repo repository.TodoRepository) iris.Handler {
+ return func(ctx iris.Context) {
+ userID := ctx.Params().Get("id")
+ todos, err := repo.GetAllByUser(userID)
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.JSON(todos)
+ }
+}
+*/
diff --git a/_examples/auth/jwt/tutorial/domain/model/role.go b/_examples/auth/jwt/tutorial/domain/model/role.go
new file mode 100644
index 000000000..00fd1c256
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/domain/model/role.go
@@ -0,0 +1,9 @@
+package model
+
+// Role represents a role.
+type Role string
+
+const (
+ // Admin represents the Admin access role.
+ Admin Role = "admin"
+)
diff --git a/_examples/auth/jwt/tutorial/domain/model/todo.go b/_examples/auth/jwt/tutorial/domain/model/todo.go
new file mode 100644
index 000000000..d0a2ea273
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/domain/model/todo.go
@@ -0,0 +1,10 @@
+package model
+
+// Todo represents the Todo model.
+type Todo struct {
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ Title string `json:"title"`
+ Body string `json:"body"`
+ CreatedAt int64 `json:"created_at"` // unix seconds.
+}
diff --git a/_examples/auth/jwt/tutorial/domain/model/user.go b/_examples/auth/jwt/tutorial/domain/model/user.go
new file mode 100644
index 000000000..9fa8bcdfd
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/domain/model/user.go
@@ -0,0 +1,9 @@
+package model
+
+// User represents our User model.
+type User struct {
+ ID string `json:"id"`
+ Username string `json:"username"`
+ HashedPassword []byte `json:"-"`
+ Roles []Role `json:"roles"`
+}
diff --git a/_examples/auth/jwt/tutorial/domain/repository/samples.go b/_examples/auth/jwt/tutorial/domain/repository/samples.go
new file mode 100644
index 000000000..b7d267ba0
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/domain/repository/samples.go
@@ -0,0 +1,45 @@
+package repository
+
+import (
+ "fmt"
+
+ "myapp/domain/model"
+)
+
+// GenerateSamples generates data samples.
+func GenerateSamples(userRepo UserRepository, todoRepo TodoRepository) error {
+ // Create users.
+ for _, username := range []string{"vasiliki", "george", "kwstas"} {
+ // My grandmother.
+ // My young brother.
+ // My youngest brother.
+ password := fmt.Sprintf("%s_pass", username)
+ if _, err := userRepo.Create(username, password); err != nil {
+ return err
+ }
+ }
+
+ // Create a user with admin role.
+ if _, err := userRepo.Create("admin", "admin", model.Admin); err != nil {
+ return err
+ }
+
+ // Create two todos per user.
+ users, err := userRepo.GetAll()
+ if err != nil {
+ return err
+ }
+
+ for i, u := range users {
+ for j := 0; j < 2; j++ {
+ title := fmt.Sprintf("%s todo %d:%d title", u.Username, i, j)
+ body := fmt.Sprintf("%s todo %d:%d body", u.Username, i, j)
+ _, err := todoRepo.Create(u.ID, title, body)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/_examples/auth/jwt/tutorial/domain/repository/todo_repository.go b/_examples/auth/jwt/tutorial/domain/repository/todo_repository.go
new file mode 100644
index 000000000..a3c8429f3
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/domain/repository/todo_repository.go
@@ -0,0 +1,94 @@
+package repository
+
+import (
+ "errors"
+ "sync"
+
+ "myapp/domain/model"
+ "myapp/util"
+)
+
+// ErrNotFound indicates that an entry was not found.
+// Usage: errors.Is(err, ErrNotFound)
+var ErrNotFound = errors.New("not found")
+
+// TodoRepository is responsible for Todo CRUD operations,
+// however, for the sake of the example we only implement the Create and Read ones.
+type TodoRepository interface {
+ Create(userID, title, body string) (model.Todo, error)
+ GetByID(id string) (model.Todo, error)
+ GetAll() ([]model.Todo, error)
+ GetAllByUser(userID string) ([]model.Todo, error)
+}
+
+var (
+ _ TodoRepository = (*memoryTodoRepository)(nil)
+)
+
+type memoryTodoRepository struct {
+ todos []model.Todo // map[string]model.Todo
+ mu sync.RWMutex
+}
+
+// NewMemoryTodoRepository returns the default in-memory todo repository.
+func NewMemoryTodoRepository() TodoRepository {
+ r := new(memoryTodoRepository)
+ return r
+}
+
+func (r *memoryTodoRepository) Create(userID, title, body string) (model.Todo, error) {
+ id, err := util.GenerateUUID()
+ if err != nil {
+ return model.Todo{}, err
+ }
+
+ todo := model.Todo{
+ ID: id,
+ UserID: userID,
+ Title: title,
+ Body: body,
+ CreatedAt: util.Now().Unix(),
+ }
+
+ r.mu.Lock()
+ r.todos = append(r.todos, todo)
+ r.mu.Unlock()
+
+ return todo, nil
+}
+
+func (r *memoryTodoRepository) GetByID(id string) (model.Todo, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ for _, todo := range r.todos {
+ if todo.ID == id {
+ return todo, nil
+ }
+ }
+
+ return model.Todo{}, ErrNotFound
+}
+
+func (r *memoryTodoRepository) GetAll() ([]model.Todo, error) {
+ r.mu.RLock()
+ tmp := make([]model.Todo, len(r.todos))
+ copy(tmp, r.todos)
+ r.mu.RUnlock()
+ return tmp, nil
+}
+
+func (r *memoryTodoRepository) GetAllByUser(userID string) ([]model.Todo, error) {
+ // initialize a slice, so we don't have "null" at empty response.
+ todos := make([]model.Todo, 0)
+
+ r.mu.RLock()
+ for _, todo := range r.todos {
+ if todo.UserID == userID {
+ todos = append(todos, todo)
+ }
+ }
+ r.mu.RUnlock()
+
+ return todos, nil
+}
diff --git a/_examples/auth/jwt/tutorial/domain/repository/user_repository.go b/_examples/auth/jwt/tutorial/domain/repository/user_repository.go
new file mode 100644
index 000000000..b6e0bd764
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/domain/repository/user_repository.go
@@ -0,0 +1,82 @@
+package repository
+
+import (
+ "sync"
+
+ "myapp/domain/model"
+ "myapp/util"
+)
+
+// UserRepository is responsible for User CRUD operations,
+// however, for the sake of the example we only implement the Read one.
+type UserRepository interface {
+ Create(username, password string, roles ...model.Role) (model.User, error)
+ // GetByUsernameAndPassword should return a User based on the given input.
+ GetByUsernameAndPassword(username, password string) (model.User, bool)
+ GetAll() ([]model.User, error)
+}
+
+var (
+ _ UserRepository = (*memoryUserRepository)(nil)
+)
+
+type memoryUserRepository struct {
+ // Users represents a user database.
+ // For the sake of the tutorial we use a simple slice of users.
+ users []model.User
+ mu sync.RWMutex
+}
+
+// NewMemoryUserRepository returns the default in-memory user repository.
+func NewMemoryUserRepository() UserRepository {
+ r := new(memoryUserRepository)
+ return r
+}
+
+func (r *memoryUserRepository) Create(username, password string, roles ...model.Role) (model.User, error) {
+ id, err := util.GenerateUUID()
+ if err != nil {
+ return model.User{}, err
+ }
+
+ hashedPassword, err := util.GeneratePassword(password)
+ if err != nil {
+ return model.User{}, err
+ }
+
+ user := model.User{
+ ID: id,
+ Username: username,
+ HashedPassword: hashedPassword,
+ Roles: roles,
+ }
+
+ r.mu.Lock()
+ r.users = append(r.users, user)
+ r.mu.Unlock()
+
+ return user, nil
+}
+
+// GetByUsernameAndPassword returns a user from the storage based on the given "username" and "password".
+func (r *memoryUserRepository) GetByUsernameAndPassword(username, password string) (model.User, bool) {
+ for _, u := range r.users { // our example uses a static slice.
+ if u.Username == username {
+ // we compare the user input and the stored hashed password.
+ ok := util.ValidatePassword(password, u.HashedPassword)
+ if ok {
+ return u, true
+ }
+ }
+ }
+
+ return model.User{}, false
+}
+
+func (r *memoryUserRepository) GetAll() ([]model.User, error) {
+ r.mu.RLock()
+ tmp := make([]model.User, len(r.users))
+ copy(tmp, r.users)
+ r.mu.RUnlock()
+ return tmp, nil
+}
diff --git a/_examples/auth/jwt/tutorial/go-client/README.md b/_examples/auth/jwt/tutorial/go-client/README.md
new file mode 100644
index 000000000..f7a96a9cf
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/go-client/README.md
@@ -0,0 +1,12 @@
+# Go Client
+
+```sh
+$ go run .
+```
+
+```sh
+2020/11/04 21:08:40 Access Token:
+"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYTAwYzI3ZDEtYjVhYS00NjU0LWFmMTYtYjExNzNkZTY1NjI5Iiwicm9sZXMiOlsiYWRtaW4iXSwiaWF0IjoxNjA0NTE2OTIwLCJleHAiOjE2MDQ1MTc4MjAsImp0aSI6IjYzNmVmMDc0LTE2MzktNGJhZi1hNGNiLTQ4ZDM4NGMxMzliYSIsImlzcyI6Im15YXBwIn0.T9B0zG0AHShO5JfQgrMQBlToH33KHgp8nLMPFpN6QmM"
+2020/11/04 21:08:40 Todo Created:
+model.Todo{ID:"cfa38d7a-c556-4301-ae1f-fb90f705071c", UserID:"a00c27d1-b5aa-4654-af16-b1173de65629", Title:"test todo title", Body:"test todo body contents", CreatedAt:1604516920}
+```
diff --git a/_examples/auth/jwt/tutorial/go-client/client.go b/_examples/auth/jwt/tutorial/go-client/client.go
new file mode 100644
index 000000000..e96b769f2
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/go-client/client.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+// Client is the default http client instance used by the following methods.
+var Client = http.DefaultClient
+
+// RequestOption is a function which can be used to modify
+// a request instance before Do.
+type RequestOption func(*http.Request) error
+
+// WithAccessToken sets the given "token" to the authorization request header.
+func WithAccessToken(token []byte) RequestOption {
+ bearer := "Bearer " + string(token)
+ return func(req *http.Request) error {
+ req.Header.Add("Authorization", bearer)
+ return nil
+ }
+}
+
+// WithContentType sets the content-type request header.
+func WithContentType(cType string) RequestOption {
+ return func(req *http.Request) error {
+ req.Header.Set("Content-Type", cType)
+ return nil
+ }
+}
+
+// WithContentLength sets the content-length request header.
+func WithContentLength(length int) RequestOption {
+ return func(req *http.Request) error {
+ req.Header.Set("Content-Length", strconv.Itoa(length))
+ return nil
+ }
+}
+
+// Do fires a request to the server.
+func Do(method, url string, body io.Reader, opts ...RequestOption) (*http.Response, error) {
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, opt := range opts {
+ if err = opt(req); err != nil {
+ return nil, err
+ }
+ }
+
+ return Client.Do(req)
+}
+
+// JSON fires a request with "v" as client json data.
+func JSON(method, url string, v interface{}, opts ...RequestOption) (*http.Response, error) {
+ buf := new(bytes.Buffer)
+ err := json.NewEncoder(buf).Encode(v)
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, WithContentType("application/json; charset=utf-8"))
+ return Do(method, url, buf, opts...)
+}
+
+// Form fires a request with "formData" as client form data.
+func Form(method, url string, formData url.Values, opts ...RequestOption) (*http.Response, error) {
+ encoded := formData.Encode()
+ body := strings.NewReader(encoded)
+
+ opts = append([]RequestOption{
+ WithContentType("application/x-www-form-urlencoded"),
+ WithContentLength(len(encoded)),
+ }, opts...)
+
+ return Do(method, url, body, opts...)
+}
+
+// BindResponse binds a response body to the "dest" pointer and closes the body.
+func BindResponse(resp *http.Response, dest interface{}) error {
+ contentType := resp.Header.Get("Content-Type")
+ if idx := strings.IndexRune(contentType, ';'); idx > 0 {
+ contentType = contentType[0:idx]
+ }
+
+ switch contentType {
+ case "application/json":
+ defer resp.Body.Close()
+ return json.NewDecoder(resp.Body).Decode(dest)
+ default:
+ return fmt.Errorf("unsupported content type: %s", contentType)
+ }
+}
+
+// RawResponse simply returns the raw response body.
+func RawResponse(resp *http.Response) ([]byte, error) {
+ defer resp.Body.Close()
+
+ return ioutil.ReadAll(resp.Body)
+}
diff --git a/_examples/auth/jwt/tutorial/go-client/main.go b/_examples/auth/jwt/tutorial/go-client/main.go
new file mode 100644
index 000000000..d22688e13
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/go-client/main.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+
+ "myapp/api"
+ "myapp/domain/model"
+)
+
+const base = "http://localhost:8080"
+
+func main() {
+ accessToken, err := authenticate("admin", "admin")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Printf("Access Token:\n%q", accessToken)
+
+ todo, err := createTodo(accessToken, "test todo title", "test todo body contents")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Printf("Todo Created:\n%#+v", todo)
+}
+
+func authenticate(username, password string) ([]byte, error) {
+ endpoint := base + "/signin"
+
+ data := make(url.Values)
+ data.Set("username", username)
+ data.Set("password", password)
+
+ resp, err := Form(http.MethodPost, endpoint, data)
+ if err != nil {
+ return nil, err
+ }
+
+ accessToken, err := RawResponse(resp)
+ return accessToken, err
+}
+
+func createTodo(accessToken []byte, title, body string) (model.Todo, error) {
+ var todo model.Todo
+
+ endpoint := base + "/todos"
+
+ req := api.TodoRequest{
+ Title: title,
+ Body: body,
+ }
+
+ resp, err := JSON(http.MethodPost, endpoint, req, WithAccessToken(accessToken))
+ if err != nil {
+ return todo, err
+ }
+
+ if resp.StatusCode != http.StatusCreated {
+ rawData, _ := RawResponse(resp)
+ return todo, fmt.Errorf("failed to create a todo: %s", string(rawData))
+ }
+
+ err = BindResponse(resp, &todo)
+ return todo, err
+}
diff --git a/_examples/auth/jwt/tutorial/go.mod b/_examples/auth/jwt/tutorial/go.mod
new file mode 100644
index 000000000..3bcd36bd3
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/go.mod
@@ -0,0 +1,11 @@
+module myapp
+
+go 1.15
+
+require (
+ github.com/google/uuid v1.1.2
+ github.com/kataras/iris/v12 v12.2.0-alpha.0.20201031040657-23d4c411cadb
+ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
+)
+
+replace github.com/kataras/iris/v12 => ../../../../
diff --git a/_examples/auth/jwt/tutorial/main.go b/_examples/auth/jwt/tutorial/main.go
new file mode 100644
index 000000000..8be430811
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/main.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "myapp/api"
+ "myapp/domain/repository"
+
+ "github.com/kataras/iris/v12"
+)
+
+var (
+ userRepository = repository.NewMemoryUserRepository()
+ todoRepository = repository.NewMemoryTodoRepository()
+)
+
+func main() {
+ if err := repository.GenerateSamples(userRepository, todoRepository); err != nil {
+ panic(err)
+ }
+
+ app := iris.New()
+
+ app.Post("/signin", api.SignIn(userRepository))
+
+ verify := api.Verify()
+
+ todosAPI := app.Party("/todos", verify)
+ todosAPI.Post("/", api.CreateTodo(todoRepository))
+ todosAPI.Get("/", api.ListTodos(todoRepository))
+ todosAPI.Get("/{id}", api.GetTodo(todoRepository))
+
+ adminAPI := app.Party("/admin", verify, api.AllowAdmin)
+ adminAPI.Get("/todos", api.ListAllTodos(todoRepository))
+
+ // POST http://localhost:8080/signin (Form: username, password)
+ // GET http://localhost:8080/todos
+ // GET http://localhost:8080/todos/{id}
+ // POST http://localhost:8080/todos (JSON, Form or URL: title, body)
+ // GET http://localhost:8080/admin/todos
+ app.Listen(":8080")
+}
diff --git a/_examples/auth/jwt/tutorial/util/app.go b/_examples/auth/jwt/tutorial/util/app.go
new file mode 100644
index 000000000..7a991f01c
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/util/app.go
@@ -0,0 +1,7 @@
+package util
+
+// Constants for the application.
+const (
+ Version = "0.0.1"
+ AppName = "myapp"
+)
diff --git a/_examples/auth/jwt/tutorial/util/clock.go b/_examples/auth/jwt/tutorial/util/clock.go
new file mode 100644
index 000000000..34e35eb22
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/util/clock.go
@@ -0,0 +1,7 @@
+package util
+
+import "time"
+
+// Now is the default current time for the whole application.
+// Can be modified for testing or custom timezone.
+var Now = time.Now
diff --git a/_examples/auth/jwt/tutorial/util/password.go b/_examples/auth/jwt/tutorial/util/password.go
new file mode 100644
index 000000000..c18d0a916
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/util/password.go
@@ -0,0 +1,25 @@
+package util
+
+import "golang.org/x/crypto/bcrypt"
+
+// MustGeneratePassword same as GeneratePassword but panics on errors.
+func MustGeneratePassword(userPassword string) []byte {
+ hashed, err := GeneratePassword(userPassword)
+ if err != nil {
+ panic(err)
+ }
+
+ return hashed
+}
+
+// GeneratePassword will generate a hashed password for us based on the
+// user's input.
+func GeneratePassword(userPassword string) ([]byte, error) {
+ return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
+}
+
+// ValidatePassword will check if passwords are matched.
+func ValidatePassword(userPassword string, hashed []byte) bool {
+ err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword))
+ return err == nil
+}
diff --git a/_examples/auth/jwt/tutorial/util/uuid.go b/_examples/auth/jwt/tutorial/util/uuid.go
new file mode 100644
index 000000000..0269d62d7
--- /dev/null
+++ b/_examples/auth/jwt/tutorial/util/uuid.go
@@ -0,0 +1,23 @@
+package util
+
+import "github.com/google/uuid"
+
+// MustGenerateUUID returns a new v4 UUID or panics.
+func MustGenerateUUID() string {
+ id, err := GenerateUUID()
+ if err != nil {
+ panic(err)
+ }
+
+ return id
+}
+
+// GenerateUUID returns a new v4 UUID.
+func GenerateUUID() (string, error) {
+ id, err := uuid.NewRandom()
+ if err != nil {
+ return "", err
+ }
+
+ return id.String(), nil
+}
diff --git a/_examples/dependency-injection/jwt/contrib/go.mod b/_examples/dependency-injection/jwt/contrib/go.mod
index 990ef4e0b..97b9162e9 100644
--- a/_examples/dependency-injection/jwt/contrib/go.mod
+++ b/_examples/dependency-injection/jwt/contrib/go.mod
@@ -2,4 +2,6 @@ module github.com/kataras/iris/_examples/dependency-injection/jwt/contrib
go 1.15
-require github.com/iris-contrib/middleware/jwt v0.0.0-20200810001613-32cf668f999f
+require (
+ github.com/iris-contrib/middleware/jwt v0.0.0-20201017024110-39b50ffeb885
+)
diff --git a/_examples/dependency-injection/jwt/main.go b/_examples/dependency-injection/jwt/main.go
index c705fac9a..ff328eb9a 100644
--- a/_examples/dependency-injection/jwt/main.go
+++ b/_examples/dependency-injection/jwt/main.go
@@ -11,37 +11,57 @@ func main() {
app := iris.New()
app.ConfigureContainer(register)
+ // http://localhost:8080/authenticate
+ // http://localhost:8080/restricted (Header: Authorization = Bearer $token)
app.Listen(":8080")
}
-func register(api *iris.APIContainer) {
- j := jwt.HMAC(15*time.Minute, "secret", "secretforencrypt")
+var secret = []byte("secret")
+func register(api *iris.APIContainer) {
api.RegisterDependency(func(ctx iris.Context) (claims userClaims) {
- if err := j.VerifyToken(ctx, &claims); err != nil {
+ /* Using the middleware:
+ if ctx.Proceed(verify) {
+ // ^ the "verify" middleware will stop the execution if it's failed to verify the request.
+ // Map the input parameter of "restricted" function with the claims.
+ return jwt.Get(ctx).(*userClaims)
+ }*/
+ token := jwt.FromHeader(ctx)
+ if token == "" {
+ ctx.StopWithError(iris.StatusUnauthorized, jwt.ErrMissing)
+ return
+ }
+
+ verifiedToken, err := jwt.Verify(jwt.HS256, secret, []byte(token))
+ if err != nil {
ctx.StopWithError(iris.StatusUnauthorized, err)
return
}
+ verifiedToken.Claims(&claims)
return
})
- api.Get("/authenticate", writeToken(j))
+ api.Get("/authenticate", writeToken)
api.Get("/restricted", restrictedPage)
}
type userClaims struct {
- jwt.Claims
- Username string
+ Username string `json:"username"`
}
-func writeToken(j *jwt.JWT) iris.Handler {
- return func(ctx iris.Context) {
- j.WriteToken(ctx, userClaims{
- Claims: j.Expiry(jwt.Claims{Issuer: "an-issuer"}),
- Username: "kataras",
- })
+func writeToken(ctx iris.Context) {
+ claims := userClaims{
+ Username: "kataras",
}
+
+ token, err := jwt.Sign(jwt.HS256, secret, claims, jwt.MaxAge(1*time.Minute))
+ if err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.Write(token)
}
func restrictedPage(claims userClaims) string {
diff --git a/_examples/logging/request-logger/accesslog-simple/main.go b/_examples/logging/request-logger/accesslog-simple/main.go
index c966e2be6..77880c8bf 100644
--- a/_examples/logging/request-logger/accesslog-simple/main.go
+++ b/_examples/logging/request-logger/accesslog-simple/main.go
@@ -29,7 +29,10 @@ func makeAccessLog() *accesslog.AccessLog {
ac.PanicLog = accesslog.LogHandler
// Set Custom Formatter:
- ac.SetFormatter(&accesslog.JSON{})
+ ac.SetFormatter(&accesslog.JSON{
+ Indent: " ",
+ HumanTime: true,
+ })
// ac.SetFormatter(&accesslog.CSV{})
// ac.SetFormatter(&accesslog.Template{Text: "{{.Code}}"})
diff --git a/_examples/request-body/read-url/main.go b/_examples/request-body/read-url/main.go
new file mode 100644
index 000000000..36521614c
--- /dev/null
+++ b/_examples/request-body/read-url/main.go
@@ -0,0 +1,38 @@
+// package main contains an example on how to use the ReadURL,
+// same way you can do the ReadQuery, ReadParams, ReadJSON, ReadProtobuf and e.t.c.
+package main
+
+import (
+ "github.com/kataras/iris/v12"
+)
+
+type myURL struct {
+ Name string `url:"name"` // or `param:"name"`
+ Age int `url:"age"` // >> >>
+ Tail []string `url:"tail"` // >> >>
+}
+
+func main() {
+ app := newApp()
+
+ // http://localhost:8080/iris/web/framework?name=kataras&age=27
+ // myURL: main.myURL{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}}
+ app.Listen(":8080")
+}
+
+func newApp() *iris.Application {
+ app := iris.New()
+
+ app.Get("/{tail:path}", func(ctx iris.Context) {
+ var u myURL
+ // ReadURL is a shortcut of ReadParams + ReadQuery.
+ if err := ctx.ReadURL(&u); err != nil {
+ ctx.StopWithError(iris.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.Writef("myURL: %#v", u)
+ })
+
+ return app
+}
diff --git a/_examples/request-body/read-url/main_test.go b/_examples/request-body/read-url/main_test.go
new file mode 100644
index 000000000..211069004
--- /dev/null
+++ b/_examples/request-body/read-url/main_test.go
@@ -0,0 +1,16 @@
+package main
+
+import (
+ "testing"
+
+ "github.com/kataras/iris/v12/httptest"
+)
+
+func TestReadURL(t *testing.T) {
+ app := newApp()
+
+ e := httptest.New(t, app)
+
+ expectedBody := `myURL: main.myURL{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}}`
+ e.GET("/iris/web/framework").WithQuery("name", "kataras").WithQuery("age", 27).Expect().Status(httptest.StatusOK).Body().Equal(expectedBody)
+}
diff --git a/_examples/sessions/overview/example/example.go b/_examples/sessions/overview/example/example.go
index ed4fb5e49..e47624d72 100644
--- a/_examples/sessions/overview/example/example.go
+++ b/_examples/sessions/overview/example/example.go
@@ -38,9 +38,9 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
session := sessions.Get(ctx)
isNew := session.IsNew()
- session.Set("name", "iris")
+ session.Set("username", "iris")
- ctx.Writef("All ok session set to: %s [isNew=%t]", session.GetString("name"), isNew)
+ ctx.Writef("All ok session set to: %s [isNew=%t]", session.GetString("username"), isNew)
})
app.Get("/get", func(ctx iris.Context) {
@@ -48,9 +48,9 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
// get a specific value, as string,
// if not found then it returns just an empty string.
- name := session.GetString("name")
+ name := session.GetString("username")
- ctx.Writef("The name on the /set was: %s", name)
+ ctx.Writef("The username on the /set was: %s", name)
})
app.Get("/set-struct", func(ctx iris.Context) {
diff --git a/_examples/sessions/viewdata/main.go b/_examples/sessions/viewdata/main.go
new file mode 100644
index 000000000..44aae9c1e
--- /dev/null
+++ b/_examples/sessions/viewdata/main.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/sessions"
+)
+
+func main() {
+ app := iris.New()
+ app.RegisterView(iris.HTML("./views", ".html"))
+
+ sess := sessions.New(sessions.Config{Cookie: "session_cookie", AllowReclaim: true})
+ app.Use(sess.Handler())
+ // ^ use app.UseRouter instead to access sessions on HTTP errors too.
+
+ // Register our custom middleware, after the sessions middleware.
+ app.Use(setSessionViewData)
+
+ app.Get("/", index)
+ app.Listen(":8080")
+}
+
+func setSessionViewData(ctx iris.Context) {
+ session := sessions.Get(ctx)
+ ctx.ViewData("session", session)
+ ctx.Next()
+}
+
+func index(ctx iris.Context) {
+ session := sessions.Get(ctx)
+ session.Set("username", "kataras")
+ ctx.View("index")
+ /* OR without middleware:
+ ctx.View("index", iris.Map{
+ "session": session,
+ // {{.session.Get "username"}}
+ // OR to pass only the 'username':
+ // "username": session.Get("username"),
+ // {{.username}}
+ })
+ */
+}
diff --git a/_examples/sessions/viewdata/views/index.html b/_examples/sessions/viewdata/views/index.html
new file mode 100644
index 000000000..f93e5a382
--- /dev/null
+++ b/_examples/sessions/viewdata/views/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Sessions View Data
+
+
+ Hello {{.session.Get "username"}}
+
+
\ No newline at end of file
diff --git a/aliases.go b/aliases.go
index cd49bec78..b21ace34a 100644
--- a/aliases.go
+++ b/aliases.go
@@ -59,6 +59,10 @@ type (
Filter = context.Filter
// A Map is an alias of map[string]interface{}.
Map = context.Map
+ // User is a generic view of an authorized client.
+ // See `Context.User` and `SetUser` methods for more.
+ // An alias for the `context/User` type.
+ User = context.User
// Problem Details for HTTP APIs.
// Pass a Problem value to `context.Problem` to
// write an "application/problem+json" response.
@@ -475,8 +479,6 @@ var (
// on post data, versioning feature and others.
// An alias of `context.ErrNotFound`.
ErrNotFound = context.ErrNotFound
- // IsErrPrivate reports whether the given "err" is a private one.
- IsErrPrivate = context.IsErrPrivate
// NewProblem returns a new Problem.
// Head over to the `Problem` type godoc for more.
//
@@ -502,6 +504,9 @@ var (
//
// A shortcut for the `context#ErrPushNotSupported`.
ErrPushNotSupported = context.ErrPushNotSupported
+ // PrivateError accepts an error and returns a wrapped private one.
+ // A shortcut for the `context#PrivateError`.
+ PrivateError = context.PrivateError
)
// HTTP Methods copied from `net/http`.
diff --git a/configuration.go b/configuration.go
index e653e7df0..bc9f04e8e 100644
--- a/configuration.go
+++ b/configuration.go
@@ -715,7 +715,7 @@ type Configuration struct {
// The body will not be changed and existing data before the
// context.UnmarshalBody/ReadJSON/ReadXML will be not consumed.
//
- // See `Context.RecordBody` method for the same feature, per-request.
+ // See `Context.RecordRequestBody` method for the same feature, per-request.
DisableBodyConsumptionOnUnmarshal bool `ini:"disable_body_consumption" json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"`
// FireEmptyFormError returns if set to tue true then the `context.ReadBody/ReadForm`
// will return an `iris.ErrEmptyForm` on empty request form data.
diff --git a/context/context.go b/context/context.go
index 42b623932..714980102 100644
--- a/context/context.go
+++ b/context/context.go
@@ -714,9 +714,10 @@ func (ctx *Context) StopWithError(statusCode int, err error) {
}
ctx.SetErr(err)
- if IsErrPrivate(err) {
- // error is private, we can't render it, instead .
- // let the error handler render the code text.
+ if _, ok := err.(ErrPrivate); ok {
+ // error is private, we SHOULD not render it,
+ // leave the error handler alone to
+ // render the code's text instead.
ctx.StopWithStatus(statusCode)
return
}
@@ -2129,11 +2130,11 @@ func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
-// RecordBody same as the Application's DisableBodyConsumptionOnUnmarshal configuration field
-// but acts for the current request.
+// RecordRequestBody same as the Application's DisableBodyConsumptionOnUnmarshal
+// configuration field but acts only for the current request.
// It makes the request body readable more than once.
-func (ctx *Context) RecordBody() {
- ctx.values.Set(disableRequestBodyConsumptionContextKey, true)
+func (ctx *Context) RecordRequestBody(b bool) {
+ ctx.values.Set(disableRequestBodyConsumptionContextKey, b)
}
// IsRecordingBody reports whether the request body can be readen multiple times.
@@ -2145,7 +2146,7 @@ func (ctx *Context) IsRecordingBody() bool {
// GetBody reads and returns the request body.
// The default behavior for the http request reader is to consume the data readen
// but you can change that behavior by passing the `WithoutBodyConsumptionOnUnmarshal` Iris option
-// or by calling the `RecordBody` method.
+// or by calling the `RecordRequestBody` method.
//
// However, whenever you can use the `ctx.Request().Body` instead.
func (ctx *Context) GetBody() ([]byte, error) {
@@ -2358,6 +2359,30 @@ func (ctx *Context) ReadParams(ptr interface{}) error {
return ctx.app.Validate(ptr)
}
+// ReadURL is a shortcut of ReadParams and ReadQuery.
+// It binds dynamic path parameters and URL query parameters
+// to the "ptr" pointer struct value.
+// The struct fields may contain "url" or "param" binding tags.
+// If a validator exists then it validates the result too.
+func (ctx *Context) ReadURL(ptr interface{}) error {
+ values := make(map[string][]string, ctx.params.Len())
+ ctx.params.Visit(func(key string, value string) {
+ values[key] = strings.Split(value, "/")
+ })
+
+ for k, v := range ctx.getQuery() {
+ values[k] = append(values[k], v...)
+ }
+
+ // Decode using all available binding tags (url, header, param).
+ err := schema.Decode(values, ptr)
+ if err != nil {
+ return err
+ }
+
+ return ctx.app.Validate(ptr)
+}
+
// ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error.
// Look `ReadJSONProtobuf` too.
func (ctx *Context) ReadProtobuf(ptr proto.Message) error {
@@ -2409,7 +2434,28 @@ func (ctx *Context) ReadMsgPack(ptr interface{}) error {
// If a GET method request then it reads from a form (or URL Query), otherwise
// it tries to match (depending on the request content-type) the data format e.g.
// JSON, Protobuf, MsgPack, XML, YAML, MultipartForm and binds the result to the "ptr".
+// As a special case if the "ptr" was a pointer to string or []byte
+// then it will bind it to the request body as it is.
func (ctx *Context) ReadBody(ptr interface{}) error {
+
+ // If the ptr is string or byte, read the body as it's.
+ switch v := ptr.(type) {
+ case *string:
+ b, err := ctx.GetBody()
+ if err != nil {
+ return err
+ }
+
+ *v = string(b)
+ case *[]byte:
+ b, err := ctx.GetBody()
+ if err != nil {
+ return err
+ }
+
+ copy(*v, b)
+ }
+
if ctx.Method() == http.MethodGet {
if ctx.Request().URL.RawQuery != "" {
// try read from query.
@@ -5065,8 +5111,6 @@ func (ctx *Context) IsDebug() bool {
return ctx.app.IsDebug()
}
-const errorContextKey = "iris.context.error"
-
// SetErr is just a helper that sets an error value
// as a context value, it does nothing more.
// Also, by-default this error's value is written to the client
@@ -5088,14 +5132,71 @@ func (ctx *Context) SetErr(err error) {
// GetErr is a helper which retrieves
// the error value stored by `SetErr`.
+//
+// Note that, if an error was stored by `SetErrPrivate`
+// then it returns the underline/original error instead
+// of the internal error wrapper.
func (ctx *Context) GetErr() error {
+ _, err := ctx.GetErrPublic()
+ return err
+}
+
+// ErrPrivate if provided then the error saved in context
+// should NOT be visible to the client no matter what.
+type ErrPrivate interface {
+ error
+ IrisPrivateError()
+}
+
+// An internal wrapper for the `SetErrPrivate` method.
+type privateError struct{ error }
+
+func (e privateError) IrisPrivateError() {}
+
+// PrivateError accepts an error and returns a wrapped private one.
+func PrivateError(err error) ErrPrivate {
+ if err == nil {
+ return nil
+ }
+
+ errPrivate, ok := err.(ErrPrivate)
+ if !ok {
+ errPrivate = privateError{err}
+ }
+
+ return errPrivate
+}
+
+const errorContextKey = "iris.context.error"
+
+// SetErrPrivate sets an error that it's only accessible through `GetErr`
+// and it should never be sent to the client.
+//
+// Same as ctx.SetErr with an error that completes the `ErrPrivate` interface.
+// See `GetErrPublic` too.
+func (ctx *Context) SetErrPrivate(err error) {
+ ctx.SetErr(PrivateError(err))
+}
+
+// GetErrPublic reports whether the stored error
+// can be displayed to the client without risking
+// to expose security server implementation to the client.
+//
+// If the error is not nil, it is always the original one.
+func (ctx *Context) GetErrPublic() (bool, error) {
if v := ctx.values.Get(errorContextKey); v != nil {
- if err, ok := v.(error); ok {
- return err
+ switch err := v.(type) {
+ case privateError:
+ // If it's an error set by SetErrPrivate then unwrap it.
+ return false, err.error
+ case ErrPrivate:
+ return false, err
+ case error:
+ return true, err
}
}
- return nil
+ return false, nil
}
// ErrPanicRecovery may be returned from `Context` actions of a `Handler`
@@ -5135,22 +5236,6 @@ func IsErrPanicRecovery(err error) (*ErrPanicRecovery, bool) {
return v, ok
}
-// ErrPrivate if provided then the error saved in context
-// should NOT be visible to the client no matter what.
-type ErrPrivate interface {
- IrisPrivateError()
-}
-
-// IsErrPrivate reports whether the given "err" is a private one.
-func IsErrPrivate(err error) bool {
- if err == nil {
- return false
- }
-
- _, ok := err.(ErrPrivate)
- return ok
-}
-
// IsRecovered reports whether this handler has been recovered
// by the Iris recover middleware.
func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) {
@@ -5255,12 +5340,34 @@ func (ctx *Context) Logout(args ...interface{}) error {
const userContextKey = "iris.user"
-// SetUser sets a User for this request.
+// SetUser sets a value as a User for this request.
// It's used by auth middlewares as a common
// method to provide user information to the
-// next handlers in the chain.
-func (ctx *Context) SetUser(u User) {
+// next handlers in the chain
+// Look the `User` method to retrieve it.
+func (ctx *Context) SetUser(i interface{}) error {
+ if i == nil {
+ ctx.values.Remove(userContextKey)
+ return nil
+ }
+
+ u, ok := i.(User)
+ if !ok {
+ if m, ok := i.(Map); ok { // it's a map, convert it to a User.
+ u = UserMap(m)
+ } else {
+ // It's a structure, wrap it and let
+ // runtime decide the features.
+ p := newUserPartial(i)
+ if p == nil {
+ return ErrNotSupported
+ }
+ u = p
+ }
+ }
+
ctx.values.Set(userContextKey, u)
+ return nil
}
// User returns the registered User of this request.
diff --git a/context/context_user.go b/context/context_user.go
index 093f3dcd6..893b86d60 100644
--- a/context/context_user.go
+++ b/context/context_user.go
@@ -1,8 +1,11 @@
package context
import (
+ "encoding/json"
"errors"
+ "strings"
"time"
+ "unicode"
)
// ErrNotSupported is fired when a specific method is not implemented
@@ -21,124 +24,458 @@ var ErrNotSupported = errors.New("not supported")
//
// The caller is free to cast this with the implementation directly
// when special features are offered by the authorization system.
+//
+// To make optional some of the fields you can just embed the User interface
+// and implement whatever methods you want to support.
+//
+// There are three builtin implementations of the User interface:
+// - SimpleUser
+// - UserMap (a wrapper by SetUser)
+// - UserPartial (a wrapper by SetUser)
type User interface {
// GetAuthorization should return the authorization method,
// e.g. Basic Authentication.
- GetAuthorization() string
+ GetAuthorization() (string, error)
// GetAuthorizedAt should return the exact time the
// client has been authorized for the "first" time.
- GetAuthorizedAt() time.Time
+ GetAuthorizedAt() (time.Time, error)
+ // GetID should return the ID of the User.
+ GetID() (string, error)
// GetUsername should return the name of the User.
- GetUsername() string
+ GetUsername() (string, error)
// GetPassword should return the encoded or raw password
// (depends on the implementation) of the User.
- GetPassword() string
+ GetPassword() (string, error)
// GetEmail should return the e-mail of the User.
- GetEmail() string
+ GetEmail() (string, error)
+ // GetRoles should optionally return the specific user's roles.
+ // Returns `ErrNotSupported` if this method is not
+ // implemented by the User implementation.
+ GetRoles() ([]string, error)
+ // GetToken should optionally return a token used
+ // to authorize this User.
+ GetToken() ([]byte, error)
+ // GetField should optionally return a dynamic field
+ // based on its key. Useful for custom user fields.
+ // Keep in mind that these fields are encoded as a separate JSON key.
+ GetField(key string) (interface{}, error)
+} /* Notes:
+We could use a structure of User wrapper and separate interfaces for each of the methods
+so they return ErrNotSupported if the implementation is missing it, so the `Features`
+field and HasUserFeature can be omitted and
+add a Raw() interface{} to return the underline User implementation too.
+The advandages of the above idea is that we don't have to add new methods
+for each of the builtin features and we can keep the (assumed) struct small.
+But we dont as it has many disadvantages, unless is requested.
+^ UPDATE: this is done through UserPartial.
+
+The disadvantage of the current implementation is that the developer MUST
+complete the whole interface in order to be a valid User and if we add
+new methods in the future their implementation will break
+(unless they have a static interface implementation check as we have on SimpleUser).
+We kind of by-pass this disadvantage by providing a SimpleUser which can be embedded (as pointer)
+to the end-developer's custom implementations.
+*/
+
+// SimpleUser is a simple implementation of the User interface.
+type SimpleUser struct {
+ Authorization string `json:"authorization,omitempty"`
+ AuthorizedAt time.Time `json:"authorized_at,omitempty"`
+ ID string `json:"id,omitempty"`
+ Username string `json:"username,omitempty"`
+ Password string `json:"-"`
+ Email string `json:"email,omitempty"`
+ Roles []string `json:"roles,omitempty"`
+ Token json.RawMessage `json:"token,omitempty"`
+ Fields Map `json:"fields,omitempty"`
+}
+
+var _ User = (*SimpleUser)(nil)
+
+// GetAuthorization returns the authorization method,
+// e.g. Basic Authentication.
+func (u *SimpleUser) GetAuthorization() (string, error) {
+ return u.Authorization, nil
}
-// FeaturedUser optional interface that a User can implement.
-type FeaturedUser interface {
- User
- // GetFeatures should optionally return a list of features
- // the User implementation offers.
- GetFeatures() []UserFeature
+// GetAuthorizedAt returns the exact time the
+// client has been authorized for the "first" time.
+func (u *SimpleUser) GetAuthorizedAt() (time.Time, error) {
+ return u.AuthorizedAt, nil
}
-// UserFeature a type which represents a user's optional feature.
-// See `HasUserFeature` function for more.
-type UserFeature uint32
+// GetID returns the ID of the User.
+func (u *SimpleUser) GetID() (string, error) {
+ return u.ID, nil
+}
-// The list of standard UserFeatures.
-const (
- AuthorizedAtFeature UserFeature = iota
- UsernameFeature
- PasswordFeature
- EmailFeature
-)
+// GetUsername returns the name of the User.
+func (u *SimpleUser) GetUsername() (string, error) {
+ return u.Username, nil
+}
-// HasUserFeature reports whether the "u" User
-// implements a specific "feature" User Feature.
+// GetPassword returns the raw password of the User.
+func (u *SimpleUser) GetPassword() (string, error) {
+ return u.Password, nil
+}
+
+// GetEmail returns the e-mail of (string,error) User.
+func (u *SimpleUser) GetEmail() (string, error) {
+ return u.Email, nil
+}
+
+// GetRoles returns the specific user's roles.
+// Returns with `ErrNotSupported` if the Roles field is not initialized.
+func (u *SimpleUser) GetRoles() ([]string, error) {
+ if u.Roles == nil {
+ return nil, ErrNotSupported
+ }
+
+ return u.Roles, nil
+}
+
+// GetToken returns the token associated with this User.
+// It may return empty if the User is not featured with a Token.
//
-// It returns ErrNotSupported if a user does not implement
-// the FeaturedUser interface.
-func HasUserFeature(user User, feature UserFeature) (bool, error) {
- if u, ok := user.(FeaturedUser); ok {
- for _, f := range u.GetFeatures() {
- if f == feature {
- return true, nil
- }
+// The implementation can change that behavior.
+// Returns with `ErrNotSupported` if the Token field is empty.
+func (u *SimpleUser) GetToken() ([]byte, error) {
+ if len(u.Token) == 0 {
+ return nil, ErrNotSupported
+ }
+
+ return u.Token, nil
+}
+
+// GetField optionally returns a dynamic field from the `Fields` field
+// based on its key.
+func (u *SimpleUser) GetField(key string) (interface{}, error) {
+ if u.Fields == nil {
+ return nil, ErrNotSupported
+ }
+
+ return u.Fields[key], nil
+}
+
+// UserMap can be used to convert a common map[string]interface{} to a User.
+// Usage:
+// user := map[string]interface{}{
+// "username": "kataras",
+// "age" : 27,
+// }
+// ctx.SetUser(user)
+// OR
+// user := UserStruct{....}
+// ctx.SetUser(user)
+// [...]
+// username, err := ctx.User().GetUsername()
+// field,err := ctx.User().GetField("age")
+// age := field.(int)
+// OR cast it:
+// user := ctx.User().(UserMap)
+// username := user["username"].(string)
+// age := user["age"].(int)
+type UserMap Map
+
+var _ User = UserMap{}
+
+// GetAuthorization returns the authorization or Authorization value of the map.
+func (u UserMap) GetAuthorization() (string, error) {
+ return u.str("authorization")
+}
+
+// GetAuthorizedAt returns the authorized_at or Authorized_At value of the map.
+func (u UserMap) GetAuthorizedAt() (time.Time, error) {
+ return u.time("authorized_at")
+}
+
+// GetID returns the id or Id or ID value of the map.
+func (u UserMap) GetID() (string, error) {
+ return u.str("id")
+}
+
+// GetUsername returns the username or Username value of the map.
+func (u UserMap) GetUsername() (string, error) {
+ return u.str("username")
+}
+
+// GetPassword returns the password or Password value of the map.
+func (u UserMap) GetPassword() (string, error) {
+ return u.str("password")
+}
+
+// GetEmail returns the email or Email value of the map.
+func (u UserMap) GetEmail() (string, error) {
+ return u.str("email")
+}
+
+// GetRoles returns the roles or Roles value of the map.
+func (u UserMap) GetRoles() ([]string, error) {
+ return u.strSlice("roles")
+}
+
+// GetToken returns the roles or Roles value of the map.
+func (u UserMap) GetToken() ([]byte, error) {
+ return u.bytes("token")
+}
+
+// GetField returns the raw map's value based on its "key".
+// It's not kind of useful here as you can just use the map.
+func (u UserMap) GetField(key string) (interface{}, error) {
+ return u[key], nil
+}
+
+func (u UserMap) val(key string) interface{} {
+ isTitle := unicode.IsTitle(rune(key[0])) // if starts with uppercase.
+ if isTitle {
+ key = strings.ToLower(key)
+ }
+
+ return u[key]
+}
+
+func (u UserMap) bytes(key string) ([]byte, error) {
+ if v := u.val(key); v != nil {
+ switch s := v.(type) {
+ case []byte:
+ return s, nil
+ case string:
+ return []byte(s), nil
}
+ }
- return false, nil
+ return nil, ErrNotSupported
+}
+
+func (u UserMap) str(key string) (string, error) {
+ if v := u.val(key); v != nil {
+ if s, ok := v.(string); ok {
+ return s, nil
+ }
+
+ // exists or not we don't care, if it's invalid type we don't fill it.
}
- return false, ErrNotSupported
+ return "", ErrNotSupported
}
-// SimpleUser is a simple implementation of the User interface.
-type SimpleUser struct {
- Authorization string `json:"authorization"`
- AuthorizedAt time.Time `json:"authorized_at"`
- Username string `json:"username"`
- Password string `json:"-"`
- Email string `json:"email,omitempty"`
- Features []UserFeature `json:"-"`
+func (u UserMap) strSlice(key string) ([]string, error) {
+ if v := u.val(key); v != nil {
+ if s, ok := v.([]string); ok {
+ return s, nil
+ }
+ }
+
+ return nil, ErrNotSupported
}
-var _ User = (*SimpleUser)(nil)
+func (u UserMap) time(key string) (time.Time, error) {
+ if v := u.val(key); v != nil {
+ if t, ok := v.(time.Time); ok {
+ return t, nil
+ }
+ }
-// GetAuthorization returns the authorization method,
+ return time.Time{}, ErrNotSupported
+}
+
+type (
+ userGetAuthorization interface {
+ GetAuthorization() string
+ }
+
+ userGetAuthorizedAt interface {
+ GetAuthorizedAt() time.Time
+ }
+
+ userGetID interface {
+ GetID() string
+ }
+
+ userGetUsername interface {
+ GetUsername() string
+ }
+
+ userGetPassword interface {
+ GetPassword() string
+ }
+
+ userGetEmail interface {
+ GetEmail() string
+ }
+
+ userGetRoles interface {
+ GetRoles() []string
+ }
+
+ userGetToken interface {
+ GetToken() []byte
+ }
+
+ userGetField interface {
+ GetField(string) interface{}
+ }
+
+ // UserPartial is a User.
+ // It's a helper which wraps a struct value that
+ // may or may not complete the whole User interface.
+ UserPartial struct {
+ Raw interface{}
+ userGetAuthorization
+ userGetAuthorizedAt
+ userGetID
+ userGetUsername
+ userGetPassword
+ userGetEmail
+ userGetRoles
+ userGetToken
+ userGetField
+ }
+)
+
+var _ User = (*UserPartial)(nil)
+
+func newUserPartial(i interface{}) *UserPartial {
+ containsAtLeastOneMethod := false
+ p := &UserPartial{Raw: i}
+
+ if u, ok := i.(userGetAuthorization); ok {
+ p.userGetAuthorization = u
+ containsAtLeastOneMethod = true
+ }
+
+ if u, ok := i.(userGetAuthorizedAt); ok {
+ p.userGetAuthorizedAt = u
+ containsAtLeastOneMethod = true
+ }
+
+ if u, ok := i.(userGetID); ok {
+ p.userGetID = u
+ containsAtLeastOneMethod = true
+ }
+
+ if u, ok := i.(userGetUsername); ok {
+ p.userGetUsername = u
+ containsAtLeastOneMethod = true
+ }
+
+ if u, ok := i.(userGetPassword); ok {
+ p.userGetPassword = u
+ containsAtLeastOneMethod = true
+ }
+
+ if u, ok := i.(userGetEmail); ok {
+ p.userGetEmail = u
+ containsAtLeastOneMethod = true
+ }
+
+ if u, ok := i.(userGetRoles); ok {
+ p.userGetRoles = u
+ containsAtLeastOneMethod = true
+ }
+
+ if u, ok := i.(userGetToken); ok {
+ p.userGetToken = u
+ containsAtLeastOneMethod = true
+ }
+
+ if u, ok := i.(userGetField); ok {
+ p.userGetField = u
+ containsAtLeastOneMethod = true
+ }
+
+ if !containsAtLeastOneMethod {
+ return nil
+ }
+
+ return p
+}
+
+// GetAuthorization should return the authorization method,
// e.g. Basic Authentication.
-func (u *SimpleUser) GetAuthorization() string {
- return u.Authorization
+func (u *UserPartial) GetAuthorization() (string, error) {
+ if v := u.userGetAuthorization; v != nil {
+ return v.GetAuthorization(), nil
+ }
+
+ return "", ErrNotSupported
}
-// GetAuthorizedAt returns the exact time the
+// GetAuthorizedAt should return the exact time the
// client has been authorized for the "first" time.
-func (u *SimpleUser) GetAuthorizedAt() time.Time {
- return u.AuthorizedAt
-}
+func (u *UserPartial) GetAuthorizedAt() (time.Time, error) {
+ if v := u.userGetAuthorizedAt; v != nil {
+ return v.GetAuthorizedAt(), nil
+ }
-// GetUsername returns the name of the User.
-func (u *SimpleUser) GetUsername() string {
- return u.Username
+ return time.Time{}, ErrNotSupported
}
-// GetPassword returns the raw password of the User.
-func (u *SimpleUser) GetPassword() string {
- return u.Password
+// GetID should return the ID of the User.
+func (u *UserPartial) GetID() (string, error) {
+ if v := u.userGetID; v != nil {
+ return v.GetID(), nil
+ }
+
+ return "", ErrNotSupported
}
-// GetEmail returns the e-mail of the User.
-func (u *SimpleUser) GetEmail() string {
- return u.Email
+// GetUsername should return the name of the User.
+func (u *UserPartial) GetUsername() (string, error) {
+ if v := u.userGetUsername; v != nil {
+ return v.GetUsername(), nil
+ }
+
+ return "", ErrNotSupported
}
-// GetFeatures returns a list of features
-// this User implementation offers.
-func (u *SimpleUser) GetFeatures() []UserFeature {
- if u.Features != nil {
- return u.Features
+// GetPassword should return the encoded or raw password
+// (depends on the implementation) of the User.
+func (u *UserPartial) GetPassword() (string, error) {
+ if v := u.userGetPassword; v != nil {
+ return v.GetPassword(), nil
}
- var features []UserFeature
+ return "", ErrNotSupported
+}
- if !u.AuthorizedAt.IsZero() {
- features = append(features, AuthorizedAtFeature)
+// GetEmail should return the e-mail of the User.
+func (u *UserPartial) GetEmail() (string, error) {
+ if v := u.userGetEmail; v != nil {
+ return v.GetEmail(), nil
}
- if u.Username != "" {
- features = append(features, UsernameFeature)
+ return "", ErrNotSupported
+}
+
+// GetRoles should optionally return the specific user's roles.
+// Returns `ErrNotSupported` if this method is not
+// implemented by the User implementation.
+func (u *UserPartial) GetRoles() ([]string, error) {
+ if v := u.userGetRoles; v != nil {
+ return v.GetRoles(), nil
}
- if u.Password != "" {
- features = append(features, PasswordFeature)
+ return nil, ErrNotSupported
+}
+
+// GetToken should optionally return a token used
+// to authorize this User.
+func (u *UserPartial) GetToken() ([]byte, error) {
+ if v := u.userGetToken; v != nil {
+ return v.GetToken(), nil
}
- if u.Email != "" {
- features = append(features, EmailFeature)
+ return nil, ErrNotSupported
+}
+
+// GetField should optionally return a dynamic field
+// based on its key. Useful for custom user fields.
+// Keep in mind that these fields are encoded as a separate JSON key.
+func (u *UserPartial) GetField(key string) (interface{}, error) {
+ if v := u.userGetField; v != nil {
+ return v.GetField(key), nil
}
- return features
+ return nil, ErrNotSupported
}
diff --git a/core/router/handler.go b/core/router/handler.go
index 08195abfe..3ec5a4834 100644
--- a/core/router/handler.go
+++ b/core/router/handler.go
@@ -129,15 +129,14 @@ type RoutesProvider interface { // api builder
}
func defaultErrorHandler(ctx *context.Context) {
- if err := ctx.GetErr(); err != nil {
- if !context.IsErrPrivate(err) {
- ctx.WriteString(err.Error())
- return
- }
+ if ok, err := ctx.GetErrPublic(); ok {
+ // If an error is stored and it's not a private one
+ // write it to the response body.
+ ctx.WriteString(err.Error())
+ return
}
-
+ // Otherwise, write the code's text instead.
ctx.WriteString(context.StatusText(ctx.GetStatusCode()))
-
}
func (h *routerHandler) Build(provider RoutesProvider) error {
diff --git a/go.mod b/go.mod
index fd3c1ee95..20a4c918d 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.0.3
+ 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.2.3
+ 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,26 +21,26 @@ 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.5
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.7
- 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-20201002170205-7f63de1d35b0
- golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c
- golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
- 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.61.0
+ gopkg.in/ini.v1 v1.62.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
)
diff --git a/hero/container.go b/hero/container.go
index a9b142b1a..ded50532c 100644
--- a/hero/container.go
+++ b/hero/container.go
@@ -170,9 +170,23 @@ var BuiltinDependencies = []*Dependency{
NewDependency(func(ctx *context.Context) Code {
return Code(ctx.GetStatusCode())
}).Explicitly(),
+ // Context Error. May be nil
NewDependency(func(ctx *context.Context) Err {
- return Err(ctx.GetErr())
+ err := ctx.GetErr()
+ if err == nil {
+ return nil
+ }
+ return err
}).Explicitly(),
+ // Context User, e.g. from basic authentication.
+ NewDependency(func(ctx *context.Context) context.User {
+ u := ctx.User()
+ if u == nil {
+ return nil
+ }
+
+ return u
+ }),
// payload and param bindings are dynamically allocated and declared at the end of the `binding` source file.
}
diff --git a/httptest/httptest.go b/httptest/httptest.go
index cbd7edc32..4a5a6a18b 100644
--- a/httptest/httptest.go
+++ b/httptest/httptest.go
@@ -67,11 +67,11 @@ var (
}
}
- // LogLevel sets the application's log level "val".
+ // LogLevel sets the application's log level.
// Defaults to disabled when testing.
- LogLevel = func(val string) OptionSet {
+ LogLevel = func(level string) OptionSet {
return func(c *Configuration) {
- c.LogLevel = val
+ c.LogLevel = level
}
}
)
diff --git a/middleware/README.md b/middleware/README.md
index 3c0469325..2f11609e7 100644
--- a/middleware/README.md
+++ b/middleware/README.md
@@ -32,7 +32,6 @@ Most of the experimental handlers are ported to work with _iris_'s handler form,
| [casbin](https://github.com/iris-contrib/middleware/tree/master/casbin)| An authorization library that supports access control models like ACL, RBAC, ABAC | [iris-contrib/middleware/casbin/_examples](https://github.com/iris-contrib/middleware/tree/master/casbin/_examples) |
| [sentry-go (ex. raven)](https://github.com/getsentry/sentry-go/tree/master/iris)| Sentry client in Go | [sentry-go/example/iris](https://github.com/getsentry/sentry-go/blob/master/example/iris/main.go) |
| [csrf](https://github.com/iris-contrib/middleware/tree/master/csrf)| Cross-Site Request Forgery Protection | [iris-contrib/middleware/csrf/_example](https://github.com/iris-contrib/middleware/blob/master/csrf/_example/main.go) |
-| [go-i18n](https://github.com/iris-contrib/middleware/tree/master/go-i18n)| i18n Iris Loader for nicksnyder/go-i18n | [iris-contrib/middleware/go-i18n/_example](https://github.com/iris-contrib/middleware/blob/master/go-i18n/_example/main.go) |
| [throttler](https://github.com/iris-contrib/middleware/tree/master/throttler)| Rate limiting access to HTTP endpoints | [iris-contrib/middleware/throttler/_example](https://github.com/iris-contrib/middleware/blob/master/throttler/_example/main.go) |
Third-Party Handlers
diff --git a/middleware/accesslog/accesslog.go b/middleware/accesslog/accesslog.go
index 090cad818..195669b33 100644
--- a/middleware/accesslog/accesslog.go
+++ b/middleware/accesslog/accesslog.go
@@ -718,7 +718,7 @@ func (ac *AccessLog) Handler(ctx *context.Context) {
// Enable reading the request body
// multiple times (route handler and this middleware).
if ac.shouldReadRequestBody() {
- ctx.RecordBody()
+ ctx.RecordRequestBody(true)
}
// Set the fields context value so they can be modified
diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go
index 5ff44ab1d..2eff61dc9 100644
--- a/middleware/basicauth/basicauth_test.go
+++ b/middleware/basicauth/basicauth_test.go
@@ -26,7 +26,11 @@ func TestBasicAuthUseRouter(t *testing.T) {
app.Get("/user_string", func(ctx iris.Context) {
user := ctx.User()
- ctx.Writef("%s\n%s\n%s", user.GetAuthorization(), user.GetUsername(), user.GetPassword())
+
+ authorization, _ := user.GetAuthorization()
+ username, _ := user.GetUsername()
+ password, _ := user.GetPassword()
+ ctx.Writef("%s\n%s\n%s", authorization, username, password)
})
app.Get("/", func(ctx iris.Context) {
diff --git a/middleware/jwt/aliases.go b/middleware/jwt/aliases.go
new file mode 100644
index 000000000..475d4692f
--- /dev/null
+++ b/middleware/jwt/aliases.go
@@ -0,0 +1,126 @@
+package jwt
+
+import "github.com/kataras/jwt"
+
+// Error values.
+var (
+ ErrBlocked = jwt.ErrBlocked
+ ErrDecrypt = jwt.ErrDecrypt
+ ErrExpected = jwt.ErrExpected
+ ErrExpired = jwt.ErrExpired
+ ErrInvalidKey = jwt.ErrInvalidKey
+ ErrIssuedInTheFuture = jwt.ErrIssuedInTheFuture
+ ErrMissing = jwt.ErrMissing
+ ErrMissingKey = jwt.ErrMissingKey
+ ErrNotValidYet = jwt.ErrNotValidYet
+ ErrTokenAlg = jwt.ErrTokenAlg
+ ErrTokenForm = jwt.ErrTokenForm
+ ErrTokenSignature = jwt.ErrTokenSignature
+)
+
+// Signature algorithms.
+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
+)
+
+// 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
+)
+
+// Type alises for the underline jwt package.
+type (
+ // Alg is the signature algorithm interface alias.
+ Alg = jwt.Alg
+ // Claims represents the standard claim values (as specified in RFC 7519).
+ Claims = jwt.Claims
+ // 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
+
+ // 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
+)
+
+// 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
+
+ // ID is a shurtcut to set jwt ID on Sign.
+ ID = func(id string) jwt.SignOptionFunc {
+ return func(c *Claims) {
+ c.ID = id
+ }
+ }
+)
+
+// Shortcuts for Signing and Verifying.
+var (
+ Verify = jwt.Verify
+ VerifyEncryptedToken = jwt.VerifyEncrypted
+ Sign = jwt.Sign
+ SignEncrypted = jwt.SignEncrypted
+)
diff --git a/middleware/jwt/alises.go b/middleware/jwt/alises.go
deleted file mode 100644
index c7e4f9c77..000000000
--- a/middleware/jwt/alises.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package jwt
-
-import (
- "github.com/square/go-jose/v3"
- "github.com/square/go-jose/v3/jwt"
-)
-
-type (
- // Claims represents public 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
-)
-
-var (
- // NewNumericDate constructs NumericDate from time.Time value.
- NewNumericDate = jwt.NewNumericDate
-)
-
-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
-)
-
-// 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
-)
-
-// 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
-)
diff --git a/middleware/jwt/blocklist.go b/middleware/jwt/blocklist.go
new file mode 100644
index 000000000..da1ed6e66
--- /dev/null
+++ b/middleware/jwt/blocklist.go
@@ -0,0 +1,31 @@
+package jwt
+
+import (
+ "github.com/kataras/jwt"
+)
+
+// Blocklist should hold and manage invalidated-by-server tokens.
+// The `NewBlocklist` and `NewBlocklistContext` functions
+// returns a memory storage of tokens,
+// it is the internal "blocklist" struct.
+//
+// The end-developer can implement her/his own blocklist,
+// 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 {
+ jwt.TokenValidator
+
+ // InvalidateToken should invalidate a verified JWT token.
+ InvalidateToken(token []byte, c Claims) error
+ // Del should remove a token from the storage.
+ Del(key string) error
+ // Has should report whether a specific token exists in the storage.
+ Has(key string) (bool, error)
+ // Count should return the total amount of tokens stored.
+ Count() (int64, error)
+}
+
+type blocklistConnect interface {
+ Connect() error
+ IsConnected() bool
+}
diff --git a/middleware/jwt/blocklist/redis/blocklist.go b/middleware/jwt/blocklist/redis/blocklist.go
new file mode 100644
index 000000000..74b63cd84
--- /dev/null
+++ b/middleware/jwt/blocklist/redis/blocklist.go
@@ -0,0 +1,185 @@
+package redis
+
+import (
+ "context"
+ "io"
+ "sync/atomic"
+
+ "github.com/kataras/iris/v12/core/host"
+ "github.com/kataras/iris/v12/middleware/jwt"
+
+ "github.com/go-redis/redis/v8"
+)
+
+var defaultContext = context.Background()
+
+type (
+ // Options is just a type alias for the go-redis Client Options.
+ Options = redis.Options
+ // ClusterOptions is just a type alias for the go-redis Cluster Client Options.
+ ClusterOptions = redis.ClusterOptions
+)
+
+// Client is the interface which both
+// go-redis Client and Cluster Client implements.
+type Client interface {
+ redis.Cmdable // Commands.
+ io.Closer // CloseConnection.
+}
+
+// Blocklist is a jwt.Blocklist backed by Redis.
+type Blocklist struct {
+ // GetKey is a function which can be used how to extract
+ // the unique identifier for a token.
+ // Required. By default the token key is extracted through the claims.ID ("jti").
+ GetKey func(token []byte, claims jwt.Claims) string
+ // Prefix the token key into the redis database.
+ // Note that if you can also select a different database
+ // through ClientOptions (or ClusterOptions).
+ // Defaults to empty string (no prefix).
+ Prefix string
+ // Both Client and ClusterClient implements this interface.
+ client Client
+ connected uint32
+ // Customize any go-redis fields manually
+ // before Connect.
+ ClientOptions Options
+ ClusterOptions ClusterOptions
+}
+
+var _ jwt.Blocklist = (*Blocklist)(nil)
+
+// NewBlocklist returns a new redis-based Blocklist.
+// Modify its ClientOptions or ClusterOptions depending the application needs
+// and call its Connect.
+//
+// Usage:
+//
+// blocklist := NewBlocklist()
+// blocklist.ClientOptions.Addr = ...
+// err := blocklist.Connect()
+//
+// And register it:
+//
+// verifier := jwt.NewVerifier(...)
+// verifier.Blocklist = blocklist
+func NewBlocklist() *Blocklist {
+ return &Blocklist{
+ GetKey: defaultGetKey,
+ Prefix: "",
+ ClientOptions: Options{
+ Addr: "127.0.0.1:6379",
+ // The rest are defaulted to good values already.
+ },
+ // If its Addrs > 0 before connect then cluster client is used instead.
+ ClusterOptions: ClusterOptions{},
+ }
+}
+
+func defaultGetKey(_ []byte, claims jwt.Claims) string {
+ return claims.ID
+}
+
+// Connect prepares the redis client and fires a ping response to it.
+func (b *Blocklist) Connect() error {
+ if b.Prefix != "" {
+ getKey := b.GetKey
+ b.GetKey = func(token []byte, claims jwt.Claims) string {
+ return b.Prefix + getKey(token, claims)
+ }
+ }
+
+ if len(b.ClusterOptions.Addrs) > 0 {
+ // Use cluster client.
+ b.client = redis.NewClusterClient(&b.ClusterOptions)
+ } else {
+ b.client = redis.NewClient(&b.ClientOptions)
+ }
+
+ _, err := b.client.Ping(defaultContext).Result()
+ if err != nil {
+ return err
+ }
+
+ host.RegisterOnInterrupt(func() {
+ atomic.StoreUint32(&b.connected, 0)
+ b.client.Close()
+ })
+ atomic.StoreUint32(&b.connected, 1)
+
+ return nil
+}
+
+// IsConnected reports whether the Connect function was called.
+func (b *Blocklist) IsConnected() bool {
+ return atomic.LoadUint32(&b.connected) > 0
+}
+
+// ValidateToken checks if the token exists and
+func (b *Blocklist) ValidateToken(token []byte, c jwt.Claims, err error) error {
+ if err != nil {
+ if err == jwt.ErrExpired {
+ b.Del(b.GetKey(token, c))
+ }
+
+ return err // respect the previous error.
+ }
+
+ has, err := b.Has(b.GetKey(token, c))
+ if err != nil {
+ return err
+ } else if has {
+ return jwt.ErrBlocked
+ }
+
+ return nil
+}
+
+// InvalidateToken invalidates a verified JWT token.
+func (b *Blocklist) InvalidateToken(token []byte, c jwt.Claims) error {
+ key := b.GetKey(token, c)
+ return b.client.SetEX(defaultContext, key, token, c.Timeleft()).Err()
+}
+
+// Del removes a token from the storage.
+func (b *Blocklist) Del(key string) error {
+ return b.client.Del(defaultContext, key).Err()
+}
+
+// Has reports whether a specific token exists in the storage.
+func (b *Blocklist) Has(key string) (bool, error) {
+ n, err := b.client.Exists(defaultContext, key).Result()
+ return n > 0, err
+}
+
+// Count returns the total amount of tokens stored.
+func (b *Blocklist) Count() (int64, error) {
+ if b.Prefix == "" {
+ return b.client.DBSize(defaultContext).Result()
+ }
+
+ keys, err := b.getKeys(0)
+ if err != nil {
+ return 0, err
+ }
+
+ return int64(len(keys)), nil
+}
+
+func (b *Blocklist) getKeys(cursor uint64) ([]string, error) {
+ keys, cursor, err := b.client.Scan(defaultContext, cursor, b.Prefix+"*", 300000).Result()
+ if err != nil {
+ return nil, err
+ }
+
+ if cursor != 0 {
+ moreKeys, err := b.getKeys(cursor)
+ if err != nil {
+ return nil, err
+ }
+
+ keys = append(keys, moreKeys...)
+ }
+
+ return keys, nil
+}
diff --git a/middleware/jwt/extractor.go b/middleware/jwt/extractor.go
new file mode 100644
index 000000000..a04dcb98b
--- /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 196acd115..e2adc3128 100644
--- a/middleware/jwt/jwt.go
+++ b/middleware/jwt/jwt.go
@@ -1,613 +1,7 @@
package jwt
-import (
- "crypto"
- "encoding/json"
- "errors"
- "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
- 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.
-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{}
-}
-
-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.
-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
-}
-
-// Expiry returns a new standard Claims with
-// the `Expiry` and `IssuedAt` fields of the "claims" filled
-// based on the given "maxAge" duration.
-//
-// See the `JWT.Expiry` method too.
-func Expiry(maxAge time.Duration, claims Claims) Claims {
- now := time.Now()
- claims.Expiry = NewNumericDate(now.Add(maxAge))
- claims.IssuedAt = NewNumericDate(now)
- return claims
-}
-
-// Expiry method same as `Expiry` package-level function,
-// it returns a Claims with the expiration fields of the "claims"
-// filled based on the JWT's `MaxAge` field.
-// Only use it when this standard "claims"
-// is embedded on a custom claims structure.
-// Usage:
-// type UserClaims struct {
-// jwt.Claims
-// Username string
-// }
-// [...]
-// standardClaims := j.Expiry(jwt.Claims{...})
-// customClaims := UserClaims{
-// Claims: standardClaims,
-// Username: "kataras",
-// }
-// j.WriteToken(ctx, customClaims)
-func (j *JWT) Expiry(claims Claims) Claims {
- return Expiry(j.MaxAge, claims)
-}
-
-// Token generates and returns a new token string.
-// See `VerifyToken` too.
-func (j *JWT) Token(claims interface{}) (string, error) {
- // switch c := claims.(type) {
- // case Claims:
- // claims = Expiry(j.MaxAge, c)
- // case map[string]interface{}: let's not support map.
- // now := time.Now()
- // c["iat"] = now.Unix()
- // c["exp"] = now.Add(j.MaxAge).Unix()
- // }
- if c, ok := claims.(Claims); ok {
- claims = Expiry(j.MaxAge, c)
- }
-
- var (
- token string
- err error
- )
-
- // jwt.Builder and jwt.NestedBuilder contain same methods but they are not the same.
- if j.DecriptionKey != nil {
- token, err = jwt.SignedAndEncrypted(j.Signer, j.Encrypter).Claims(claims).CompactSerialize()
- } else {
- token, err = jwt.Signed(j.Signer).Claims(claims).CompactSerialize()
- }
-
- if err != nil {
- return "", err
- }
-
- return token, nil
-}
-
-/* Let's no support maps, typed claim is the way to go.
-// validateMapClaims validates claims of map type.
-func validateMapClaims(m map[string]interface{}, e jwt.Expected, leeway time.Duration) error {
- if !e.Time.IsZero() {
- if v, ok := m["nbf"]; ok {
- if notBefore, ok := v.(NumericDate); ok {
- if e.Time.Add(leeway).Before(notBefore.Time()) {
- return ErrNotValidYet
- }
- }
- }
-
- if v, ok := m["exp"]; ok {
- if exp, ok := v.(int64); ok {
- if e.Time.Add(-leeway).Before(time.Unix(exp, 0)) {
- return ErrExpired
- }
- }
- }
-
- if v, ok := m["iat"]; ok {
- if issuedAt, ok := v.(int64); ok {
- if e.Time.Add(leeway).Before(time.Unix(issuedAt, 0)) {
- return ErrIssuedInTheFuture
- }
- }
- }
- }
-
- return 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
-}
-
-var (
- // ErrMissing when token cannot be extracted from the request.
- ErrMissing = errors.New("token is missing")
- // 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)")
-)
-
-type (
- claimsValidator interface {
- ValidateWithLeeway(e jwt.Expected, leeway time.Duration) error
- }
- claimsAlternativeValidator interface { // to keep iris-contrib/jwt MapClaims compatible.
- Validate() error
- }
- claimsContextValidator interface {
- Validate(ctx *context.Context) error
- }
-)
-
-// IsValidated reports whether a token is already validated through
-// `VerifyToken`. It returns true when the claims are compatible
-// validators: a `Claims` value or a value that implements the `Validate() error` method.
-func IsValidated(ctx *context.Context) bool { // see the `ReadClaims`.
- return ctx.Values().Get(needsValidationContextKey) == nil
-}
-
-func validateClaims(ctx *context.Context, claims interface{}) (err error) {
- switch c := claims.(type) {
- case claimsValidator:
- err = c.ValidateWithLeeway(jwt.Expected{Time: time.Now()}, 0)
- case claimsAlternativeValidator:
- err = c.Validate()
- case claimsContextValidator:
- err = c.Validate(ctx)
- case *json.RawMessage:
- // if the data type is raw message (json []byte)
- // then it should contain exp (and iat and nbf) keys.
- // Unmarshal raw message to validate against.
- v := new(Claims)
- err = json.Unmarshal(*c, v)
- if err == nil {
- return validateClaims(ctx, v)
- }
- default:
- ctx.Values().Set(needsValidationContextKey, struct{}{})
- }
-
- if err != nil {
- switch err {
- case jwt.ErrExpired:
- return ErrExpired
- case jwt.ErrNotValidYet:
- return ErrNotValidYet
- case jwt.ErrIssuedInTheFuture:
- return ErrIssuedInTheFuture
- }
- }
-
- return err
-}
-
-// VerifyToken verifies (and decrypts) the request token,
-// it also validates and binds the parsed token's claims to the "claimsPtr" (destination).
-// It does return a nil error on success.
-func (j *JWT) VerifyToken(ctx *context.Context, claimsPtr interface{}) error {
- var token string
-
- for _, extract := range j.Extractors {
- if token = extract(ctx); token != "" {
- break // ok we found it.
- }
- }
-
- return j.VerifyTokenString(ctx, token, claimsPtr)
-}
-
-// VerifyTokenString verifies and unmarshals an extracted token to "claimsPtr" destination.
-// The Context is required when the claims validator needs it, otherwise can be nil.
-func (j *JWT) VerifyTokenString(ctx *context.Context, token string, claimsPtr interface{}) error {
- if token == "" {
- return ErrMissing
- }
-
- var (
- parsedToken *jwt.JSONWebToken
- err error
- )
-
- if j.DecriptionKey != nil {
- t, cerr := jwt.ParseSignedAndEncrypted(token)
- if cerr != nil {
- return cerr
- }
-
- parsedToken, err = t.Decrypt(j.DecriptionKey)
- } else {
- parsedToken, err = jwt.ParseSigned(token)
- }
- if err != nil {
- return err
- }
-
- if err = parsedToken.Claims(j.VerificationKey, claimsPtr); err != nil {
- return err
- }
-
- return validateClaims(ctx, claimsPtr)
-}
-
-const (
- // ClaimsContextKey is the context key which the jwt claims are stored from the `Verify` method.
- ClaimsContextKey = "iris.jwt.claims"
- needsValidationContextKey = "iris.jwt.claims.unvalidated"
-)
-
-// Verify is a middleware. It verifies and optionally decrypts an incoming request token.
-// It does write a 401 unauthorized status code if verification or decryption failed.
-// It calls the `ctx.Next` on verified requests.
-//
-// See `VerifyToken` instead to verify, decrypt, validate and acquire the claims at once.
-//
-// A call of `ReadClaims` is required to validate and acquire the jwt claims
-// on the next request.
-func (j *JWT) Verify(ctx *context.Context) {
- var raw json.RawMessage
- if err := j.VerifyToken(ctx, &raw); err != nil {
- ctx.StopWithStatus(401)
- return
- }
-
- ctx.Values().Set(ClaimsContextKey, raw)
- ctx.Next()
-}
-
-// ReadClaims binds the "claimsPtr" (destination)
-// to the verified (and decrypted) claims.
-// The `Verify` method should be called first (registered as middleware).
-func ReadClaims(ctx *context.Context, claimsPtr interface{}) error {
- v := ctx.Values().Get(ClaimsContextKey)
- if v == nil {
- return ErrMissing
- }
-
- raw, ok := v.(json.RawMessage)
- if !ok {
- return ErrMissing
- }
-
- err := json.Unmarshal(raw, claimsPtr)
- if err != nil {
- return err
- }
-
- if !IsValidated(ctx) {
- // If already validated on `Verify/VerifyToken`
- // then no need to perform the check again.
- ctx.Values().Remove(needsValidationContextKey)
- return validateClaims(ctx, claimsPtr)
- }
-
- return nil
-}
-
-// Get returns and validates (if not already) the claims
-// stored on request context's values storage.
-//
-// Should be used instead of the `ReadClaims` method when
-// a custom verification middleware was registered (see the `Verify` method for an example).
-//
-// Usage:
-// j := jwt.New(...)
-// [...]
-// app.Use(func(ctx iris.Context) {
-// var claims CustomClaims_or_jwt.Claims
-// if err := j.VerifyToken(ctx, &claims); err != nil {
-// ctx.StopWithStatus(iris.StatusUnauthorized)
-// return
-// }
-//
-// ctx.Values().Set(jwt.ClaimsContextKey, claims)
-// ctx.Next()
-// })
-// [...]
-// app.Post("/restricted", func(ctx iris.Context){
-// v, err := jwt.Get(ctx)
-// [handle error...]
-// claims,ok := v.(CustomClaims_or_jwt.Claims)
-// if !ok {
-// [do you support more than one type of claims? Handle here]
-// }
-// [use claims...]
-// })
-func Get(ctx *context.Context) (interface{}, error) {
- claims := ctx.Values().Get(ClaimsContextKey)
- if claims == nil {
- return nil, ErrMissing
- }
-
- if !IsValidated(ctx) {
- ctx.Values().Remove(needsValidationContextKey)
- err := validateClaims(ctx, claims)
- if err != nil {
- return nil, err
- }
- }
-
- return claims, nil
-}
diff --git a/middleware/jwt/jwt_test.go b/middleware/jwt/jwt_test.go
index 0d30d6fd4..b65c02b7e 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,129 +10,56 @@ import (
"github.com/kataras/iris/v12/middleware/jwt"
)
-type userClaims struct {
- jwt.Claims
- Username string
-}
-
-const testMaxAge = 3 * 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)
- })
- testWriteVerifyToken(t, j)
-}
-
-// HMAC verification and encryption.
-func TestHMAC(t *testing.T) {
- j := jwt.HMAC(testMaxAge, "secret", "itsa16bytesecret")
- testWriteVerifyToken(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)
- }
+var testAlg, testSecret = jwt.HS256, []byte("sercrethatmaycontainch@r$")
- testWriteVerifyToken(t, j)
+type fooClaims struct {
+ Foo string `json:"foo"`
}
-// 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)
- }
- testWriteVerifyToken(t, j)
-}
-
-func testWriteVerifyToken(t *testing.T, j *jwt.JWT) {
- t.Helper()
-
- j.Extractors = append(j.Extractors, jwt.FromJSON("access_token"))
- standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
- expectedClaims := userClaims{
- Claims: j.Expiry(standardClaims),
- 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.Get("/auth", func(ctx iris.Context) {
- j.WriteToken(ctx, expectedClaims)
- })
-
- app.Post("/restricted", func(ctx iris.Context) {
- var claims userClaims
- if err := j.VerifyToken(ctx, &claims); err != nil {
- ctx.StopWithStatus(iris.StatusUnauthorized)
- return
- }
-
- ctx.JSON(claims)
- })
-
- app.Post("/restricted_middleware_readclaims", j.Verify, func(ctx iris.Context) {
- var claims userClaims
- if err := jwt.ReadClaims(ctx, &claims); err != nil {
- ctx.StopWithStatus(iris.StatusUnauthorized)
- return
- }
-
- ctx.JSON(claims)
- })
- app.Post("/restricted_middleware_get", j.Verify, func(ctx iris.Context) {
- claims, err := jwt.Get(ctx)
+ 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.StopWithStatus(iris.StatusUnauthorized)
+ ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
-
- ctx.JSON(claims)
+ ctx.Write(token)
})
- e := httptest.New(t, app)
-
- // Get token.
- rawToken := e.GET("/auth").Expect().Status(httptest.StatusOK).Body().Raw()
- if rawToken == "" {
- t.Fatalf("empty token")
+ 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)
+ })
- restrictedPaths := [...]string{"/restricted", "/restricted_middleware_readclaims", "/restricted_middleware_get"}
-
- now := time.Now()
- for _, path := range restrictedPaths {
- // Authorization Header.
- e.POST(path).WithHeader("Authorization", "Bearer "+rawToken).Expect().
- Status(httptest.StatusOK).JSON().Equal(expectedClaims)
-
- // URL Query.
- e.POST(path).WithQuery("token", rawToken).Expect().
- Status(httptest.StatusOK).JSON().Equal(expectedClaims)
-
- // JSON Body.
- e.POST(path).WithJSON(iris.Map{"access_token": rawToken}).Expect().
- Status(httptest.StatusOK).JSON().Equal(expectedClaims)
-
- // Missing "Bearer".
- e.POST(path).WithHeader("Authorization", rawToken).Expect().
- Status(httptest.StatusUnauthorized)
- }
- expireRemDur := testMaxAge - time.Since(now)
+ e := httptest.New(t, app)
- // Expiration.
- time.Sleep(expireRemDur /* -end */)
- for _, path := range restrictedPaths {
- e.POST(path).WithQuery("token", rawToken).Expect().Status(httptest.StatusUnauthorized)
- }
+ // 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 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 f40f69ab3..000000000
--- 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 000000000..583f8c00f
--- /dev/null
+++ b/middleware/jwt/signer.go
@@ -0,0 +1,99 @@
+package jwt
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/kataras/jwt"
+)
+
+// Signer holds common options to sign and generate a token.
+// Its Sign method can be used to generate a token which can be sent to the client.
+// Its NewTokenPair can be used to construct a token pair (access_token, refresh_token).
+//
+// It does not support JWE, JWK.
+type Signer struct {
+ Alg Alg
+ Key interface{}
+
+ // MaxAge to set "exp" and "iat".
+ // Recommended value for access tokens: 15 minutes.
+ // Defaults to 0, no limit.
+ MaxAge time.Duration
+ Options []SignOption
+
+ Encrypt func([]byte) ([]byte, error)
+}
+
+// NewSigner accepts the signature algorithm among with its (private or shared) key
+// and the max life time duration of generated tokens and returns a JWT signer.
+// See its Sign method.
+//
+// Usage:
+//
+// signer := NewSigner(HS256, secret, 15*time.Minute)
+// token, err := signer.Sign(userClaims{Username: "kataras"})
+func NewSigner(signatureAlg Alg, signatureKey interface{}, maxAge time.Duration) *Signer {
+ if signatureAlg == HS256 {
+ // A tiny helper if the end-developer uses string instead of []byte for hmac keys.
+ if k, ok := signatureKey.(string); ok {
+ signatureKey = []byte(k)
+ }
+ }
+
+ s := &Signer{
+ Alg: signatureAlg,
+ Key: signatureKey,
+ MaxAge: maxAge,
+ }
+
+ if maxAge > 0 {
+ s.Options = []SignOption{MaxAge(maxAge)}
+ }
+
+ return s
+}
+
+// WithEncryption enables AES-GCM payload-only decryption.
+func (s *Signer) WithEncryption(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
+}
+
+// Sign generates a new token based on the given "claims" which is valid up to "s.MaxAge".
+func (s *Signer) Sign(claims interface{}, opts ...SignOption) ([]byte, error) {
+ if len(opts) > 0 {
+ opts = append(opts, s.Options...)
+ } else {
+ opts = s.Options
+ }
+
+ return SignEncrypted(s.Alg, s.Key, s.Encrypt, claims, opts...)
+}
+
+// NewTokenPair accepts the access and refresh claims plus the life time duration for the refresh token
+// and generates a new token pair which can be sent to the client.
+// The same token pair can be json-decoded.
+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/verifier.go b/middleware/jwt/verifier.go
new file mode 100644
index 000000000..b5fd75c00
--- /dev/null
+++ b/middleware/jwt/verifier.go
@@ -0,0 +1,263 @@
+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.
+//
+// It does not support JWE, JWK.
+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)
+ // DisableContextUser disables the registration of the claims as context User.
+ DisableContextUser bool
+}
+
+// NewVerifier accepts the algorithm for the token's signature among with its (public) key
+// and optionally some token validators for all verify middlewares that may initialized under this Verifier.
+// See its Verify method.
+//
+// Usage:
+//
+// verifier := NewVerifier(HS256, secret)
+// OR
+// verifier := NewVerifier(HS256, secret, Expected{Issuer: "my-app"})
+//
+// claimsGetter := func() interface{} { return new(userClaims) }
+// middleware := verifier.Verify(claimsGetter)
+// OR
+// middleware := verifier.Verify(claimsGetter, Expected{Issuer: "my-app"})
+//
+// Register the middleware, e.g.
+// app.Use(middleware)
+//
+// Get the claims:
+// claims := jwt.Get(ctx).(*userClaims)
+// username := claims.Username
+//
+// Get the context user:
+// username, err := ctx.User().GetUsername()
+func NewVerifier(signatureAlg Alg, signatureKey interface{}, validators ...TokenValidator) *Verifier {
+ if signatureAlg == HS256 {
+ // A tiny helper if the end-developer uses string instead of []byte for hmac keys.
+ if k, ok := signatureKey.(string); ok {
+ signatureKey = []byte(k)
+ }
+ }
+
+ 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,
+ }
+}
+
+// WithDecryption enables AES-GCM payload-only encryption.
+func (v *Verifier) WithDecryption(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)
+ 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.
+//
+// By default it extracts the token from Authorization: Bearer $token header and ?token URL Query parameter,
+// to change that behavior modify its Extractors field.
+//
+// By default a 401 status code with a generic message will be sent to the client on
+// a token verification or claims validation failure, to change that behavior
+// modify its ErrorHandler field or register OnErrorCode(401, errorHandler) and
+// retrieve the error through Context.GetErr method.
+//
+// If the "claimsType" is nil then only the jwt.GetVerifiedToken is available
+// and the handler should unmarshal the payload to extract the claims by itself.
+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 {
+ // If blocklist implements the connect interface,
+ // try to connect if it's not already connected manually by developer,
+ // if errored then just return a handler which will fire this error every single time.
+ if bc, ok := v.Blocklist.(blocklistConnect); ok {
+ if !bc.IsConnected() {
+ if err := bc.Connect(); err != nil {
+ return func(ctx *context.Context) {
+ v.ErrorHandler(ctx, err)
+ }
+ }
+ }
+ }
+
+ 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 !v.DisableContextUser {
+ ctx.SetUser(dest)
+ }
+
+ 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
+}
diff --git a/sessions/sessiondb/redis/database.go b/sessions/sessiondb/redis/database.go
index 6eabee94a..1773a208a 100644
--- a/sessions/sessiondb/redis/database.go
+++ b/sessions/sessiondb/redis/database.go
@@ -16,7 +16,7 @@ const (
DefaultRedisNetwork = "tcp"
// DefaultRedisAddr the redis address option, "127.0.0.1:6379".
DefaultRedisAddr = "127.0.0.1:6379"
- // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second
+ // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second.
DefaultRedisTimeout = time.Duration(30) * time.Second
)