Merge pull request #1662 from kataras/jwt-new-features

New JWT Middleware features and more
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-11-06 11:42:35 +02:00 committed by GitHub
commit 3d5ed9926e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2928 additions and 1409 deletions

View File

@ -29,7 +29,7 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements ## 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 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 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` - 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 [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)). - 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 ## 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.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.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.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.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.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.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.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.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.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.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). - `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())` | | [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.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` | | [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`, | | | `string`, | |
| `int, int8, int16, int32, int64`, | | | `int, int8, int16, int32, int64`, | |
| `uint, uint8, uint16, uint32, uint64`, | | | `uint, uint8, uint16, uint32, uint64`, | |

6
NOTICE
View File

@ -101,9 +101,9 @@ Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca
toml 3012a1dbe2e4bd1 https://github.com/BurntSushi/toml toml 3012a1dbe2e4bd1 https://github.com/BurntSushi/toml
391d42b32f0577c 391d42b32f0577c
b7bbc7f005 b7bbc7f005
jose d84c719419c2a90 https://github.com/square/go-jose jwt 5f34e0a4e28178b https://github.com/kataras/jwt
8d188ea67e09652 3781df69552bdc5
f5c1929ae8 481a0d4bef
uuid cb32006e483f2a2 https://github.com/google/uuid uuid cb32006e483f2a2 https://github.com/google/uuid
3230e24209cf185 3230e24209cf185
c65b477dbf c65b477dbf

View File

@ -168,8 +168,9 @@
* [Bind Form](request-body/read-form/main.go) * [Bind Form](request-body/read-form/main.go)
* [Checkboxes](request-body/read-form/checkboxes/main.go) * [Checkboxes](request-body/read-form/checkboxes/main.go)
* [Bind Query](request-body/read-query/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 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 Body](request-body/read-body/main.go)
* [Bind Custom per type](request-body/read-custom-per-type/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) * [Bind Custom via Unmarshaler](request-body/read-custom-via-unmarshaler/main.go)
@ -197,8 +198,12 @@
* Authentication, Authorization & Bot Detection * Authentication, Authorization & Bot Detection
* [Basic Authentication](auth/basicauth/main.go) * [Basic Authentication](auth/basicauth/main.go)
* [CORS](auth/cors) * [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) * [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) * [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go)
* [OAUth2](auth/goth/main.go) * [OAUth2](auth/goth/main.go)
* [Manage Permissions](auth/permissions/main.go) * [Manage Permissions](auth/permissions/main.go)
@ -218,6 +223,7 @@
* [Badger](sessions/database/badger/main.go) * [Badger](sessions/database/badger/main.go)
* [BoltDB](sessions/database/boltdb/main.go) * [BoltDB](sessions/database/boltdb/main.go)
* [Redis](sessions/database/redis/main.go) * [Redis](sessions/database/redis/main.go)
* [View Data](sessions/viewdata)
* Websocket * Websocket
* [Gorilla FileWatch (3rd-party)](websocket/gorilla-filewatch/main.go) * [Gorilla FileWatch (3rd-party)](websocket/gorilla-filewatch/main.go)
* [Basic](websocket/basic) * [Basic](websocket/basic)

View File

@ -56,7 +56,9 @@ func h(ctx iris.Context) {
// makes sure for that, otherwise this handler will not be executed. // makes sure for that, otherwise this handler will not be executed.
// OR: // OR:
user := ctx.User() 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) { func logout(ctx iris.Context) {

View File

@ -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
```

View File

@ -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 + `<br/><br/>
<a href="/protected?token=` + tokenString + `">/protected?token=` + tokenString + `</a>`)
}
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)
}

View File

@ -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)
}

View File

@ -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)
}
}
*/

View File

@ -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")
}
}

View File

@ -1,139 +1,183 @@
package main package main
import ( import (
"fmt"
"time" "time"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt" "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 { type UserClaims struct {
jwt.Claims ID string `json:"user_id"`
Username string // 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. // GetID implements the partial context user's ID interface.
type TokenPair struct { // Note that if claims were a map then the claims value converted to UserClaims
AccessToken string `json:"access_token"` // and no need to implement any method.
RefreshToken string `json:"refresh_token"` //
// 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
} }
// GetUsername implements the partial context user's Username interface.
func (u *UserClaims) GetUsername() string {
return u.Username
}
// Validate completes the middleware's custom ClaimsValidator.
// It will not accept a token which its claims missing the username field
// (useful to not accept refresh tokens generated by the same algorithm).
func (u *UserClaims) Validate() error {
if u.Username == "" {
return fmt.Errorf("username field is missing")
}
return nil
}
// For refresh token, we will just use the jwt.Claims
// structure which contains the standard JWT fields.
func main() { func main() {
app := iris.New() app := iris.New()
app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized)
// Access token, short-live. app.Get("/authenticate", generateTokenPair)
accessJWT := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret") app.Get("/refresh", refreshToken)
// Refresh token, long-live. Important: Give different secret keys(!)
refreshJWT := jwt.HMAC(1*time.Hour, "other secret", "other16bytesecre") protectedAPI := app.Party("/protected")
// On refresh token, we extract it only from a request body {
// of JSON, e.g. {"refresh_token": $token }. verifyMiddleware := verifier.Verify(func() interface{} {
// You can also do it manually in the handler level though. return new(UserClaims)
refreshJWT.Extractors = []jwt.TokenExtractor{ })
jwt.FromJSON("refresh_token"),
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)
})
} }
// Generate access and refresh tokens and send to the client. // http://localhost:8080/protected (401)
app.Get("/authenticate", func(ctx iris.Context) {
tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
if err != nil {
ctx.StopWithStatus(iris.StatusInternalServerError)
return
}
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
}
userID := claims.Subject
if userID == "" {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
// Simulate a database call against our jwt subject.
if userID != "53afcf05-38a3-43c3-82af-8bbbe0e4a149" {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
// All OK, re-generate the new pair and send to client.
tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
if err != nil {
ctx.StopWithStatus(iris.StatusInternalServerError)
return
}
ctx.JSON(tokenPair)
})
app.Get("/", func(ctx iris.Context) {
var claims UserClaims
if err := accessJWT.VerifyToken(ctx, &claims); err != nil {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, claims.Expiry.Time())
})
// http://localhost:8080 (401)
// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token}) // http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
// http://localhost:8080?token={access_token} (200) // http://localhost:8080/protected?token={access_token} (200)
// http://localhost:8080?token={refresh_token} (401) // http://localhost:8080/protected?token={refresh_token} (401)
// http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token}) // http://localhost:8080/refresh?refresh_token={refresh_token}
// OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
// http://localhost:8080/refresh?refresh_token={access_token} (401)
app.Listen(":8080") app.Listen(":8080")
} }
func generateTokenPair(accessJWT, refreshJWT *jwt.JWT) (TokenPair, error) { func generateTokenPair(ctx iris.Context) {
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}} // Simulate a user...
userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
customClaims := UserClaims{ // Map the current user with the refresh token,
Claims: accessJWT.Expiry(standardClaims), // so we make sure, on refresh route, that this refresh token owns
// to that user before re-generate.
refreshClaims := jwt.Claims{Subject: userID}
accessClaims := UserClaims{
ID: userID,
Username: "kataras", Username: "kataras",
} }
accessToken, err := accessJWT.Token(customClaims) // 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 { if err != nil {
return TokenPair{}, err ctx.Application().Logger().Errorf("token pair: %v", err)
ctx.StopWithStatus(iris.StatusInternalServerError)
return
} }
// At refresh tokens you don't need any custom claims. // Send the generated token pair to the client.
refreshClaims := refreshJWT.Expiry(jwt.Claims{ // The tokenPair looks like: {"access_token": $token, "refresh_token": $token}
ID: "refresh_kataras", ctx.JSON(tokenPair)
// 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. // There are various methods of refresh token, depending on the application requirements.
Subject: "53afcf05-38a3-43c3-82af-8bbbe0e4a149", // 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
refreshToken, err := refreshJWT.Token(refreshClaims) // and check if its going to expire soon, then generate a single access token.
if err != nil { func refreshToken(ctx iris.Context) {
return TokenPair{}, err // Assuming you have access to the current user, e.g. sessions.
} //
// Simulate a database call against our jwt subject
return TokenPair{ // to make sure that this refresh token is a pair generated by this user.
AccessToken: accessToken, // * Note: You can remove the ExpectSubject and do this validation later on by yourself.
RefreshToken: refreshToken, currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
}, nil
// Get the refresh token from ?refresh_token=$token OR
// the request body's JSON{"refresh_token": "$token"}.
refreshToken := []byte(ctx.URLParam("refresh_token"))
if len(refreshToken) == 0 {
// You can read the whole body with ctx.GetBody/ReadBody too.
var tokenPair jwt.TokenPair
if err := ctx.ReadJSON(&tokenPair); err != nil {
ctx.StopWithError(iris.StatusBadRequest, err)
return
}
refreshToken = tokenPair.RefreshToken
}
// Verify the refresh token, which its subject MUST match the "currentUserID".
_, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})
if err != nil {
ctx.Application().Logger().Errorf("verify refresh token: %v", err)
ctx.StatusCode(iris.StatusUnauthorized)
return
}
/* Custom validation checks can be performed after Verify calls too:
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
userID := verifiedToken.StandardClaims.Subject
if userID != currentUserID {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
*/
// All OK, re-generate the new pair and send to client,
// we could only generate an access token as well.
generateTokenPair(ctx)
}
func handleUnauthorized(ctx iris.Context) {
if err := ctx.GetErr(); err != nil {
ctx.Application().Logger().Errorf("unauthorized: %v", err)
}
ctx.WriteString("Unauthorized")
} }

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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
```

View File

@ -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
}

View File

@ -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)
}
}
*/

View File

@ -0,0 +1,9 @@
package model
// Role represents a role.
type Role string
const (
// Admin represents the Admin access role.
Admin Role = "admin"
)

View File

@ -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.
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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}
```

View File

@ -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)
}

View File

@ -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
}

View File

@ -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 => ../../../../

View File

@ -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")
}

View File

@ -0,0 +1,7 @@
package util
// Constants for the application.
const (
Version = "0.0.1"
AppName = "myapp"
)

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -2,4 +2,6 @@ module github.com/kataras/iris/_examples/dependency-injection/jwt/contrib
go 1.15 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
)

View File

@ -11,37 +11,57 @@ func main() {
app := iris.New() app := iris.New()
app.ConfigureContainer(register) app.ConfigureContainer(register)
// http://localhost:8080/authenticate
// http://localhost:8080/restricted (Header: Authorization = Bearer $token)
app.Listen(":8080") app.Listen(":8080")
} }
func register(api *iris.APIContainer) { var secret = []byte("secret")
j := jwt.HMAC(15*time.Minute, "secret", "secretforencrypt")
func register(api *iris.APIContainer) {
api.RegisterDependency(func(ctx iris.Context) (claims userClaims) { 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) ctx.StopWithError(iris.StatusUnauthorized, err)
return return
} }
verifiedToken.Claims(&claims)
return return
}) })
api.Get("/authenticate", writeToken(j)) api.Get("/authenticate", writeToken)
api.Get("/restricted", restrictedPage) api.Get("/restricted", restrictedPage)
} }
type userClaims struct { type userClaims struct {
jwt.Claims Username string `json:"username"`
Username string
} }
func writeToken(j *jwt.JWT) iris.Handler { func writeToken(ctx iris.Context) {
return func(ctx iris.Context) { claims := userClaims{
j.WriteToken(ctx, userClaims{ Username: "kataras",
Claims: j.Expiry(jwt.Claims{Issuer: "an-issuer"}),
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 { func restrictedPage(claims userClaims) string {

View File

@ -29,7 +29,10 @@ func makeAccessLog() *accesslog.AccessLog {
ac.PanicLog = accesslog.LogHandler ac.PanicLog = accesslog.LogHandler
// Set Custom Formatter: // Set Custom Formatter:
ac.SetFormatter(&accesslog.JSON{}) ac.SetFormatter(&accesslog.JSON{
Indent: " ",
HumanTime: true,
})
// ac.SetFormatter(&accesslog.CSV{}) // ac.SetFormatter(&accesslog.CSV{})
// ac.SetFormatter(&accesslog.Template{Text: "{{.Code}}"}) // ac.SetFormatter(&accesslog.Template{Text: "{{.Code}}"})

View File

@ -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
}

View File

@ -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)
}

View File

@ -38,9 +38,9 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
session := sessions.Get(ctx) session := sessions.Get(ctx)
isNew := session.IsNew() 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) { app.Get("/get", func(ctx iris.Context) {
@ -48,9 +48,9 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
// get a specific value, as string, // get a specific value, as string,
// if not found then it returns just an empty 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) { app.Get("/set-struct", func(ctx iris.Context) {

View File

@ -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}}
})
*/
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sessions View Data</title>
</head>
<body>
Hello {{.session.Get "username"}}
</body>
</html>

View File

@ -59,6 +59,10 @@ type (
Filter = context.Filter Filter = context.Filter
// A Map is an alias of map[string]interface{}. // A Map is an alias of map[string]interface{}.
Map = context.Map 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. // Problem Details for HTTP APIs.
// Pass a Problem value to `context.Problem` to // Pass a Problem value to `context.Problem` to
// write an "application/problem+json" response. // write an "application/problem+json" response.
@ -475,8 +479,6 @@ var (
// on post data, versioning feature and others. // on post data, versioning feature and others.
// An alias of `context.ErrNotFound`. // An alias of `context.ErrNotFound`.
ErrNotFound = context.ErrNotFound ErrNotFound = context.ErrNotFound
// IsErrPrivate reports whether the given "err" is a private one.
IsErrPrivate = context.IsErrPrivate
// NewProblem returns a new Problem. // NewProblem returns a new Problem.
// Head over to the `Problem` type godoc for more. // Head over to the `Problem` type godoc for more.
// //
@ -502,6 +504,9 @@ var (
// //
// A shortcut for the `context#ErrPushNotSupported`. // A shortcut for the `context#ErrPushNotSupported`.
ErrPushNotSupported = 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`. // HTTP Methods copied from `net/http`.

View File

@ -715,7 +715,7 @@ type Configuration struct {
// The body will not be changed and existing data before the // The body will not be changed and existing data before the
// context.UnmarshalBody/ReadJSON/ReadXML will be not consumed. // 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"` 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` // FireEmptyFormError returns if set to tue true then the `context.ReadBody/ReadForm`
// will return an `iris.ErrEmptyForm` on empty request form data. // will return an `iris.ErrEmptyForm` on empty request form data.

View File

@ -714,9 +714,10 @@ func (ctx *Context) StopWithError(statusCode int, err error) {
} }
ctx.SetErr(err) ctx.SetErr(err)
if IsErrPrivate(err) { if _, ok := err.(ErrPrivate); ok {
// error is private, we can't render it, instead . // error is private, we SHOULD not render it,
// let the error handler render the code text. // leave the error handler alone to
// render the code's text instead.
ctx.StopWithStatus(statusCode) ctx.StopWithStatus(statusCode)
return return
} }
@ -2129,11 +2130,11 @@ func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
const disableRequestBodyConsumptionContextKey = "iris.request.body.record" const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
// RecordBody same as the Application's DisableBodyConsumptionOnUnmarshal configuration field // RecordRequestBody same as the Application's DisableBodyConsumptionOnUnmarshal
// but acts for the current request. // configuration field but acts only for the current request.
// It makes the request body readable more than once. // It makes the request body readable more than once.
func (ctx *Context) RecordBody() { func (ctx *Context) RecordRequestBody(b bool) {
ctx.values.Set(disableRequestBodyConsumptionContextKey, true) ctx.values.Set(disableRequestBodyConsumptionContextKey, b)
} }
// IsRecordingBody reports whether the request body can be readen multiple times. // 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. // GetBody reads and returns the request body.
// The default behavior for the http request reader is to consume the data readen // 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 // 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. // However, whenever you can use the `ctx.Request().Body` instead.
func (ctx *Context) GetBody() ([]byte, error) { func (ctx *Context) GetBody() ([]byte, error) {
@ -2358,6 +2359,30 @@ func (ctx *Context) ReadParams(ptr interface{}) error {
return ctx.app.Validate(ptr) 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. // ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error.
// Look `ReadJSONProtobuf` too. // Look `ReadJSONProtobuf` too.
func (ctx *Context) ReadProtobuf(ptr proto.Message) error { 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 // 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. // 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". // 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 { 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.Method() == http.MethodGet {
if ctx.Request().URL.RawQuery != "" { if ctx.Request().URL.RawQuery != "" {
// try read from query. // try read from query.
@ -5065,8 +5111,6 @@ func (ctx *Context) IsDebug() bool {
return ctx.app.IsDebug() return ctx.app.IsDebug()
} }
const errorContextKey = "iris.context.error"
// SetErr is just a helper that sets an error value // SetErr is just a helper that sets an error value
// as a context value, it does nothing more. // as a context value, it does nothing more.
// Also, by-default this error's value is written to the client // 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 // GetErr is a helper which retrieves
// the error value stored by `SetErr`. // 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 { 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 v := ctx.values.Get(errorContextKey); v != nil {
if err, ok := v.(error); ok { switch err := v.(type) {
return err 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` // ErrPanicRecovery may be returned from `Context` actions of a `Handler`
@ -5135,22 +5236,6 @@ func IsErrPanicRecovery(err error) (*ErrPanicRecovery, bool) {
return v, ok 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 // IsRecovered reports whether this handler has been recovered
// by the Iris recover middleware. // by the Iris recover middleware.
func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) { func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) {
@ -5255,12 +5340,34 @@ func (ctx *Context) Logout(args ...interface{}) error {
const userContextKey = "iris.user" 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 // It's used by auth middlewares as a common
// method to provide user information to the // method to provide user information to the
// next handlers in the chain. // next handlers in the chain
func (ctx *Context) SetUser(u User) { // 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) ctx.values.Set(userContextKey, u)
return nil
} }
// User returns the registered User of this request. // User returns the registered User of this request.

View File

@ -1,8 +1,11 @@
package context package context
import ( import (
"encoding/json"
"errors" "errors"
"strings"
"time" "time"
"unicode"
) )
// ErrNotSupported is fired when a specific method is not implemented // 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 // The caller is free to cast this with the implementation directly
// when special features are offered by the authorization system. // 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 { type User interface {
// GetAuthorization should return the authorization method, // GetAuthorization should return the authorization method,
// e.g. Basic Authentication. // e.g. Basic Authentication.
GetAuthorization() string GetAuthorization() (string, error)
// GetAuthorizedAt should return the exact time the // GetAuthorizedAt should return the exact time the
// client has been authorized for the "first" time. // 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 should return the name of the User.
GetUsername() string GetUsername() (string, error)
// GetPassword should return the encoded or raw password // GetPassword should return the encoded or raw password
// (depends on the implementation) of the User. // (depends on the implementation) of the User.
GetPassword() string GetPassword() (string, error)
// GetEmail should return the e-mail of the User. // 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.
// FeaturedUser optional interface that a User can implement. The disadvantage of the current implementation is that the developer MUST
type FeaturedUser interface { complete the whole interface in order to be a valid User and if we add
User new methods in the future their implementation will break
// GetFeatures should optionally return a list of features (unless they have a static interface implementation check as we have on SimpleUser).
// the User implementation offers. We kind of by-pass this disadvantage by providing a SimpleUser which can be embedded (as pointer)
GetFeatures() []UserFeature to the end-developer's custom implementations.
} */
// UserFeature a type which represents a user's optional feature.
// See `HasUserFeature` function for more.
type UserFeature uint32
// The list of standard UserFeatures.
const (
AuthorizedAtFeature UserFeature = iota
UsernameFeature
PasswordFeature
EmailFeature
)
// HasUserFeature reports whether the "u" User
// implements a specific "feature" User Feature.
//
// 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
}
}
return false, nil
}
return false, ErrNotSupported
}
// SimpleUser is a simple implementation of the User interface. // SimpleUser is a simple implementation of the User interface.
type SimpleUser struct { type SimpleUser struct {
Authorization string `json:"authorization"` Authorization string `json:"authorization,omitempty"`
AuthorizedAt time.Time `json:"authorized_at"` AuthorizedAt time.Time `json:"authorized_at,omitempty"`
Username string `json:"username"` ID string `json:"id,omitempty"`
Password string `json:"-"` Username string `json:"username,omitempty"`
Email string `json:"email,omitempty"` Password string `json:"-"`
Features []UserFeature `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) var _ User = (*SimpleUser)(nil)
// GetAuthorization returns the authorization method, // GetAuthorization returns the authorization method,
// e.g. Basic Authentication. // e.g. Basic Authentication.
func (u *SimpleUser) GetAuthorization() string { func (u *SimpleUser) GetAuthorization() (string, error) {
return u.Authorization return u.Authorization, nil
} }
// GetAuthorizedAt returns the exact time the // GetAuthorizedAt returns the exact time the
// client has been authorized for the "first" time. // client has been authorized for the "first" time.
func (u *SimpleUser) GetAuthorizedAt() time.Time { func (u *SimpleUser) GetAuthorizedAt() (time.Time, error) {
return u.AuthorizedAt return u.AuthorizedAt, nil
}
// GetID returns the ID of the User.
func (u *SimpleUser) GetID() (string, error) {
return u.ID, nil
} }
// GetUsername returns the name of the User. // GetUsername returns the name of the User.
func (u *SimpleUser) GetUsername() string { func (u *SimpleUser) GetUsername() (string, error) {
return u.Username return u.Username, nil
} }
// GetPassword returns the raw password of the User. // GetPassword returns the raw password of the User.
func (u *SimpleUser) GetPassword() string { func (u *SimpleUser) GetPassword() (string, error) {
return u.Password return u.Password, nil
} }
// GetEmail returns the e-mail of the User. // GetEmail returns the e-mail of (string,error) User.
func (u *SimpleUser) GetEmail() string { func (u *SimpleUser) GetEmail() (string, error) {
return u.Email return u.Email, nil
} }
// GetFeatures returns a list of features // GetRoles returns the specific user's roles.
// this User implementation offers. // Returns with `ErrNotSupported` if the Roles field is not initialized.
func (u *SimpleUser) GetFeatures() []UserFeature { func (u *SimpleUser) GetRoles() ([]string, error) {
if u.Features != nil { if u.Roles == nil {
return u.Features return nil, ErrNotSupported
} }
var features []UserFeature return u.Roles, nil
}
if !u.AuthorizedAt.IsZero() {
features = append(features, AuthorizedAtFeature) // GetToken returns the token associated with this User.
} // It may return empty if the User is not featured with a Token.
//
if u.Username != "" { // The implementation can change that behavior.
features = append(features, UsernameFeature) // Returns with `ErrNotSupported` if the Token field is empty.
} func (u *SimpleUser) GetToken() ([]byte, error) {
if len(u.Token) == 0 {
if u.Password != "" { return nil, ErrNotSupported
features = append(features, PasswordFeature) }
}
return u.Token, nil
if u.Email != "" { }
features = append(features, EmailFeature)
} // GetField optionally returns a dynamic field from the `Fields` field
// based on its key.
return features 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 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 "", ErrNotSupported
}
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
}
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
}
}
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 *UserPartial) GetAuthorization() (string, error) {
if v := u.userGetAuthorization; v != nil {
return v.GetAuthorization(), nil
}
return "", ErrNotSupported
}
// GetAuthorizedAt should return the exact time the
// client has been authorized for the "first" time.
func (u *UserPartial) GetAuthorizedAt() (time.Time, error) {
if v := u.userGetAuthorizedAt; v != nil {
return v.GetAuthorizedAt(), nil
}
return time.Time{}, ErrNotSupported
}
// 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
}
// 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
}
// 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
}
return "", ErrNotSupported
}
// 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
}
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
}
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
}
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 nil, ErrNotSupported
} }

View File

@ -129,15 +129,14 @@ type RoutesProvider interface { // api builder
} }
func defaultErrorHandler(ctx *context.Context) { func defaultErrorHandler(ctx *context.Context) {
if err := ctx.GetErr(); err != nil { if ok, err := ctx.GetErrPublic(); ok {
if !context.IsErrPrivate(err) { // If an error is stored and it's not a private one
ctx.WriteString(err.Error()) // write it to the response body.
return ctx.WriteString(err.Error())
} return
} }
// Otherwise, write the code's text instead.
ctx.WriteString(context.StatusText(ctx.GetStatusCode())) ctx.WriteString(context.StatusText(ctx.GetStatusCode()))
} }
func (h *routerHandler) Build(provider RoutesProvider) error { func (h *routerHandler) Build(provider RoutesProvider) error {

22
go.mod
View File

@ -4,7 +4,7 @@ go 1.15
require ( require (
github.com/BurntSushi/toml v0.3.1 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/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398
github.com/andybalholm/brotli v1.0.1 github.com/andybalholm/brotli v1.0.1
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible 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/eknkc/amber v0.0.0-20171010120322-cdade1c07385
github.com/fatih/structs v1.1.0 github.com/fatih/structs v1.1.0
github.com/flosch/pongo2/v4 v4.0.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/google/uuid v1.1.2
github.com/hashicorp/go-version v1.2.1 github.com/hashicorp/go-version v1.2.1
github.com/iris-contrib/httpexpect/v2 v2.0.5 github.com/iris-contrib/httpexpect/v2 v2.0.5
@ -21,26 +21,26 @@ require (
github.com/json-iterator/go v1.1.10 github.com/json-iterator/go v1.1.10
github.com/kataras/blocks v0.0.4 github.com/kataras/blocks v0.0.4
github.com/kataras/golog v0.1.5 github.com/kataras/golog v0.1.5
github.com/kataras/jwt v0.0.5
github.com/kataras/neffos v0.0.16 github.com/kataras/neffos v0.0.16
github.com/kataras/pio v0.0.10 github.com/kataras/pio v0.0.10
github.com/kataras/sitemap v0.0.5 github.com/kataras/sitemap v0.0.5
github.com/kataras/tunnel v0.0.2 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/mailru/easyjson v0.7.6
github.com/microcosm-cc/bluemonday v1.0.4 github.com/microcosm-cc/bluemonday v1.0.4
github.com/russross/blackfriday/v2 v2.0.1 github.com/russross/blackfriday/v2 v2.0.1
github.com/schollz/closestmatch v2.1.0+incompatible 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.10
github.com/tdewolff/minify/v2 v2.9.7 github.com/vmihailenco/msgpack/v5 v5.0.0-beta.9
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1
github.com/yosssi/ace v0.0.5 github.com/yosssi/ace v0.0.5
go.etcd.io/bbolt v1.3.5 go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c golang.org/x/net v0.0.0-20201027133719-8eef5233e2a1
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f golang.org/x/sys v0.0.0-20201028094953-708e7fb298ac
golang.org/x/text v0.3.3 golang.org/x/text v0.3.4
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
google.golang.org/protobuf v1.25.0 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 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
) )

View File

@ -170,9 +170,23 @@ var BuiltinDependencies = []*Dependency{
NewDependency(func(ctx *context.Context) Code { NewDependency(func(ctx *context.Context) Code {
return Code(ctx.GetStatusCode()) return Code(ctx.GetStatusCode())
}).Explicitly(), }).Explicitly(),
// Context Error. May be nil
NewDependency(func(ctx *context.Context) Err { NewDependency(func(ctx *context.Context) Err {
return Err(ctx.GetErr()) err := ctx.GetErr()
if err == nil {
return nil
}
return err
}).Explicitly(), }).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. // payload and param bindings are dynamically allocated and declared at the end of the `binding` source file.
} }

View File

@ -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. // Defaults to disabled when testing.
LogLevel = func(val string) OptionSet { LogLevel = func(level string) OptionSet {
return func(c *Configuration) { return func(c *Configuration) {
c.LogLevel = val c.LogLevel = level
} }
} }
) )

View File

@ -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) | | [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) | <!-- raven was deprecated by its company, the successor is sentry-go, they contain an Iris middleware. --> | [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) | <!-- raven was deprecated by its company, the successor is sentry-go, they contain an Iris middleware. -->
| [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) | | [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) | | [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 Third-Party Handlers

View File

@ -718,7 +718,7 @@ func (ac *AccessLog) Handler(ctx *context.Context) {
// Enable reading the request body // Enable reading the request body
// multiple times (route handler and this middleware). // multiple times (route handler and this middleware).
if ac.shouldReadRequestBody() { if ac.shouldReadRequestBody() {
ctx.RecordBody() ctx.RecordRequestBody(true)
} }
// Set the fields context value so they can be modified // Set the fields context value so they can be modified

View File

@ -26,7 +26,11 @@ func TestBasicAuthUseRouter(t *testing.T) {
app.Get("/user_string", func(ctx iris.Context) { app.Get("/user_string", func(ctx iris.Context) {
user := ctx.User() 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) { app.Get("/", func(ctx iris.Context) {

126
middleware/jwt/aliases.go Normal file
View File

@ -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
)

View File

@ -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
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -1,613 +1,7 @@
package jwt package jwt
import ( import "github.com/kataras/iris/v12/context"
"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"
)
func init() { func init() {
context.SetHandlerName("iris/middleware/jwt.*", "iris.jwt") 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
}

View File

@ -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 package jwt_test
import ( import (
"os" "fmt"
"testing" "testing"
"time" "time"
@ -11,129 +10,56 @@ import (
"github.com/kataras/iris/v12/middleware/jwt" "github.com/kataras/iris/v12/middleware/jwt"
) )
type userClaims struct { var testAlg, testSecret = jwt.HS256, []byte("sercrethatmaycontainch@r$")
jwt.Claims
Username string type fooClaims struct {
Foo string `json:"foo"`
} }
const testMaxAge = 3 * time.Second // The actual tests are inside the kataras/jwt repository.
// This runs simple checks of just the middleware part.
// Random RSA verification and encryption. func TestJWT(t *testing.T) {
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)
}
testWriteVerifyToken(t, j)
}
// HMAC verification only (unecrypted).
func TestVerify(t *testing.T) {
j, err := jwt.New(testMaxAge, jwt.HS256, []byte("another secret"))
if err != nil {
t.Fatal(err)
}
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",
}
app := iris.New() app := iris.New()
app.Get("/auth", func(ctx iris.Context) {
j.WriteToken(ctx, expectedClaims)
})
app.Post("/restricted", func(ctx iris.Context) { signer := jwt.NewSigner(testAlg, testSecret, 3*time.Second)
var claims userClaims app.Get("/", func(ctx iris.Context) {
if err := j.VerifyToken(ctx, &claims); err != nil { claims := fooClaims{Foo: "bar"}
ctx.StopWithStatus(iris.StatusUnauthorized) token, err := signer.Sign(claims)
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)
if err != nil { if err != nil {
ctx.StopWithStatus(iris.StatusUnauthorized) ctx.StopWithError(iris.StatusInternalServerError, err)
return return
} }
ctx.Write(token)
})
ctx.JSON(claims) verifier := jwt.NewVerifier(testAlg, testSecret)
verifier.ErrorHandler = func(ctx iris.Context, err error) { // app.OnErrorCode(401, ...)
ctx.StopWithError(iris.StatusUnauthorized, err)
}
middleware := verifier.Verify(func() interface{} { return new(fooClaims) })
app.Get("/protected", middleware, func(ctx iris.Context) {
claims := jwt.Get(ctx).(*fooClaims)
ctx.WriteString(claims.Foo)
}) })
e := httptest.New(t, app) e := httptest.New(t, app)
// Get token. // Get generated token.
rawToken := e.GET("/auth").Expect().Status(httptest.StatusOK).Body().Raw() token := e.GET("/").Expect().Status(iris.StatusOK).Body().Raw()
if rawToken == "" { // Test Header.
t.Fatalf("empty token") 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")
restrictedPaths := [...]string{"/restricted", "/restricted_middleware_readclaims", "/restricted_middleware_get"} // Test unauthorized.
e.GET("/protected").Expect().Status(iris.StatusUnauthorized)
now := time.Now() e.GET("/protected").WithHeader("Authorization", "missing bearer").Expect().Status(iris.StatusUnauthorized)
for _, path := range restrictedPaths { e.GET("/protected").WithQuery("token", "invalid_token").Expect().Status(iris.StatusUnauthorized)
// Authorization Header. // Test expired (note checks happen based on second round).
e.POST(path).WithHeader("Authorization", "Bearer "+rawToken).Expect(). time.Sleep(5 * time.Second)
Status(httptest.StatusOK).JSON().Equal(expectedClaims) e.GET("/protected").WithHeader("Authorization", headerValue).Expect().
Status(iris.StatusUnauthorized).Body().Equal("token expired")
// 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)
// Expiration.
time.Sleep(expireRemDur /* -end */)
for _, path := range restrictedPaths {
e.POST(path).WithQuery("token", rawToken).Expect().Status(httptest.StatusUnauthorized)
}
} }

View File

@ -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
}

99
middleware/jwt/signer.go Normal file
View File

@ -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
}

263
middleware/jwt/verifier.go Normal file
View File

@ -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
}

View File

@ -16,7 +16,7 @@ const (
DefaultRedisNetwork = "tcp" DefaultRedisNetwork = "tcp"
// DefaultRedisAddr the redis address option, "127.0.0.1:6379". // DefaultRedisAddr the redis address option, "127.0.0.1:6379".
DefaultRedisAddr = "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 DefaultRedisTimeout = time.Duration(30) * time.Second
) )