mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
New JWT features and changes (examples updated). Improvements on the Context User and Private Error features
TODO: Write the new e-book JWT section and the HISTORY entry of the chnages and add a simple example on site docs
This commit is contained in:
parent
b816156e77
commit
1864f99145
|
@ -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/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`
|
||||||
|
@ -315,7 +315,7 @@ var dirOpts = iris.DirOptions{
|
||||||
- `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.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.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).
|
||||||
|
@ -490,6 +490,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`, | |
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Package main shows how you can use the Iris unique JWT middleware.
|
||||||
|
// The file contains different kind of examples that all do the same job but,
|
||||||
|
// depending on your code style and your application's requirements, you may choose one over other.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -7,10 +10,31 @@ import (
|
||||||
"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.
|
// Claims a custom claims structure.
|
||||||
type UserClaims struct {
|
type Claims struct {
|
||||||
jwt.Claims
|
// Optionally define JWT's "iss" (Issuer),
|
||||||
Username string
|
// "sub" (Subject) and "aud" (Audience) for issuer and subject.
|
||||||
|
// The JWT's "exp" (expiration) and "iat" (issued at) are automatically
|
||||||
|
// set by the middleware.
|
||||||
|
Issuer string `json:"iss"`
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
Audience []string `json:"aud"`
|
||||||
|
/*
|
||||||
|
Note that the above fields can be also extracted via:
|
||||||
|
jwt.GetTokenInfo(ctx).Claims
|
||||||
|
But in that example, we just showcase how these info can be embedded
|
||||||
|
inside your own Go structure.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Optionally define a "exp" (Expiry),
|
||||||
|
// unlike the rest, this is unset on creation
|
||||||
|
// (unless you want to override the middleware's max age option),
|
||||||
|
// it's filled automatically by the JWT middleware
|
||||||
|
// when the request token is verified.
|
||||||
|
// See the POST /user route.
|
||||||
|
Expiry *jwt.NumericDate `json:"exp"`
|
||||||
|
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -20,56 +44,241 @@ func main() {
|
||||||
//
|
//
|
||||||
// Use the `jwt.New` instead for more flexibility, if necessary.
|
// Use the `jwt.New` instead for more flexibility, if necessary.
|
||||||
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
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:
|
By default it extracts the token from url parameter "token={token}"
|
||||||
// j.Extractors = append(j.Extractors, jwt.FromJSON)
|
and the Authorization Bearer {token} header.
|
||||||
|
You can also take token from JSON body:
|
||||||
|
j.Extractors = append(j.Extractors, jwt.FromJSON)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Optionally, enable block list to force-invalidate
|
||||||
|
verified tokens even before their expiration time.
|
||||||
|
This is useful when the client doesn't clear
|
||||||
|
the token on a user logout by itself.
|
||||||
|
|
||||||
|
The duration argument clears any expired token on each every tick.
|
||||||
|
There is a GC() method that can be manually called to clear expired blocked tokens
|
||||||
|
from the memory.
|
||||||
|
|
||||||
|
j.Blocklist = jwt.NewBlocklist(30*time.Minute)
|
||||||
|
OR NewBlocklistContext(stdContext, 30*time.Minute)
|
||||||
|
|
||||||
|
|
||||||
|
To invalidate a verified token just call:
|
||||||
|
j.Invalidate(ctx) inside a route handler.
|
||||||
|
*/
|
||||||
|
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
app.Logger().SetLevel("debug")
|
app.Logger().SetLevel("debug")
|
||||||
|
|
||||||
|
app.OnErrorCode(iris.StatusUnauthorized, func(ctx iris.Context) {
|
||||||
|
// Note that, any error stored by an authentication
|
||||||
|
// method in Iris is an iris.ErrPrivate.
|
||||||
|
// Available jwt errors:
|
||||||
|
// - ErrMissing
|
||||||
|
// - ErrMissingKey
|
||||||
|
// - ErrExpired
|
||||||
|
// - ErrNotValidYet
|
||||||
|
// - ErrIssuedInTheFuture
|
||||||
|
// - ErrBlocked
|
||||||
|
// An iris.ErrPrivate SHOULD never be displayed to the client as it is;
|
||||||
|
// because it may contain critical security information about the server.
|
||||||
|
//
|
||||||
|
// Also keep in mind that JWT middleware logs verification errors to the
|
||||||
|
// application's logger ("debug") so, normally you don't have to
|
||||||
|
// bother showing the verification error to the browser/client.
|
||||||
|
// However, you can retrieve that error and do what ever you feel right:
|
||||||
|
if err := ctx.GetErr(); err != nil {
|
||||||
|
// If we have an error stored,
|
||||||
|
// (JWT middleware stores any verification errors to the Context),
|
||||||
|
// set the error as response body,
|
||||||
|
// which is the default behavior if that
|
||||||
|
// wasn't an authentication error (as explained above)
|
||||||
|
ctx.WriteString(err.Error())
|
||||||
|
} else {
|
||||||
|
// Else, the default behavior when no error was occured;
|
||||||
|
// write the status text of the status code:
|
||||||
|
ctx.WriteString(iris.StatusText(iris.StatusUnauthorized))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
app.Get("/authenticate", func(ctx iris.Context) {
|
app.Get("/authenticate", func(ctx iris.Context) {
|
||||||
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
|
claims := &Claims{
|
||||||
// NOTE: if custom claims then the `j.Expiry(claims)` (or jwt.Expiry(duration, claims))
|
Issuer: "server",
|
||||||
// MUST be called in order to set the expiration time.
|
Audience: []string{"user"},
|
||||||
customClaims := UserClaims{
|
|
||||||
Claims: j.Expiry(standardClaims),
|
|
||||||
Username: "kataras",
|
Username: "kataras",
|
||||||
}
|
}
|
||||||
|
|
||||||
j.WriteToken(ctx, customClaims)
|
// WriteToken generates and sends the token to the client.
|
||||||
|
// To generate a token use: tok, err := j.Token(claims)
|
||||||
|
// then you can write it in any form you'd like.
|
||||||
|
// The expiration JWT fields are automatically
|
||||||
|
// set by the middleware, that means that your claims value
|
||||||
|
// only needs to fill fields that your application specifically requires.
|
||||||
|
j.WriteToken(ctx, claims)
|
||||||
})
|
})
|
||||||
|
|
||||||
userRouter := app.Party("/user")
|
// Middleware + type-safe method,
|
||||||
|
// useful in 99% of the cases, when your application
|
||||||
|
// requires token verification under a whole path prefix, e.g. /protected:
|
||||||
|
protectedAPI := app.Party("/protected")
|
||||||
{
|
{
|
||||||
// userRouter.Use(j.Verify)
|
protectedAPI.Use(j.Verify(func() interface{} {
|
||||||
// userRouter.Get("/", func(ctx iris.Context) {
|
// Must return a pointer to a type.
|
||||||
// var claims UserClaims
|
//
|
||||||
// if err := jwt.ReadClaims(ctx, &claims); err != nil {
|
// The Iris JWT implementation is very sophisticated.
|
||||||
// // Validation-only errors, the rest are already
|
// We keep our claims in type-safe form.
|
||||||
// // checked on `j.Verify` middleware.
|
// However, you are free to use raw Go maps
|
||||||
// ctx.StopWithStatus(iris.StatusUnauthorized)
|
// (map[string]interface{} or iris.Map) too (example later on).
|
||||||
// return
|
//
|
||||||
// }
|
// Note that you can use the same "j" JWT instance
|
||||||
//
|
// to serve different types of claims on other group of routes,
|
||||||
// ctx.Writef("Claims: %#+v\n", claims)
|
// e.g. postRouter.Use(j.Verify(... return new(Post))).
|
||||||
// })
|
return new(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())
|
protectedAPI.Get("/", func(ctx iris.Context) {
|
||||||
|
claims := jwt.Get(ctx).(*Claims)
|
||||||
|
// All fields parsed from token are set to the claims,
|
||||||
|
// including the Expiry (if defined).
|
||||||
|
ctx.Writef("Username: %s\nExpires at: %s\nAudience: %s",
|
||||||
|
claims.Username, claims.Expiry.Time(), claims.Audience)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify token inside a handler method,
|
||||||
|
// useful when you just need to verify a token on a single spot:
|
||||||
|
app.Get("/inline", func(ctx iris.Context) {
|
||||||
|
var claims Claims
|
||||||
|
_, err := j.VerifyToken(ctx, &claims)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Writef("Username: %s\nExpires at: %s\n",
|
||||||
|
claims.Username, claims.Expiry.Time())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use a common map as claims method,
|
||||||
|
// not recommended, as we support typed claims but
|
||||||
|
// you can do it:
|
||||||
|
app.Get("/map/authenticate", func(ctx iris.Context) {
|
||||||
|
claims := map[string]interface{}{ // or iris.Map for shortcut.
|
||||||
|
"username": "kataras",
|
||||||
|
}
|
||||||
|
|
||||||
|
j.WriteToken(ctx, claims)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/map/verify/middleware", j.Verify(func() interface{} {
|
||||||
|
return &iris.Map{} // or &map[string]interface{}{}
|
||||||
|
}), func(ctx iris.Context) {
|
||||||
|
claims := jwt.Get(ctx).(iris.Map)
|
||||||
|
// The Get method will unwrap the *iris.Map for you,
|
||||||
|
// so its values are directly accessible:
|
||||||
|
ctx.Writef("Username: %s\nExpires at: %s\n",
|
||||||
|
claims["username"], claims["exp"].(*jwt.NumericDate).Time())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/map/verify", func(ctx iris.Context) {
|
||||||
|
claims := make(iris.Map) // or make(map[string]interface{})
|
||||||
|
|
||||||
|
tokenInfo, err := j.VerifyToken(ctx, &claims)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Writef("Username: %s\nExpires at: %s\n",
|
||||||
|
claims["username"], tokenInfo.Claims.Expiry.Time()) /* the claims["exp"] is also set. */
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use the new Context.User() to retrieve the verified client method:
|
||||||
|
// 1. Create a go stuct that implements the context.User interface:
|
||||||
|
app.Get("/users/authenticate", func(ctx iris.Context) {
|
||||||
|
user := &User{Username: "kataras"}
|
||||||
|
j.WriteToken(ctx, user)
|
||||||
|
})
|
||||||
|
usersAPI := app.Party("/users")
|
||||||
|
{
|
||||||
|
usersAPI.Use(j.Verify(func() interface{} {
|
||||||
|
return new(User)
|
||||||
|
}))
|
||||||
|
|
||||||
|
usersAPI.Get("/", func(ctx iris.Context) {
|
||||||
|
user := ctx.User()
|
||||||
|
userToken, _ := user.GetToken()
|
||||||
|
/*
|
||||||
|
You can also cast it to the underline implementation
|
||||||
|
and work with its fields:
|
||||||
|
expires := user.(*User).Expiry.Time()
|
||||||
|
*/
|
||||||
|
// OR use the GetTokenInfo to get the parsed token information:
|
||||||
|
expires := jwt.GetTokenInfo(ctx).Claims.Expiry.Time()
|
||||||
|
lifetime := expires.Sub(time.Now()) // remeaning time to be expired.
|
||||||
|
|
||||||
|
ctx.Writef("Username: %s\nAuthenticated at: %s\nLifetime: %s\nToken: %s\n",
|
||||||
|
user.GetUsername(), user.GetAuthorizedAt(), lifetime, userToken)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://localhost:8080/authenticate
|
||||||
|
// http://localhost:8080/protected?token={token}
|
||||||
|
// http://localhost:8080/inline?token={token}
|
||||||
|
//
|
||||||
|
// http://localhost:8080/map/authenticate
|
||||||
|
// http://localhost:8080/map/verify?token={token}
|
||||||
|
// http://localhost:8080/map/verify/middleware?token={token}
|
||||||
|
//
|
||||||
|
// http://localhost:8080/users/authenticate
|
||||||
|
// http://localhost:8080/users?token={token}
|
||||||
app.Listen(":8080")
|
app.Listen(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User is a custom implementation of the Iris Context User interface.
|
||||||
|
// Optionally, for JWT, you can also implement
|
||||||
|
// the SetToken(tok string) and
|
||||||
|
// Validate(ctx iris.Context, claims jwt.Claims, e jwt.Expected) error
|
||||||
|
// methods to set a token and add custom validation
|
||||||
|
// to a User value parsed from a token.
|
||||||
|
type User struct {
|
||||||
|
iris.User
|
||||||
|
Username string `json:"username"`
|
||||||
|
|
||||||
|
// Optionally, declare some JWT fields,
|
||||||
|
// they are automatically filled by the middleware itself.
|
||||||
|
IssuedAt *jwt.NumericDate `json:"iat"`
|
||||||
|
Expiry *jwt.NumericDate `json:"exp"`
|
||||||
|
Token string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsername returns the Username.
|
||||||
|
// Look the iris/context.SimpleUser type
|
||||||
|
// for all the methods you can implement.
|
||||||
|
func (u *User) GetUsername() string {
|
||||||
|
return u.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorizedAt returns the IssuedAt time.
|
||||||
|
// This and the Get/SetToken methods showcase how you can map JWT standard fields
|
||||||
|
// to an Iris Context User.
|
||||||
|
func (u *User) GetAuthorizedAt() time.Time {
|
||||||
|
return u.IssuedAt.Time()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken is a User interface method.
|
||||||
|
func (u *User) GetToken() (string, error) {
|
||||||
|
return u.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetToken is a special jwt.TokenSetter interface which is
|
||||||
|
// called automatically when a token is parsed to this User value.
|
||||||
|
func (u *User) SetToken(tok string) {
|
||||||
|
u.Token = tok
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
func default_RSA_Example() {
|
func default_RSA_Example() {
|
||||||
j := jwt.RSA(15*time.Minute)
|
j := jwt.RSA(15*time.Minute)
|
||||||
|
|
|
@ -7,133 +7,122 @@ import (
|
||||||
"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.
|
// UserClaims a custom access claims structure.
|
||||||
type UserClaims struct {
|
type UserClaims struct {
|
||||||
jwt.Claims
|
// We could that JWT field to separate the access and refresh token:
|
||||||
Username string
|
// Issuer string `json:"iss"`
|
||||||
|
// But let's cover the "required" feature too, see below:
|
||||||
|
ID string `json:"user_id,required"`
|
||||||
|
Username string `json:"username,required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenPair holds the access token and refresh token response.
|
// For refresh token, we will just use the jwt.Claims
|
||||||
type TokenPair struct {
|
// structure which contains the standard JWT fields.
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
// Access token, short-live.
|
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
||||||
accessJWT := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
|
||||||
// Refresh token, long-live. Important: Give different secret keys(!)
|
app.Get("/authenticate", func(ctx iris.Context) {
|
||||||
refreshJWT := jwt.HMAC(1*time.Hour, "other secret", "other16bytesecre")
|
generateTokenPair(ctx, j)
|
||||||
// On refresh token, we extract it only from a request body
|
})
|
||||||
// of JSON, e.g. {"refresh_token": $token }.
|
|
||||||
// You can also do it manually in the handler level though.
|
app.Get("/refresh_json", func(ctx iris.Context) {
|
||||||
refreshJWT.Extractors = []jwt.TokenExtractor{
|
refreshTokenFromJSON(ctx, j)
|
||||||
jwt.FromJSON("refresh_token"),
|
})
|
||||||
|
|
||||||
|
protectedAPI := app.Party("/protected")
|
||||||
|
{
|
||||||
|
protectedAPI.Use(j.Verify(func() interface{} {
|
||||||
|
return new(UserClaims)
|
||||||
|
})) // OR j.VerifyToken(ctx, &claims, jwt.MeetRequirements(&UserClaims{}))
|
||||||
|
|
||||||
|
protectedAPI.Get("/", func(ctx iris.Context) {
|
||||||
|
// Get token info, even if our UserClaims does not embed those
|
||||||
|
// through GetTokenInfo:
|
||||||
|
expiresAt := jwt.GetTokenInfo(ctx).Claims.Expiry.Time()
|
||||||
|
// Get your custom JWT claims through Get,
|
||||||
|
// which is a shortcut of GetTokenInfo(ctx).Value:
|
||||||
|
claims := jwt.Get(ctx).(*UserClaims)
|
||||||
|
|
||||||
|
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, expiresAt)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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_json (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
|
||||||
app.Listen(":8080")
|
app.Listen(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateTokenPair(accessJWT, refreshJWT *jwt.JWT) (TokenPair, error) {
|
func generateTokenPair(ctx iris.Context, j *jwt.JWT) {
|
||||||
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.
|
||||||
|
refresh := jwt.Claims{Subject: userID}
|
||||||
|
|
||||||
|
access := 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.
|
||||||
|
// Second argument is the refresh claims and,
|
||||||
|
// the last one is the access token's claims.
|
||||||
|
tokenPair, err := j.TokenPair(1*time.Hour, refresh, access)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return TokenPair{}, err
|
ctx.Application().Logger().Debugf("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.
|
func refreshTokenFromJSON(ctx iris.Context, j *jwt.JWT) {
|
||||||
Subject: "53afcf05-38a3-43c3-82af-8bbbe0e4a149",
|
var tokenPair jwt.TokenPair
|
||||||
})
|
|
||||||
|
// Grab the refresh token from a JSON body (you can let it fetch by URL parameter too but
|
||||||
refreshToken, err := refreshJWT.Token(refreshClaims)
|
// it's common practice that you read it from a json body as
|
||||||
if err != nil {
|
// it may contain the access token too (the same response we sent on generateTokenPair)).
|
||||||
return TokenPair{}, err
|
err := ctx.ReadJSON(&tokenPair)
|
||||||
}
|
if err != nil {
|
||||||
|
ctx.StatusCode(iris.StatusBadRequest)
|
||||||
return TokenPair{
|
return
|
||||||
AccessToken: accessToken,
|
}
|
||||||
RefreshToken: refreshToken,
|
|
||||||
}, nil
|
var refreshClaims jwt.Claims
|
||||||
|
err = j.VerifyTokenString(ctx, tokenPair.RefreshToken, &refreshClaims)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Application().Logger().Debugf("verify refresh token: %v", err)
|
||||||
|
ctx.StatusCode(iris.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming you have access to the current user, e.g. sessions.
|
||||||
|
//
|
||||||
|
// Simulate a database call against our jwt subject
|
||||||
|
// to make sure that this refresh token is a pair generated by this user.
|
||||||
|
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||||
|
|
||||||
|
userID := refreshClaims.Subject
|
||||||
|
if userID != currentUserID {
|
||||||
|
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Otherwise, the request must contain the (old) access token too,
|
||||||
|
// even if it's invalid, we can still fetch its fields, such as the user id.
|
||||||
|
// [...leave it for you]
|
||||||
|
|
||||||
|
// All OK, re-generate the new pair and send to client.
|
||||||
|
generateTokenPair(ctx, j)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}}"})
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -5065,8 +5066,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 +5087,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 +5191,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) {
|
||||||
|
|
|
@ -2,7 +2,9 @@ package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"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,6 +23,13 @@ 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 two builtin implementations of the User interface:
|
||||||
|
// - SimpleUser (type-safe)
|
||||||
|
// - UserMap (wraps a map[string]interface{})
|
||||||
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.
|
||||||
|
@ -35,7 +44,33 @@ type User interface {
|
||||||
GetPassword() string
|
GetPassword() string
|
||||||
// GetEmail should return the e-mail of the User.
|
// GetEmail should return the e-mail of the User.
|
||||||
GetEmail() string
|
GetEmail() string
|
||||||
}
|
// 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() (string, 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.
|
||||||
|
|
||||||
|
The disadvantage of the current implementation is that the developer MUST
|
||||||
|
complete the whole interface in order to be a valid User and if we add
|
||||||
|
new methods in the future their implementation will break
|
||||||
|
(unless they have a static interface implementation check as we have on SimpleUser).
|
||||||
|
We kind of by-pass this disadvantage by providing a SimpleUser which can be embedded (as pointer)
|
||||||
|
to the end-developer's custom implementations.
|
||||||
|
*/
|
||||||
|
|
||||||
// FeaturedUser optional interface that a User can implement.
|
// FeaturedUser optional interface that a User can implement.
|
||||||
type FeaturedUser interface {
|
type FeaturedUser interface {
|
||||||
|
@ -55,6 +90,9 @@ const (
|
||||||
UsernameFeature
|
UsernameFeature
|
||||||
PasswordFeature
|
PasswordFeature
|
||||||
EmailFeature
|
EmailFeature
|
||||||
|
RolesFeature
|
||||||
|
TokenFeature
|
||||||
|
FieldsFeature
|
||||||
)
|
)
|
||||||
|
|
||||||
// HasUserFeature reports whether the "u" User
|
// HasUserFeature reports whether the "u" User
|
||||||
|
@ -80,13 +118,16 @@ func HasUserFeature(user User, feature UserFeature) (bool, error) {
|
||||||
type SimpleUser struct {
|
type SimpleUser struct {
|
||||||
Authorization string `json:"authorization"`
|
Authorization string `json:"authorization"`
|
||||||
AuthorizedAt time.Time `json:"authorized_at"`
|
AuthorizedAt time.Time `json:"authorized_at"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username,omitempty"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
Features []UserFeature `json:"-"`
|
Roles []string `json:"roles,omitempty"`
|
||||||
|
Features []UserFeature `json:"features,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
Fields Map `json:"fields,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ User = (*SimpleUser)(nil)
|
var _ FeaturedUser = (*SimpleUser)(nil)
|
||||||
|
|
||||||
// GetAuthorization returns the authorization method,
|
// GetAuthorization returns the authorization method,
|
||||||
// e.g. Basic Authentication.
|
// e.g. Basic Authentication.
|
||||||
|
@ -115,6 +156,39 @@ func (u *SimpleUser) GetEmail() string {
|
||||||
return u.Email
|
return u.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRoles returns the specific user's roles.
|
||||||
|
// Returns with `ErrNotSupported` if the Roles field is not initialized.
|
||||||
|
func (u *SimpleUser) GetRoles() ([]string, error) {
|
||||||
|
if u.Roles == nil {
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken returns the token associated with this User.
|
||||||
|
// It may return empty if the User is not featured with a Token.
|
||||||
|
//
|
||||||
|
// The implementation can change that behavior.
|
||||||
|
// Returns with `ErrNotSupported` if the Token field is empty.
|
||||||
|
func (u *SimpleUser) GetToken() (string, error) {
|
||||||
|
if u.Token == "" {
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetField optionally returns a dynamic field from the `Fields` field
|
||||||
|
// based on its key.
|
||||||
|
func (u *SimpleUser) GetField(key string) (interface{}, error) {
|
||||||
|
if u.Fields == nil {
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Fields[key], nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetFeatures returns a list of features
|
// GetFeatures returns a list of features
|
||||||
// this User implementation offers.
|
// this User implementation offers.
|
||||||
func (u *SimpleUser) GetFeatures() []UserFeature {
|
func (u *SimpleUser) GetFeatures() []UserFeature {
|
||||||
|
@ -140,5 +214,159 @@ func (u *SimpleUser) GetFeatures() []UserFeature {
|
||||||
features = append(features, EmailFeature)
|
features = append(features, EmailFeature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u.Roles != nil {
|
||||||
|
features = append(features, RolesFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Fields != nil {
|
||||||
|
features = append(features, FieldsFeature)
|
||||||
|
}
|
||||||
|
|
||||||
return features
|
return features
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(UserMap(user))
|
||||||
|
// OR
|
||||||
|
// user := UserMap{"key": "value",...}
|
||||||
|
// ctx.SetUser(user)
|
||||||
|
// [...]
|
||||||
|
// username := ctx.User().GetUsername()
|
||||||
|
// age := ctx.User().GetField("age").(int)
|
||||||
|
// OR cast it:
|
||||||
|
// user := ctx.User().(UserMap)
|
||||||
|
// username := user["username"].(string)
|
||||||
|
// age := user["age"].(int)
|
||||||
|
type UserMap Map
|
||||||
|
|
||||||
|
var _ FeaturedUser = UserMap{}
|
||||||
|
|
||||||
|
// GetAuthorization returns the authorization or Authorization value of the map.
|
||||||
|
func (u UserMap) GetAuthorization() string {
|
||||||
|
return u.str("authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorizedAt returns the authorized_at or Authorized_At value of the map.
|
||||||
|
func (u UserMap) GetAuthorizedAt() time.Time {
|
||||||
|
return u.time("authorized_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsername returns the username or Username value of the map.
|
||||||
|
func (u UserMap) GetUsername() string {
|
||||||
|
return u.str("username")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPassword returns the password or Password value of the map.
|
||||||
|
func (u UserMap) GetPassword() string {
|
||||||
|
return u.str("password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmail returns the email or Email value of the map.
|
||||||
|
func (u UserMap) GetEmail() string {
|
||||||
|
return u.str("email")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoles returns the roles or Roles value of the map.
|
||||||
|
func (u UserMap) GetRoles() ([]string, error) {
|
||||||
|
if s := u.strSlice("roles"); s != nil {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken returns the roles or Roles value of the map.
|
||||||
|
func (u UserMap) GetToken() (string, error) {
|
||||||
|
if s := u.str("token"); s != "" {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFeatures returns a list of features
|
||||||
|
// this map offers.
|
||||||
|
func (u UserMap) GetFeatures() []UserFeature {
|
||||||
|
if v := u.val("features"); v != nil { // if already contain features.
|
||||||
|
if features, ok := v.([]UserFeature); ok {
|
||||||
|
return features
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// else try to resolve from map values.
|
||||||
|
features := []UserFeature{FieldsFeature}
|
||||||
|
|
||||||
|
if !u.GetAuthorizedAt().IsZero() {
|
||||||
|
features = append(features, AuthorizedAtFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.GetUsername() != "" {
|
||||||
|
features = append(features, UsernameFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.GetPassword() != "" {
|
||||||
|
features = append(features, PasswordFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.GetEmail() != "" {
|
||||||
|
features = append(features, EmailFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if roles, err := u.GetRoles(); err == nil && roles != nil {
|
||||||
|
features = append(features, RolesFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
return features
|
||||||
|
}
|
||||||
|
|
||||||
|
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) str(key string) string {
|
||||||
|
if v := u.val(key); v != nil {
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// exists or not we don't care, if it's invalid type we don't fill it.
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserMap) strSlice(key string) []string {
|
||||||
|
if v := u.val(key); v != nil {
|
||||||
|
if s, ok := v.([]string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserMap) time(key string) time.Time {
|
||||||
|
if v := u.val(key); v != nil {
|
||||||
|
if t, ok := v.(time.Time); ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
14
go.mod
14
go.mod
|
@ -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.0
|
||||||
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.1
|
||||||
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
|
||||||
|
@ -31,16 +31,16 @@ require (
|
||||||
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/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
|
||||||
github.com/tdewolff/minify/v2 v2.9.7
|
github.com/tdewolff/minify/v2 v2.9.9
|
||||||
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1
|
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-20201012173705-84dcc777aaee
|
||||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c
|
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
|
golang.org/x/sys v0.0.0-20201016160150-f659759dc4ca
|
||||||
golang.org/x/text v0.3.3
|
golang.org/x/text v0.3.3
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,6 +2,7 @@ package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/square/go-jose/v3"
|
"github.com/square/go-jose/v3"
|
||||||
|
"github.com/square/go-jose/v3/json"
|
||||||
"github.com/square/go-jose/v3/jwt"
|
"github.com/square/go-jose/v3/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,11 +15,19 @@ type (
|
||||||
// epoch, including leap seconds. Non-integer values can be represented
|
// epoch, including leap seconds. Non-integer values can be represented
|
||||||
// in the serialized format, but we round to the nearest second.
|
// in the serialized format, but we round to the nearest second.
|
||||||
NumericDate = jwt.NumericDate
|
NumericDate = jwt.NumericDate
|
||||||
|
// Expected defines values used for protected claims validation.
|
||||||
|
// If field has zero value then validation is skipped.
|
||||||
|
Expected = jwt.Expected
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// NewNumericDate constructs NumericDate from time.Time value.
|
// NewNumericDate constructs NumericDate from time.Time value.
|
||||||
NewNumericDate = jwt.NewNumericDate
|
NewNumericDate = jwt.NewNumericDate
|
||||||
|
// Marshal returns the JSON encoding of v.
|
||||||
|
Marshal = json.Marshal
|
||||||
|
// Unmarshal parses the JSON-encoded data and stores the result
|
||||||
|
// in the value pointed to by v.
|
||||||
|
Unmarshal = json.Unmarshal
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
131
middleware/jwt/blocklist.go
Normal file
131
middleware/jwt/blocklist.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdContext "context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Blocklist is an in-memory storage of tokens that should be
|
||||||
|
// immediately invalidated by the server-side.
|
||||||
|
// The most common way to invalidate a token, e.g. on user logout,
|
||||||
|
// is to make the client-side remove the token itself.
|
||||||
|
// However, if someone else has access to that token,
|
||||||
|
// it could be still valid for new requests until its expiration.
|
||||||
|
type Blocklist struct {
|
||||||
|
entries map[string]time.Time // key = token | value = expiration time (to remove expired).
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlocklist returns a new up and running in-memory Token Blocklist.
|
||||||
|
// The returned value can be set to the JWT instance's Blocklist field.
|
||||||
|
func NewBlocklist(gcEvery time.Duration) *Blocklist {
|
||||||
|
return NewBlocklistContext(stdContext.Background(), gcEvery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlocklistContext same as `NewBlocklist`
|
||||||
|
// but it also accepts a standard Go Context for GC cancelation.
|
||||||
|
func NewBlocklistContext(ctx stdContext.Context, gcEvery time.Duration) *Blocklist {
|
||||||
|
b := &Blocklist{
|
||||||
|
entries: make(map[string]time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
if gcEvery > 0 {
|
||||||
|
go b.runGC(ctx, gcEvery)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set upserts a given token, with its expiration time,
|
||||||
|
// to the block list, so it's immediately invalidated by the server-side.
|
||||||
|
func (b *Blocklist) Set(token string, expiresAt time.Time) {
|
||||||
|
b.mu.Lock()
|
||||||
|
b.entries[token] = expiresAt
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del removes a "token" from the block list.
|
||||||
|
func (b *Blocklist) Del(token string) {
|
||||||
|
b.mu.Lock()
|
||||||
|
delete(b.entries, token)
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the total amount of blocked tokens.
|
||||||
|
func (b *Blocklist) Count() int {
|
||||||
|
b.mu.RLock()
|
||||||
|
n := len(b.entries)
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has reports whether the given "token" is blocked by the server.
|
||||||
|
// This method is called before the token verification,
|
||||||
|
// so even if was expired it is removed from the block list.
|
||||||
|
func (b *Blocklist) Has(token string) bool {
|
||||||
|
if token == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
_, ok := b.entries[token]
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
/* No, the Blocklist will be used after the token is parsed,
|
||||||
|
there we can call the Del method if err was ErrExpired.
|
||||||
|
if ok {
|
||||||
|
// As an extra step, to keep the list size as small as possible,
|
||||||
|
// we delete it from list if it's going to be expired
|
||||||
|
// ~in the next `blockedExpireLeeway` seconds.~
|
||||||
|
// - Let's keep it easier for testing by not setting a leeway.
|
||||||
|
// if time.Now().Add(blockedExpireLeeway).After(expiresAt) {
|
||||||
|
if time.Now().After(expiresAt) {
|
||||||
|
b.Del(token)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GC iterates over all entries and removes expired tokens.
|
||||||
|
// This method is helpful to keep the list size small.
|
||||||
|
// Depending on the application, the GC method can be scheduled
|
||||||
|
// to called every half or a whole hour.
|
||||||
|
// A good value for a GC cron task is the JWT's max age (default).
|
||||||
|
func (b *Blocklist) GC() int {
|
||||||
|
now := time.Now()
|
||||||
|
var markedForDeletion []string
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
for token, expiresAt := range b.entries {
|
||||||
|
if now.After(expiresAt) {
|
||||||
|
markedForDeletion = append(markedForDeletion, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
n := len(markedForDeletion)
|
||||||
|
if n > 0 {
|
||||||
|
for _, token := range markedForDeletion {
|
||||||
|
b.Del(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Blocklist) runGC(ctx stdContext.Context, every time.Duration) {
|
||||||
|
t := time.NewTicker(every)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Stop()
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
b.GC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,6 @@ package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -85,6 +83,9 @@ func FromJSON(jsonKey string) TokenExtractor {
|
||||||
//
|
//
|
||||||
// The `RSA(privateFile, publicFile, password)` package-level helper function
|
// The `RSA(privateFile, publicFile, password)` package-level helper function
|
||||||
// can be used to decode the SignKey and VerifyKey.
|
// can be used to decode the SignKey and VerifyKey.
|
||||||
|
//
|
||||||
|
// For an easy use look the `HMAC` package-level function
|
||||||
|
// and the its `NewUser` and `VerifyUser` methods.
|
||||||
type JWT struct {
|
type JWT struct {
|
||||||
// MaxAge is the expiration duration of the generated tokens.
|
// MaxAge is the expiration duration of the generated tokens.
|
||||||
MaxAge time.Duration
|
MaxAge time.Duration
|
||||||
|
@ -109,6 +110,17 @@ type JWT struct {
|
||||||
Encrypter jose.Encrypter
|
Encrypter jose.Encrypter
|
||||||
// DecriptionKey is used to decrypt the token (private key)
|
// DecriptionKey is used to decrypt the token (private key)
|
||||||
DecriptionKey interface{}
|
DecriptionKey interface{}
|
||||||
|
|
||||||
|
// Blocklist holds the invalidated-by-server tokens (that are not yet expired).
|
||||||
|
// It is not initialized by default.
|
||||||
|
// Initialization Usage:
|
||||||
|
// j.UseBlocklist()
|
||||||
|
// OR
|
||||||
|
// j.Blocklist = jwt.NewBlocklist(gcEveryDuration)
|
||||||
|
// Usage:
|
||||||
|
// - ctx.Logout()
|
||||||
|
// - j.Invalidate(ctx)
|
||||||
|
Blocklist *Blocklist
|
||||||
}
|
}
|
||||||
|
|
||||||
type privateKey interface{ Public() crypto.PublicKey }
|
type privateKey interface{ Public() crypto.PublicKey }
|
||||||
|
@ -284,64 +296,68 @@ func (j *JWT) WithEncryption(contentEncryption ContentEncryption, alg KeyAlgorit
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expiry returns a new standard Claims with
|
// UseBlocklist initializes the Blocklist.
|
||||||
// the `Expiry` and `IssuedAt` fields of the "claims" filled
|
// Should be called on jwt middleware creation-time,
|
||||||
// based on the given "maxAge" duration.
|
// after this, the developer can use the Context.Logout method
|
||||||
//
|
// to invalidate a verified token by the server-side.
|
||||||
// See the `JWT.Expiry` method too.
|
func (j *JWT) UseBlocklist() {
|
||||||
func Expiry(maxAge time.Duration, claims Claims) Claims {
|
gcEvery := 30 * time.Minute
|
||||||
now := time.Now()
|
if j.MaxAge > 0 {
|
||||||
claims.Expiry = NewNumericDate(now.Add(maxAge))
|
gcEvery = j.MaxAge
|
||||||
claims.IssuedAt = NewNumericDate(now)
|
}
|
||||||
return claims
|
j.Blocklist = NewBlocklist(gcEvery)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expiry method same as `Expiry` package-level function,
|
// ExpiryMap adds the expiration based on the "maxAge" to the "claims" map.
|
||||||
// it returns a Claims with the expiration fields of the "claims"
|
// It's called automatically on `Token` method.
|
||||||
// filled based on the JWT's `MaxAge` field.
|
func ExpiryMap(maxAge time.Duration, claims context.Map) {
|
||||||
// Only use it when this standard "claims"
|
now := time.Now()
|
||||||
// is embedded on a custom claims structure.
|
if claims["exp"] == nil {
|
||||||
// Usage:
|
claims["exp"] = NewNumericDate(now.Add(maxAge))
|
||||||
// type UserClaims struct {
|
}
|
||||||
// jwt.Claims
|
|
||||||
// Username string
|
if claims["iat"] == nil {
|
||||||
// }
|
claims["iat"] = NewNumericDate(now)
|
||||||
// [...]
|
}
|
||||||
// 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.
|
// Token generates and returns a new token string.
|
||||||
// See `VerifyToken` too.
|
// See `VerifyToken` too.
|
||||||
func (j *JWT) Token(claims interface{}) (string, error) {
|
func (j *JWT) Token(claims interface{}) (string, error) {
|
||||||
// switch c := claims.(type) {
|
return j.token(j.MaxAge, claims)
|
||||||
// case Claims:
|
}
|
||||||
// claims = Expiry(j.MaxAge, c)
|
|
||||||
// case map[string]interface{}: let's not support map.
|
func (j *JWT) token(maxAge time.Duration, claims interface{}) (string, error) {
|
||||||
// now := time.Now()
|
if claims == nil {
|
||||||
// c["iat"] = now.Unix()
|
return "", ErrInvalidKey
|
||||||
// c["exp"] = now.Add(j.MaxAge).Unix()
|
|
||||||
// }
|
|
||||||
if c, ok := claims.(Claims); ok {
|
|
||||||
claims = Expiry(j.MaxAge, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c, nErr := normalize(claims)
|
||||||
|
if nErr != nil {
|
||||||
|
return "", nErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set expiration, if missing.
|
||||||
|
ExpiryMap(maxAge, c)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
token string
|
token string
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
// jwt.Builder and jwt.NestedBuilder contain same methods but they are not the same.
|
// jwt.Builder and jwt.NestedBuilder contain same methods but they are not the same.
|
||||||
|
//
|
||||||
|
// Note that the .Claims method there, converts a Struct to a map under the hoods.
|
||||||
|
// That means that we will not have any performance cost
|
||||||
|
// if we do it by ourselves and pass always a Map there.
|
||||||
|
// That gives us the option to allow user to pass ANY go struct
|
||||||
|
// and we can add the "exp", "nbf", "iat" map values by ourselves
|
||||||
|
// based on the j.MaxAge.
|
||||||
|
// (^ done, see normalize, all methods are
|
||||||
|
// changed to accept totally custom types, no need to embed the standard Claims anymore).
|
||||||
if j.DecriptionKey != nil {
|
if j.DecriptionKey != nil {
|
||||||
token, err = jwt.SignedAndEncrypted(j.Signer, j.Encrypter).Claims(claims).CompactSerialize()
|
token, err = jwt.SignedAndEncrypted(j.Signer, j.Encrypter).Claims(c).CompactSerialize()
|
||||||
} else {
|
} else {
|
||||||
token, err = jwt.Signed(j.Signer).Claims(claims).CompactSerialize()
|
token, err = jwt.Signed(j.Signer).Claims(c).CompactSerialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -351,39 +367,6 @@ func (j *JWT) Token(claims interface{}) (string, error) {
|
||||||
return token, nil
|
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
|
// WriteToken is a helper which just generates(calls the `Token` method) and writes
|
||||||
// a new token to the client in plain text format.
|
// a new token to the client in plain text format.
|
||||||
//
|
//
|
||||||
|
@ -399,91 +382,122 @@ func (j *JWT) WriteToken(ctx *context.Context, claims interface{}) error {
|
||||||
return err
|
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,
|
// VerifyToken verifies (and decrypts) the request token,
|
||||||
// it also validates and binds the parsed token's claims to the "claimsPtr" (destination).
|
// 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 {
|
// The last, variadic, input argument is optionally, if provided then the
|
||||||
var token string
|
// parsed claims must match the expectations;
|
||||||
|
// e.g. Audience, Issuer, ID, Subject.
|
||||||
|
// See `ExpectXXX` package-functions for details.
|
||||||
|
func (j *JWT) VerifyToken(ctx *context.Context, claimsPtr interface{}, expectations ...Expectation) (*TokenInfo, error) {
|
||||||
|
token := j.RequestToken(ctx)
|
||||||
|
return j.VerifyTokenString(ctx, token, claimsPtr, expectations...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestToken extracts the token from the request.
|
||||||
|
func (j *JWT) RequestToken(ctx *context.Context) (token string) {
|
||||||
for _, extract := range j.Extractors {
|
for _, extract := range j.Extractors {
|
||||||
if token = extract(ctx); token != "" {
|
if token = extract(ctx); token != "" {
|
||||||
break // ok we found it.
|
break // ok we found it.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return j.VerifyTokenString(ctx, token, claimsPtr)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyTokenString verifies and unmarshals an extracted token to "claimsPtr" destination.
|
// TokenSetter is an interface which if implemented
|
||||||
// The Context is required when the claims validator needs it, otherwise can be nil.
|
// the extracted, verified, token is stored to the object.
|
||||||
func (j *JWT) VerifyTokenString(ctx *context.Context, token string, claimsPtr interface{}) error {
|
type TokenSetter interface {
|
||||||
|
SetToken(token string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenInfo holds the standard token information may required
|
||||||
|
// for further actions.
|
||||||
|
// This structure is mostly useful when the developer's go structure
|
||||||
|
// does not hold the standard jwt fields (e.g. "exp")
|
||||||
|
// but want access to the parsed token which contains those fields.
|
||||||
|
// Inside the middleware, it is used to invalidate tokens through server-side, see `Invalidate`.
|
||||||
|
type TokenInfo struct {
|
||||||
|
RequestToken string // The request token.
|
||||||
|
Claims Claims // The standard JWT parsed fields from the request Token.
|
||||||
|
Value interface{} // The pointer to the end-developer's custom claims structure (see `Get`).
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenInfoContextKey = "iris.jwt.token"
|
||||||
|
|
||||||
|
// Get returns the verified developer token claims.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// j := jwt.New(...)
|
||||||
|
// app.Use(j.Verify(func() interface{} { return new(CustomClaims) }))
|
||||||
|
// app.Post("/restricted", func(ctx iris.Context){
|
||||||
|
// claims := jwt.Get(ctx).(*CustomClaims)
|
||||||
|
// [use claims...]
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Note that there is one exception, if the value was a pointer
|
||||||
|
// to a map[string]interface{}, it returns the map itself so it can be
|
||||||
|
// accessible directly without the requirement of unwrapping it, e.g.
|
||||||
|
// j.Verify(func() interface{} {
|
||||||
|
// return &iris.Map{}
|
||||||
|
// }
|
||||||
|
// [...]
|
||||||
|
// claims := jwt.Get(ctx).(iris.Map)
|
||||||
|
func Get(ctx *context.Context) interface{} {
|
||||||
|
if tok := GetTokenInfo(ctx); tok != nil {
|
||||||
|
switch v := tok.Value.(type) {
|
||||||
|
case *context.Map:
|
||||||
|
return *v
|
||||||
|
default:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenInfo returns the verified token's information.
|
||||||
|
func GetTokenInfo(ctx *context.Context) *TokenInfo {
|
||||||
|
if v := ctx.Values().Get(tokenInfoContextKey); v != nil {
|
||||||
|
if t, ok := v.(*TokenInfo); ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate invalidates a verified JWT token.
|
||||||
|
// It adds the request token, retrieved by Verify methods, to the block list.
|
||||||
|
// Next request will be blocked, even if the token was not yet expired.
|
||||||
|
// This method can be used when the client-side does not clear the token
|
||||||
|
// on a user logout operation.
|
||||||
|
//
|
||||||
|
// Note: the Blocklist should be initialized before serve-time: j.UseBlocklist().
|
||||||
|
func (j *JWT) Invalidate(ctx *context.Context) {
|
||||||
|
if j.Blocklist == nil {
|
||||||
|
ctx.Application().Logger().Debug("jwt.Invalidate: Blocklist is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenInfo := GetTokenInfo(ctx)
|
||||||
|
if tokenInfo == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j.Blocklist.Set(tokenInfo.RequestToken, tokenInfo.Claims.Expiry.Time())
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyTokenString verifies and unmarshals an extracted request token to "dest" destination.
|
||||||
|
// The last variadic input indicates any further validations against the verified token claims.
|
||||||
|
// If the given "dest" is a valid context.User then ctx.User() will return it.
|
||||||
|
// If the token is missing an `ErrMissing` is returned.
|
||||||
|
// If the incoming token was expired an `ErrExpired` is returned.
|
||||||
|
// If the incoming token was blocked by the server an `ErrBlocked` is returned.
|
||||||
|
func (j *JWT) VerifyTokenString(ctx *context.Context, token string, dest interface{}, expectations ...Expectation) (*TokenInfo, error) {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return ErrMissing
|
return nil, ErrMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -494,7 +508,7 @@ func (j *JWT) VerifyTokenString(ctx *context.Context, token string, claimsPtr in
|
||||||
if j.DecriptionKey != nil {
|
if j.DecriptionKey != nil {
|
||||||
t, cerr := jwt.ParseSignedAndEncrypted(token)
|
t, cerr := jwt.ParseSignedAndEncrypted(token)
|
||||||
if cerr != nil {
|
if cerr != nil {
|
||||||
return cerr
|
return nil, cerr
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedToken, err = t.Decrypt(j.DecriptionKey)
|
parsedToken, err = t.Decrypt(j.DecriptionKey)
|
||||||
|
@ -502,112 +516,163 @@ func (j *JWT) VerifyTokenString(ctx *context.Context, token string, claimsPtr in
|
||||||
parsedToken, err = jwt.ParseSigned(token)
|
parsedToken, err = jwt.ParseSigned(token)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = parsedToken.Claims(j.VerificationKey, claimsPtr); err != nil {
|
var claims Claims
|
||||||
return err
|
if err = parsedToken.Claims(j.VerificationKey, dest, &claims); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return validateClaims(ctx, claimsPtr)
|
// Build the Expected value.
|
||||||
}
|
expected := Expected{}
|
||||||
|
for _, e := range expectations {
|
||||||
const (
|
if e != nil {
|
||||||
// ClaimsContextKey is the context key which the jwt claims are stored from the `Verify` method.
|
// expection can be used as a field validation too (see MeetRequirements).
|
||||||
ClaimsContextKey = "iris.jwt.claims"
|
if err = e(&expected, dest); err != nil {
|
||||||
needsValidationContextKey = "iris.jwt.claims.unvalidated"
|
return nil, err
|
||||||
)
|
}
|
||||||
|
|
||||||
// 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
|
// For other standard JWT claims fields such as "exp"
|
||||||
|
// The developer can just add a field of Expiry *NumericDate `json:"exp"`
|
||||||
|
// and will be filled by the parsed token automatically.
|
||||||
|
// No need for more interfaces.
|
||||||
|
|
||||||
|
err = validateClaims(ctx, dest, claims, expected)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrExpired {
|
||||||
|
// If token was expired remove it from the block list.
|
||||||
|
if j.Blocklist != nil {
|
||||||
|
j.Blocklist.Del(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.Blocklist != nil {
|
||||||
|
// If token exists in the block list, then stop here.
|
||||||
|
if j.Blocklist.Has(token) {
|
||||||
|
return nil, ErrBlocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ut, ok := dest.(TokenSetter); ok {
|
||||||
|
// The u.Token is empty even if we set it and export it on JSON structure.
|
||||||
|
// Set it manually.
|
||||||
|
ut.SetToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the information.
|
||||||
|
tokenInfo := &TokenInfo{
|
||||||
|
RequestToken: token,
|
||||||
|
Claims: claims,
|
||||||
|
Value: dest,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenPair holds the access token and refresh token response.
|
||||||
|
type TokenPair struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenPair generates a token pair of access and refresh tokens.
|
||||||
|
// The first two arguments required for the refresh token
|
||||||
|
// and the last one is the claims for the access token one.
|
||||||
|
func (j *JWT) TokenPair(refreshMaxAge time.Duration, refreshClaims interface{}, accessClaims interface{}) (TokenPair, error) {
|
||||||
|
accessToken, err := j.Token(accessClaims)
|
||||||
|
if err != nil {
|
||||||
|
return TokenPair{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := j.token(refreshMaxAge, refreshClaims)
|
||||||
|
if err != nil {
|
||||||
|
return TokenPair{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pair := TokenPair{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
return pair, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify returns a middleware which
|
||||||
|
// decrypts an incoming request token to the result of the given "newPtr".
|
||||||
|
// It does write a 401 unauthorized status code if verification or decryption failed.
|
||||||
|
// It calls the `ctx.Next` on verified requests.
|
||||||
|
//
|
||||||
|
// Iit unmarshals the token to the specific type returned from the given "newPtr" function.
|
||||||
|
// It sets the Context User and User's Token too. So the next handler(s)
|
||||||
|
// of the same chain can access the User through a `Context.User()` call.
|
||||||
|
//
|
||||||
|
// Note unlike `VerifyToken`, this method automatically protects
|
||||||
|
// the claims with JSON required tags (see `MeetRequirements` Expection).
|
||||||
|
//
|
||||||
|
// On verified tokens:
|
||||||
|
// - The information can be retrieved through `Get` and `GetTokenInfo` functions.
|
||||||
|
// - User is set if the newPtr returns a valid Context User
|
||||||
|
// - The Context Logout method is set if Blocklist was initialized
|
||||||
|
// Any error is captured to the Context,
|
||||||
|
// which can be retrieved by a `ctx.GetErr()` call.
|
||||||
|
func (j *JWT) Verify(newPtr func() interface{}, expections ...Expectation) context.Handler {
|
||||||
|
expections = append(expections, MeetRequirements(newPtr()))
|
||||||
|
|
||||||
|
return func(ctx *context.Context) {
|
||||||
|
ptr := newPtr()
|
||||||
|
|
||||||
|
tokenInfo, err := j.VerifyToken(ctx, ptr, expections...)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Application().Logger().Debugf("iris.jwt.Verify: %v", err)
|
||||||
|
ctx.StopWithError(401, context.PrivateError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := ptr.(context.User); ok {
|
||||||
|
ctx.SetUser(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.Blocklist != nil {
|
||||||
|
ctx.SetLogoutFunc(j.Invalidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Values().Set(tokenInfoContextKey, tokenInfo)
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUser returns a new User based on the given "opts".
|
||||||
|
// The caller can modify the User until its `GetToken` is called.
|
||||||
|
func (j *JWT) NewUser(opts ...UserOption) *User {
|
||||||
|
u := &User{
|
||||||
|
j: j,
|
||||||
|
SimpleUser: &context.SimpleUser{
|
||||||
|
Authorization: "IRIS_JWT_USER", // Used to separate a refresh token with a user/access one too.
|
||||||
|
Features: []context.UserFeature{
|
||||||
|
context.TokenFeature,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyUser works like the `Verify` method but instead
|
||||||
|
// it unmarshals the token to the specific User type.
|
||||||
|
// It sets the Context User too. So the next handler(s)
|
||||||
|
// of the same chain can access the User through a `Context.User()` call.
|
||||||
|
func (j *JWT) VerifyUser() context.Handler {
|
||||||
|
return j.Verify(func() interface{} {
|
||||||
|
return new(User)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type userClaims struct {
|
type userClaims struct {
|
||||||
jwt.Claims
|
// Optionally:
|
||||||
Username string
|
Issuer string `json:"iss"`
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
Audience jwt.Audience `json:"aud"`
|
||||||
|
//
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const testMaxAge = 3 * time.Second
|
const testMaxAge = 7 * time.Second
|
||||||
|
|
||||||
// Random RSA verification and encryption.
|
// Random RSA verification and encryption.
|
||||||
func TestRSA(t *testing.T) {
|
func TestRSA(t *testing.T) {
|
||||||
|
@ -25,13 +29,13 @@ func TestRSA(t *testing.T) {
|
||||||
os.Remove(jwt.DefaultSignFilename)
|
os.Remove(jwt.DefaultSignFilename)
|
||||||
os.Remove(jwt.DefaultEncFilename)
|
os.Remove(jwt.DefaultEncFilename)
|
||||||
})
|
})
|
||||||
testWriteVerifyToken(t, j)
|
testWriteVerifyBlockToken(t, j)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HMAC verification and encryption.
|
// HMAC verification and encryption.
|
||||||
func TestHMAC(t *testing.T) {
|
func TestHMAC(t *testing.T) {
|
||||||
j := jwt.HMAC(testMaxAge, "secret", "itsa16bytesecret")
|
j := jwt.HMAC(testMaxAge, "secret", "itsa16bytesecret")
|
||||||
testWriteVerifyToken(t, j)
|
testWriteVerifyBlockToken(t, j)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew_HMAC(t *testing.T) {
|
func TestNew_HMAC(t *testing.T) {
|
||||||
|
@ -44,7 +48,7 @@ func TestNew_HMAC(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testWriteVerifyToken(t, j)
|
testWriteVerifyBlockToken(t, j)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HMAC verification only (unecrypted).
|
// HMAC verification only (unecrypted).
|
||||||
|
@ -53,54 +57,60 @@ func TestVerify(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
testWriteVerifyToken(t, j)
|
testWriteVerifyBlockToken(t, j)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testWriteVerifyToken(t *testing.T, j *jwt.JWT) {
|
func testWriteVerifyBlockToken(t *testing.T, j *jwt.JWT) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
j.UseBlocklist()
|
||||||
j.Extractors = append(j.Extractors, jwt.FromJSON("access_token"))
|
j.Extractors = append(j.Extractors, jwt.FromJSON("access_token"))
|
||||||
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
|
|
||||||
expectedClaims := userClaims{
|
customClaims := &userClaims{
|
||||||
Claims: j.Expiry(standardClaims),
|
Issuer: "an-issuer",
|
||||||
|
Audience: jwt.Audience{"an-audience"},
|
||||||
|
Subject: "user",
|
||||||
Username: "kataras",
|
Username: "kataras",
|
||||||
}
|
}
|
||||||
|
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
app.OnErrorCode(iris.StatusUnauthorized, func(ctx iris.Context) {
|
||||||
|
if err := ctx.GetErr(); err != nil {
|
||||||
|
// Test accessing the private error and set this as the response body.
|
||||||
|
ctx.WriteString(err.Error())
|
||||||
|
} else { // Else the default behavior
|
||||||
|
ctx.WriteString(iris.StatusText(iris.StatusUnauthorized))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
app.Get("/auth", func(ctx iris.Context) {
|
app.Get("/auth", func(ctx iris.Context) {
|
||||||
j.WriteToken(ctx, expectedClaims)
|
j.WriteToken(ctx, customClaims)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Post("/restricted", func(ctx iris.Context) {
|
app.Post("/protected", func(ctx iris.Context) {
|
||||||
var claims userClaims
|
var claims userClaims
|
||||||
if err := j.VerifyToken(ctx, &claims); err != nil {
|
_, err := j.VerifyToken(ctx, &claims)
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(claims)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Post("/restricted_middleware_readclaims", j.Verify, func(ctx iris.Context) {
|
|
||||||
var claims userClaims
|
|
||||||
if err := jwt.ReadClaims(ctx, &claims); err != nil {
|
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(claims)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Post("/restricted_middleware_get", j.Verify, func(ctx iris.Context) {
|
|
||||||
claims, err := jwt.Get(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
// t.Logf("%s: %v", ctx.Path(), err)
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, iris.PrivateError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(claims)
|
ctx.JSON(claims)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m := app.Party("/middleware")
|
||||||
|
m.Use(j.Verify(func() interface{} {
|
||||||
|
return new(userClaims)
|
||||||
|
}))
|
||||||
|
m.Post("/protected", func(ctx iris.Context) {
|
||||||
|
claims := jwt.Get(ctx)
|
||||||
|
ctx.JSON(claims)
|
||||||
|
})
|
||||||
|
m.Post("/invalidate", func(ctx iris.Context) {
|
||||||
|
ctx.Logout() // OR j.Invalidate(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
e := httptest.New(t, app)
|
e := httptest.New(t, app)
|
||||||
|
|
||||||
// Get token.
|
// Get token.
|
||||||
|
@ -109,31 +119,186 @@ func testWriteVerifyToken(t *testing.T, j *jwt.JWT) {
|
||||||
t.Fatalf("empty token")
|
t.Fatalf("empty token")
|
||||||
}
|
}
|
||||||
|
|
||||||
restrictedPaths := [...]string{"/restricted", "/restricted_middleware_readclaims", "/restricted_middleware_get"}
|
restrictedPaths := [...]string{"/protected", "/middleware/protected"}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, path := range restrictedPaths {
|
for _, path := range restrictedPaths {
|
||||||
// Authorization Header.
|
// Authorization Header.
|
||||||
e.POST(path).WithHeader("Authorization", "Bearer "+rawToken).Expect().
|
e.POST(path).WithHeader("Authorization", "Bearer "+rawToken).Expect().
|
||||||
Status(httptest.StatusOK).JSON().Equal(expectedClaims)
|
Status(httptest.StatusOK).JSON().Equal(customClaims)
|
||||||
|
|
||||||
// URL Query.
|
// URL Query.
|
||||||
e.POST(path).WithQuery("token", rawToken).Expect().
|
e.POST(path).WithQuery("token", rawToken).Expect().
|
||||||
Status(httptest.StatusOK).JSON().Equal(expectedClaims)
|
Status(httptest.StatusOK).JSON().Equal(customClaims)
|
||||||
|
|
||||||
// JSON Body.
|
// JSON Body.
|
||||||
e.POST(path).WithJSON(iris.Map{"access_token": rawToken}).Expect().
|
e.POST(path).WithJSON(iris.Map{"access_token": rawToken}).Expect().
|
||||||
Status(httptest.StatusOK).JSON().Equal(expectedClaims)
|
Status(httptest.StatusOK).JSON().Equal(customClaims)
|
||||||
|
|
||||||
// Missing "Bearer".
|
// Missing "Bearer".
|
||||||
e.POST(path).WithHeader("Authorization", rawToken).Expect().
|
e.POST(path).WithHeader("Authorization", rawToken).Expect().
|
||||||
Status(httptest.StatusUnauthorized)
|
Status(httptest.StatusUnauthorized).Body().Equal("token is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate the token.
|
||||||
|
e.POST("/middleware/invalidate").WithQuery("token", rawToken).Expect().
|
||||||
|
Status(httptest.StatusOK)
|
||||||
|
// Token is blocked by server.
|
||||||
|
e.POST("/middleware/protected").WithQuery("token", rawToken).Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("token is blocked")
|
||||||
|
|
||||||
expireRemDur := testMaxAge - time.Since(now)
|
expireRemDur := testMaxAge - time.Since(now)
|
||||||
|
|
||||||
// Expiration.
|
// Expiration.
|
||||||
time.Sleep(expireRemDur /* -end */)
|
time.Sleep(expireRemDur /* -end */)
|
||||||
for _, path := range restrictedPaths {
|
for _, path := range restrictedPaths {
|
||||||
e.POST(path).WithQuery("token", rawToken).Expect().Status(httptest.StatusUnauthorized)
|
e.POST(path).WithQuery("token", rawToken).Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("token is expired (exp)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerifyMap(t *testing.T) {
|
||||||
|
j := jwt.HMAC(testMaxAge, "secret", "itsa16bytesecret")
|
||||||
|
expectedClaims := iris.Map{
|
||||||
|
"iss": "tester",
|
||||||
|
"username": "makis",
|
||||||
|
"roles": []string{"admin"},
|
||||||
|
}
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Get("/user/auth", func(ctx iris.Context) {
|
||||||
|
err := j.WriteToken(ctx, expectedClaims)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedClaims["exp"] == nil || expectedClaims["iat"] == nil {
|
||||||
|
ctx.StopWithText(iris.StatusBadRequest,
|
||||||
|
"exp or/and iat is nil - this means that the expiry was not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
userAPI := app.Party("/user")
|
||||||
|
userAPI.Post("/", func(ctx iris.Context) {
|
||||||
|
var claims iris.Map
|
||||||
|
if _, err := j.VerifyToken(ctx, &claims); err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, iris.PrivateError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(claims)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test map + Verify middleware.
|
||||||
|
userAPI.Post("/middleware", j.Verify(func() interface{} {
|
||||||
|
return &iris.Map{} // or &map[string]interface{}{}
|
||||||
|
}), func(ctx iris.Context) {
|
||||||
|
claims := jwt.Get(ctx)
|
||||||
|
ctx.JSON(claims)
|
||||||
|
})
|
||||||
|
|
||||||
|
e := httptest.New(t, app, httptest.LogLevel("error"))
|
||||||
|
token := e.GET("/user/auth").Expect().Status(httptest.StatusOK).Body().Raw()
|
||||||
|
if token == "" {
|
||||||
|
t.Fatalf("empty token")
|
||||||
|
}
|
||||||
|
|
||||||
|
e.POST("/user").WithHeader("Authorization", "Bearer "+token).Expect().
|
||||||
|
Status(httptest.StatusOK).JSON().Equal(expectedClaims)
|
||||||
|
|
||||||
|
e.POST("/user/middleware").WithHeader("Authorization", "Bearer "+token).Expect().
|
||||||
|
Status(httptest.StatusOK).JSON().Equal(expectedClaims)
|
||||||
|
|
||||||
|
e.POST("/user").Expect().Status(httptest.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
type customClaims struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *customClaims) SetToken(tok string) {
|
||||||
|
c.Token = tok
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyStruct(t *testing.T) {
|
||||||
|
maxAge := testMaxAge / 2
|
||||||
|
j := jwt.HMAC(maxAge, "secret", "itsa16bytesecret")
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Get("/user/auth", func(ctx iris.Context) {
|
||||||
|
err := j.WriteToken(ctx, customClaims{
|
||||||
|
Username: "makis",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
userAPI := app.Party("/user")
|
||||||
|
userAPI.Post("/", func(ctx iris.Context) {
|
||||||
|
var claims customClaims
|
||||||
|
if _, err := j.VerifyToken(ctx, &claims); err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, iris.PrivateError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(claims)
|
||||||
|
})
|
||||||
|
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
token := e.GET("/user/auth").Expect().Status(httptest.StatusOK).Body().Raw()
|
||||||
|
if token == "" {
|
||||||
|
t.Fatalf("empty token")
|
||||||
|
}
|
||||||
|
e.POST("/user").WithHeader("Authorization", "Bearer "+token).Expect().
|
||||||
|
Status(httptest.StatusOK).JSON().Object().ContainsMap(iris.Map{
|
||||||
|
"username": "makis",
|
||||||
|
"token": token, // Test SetToken.
|
||||||
|
})
|
||||||
|
|
||||||
|
e.POST("/user").Expect().Status(httptest.StatusUnauthorized)
|
||||||
|
time.Sleep(maxAge)
|
||||||
|
e.POST("/user").WithHeader("Authorization", "Bearer "+token).Expect().Status(httptest.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyUserAndExpected(t *testing.T) { // Tests the jwt.User struct + context validator + expected.
|
||||||
|
maxAge := testMaxAge / 2
|
||||||
|
j := jwt.HMAC(maxAge, "secret", "itsa16bytesecret")
|
||||||
|
expectedUser := j.NewUser(jwt.Username("makis"), jwt.Roles("admin"), jwt.Fields(iris.Map{
|
||||||
|
"custom": true,
|
||||||
|
})) // only for the sake of the test, we iniitalize it here.
|
||||||
|
expectedUser.Issuer = "tester"
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Get("/user/auth", func(ctx iris.Context) {
|
||||||
|
tok, err := expectedUser.GetToken()
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.WriteString(tok)
|
||||||
|
})
|
||||||
|
|
||||||
|
userAPI := app.Party("/user")
|
||||||
|
userAPI.Use(jwt.WithExpected(jwt.Expected{Issuer: "tester"}, j.VerifyUser()))
|
||||||
|
userAPI.Post("/", func(ctx iris.Context) {
|
||||||
|
user := ctx.User()
|
||||||
|
ctx.JSON(user)
|
||||||
|
})
|
||||||
|
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
token := e.GET("/user/auth").Expect().Status(httptest.StatusOK).Body().Raw()
|
||||||
|
if token == "" {
|
||||||
|
t.Fatalf("empty token")
|
||||||
|
}
|
||||||
|
|
||||||
|
e.POST("/user").WithHeader("Authorization", "Bearer "+token).Expect().
|
||||||
|
Status(httptest.StatusOK).JSON().Equal(expectedUser)
|
||||||
|
|
||||||
|
// Test generic client message if we don't manage the private error by ourselves.
|
||||||
|
e.POST("/user").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
}
|
||||||
|
|
187
middleware/jwt/user.go
Normal file
187
middleware/jwt/user.go
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User a common User structure for JWT.
|
||||||
|
// However, we're not limited to that one;
|
||||||
|
// any Go structure can be generated as a JWT token.
|
||||||
|
//
|
||||||
|
// Look `NewUser` and `VerifyUser` JWT middleware's methods.
|
||||||
|
// Use its `GetToken` method to generate the token when
|
||||||
|
// the User structure is set.
|
||||||
|
type User struct {
|
||||||
|
Claims
|
||||||
|
// Note: we could use a map too as the Token is generated when GetToken is called.
|
||||||
|
*context.SimpleUser
|
||||||
|
|
||||||
|
j *JWT
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ context.FeaturedUser = (*User)(nil)
|
||||||
|
_ TokenSetter = (*User)(nil)
|
||||||
|
_ ContextValidator = (*User)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserOption sets optional fields for a new User
|
||||||
|
// See `NewUser` instance function.
|
||||||
|
type UserOption func(*User)
|
||||||
|
|
||||||
|
// Username sets the Username and the JWT Claim's Subject
|
||||||
|
// to the given "username".
|
||||||
|
func Username(username string) UserOption {
|
||||||
|
return func(u *User) {
|
||||||
|
u.Username = username
|
||||||
|
u.Claims.Subject = username
|
||||||
|
u.Features = append(u.Features, context.UsernameFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email sets the Email field for the User field.
|
||||||
|
func Email(email string) UserOption {
|
||||||
|
return func(u *User) {
|
||||||
|
u.Email = email
|
||||||
|
u.Features = append(u.Features, context.EmailFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roles upserts to the User's Roles field.
|
||||||
|
func Roles(roles ...string) UserOption {
|
||||||
|
return func(u *User) {
|
||||||
|
u.Roles = roles
|
||||||
|
u.Features = append(u.Features, context.RolesFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxAge sets claims expiration and the AuthorizedAt User field.
|
||||||
|
func MaxAge(maxAge time.Duration) UserOption {
|
||||||
|
return func(u *User) {
|
||||||
|
now := time.Now()
|
||||||
|
u.Claims.Expiry = NewNumericDate(now.Add(maxAge))
|
||||||
|
u.Claims.IssuedAt = NewNumericDate(now)
|
||||||
|
u.AuthorizedAt = now
|
||||||
|
|
||||||
|
u.Features = append(u.Features, context.AuthorizedAtFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields copies the "fields" to the user's Fields field.
|
||||||
|
// This can be used to set custom fields to the User instance.
|
||||||
|
func Fields(fields context.Map) UserOption {
|
||||||
|
return func(u *User) {
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Fields == nil {
|
||||||
|
u.Fields = make(context.Map, len(fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range fields {
|
||||||
|
u.Fields[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Features = append(u.Features, context.FieldsFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetToken is called automaticaly on VerifyUser/VerifyObject.
|
||||||
|
// It sets the extracted from request, and verified from server raw token.
|
||||||
|
func (u *User) SetToken(token string) {
|
||||||
|
u.Token = token
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken overrides the SimpleUser's Token
|
||||||
|
// and returns the jwt generated token, among with
|
||||||
|
// a generator error, if any.
|
||||||
|
func (u *User) GetToken() (string, error) {
|
||||||
|
if u.Token != "" {
|
||||||
|
return u.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.j != nil { // it's always not nil.
|
||||||
|
if u.j.MaxAge > 0 {
|
||||||
|
// if the MaxAge option was not manually set, resolve it from the JWT instance.
|
||||||
|
MaxAge(u.j.MaxAge)(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we could generate a token here
|
||||||
|
// but let's do it on GetToken
|
||||||
|
// as the user fields may change
|
||||||
|
// by the caller manually until the token
|
||||||
|
// sent to the client.
|
||||||
|
tok, err := u.j.Token(u)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Token = tok
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Token == "" {
|
||||||
|
return "", ErrMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the current user's claims against
|
||||||
|
// the request. It's called automatically by the JWT instance.
|
||||||
|
func (u *User) Validate(ctx *context.Context, claims Claims, e Expected) error {
|
||||||
|
err := u.Claims.ValidateWithLeeway(e, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.SimpleUser.Authorization != "IRIS_JWT_USER" {
|
||||||
|
return ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could add specific User Expectations (new struct and accept an interface{}),
|
||||||
|
// but for the sake of code simplicity we don't, unless is requested, as the caller
|
||||||
|
// can validate specific fields by its own at the next step.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json unmarshaler interface.
|
||||||
|
func (u *User) UnmarshalJSON(data []byte) error {
|
||||||
|
err := Unmarshal(data, &u.Claims)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
simpleUser := new(context.SimpleUser)
|
||||||
|
err = Unmarshal(data, simpleUser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.SimpleUser = simpleUser
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json marshaler interface.
|
||||||
|
func (u *User) MarshalJSON() ([]byte, error) {
|
||||||
|
claimsB, err := Marshal(u.Claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userB, err := Marshal(u.SimpleUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userB) == 0 {
|
||||||
|
return claimsB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
claimsB = claimsB[0 : len(claimsB)-1] // remove last '}'
|
||||||
|
userB = userB[1:] // remove first '{'
|
||||||
|
|
||||||
|
raw := append(claimsB, ',')
|
||||||
|
raw = append(raw, userB...)
|
||||||
|
return raw, nil
|
||||||
|
}
|
212
middleware/jwt/validation.go
Normal file
212
middleware/jwt/validation.go
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
|
||||||
|
"github.com/square/go-jose/v3/json"
|
||||||
|
// Use this package instead of the standard encoding/json
|
||||||
|
// to marshal the NumericDate as expected by the implementation (see 'normalize`).
|
||||||
|
"github.com/square/go-jose/v3/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
claimsExpectedContextKey = "iris.jwt.claims.expected"
|
||||||
|
needsValidationContextKey = "iris.jwt.claims.unvalidated"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrMissing when token cannot be extracted from the request.
|
||||||
|
ErrMissing = errors.New("token is missing")
|
||||||
|
// ErrMissingKey when token does not contain a required JSON field.
|
||||||
|
ErrMissingKey = errors.New("token is missing a required field")
|
||||||
|
// ErrExpired indicates that token is used after expiry time indicated in exp claim.
|
||||||
|
ErrExpired = errors.New("token is expired (exp)")
|
||||||
|
// ErrNotValidYet indicates that token is used before time indicated in nbf claim.
|
||||||
|
ErrNotValidYet = errors.New("token not valid yet (nbf)")
|
||||||
|
// ErrIssuedInTheFuture indicates that the iat field is in the future.
|
||||||
|
ErrIssuedInTheFuture = errors.New("token issued in the future (iat)")
|
||||||
|
// ErrBlocked indicates that the token was not yet expired
|
||||||
|
// but was blocked by the server's Blocklist.
|
||||||
|
ErrBlocked = errors.New("token is blocked")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Expectation option to provide
|
||||||
|
// an extra layer of token validation, a claims type protection.
|
||||||
|
// See `VerifyToken` method.
|
||||||
|
type Expectation func(e *Expected, claims interface{}) error
|
||||||
|
|
||||||
|
// Expect protects the claims with the expected values.
|
||||||
|
func Expect(expected Expected) Expectation {
|
||||||
|
return func(e *Expected, _ interface{}) error {
|
||||||
|
*e = expected
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectID protects the claims with an ID validation.
|
||||||
|
func ExpectID(id string) Expectation {
|
||||||
|
return func(e *Expected, _ interface{}) error {
|
||||||
|
e.ID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectIssuer protects the claims with an issuer validation.
|
||||||
|
func ExpectIssuer(issuer string) Expectation {
|
||||||
|
return func(e *Expected, _ interface{}) error {
|
||||||
|
e.Issuer = issuer
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectSubject protects the claims with a subject validation.
|
||||||
|
func ExpectSubject(sub string) Expectation {
|
||||||
|
return func(e *Expected, _ interface{}) error {
|
||||||
|
e.Subject = sub
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectAudience protects the claims with an audience validation.
|
||||||
|
func ExpectAudience(audience ...string) Expectation {
|
||||||
|
return func(e *Expected, _ interface{}) error {
|
||||||
|
e.Audience = audience
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeetRequirements protects the custom fields of JWT claims
|
||||||
|
// based on the json:required tag; `json:"name,required"`.
|
||||||
|
// It accepts the value type.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// Verify/VerifyToken(... MeetRequirements(MyUser{}))
|
||||||
|
func MeetRequirements(claimsType interface{}) Expectation {
|
||||||
|
// pre-calculate if we need to use reflection at serve time to check for required fields,
|
||||||
|
// this can work as an alternative of expections for custom non-standard JWT fields.
|
||||||
|
requireFieldsIndexes := getRequiredFieldIndexes(claimsType)
|
||||||
|
|
||||||
|
return func(e *Expected, claims interface{}) error {
|
||||||
|
if len(requireFieldsIndexes) > 0 {
|
||||||
|
val := reflect.Indirect(reflect.ValueOf(claims))
|
||||||
|
for _, idx := range requireFieldsIndexes {
|
||||||
|
field := val.Field(idx)
|
||||||
|
if field.IsZero() {
|
||||||
|
return ErrMissingKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExpected is a middleware wrapper. It wraps a VerifyXXX middleware
|
||||||
|
// with expected claims fields protection.
|
||||||
|
// Usage:
|
||||||
|
// jwt.WithExpected(jwt.Expected{Issuer:"app"}, j.VerifyUser)
|
||||||
|
func WithExpected(e Expected, verifyHandler context.Handler) context.Handler {
|
||||||
|
return func(ctx *context.Context) {
|
||||||
|
ctx.Values().Set(claimsExpectedContextKey, e)
|
||||||
|
verifyHandler(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextValidator validates the object based on the given
|
||||||
|
// claims and the expected once. The end-developer
|
||||||
|
// can use this method for advanced validations based on the request Context.
|
||||||
|
type ContextValidator interface {
|
||||||
|
Validate(ctx *context.Context, claims Claims, e Expected) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateClaims(ctx *context.Context, dest interface{}, claims Claims, expected Expected) (err error) {
|
||||||
|
// Get any dynamic expectation set by prior middleware.
|
||||||
|
// See `WithExpected` middleware.
|
||||||
|
if v := ctx.Values().Get(claimsExpectedContextKey); v != nil {
|
||||||
|
if e, ok := v.(Expected); ok {
|
||||||
|
expected = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Force-set the time, it's important for expiration.
|
||||||
|
expected.Time = time.Now()
|
||||||
|
switch c := dest.(type) {
|
||||||
|
case Claims:
|
||||||
|
err = c.ValidateWithLeeway(expected, 0)
|
||||||
|
case ContextValidator:
|
||||||
|
err = c.Validate(ctx, claims, expected)
|
||||||
|
case *context.Map:
|
||||||
|
// if the dest is a map then set automatically the expiration settings here,
|
||||||
|
// so the caller can work further with it.
|
||||||
|
err = claims.ValidateWithLeeway(expected, 0)
|
||||||
|
if err == nil {
|
||||||
|
(*c)["exp"] = claims.Expiry
|
||||||
|
(*c)["iat"] = claims.IssuedAt
|
||||||
|
if claims.NotBefore != nil {
|
||||||
|
(*c)["nbf"] = claims.NotBefore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = claims.ValidateWithLeeway(expected, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case jwt.ErrExpired:
|
||||||
|
return ErrExpired
|
||||||
|
case jwt.ErrNotValidYet:
|
||||||
|
return ErrNotValidYet
|
||||||
|
case jwt.ErrIssuedInTheFuture:
|
||||||
|
return ErrIssuedInTheFuture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalize(i interface{}) (context.Map, error) {
|
||||||
|
if m, ok := i.(context.Map); ok {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make(context.Map)
|
||||||
|
|
||||||
|
raw, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d := json.NewDecoder(bytes.NewReader(raw))
|
||||||
|
d.UseNumber()
|
||||||
|
|
||||||
|
if err := d.Decode(&m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRequiredFieldIndexes(i interface{}) (v []int) {
|
||||||
|
val := reflect.Indirect(reflect.ValueOf(i))
|
||||||
|
typ := val.Type()
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
// Note: for the sake of simplicity we don't lookup for nested objects (FieldByIndex),
|
||||||
|
// we could do that as we do in dependency injection feature but unless requirested we don't.
|
||||||
|
tag := field.Tag.Get("json")
|
||||||
|
if strings.Contains(tag, ",required") {
|
||||||
|
v = append(v, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user