mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
Merge pull request #1662 from kataras/jwt-new-features
New JWT Middleware features and more
This commit is contained in:
commit
3d5ed9926e
10
HISTORY.md
10
HISTORY.md
|
@ -29,7 +29,7 @@ The codebase for Dependency Injection, Internationalization and localization and
|
||||||
## Fixes and Improvements
|
## Fixes and Improvements
|
||||||
|
|
||||||
- A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below.
|
- A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below.
|
||||||
- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/main.go) client credentials.
|
- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/main.go) or [jwt](https://github.com/kataras/iris/blob/master/_examples/auth/jwt/overview/main.go) client credentials.
|
||||||
- Add the ability to [share functions](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) between handlers chain and add an [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-services) on sharing Go structures (aka services).
|
- Add the ability to [share functions](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) between handlers chain and add an [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-services) on sharing Go structures (aka services).
|
||||||
|
|
||||||
- Add the new `Party.UseOnce` method to the `*Route`
|
- Add the new `Party.UseOnce` method to the `*Route`
|
||||||
|
@ -262,7 +262,7 @@ var dirOpts = iris.DirOptions{
|
||||||
|
|
||||||
- New builtin [requestid](https://github.com/kataras/iris/tree/master/middleware/requestid) middleware.
|
- New builtin [requestid](https://github.com/kataras/iris/tree/master/middleware/requestid) middleware.
|
||||||
|
|
||||||
- New builtin [JWT](https://github.com/kataras/iris/tree/master/middleware/jwt) middleware based on [square/go-jose](https://github.com/square/go-jose) featured with optional encryption to set claims with sensitive data when necessary.
|
- New builtin [JWT](https://github.com/kataras/iris/tree/master/middleware/jwt) middleware based on the fastest JWT implementation; [kataras/jwt](https://github.com/kataras/jwt) featured with optional wire encryption to set claims with sensitive data when necessary.
|
||||||
|
|
||||||
- New `iris.RouteOverlap` route registration rule. `Party.SetRegisterRule(iris.RouteOverlap)` to allow overlapping across multiple routes for the same request subdomain, method, path. See [1536#issuecomment-643719922](https://github.com/kataras/iris/issues/1536#issuecomment-643719922). This allows two or more **MVC Controllers** to listen on the same path based on one or more registered dependencies (see [_examples/mvc/authenticated-controller](https://github.com/kataras/iris/tree/master/_examples/mvc/authenticated-controller)).
|
- New `iris.RouteOverlap` route registration rule. `Party.SetRegisterRule(iris.RouteOverlap)` to allow overlapping across multiple routes for the same request subdomain, method, path. See [1536#issuecomment-643719922](https://github.com/kataras/iris/issues/1536#issuecomment-643719922). This allows two or more **MVC Controllers** to listen on the same path based on one or more registered dependencies (see [_examples/mvc/authenticated-controller](https://github.com/kataras/iris/tree/master/_examples/mvc/authenticated-controller)).
|
||||||
|
|
||||||
|
@ -310,13 +310,14 @@ var dirOpts = iris.DirOptions{
|
||||||
|
|
||||||
## New Context Methods
|
## New Context Methods
|
||||||
|
|
||||||
|
- `Context.ReadURL(ptr interface{}) error` shortcut of `ReadParams` and `ReadQuery`. Binds URL dynamic path parameters and URL query parameters to the given "ptr" pointer of a struct value.
|
||||||
- `Context.SetUser(User)` and `Context.User() User` to store and retrieve an authenticated client. Read more [here](https://github.com/iris-contrib/middleware/issues/63).
|
- `Context.SetUser(User)` and `Context.User() User` to store and retrieve an authenticated client. Read more [here](https://github.com/iris-contrib/middleware/issues/63).
|
||||||
- `Context.SetLogoutFunc(fn interface{}, persistenceArgs ...interface{})` and `Logout(args ...interface{}) error` methods to allow different kind of auth middlewares to be able to set a "logout" a user/client feature with a single function, the route handler may not be aware of the implementation of the authentication used.
|
- `Context.SetLogoutFunc(fn interface{}, persistenceArgs ...interface{})` and `Logout(args ...interface{}) error` methods to allow different kind of auth middlewares to be able to set a "logout" a user/client feature with a single function, the route handler may not be aware of the implementation of the authentication used.
|
||||||
- `Context.SetFunc(name string, fn interface{}, persistenceArgs ...interface{})` and `Context.CallFunc(name string, args ...interface{}) ([]reflect.Value, error)` to allow middlewares to share functions dynamically when the type of the function is not predictable, see the [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) for more.
|
- `Context.SetFunc(name string, fn interface{}, persistenceArgs ...interface{})` and `Context.CallFunc(name string, args ...interface{}) ([]reflect.Value, error)` to allow middlewares to share functions dynamically when the type of the function is not predictable, see the [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) for more.
|
||||||
- `Context.TextYAML(interface{}) error` same as `Context.YAML` but with set the Content-Type to `text/yaml` instead (Google Chrome renders it as text).
|
- `Context.TextYAML(interface{}) error` same as `Context.YAML` but with set the Content-Type to `text/yaml` instead (Google Chrome renders it as text).
|
||||||
- `Context.IsDebug() bool` reports whether the application is running under debug/development mode. It is a shortcut of Application.Logger().Level >= golog.DebugLevel.
|
- `Context.IsDebug() bool` reports whether the application is running under debug/development mode. It is a shortcut of Application.Logger().Level >= golog.DebugLevel.
|
||||||
- `Context.IsRecovered() bool` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/master/middleware/recover). Also the `iris.IsErrPrivate` function and `iris.ErrPrivate` interface have been introduced.
|
- `Context.IsRecovered() bool` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/master/middleware/recover). Also the `Context.GetErrPublic() (bool, error)`, `Context.SetErrPrivate(err error)` methods and `iris.ErrPrivate` interface have been introduced.
|
||||||
- `Context.RecordBody()` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once.
|
- `Context.RecordRequestBody(bool)` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once.
|
||||||
- `Context.IsRecordingBody() bool` reports whether the request body can be readen multiple times.
|
- `Context.IsRecordingBody() bool` reports whether the request body can be readen multiple times.
|
||||||
- `Context.ReadHeaders(ptr interface{}) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-headers/main.go).
|
- `Context.ReadHeaders(ptr interface{}) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-headers/main.go).
|
||||||
- `Context.ReadParams(ptr interface{}) error` binds dynamic path parameters to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-params/main.go).
|
- `Context.ReadParams(ptr interface{}) error` binds dynamic path parameters to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-params/main.go).
|
||||||
|
@ -490,6 +491,7 @@ Prior to this version the `iris.Context` was the only one dependency that has be
|
||||||
| [net.IP](https://golang.org/pkg/net/#IP) | `net.ParseIP(ctx.RemoteAddr())` |
|
| [net.IP](https://golang.org/pkg/net/#IP) | `net.ParseIP(ctx.RemoteAddr())` |
|
||||||
| [mvc.Code](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Code) | `ctx.GetStatusCode() int` |
|
| [mvc.Code](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Code) | `ctx.GetStatusCode() int` |
|
||||||
| [mvc.Err](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Err) | `ctx.GetErr() error` |
|
| [mvc.Err](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Err) | `ctx.GetErr() error` |
|
||||||
|
| [iris/context.User](https://pkg.go.dev/github.com/kataras/iris/v12/context?tab=doc#User) | `ctx.User()` |
|
||||||
| `string`, | |
|
| `string`, | |
|
||||||
| `int, int8, int16, int32, int64`, | |
|
| `int, int8, int16, int32, int64`, | |
|
||||||
| `uint, uint8, uint16, uint32, uint64`, | |
|
| `uint, uint8, uint16, uint32, uint64`, | |
|
||||||
|
|
6
NOTICE
6
NOTICE
|
@ -101,9 +101,9 @@ Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca
|
||||||
toml 3012a1dbe2e4bd1 https://github.com/BurntSushi/toml
|
toml 3012a1dbe2e4bd1 https://github.com/BurntSushi/toml
|
||||||
391d42b32f0577c
|
391d42b32f0577c
|
||||||
b7bbc7f005
|
b7bbc7f005
|
||||||
jose d84c719419c2a90 https://github.com/square/go-jose
|
jwt 5f34e0a4e28178b https://github.com/kataras/jwt
|
||||||
8d188ea67e09652
|
3781df69552bdc5
|
||||||
f5c1929ae8
|
481a0d4bef
|
||||||
uuid cb32006e483f2a2 https://github.com/google/uuid
|
uuid cb32006e483f2a2 https://github.com/google/uuid
|
||||||
3230e24209cf185
|
3230e24209cf185
|
||||||
c65b477dbf
|
c65b477dbf
|
||||||
|
|
|
@ -168,8 +168,9 @@
|
||||||
* [Bind Form](request-body/read-form/main.go)
|
* [Bind Form](request-body/read-form/main.go)
|
||||||
* [Checkboxes](request-body/read-form/checkboxes/main.go)
|
* [Checkboxes](request-body/read-form/checkboxes/main.go)
|
||||||
* [Bind Query](request-body/read-query/main.go)
|
* [Bind Query](request-body/read-query/main.go)
|
||||||
* [Bind Headers](request-body/read-headers/main.go)
|
|
||||||
* [Bind Params](request-body/read-params/main.go)
|
* [Bind Params](request-body/read-params/main.go)
|
||||||
|
* [Bind URL](request-body/read-url/main.go)
|
||||||
|
* [Bind Headers](request-body/read-headers/main.go)
|
||||||
* [Bind Body](request-body/read-body/main.go)
|
* [Bind Body](request-body/read-body/main.go)
|
||||||
* [Bind Custom per type](request-body/read-custom-per-type/main.go)
|
* [Bind Custom per type](request-body/read-custom-per-type/main.go)
|
||||||
* [Bind Custom via Unmarshaler](request-body/read-custom-via-unmarshaler/main.go)
|
* [Bind Custom via Unmarshaler](request-body/read-custom-via-unmarshaler/main.go)
|
||||||
|
@ -197,8 +198,12 @@
|
||||||
* Authentication, Authorization & Bot Detection
|
* Authentication, Authorization & Bot Detection
|
||||||
* [Basic Authentication](auth/basicauth/main.go)
|
* [Basic Authentication](auth/basicauth/main.go)
|
||||||
* [CORS](auth/cors)
|
* [CORS](auth/cors)
|
||||||
* [JWT](auth/jwt/main.go)
|
* JSON Web Tokens
|
||||||
|
* [Basic](auth/jwt/basic/main.go)
|
||||||
|
* [Middleware](auth/jwt/midleware/main.go)
|
||||||
|
* [Blocklist](auth/jwt/blocklist/main.go)
|
||||||
* [Refresh Token](auth/jwt/refresh-token/main.go)
|
* [Refresh Token](auth/jwt/refresh-token/main.go)
|
||||||
|
* [Tutorial](auth/jwt/tutorial)
|
||||||
* [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go)
|
* [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go)
|
||||||
* [OAUth2](auth/goth/main.go)
|
* [OAUth2](auth/goth/main.go)
|
||||||
* [Manage Permissions](auth/permissions/main.go)
|
* [Manage Permissions](auth/permissions/main.go)
|
||||||
|
@ -218,6 +223,7 @@
|
||||||
* [Badger](sessions/database/badger/main.go)
|
* [Badger](sessions/database/badger/main.go)
|
||||||
* [BoltDB](sessions/database/boltdb/main.go)
|
* [BoltDB](sessions/database/boltdb/main.go)
|
||||||
* [Redis](sessions/database/redis/main.go)
|
* [Redis](sessions/database/redis/main.go)
|
||||||
|
* [View Data](sessions/viewdata)
|
||||||
* Websocket
|
* Websocket
|
||||||
* [Gorilla FileWatch (3rd-party)](websocket/gorilla-filewatch/main.go)
|
* [Gorilla FileWatch (3rd-party)](websocket/gorilla-filewatch/main.go)
|
||||||
* [Basic](websocket/basic)
|
* [Basic](websocket/basic)
|
||||||
|
|
|
@ -56,7 +56,9 @@ func h(ctx iris.Context) {
|
||||||
// makes sure for that, otherwise this handler will not be executed.
|
// makes sure for that, otherwise this handler will not be executed.
|
||||||
// OR:
|
// OR:
|
||||||
user := ctx.User()
|
user := ctx.User()
|
||||||
ctx.Writef("%s %s:%s", ctx.Path(), user.GetUsername(), user.GetPassword())
|
username, _ := user.GetUsername()
|
||||||
|
password, _ := user.GetPassword
|
||||||
|
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logout(ctx iris.Context) {
|
func logout(ctx iris.Context) {
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
# Generate RSA
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ openssl genrsa -des3 -out private_rsa.pem 2048
|
|
||||||
```
|
|
||||||
|
|
||||||
```go
|
|
||||||
b, err := ioutil.ReadFile("./private_rsa.pem")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
key := jwt.MustParseRSAPrivateKey(b, []byte("pass"))
|
|
||||||
```
|
|
||||||
|
|
||||||
OR
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "crypto/rand"
|
|
||||||
import "crypto/rsa"
|
|
||||||
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
```
|
|
||||||
|
|
||||||
# Generate Ed25519
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ openssl genpkey -algorithm Ed25519 -out private_ed25519.pem
|
|
||||||
$ openssl req -x509 -key private_ed25519.pem -out cert_ed25519.pem -days 365
|
|
||||||
```
|
|
78
_examples/auth/jwt/basic/main.go
Normal file
78
_examples/auth/jwt/basic/main.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Learn how to use any JWT 3rd-party package with Iris.
|
||||||
|
In this example we use the kataras/jwt one.
|
||||||
|
|
||||||
|
Install with:
|
||||||
|
go get -u github.com/kataras/jwt
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
https://github.com/kataras/jwt#table-of-contents
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Replace with your own key and keep them secret.
|
||||||
|
// The "signatureSharedKey" is used for the HMAC(HS256) signature algorithm.
|
||||||
|
var signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
app.Get("/", generateToken)
|
||||||
|
app.Get("/protected", protected)
|
||||||
|
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
type fooClaims struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateToken(ctx iris.Context) {
|
||||||
|
claims := fooClaims{
|
||||||
|
Foo: "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign and generate compact form token.
|
||||||
|
token, err := jwt.Sign(jwt.HS256, signatureSharedKey, claims, jwt.MaxAge(10*time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString := string(token) // or jwt.BytesToString
|
||||||
|
ctx.HTML(`Token: ` + tokenString + `<br/><br/>
|
||||||
|
<a href="/protected?token=` + tokenString + `">/protected?token=` + tokenString + `</a>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func protected(ctx iris.Context) {
|
||||||
|
// Extract the token, e.g. cookie, Authorization: Bearer $token
|
||||||
|
// or URL query.
|
||||||
|
token := ctx.URLParam("token")
|
||||||
|
// Verify the token.
|
||||||
|
verifiedToken, err := jwt.Verify(jwt.HS256, signatureSharedKey, []byte(token))
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Writef("This is an authenticated request.\n\n")
|
||||||
|
|
||||||
|
// Decode the custom claims.
|
||||||
|
var claims fooClaims
|
||||||
|
verifiedToken.Claims(&claims)
|
||||||
|
|
||||||
|
// Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp").
|
||||||
|
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
|
||||||
|
expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
|
||||||
|
timeLeft := standardClaims.Timeleft()
|
||||||
|
|
||||||
|
ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
|
||||||
|
}
|
101
_examples/auth/jwt/blocklist/main.go
Normal file
101
_examples/auth/jwt/blocklist/main.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/middleware/jwt"
|
||||||
|
"github.com/kataras/iris/v12/middleware/jwt/blocklist/redis"
|
||||||
|
|
||||||
|
// Optionally to set token identifier.
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
|
||||||
|
|
||||||
|
signer = jwt.NewSigner(jwt.HS256, signatureSharedKey, 15*time.Minute)
|
||||||
|
verifier = jwt.NewVerifier(jwt.HS256, signatureSharedKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
type userClaims struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
// IMPORTANT
|
||||||
|
//
|
||||||
|
// To use the in-memory blocklist just:
|
||||||
|
// verifier.WithDefaultBlocklist()
|
||||||
|
// To use a persistence blocklist, e.g. redis,
|
||||||
|
// start your redis-server and:
|
||||||
|
blocklist := redis.NewBlocklist()
|
||||||
|
// To configure single client or a cluster one:
|
||||||
|
// blocklist.ClientOptions.Addr = "127.0.0.1:6379"
|
||||||
|
// blocklist.ClusterOptions.Addrs = []string{...}
|
||||||
|
// To set a prefix for jwt ids:
|
||||||
|
// blocklist.Prefix = "myapp-"
|
||||||
|
//
|
||||||
|
// To manually connect and check its error before continue:
|
||||||
|
// err := blocklist.Connect()
|
||||||
|
// By default the verifier will try to connect, if failed then it will throw http error.
|
||||||
|
//
|
||||||
|
// And then register it:
|
||||||
|
verifier.Blocklist = blocklist
|
||||||
|
verifyMiddleware := verifier.Verify(func() interface{} {
|
||||||
|
return new(userClaims)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/", authenticate)
|
||||||
|
|
||||||
|
protectedAPI := app.Party("/protected", verifyMiddleware)
|
||||||
|
protectedAPI.Get("/", protected)
|
||||||
|
protectedAPI.Get("/logout", logout)
|
||||||
|
|
||||||
|
// http://localhost:8080
|
||||||
|
// http://localhost:8080/protected?token=$token
|
||||||
|
// http://localhost:8080/logout?token=$token
|
||||||
|
// http://localhost:8080/protected?token=$token (401)
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticate(ctx iris.Context) {
|
||||||
|
claims := userClaims{
|
||||||
|
Username: "kataras",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT ID.
|
||||||
|
random, err := uuid.NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := random.String()
|
||||||
|
|
||||||
|
// Set the ID with the jwt.ID.
|
||||||
|
token, err := signer.Sign(claims, jwt.ID(id))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Write(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func protected(ctx iris.Context) {
|
||||||
|
claims := jwt.Get(ctx).(*userClaims)
|
||||||
|
|
||||||
|
// To the standard claims, e.g. the generated ID:
|
||||||
|
// jwt.GetVerifiedToken(ctx).StandardClaims.ID
|
||||||
|
|
||||||
|
ctx.WriteString(claims.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logout(ctx iris.Context) {
|
||||||
|
ctx.Logout()
|
||||||
|
|
||||||
|
ctx.Redirect("/", iris.StatusTemporaryRedirect)
|
||||||
|
}
|
|
@ -1,159 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
|
||||||
"github.com/kataras/iris/v12/middleware/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserClaims a custom claims structure. You can just use jwt.Claims too.
|
|
||||||
type UserClaims struct {
|
|
||||||
jwt.Claims
|
|
||||||
Username string
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Get keys from system's environment variables
|
|
||||||
// JWT_SECRET (for signing and verification) and JWT_SECRET_ENC(for encryption and decryption),
|
|
||||||
// or defaults to "secret" and "itsa16bytesecret" respectfully.
|
|
||||||
//
|
|
||||||
// Use the `jwt.New` instead for more flexibility, if necessary.
|
|
||||||
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
|
||||||
// By default it extracts the token from url parameter "token={token}"
|
|
||||||
// and the Authorization Bearer {token} header.
|
|
||||||
// You can also take token from JSON body:
|
|
||||||
// j.Extractors = append(j.Extractors, jwt.FromJSON)
|
|
||||||
|
|
||||||
app := iris.New()
|
|
||||||
app.Logger().SetLevel("debug")
|
|
||||||
|
|
||||||
app.Get("/authenticate", func(ctx iris.Context) {
|
|
||||||
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
|
|
||||||
// NOTE: if custom claims then the `j.Expiry(claims)` (or jwt.Expiry(duration, claims))
|
|
||||||
// MUST be called in order to set the expiration time.
|
|
||||||
customClaims := UserClaims{
|
|
||||||
Claims: j.Expiry(standardClaims),
|
|
||||||
Username: "kataras",
|
|
||||||
}
|
|
||||||
|
|
||||||
j.WriteToken(ctx, customClaims)
|
|
||||||
})
|
|
||||||
|
|
||||||
userRouter := app.Party("/user")
|
|
||||||
{
|
|
||||||
// userRouter.Use(j.Verify)
|
|
||||||
// userRouter.Get("/", func(ctx iris.Context) {
|
|
||||||
// var claims UserClaims
|
|
||||||
// if err := jwt.ReadClaims(ctx, &claims); err != nil {
|
|
||||||
// // Validation-only errors, the rest are already
|
|
||||||
// // checked on `j.Verify` middleware.
|
|
||||||
// ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// ctx.Writef("Claims: %#+v\n", claims)
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// OR:
|
|
||||||
userRouter.Get("/", func(ctx iris.Context) {
|
|
||||||
var claims UserClaims
|
|
||||||
if err := j.VerifyToken(ctx, &claims); err != nil {
|
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, claims.Expiry.Time())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Listen(":8080")
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
func default_RSA_Example() {
|
|
||||||
j := jwt.RSA(15*time.Minute)
|
|
||||||
}
|
|
||||||
|
|
||||||
Same as:
|
|
||||||
|
|
||||||
func load_File_Or_Generate_RSA_Example() {
|
|
||||||
signKey, err := jwt.LoadRSA("jwt_sign.key", 2048)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := jwt.New(15*time.Minute, jwt.RS256, signKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
encKey, err := jwt.LoadRSA("jwt_enc.key", 2048)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = j.WithEncryption(jwt.A128CBCHS256, jwt.RSA15, encKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
func hmac_Example() {
|
|
||||||
// hmac
|
|
||||||
key := []byte("secret")
|
|
||||||
j, err := jwt.New(15*time.Minute, jwt.HS256, key)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OPTIONAL encryption:
|
|
||||||
encryptionKey := []byte("itsa16bytesecret")
|
|
||||||
err = j.WithEncryption(jwt.A128GCM, jwt.DIRECT, encryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
func load_From_File_With_Password_Example() {
|
|
||||||
b, err := ioutil.ReadFile("./rsa_password_protected.key")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
signKey,err := jwt.ParseRSAPrivateKey(b, []byte("pass"))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := jwt.New(15*time.Minute, jwt.RS256, signKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
func generate_RSA_Example() {
|
|
||||||
signKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptionKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := jwt.New(15*time.Minute, jwt.RS512, signKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = j.WithEncryption(jwt.A128CBCHS256, jwt.RSA15, encryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
91
_examples/auth/jwt/middleware/main.go
Normal file
91
_examples/auth/jwt/middleware/main.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/middleware/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
sigKey = []byte("signature_hmac_secret_shared_key")
|
||||||
|
encKey = []byte("GCM_AES_256_secret_shared_key_32")
|
||||||
|
)
|
||||||
|
|
||||||
|
type fooClaims struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
In this example you will learn the essentials
|
||||||
|
of the Iris builtin JWT middleware based on the github.com/kataras/jwt package.
|
||||||
|
*/
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
signer := jwt.NewSigner(jwt.HS256, sigKey, 10*time.Minute)
|
||||||
|
// Enable payload encryption with:
|
||||||
|
// signer.WithEncryption(encKey, nil)
|
||||||
|
app.Get("/", generateToken(signer))
|
||||||
|
|
||||||
|
verifier := jwt.NewVerifier(jwt.HS256, sigKey)
|
||||||
|
// Enable server-side token block feature (even before its expiration time):
|
||||||
|
verifier.WithDefaultBlocklist()
|
||||||
|
// Enable payload decryption with:
|
||||||
|
// verifier.WithDecryption(encKey, nil)
|
||||||
|
verifyMiddleware := verifier.Verify(func() interface{} {
|
||||||
|
return new(fooClaims)
|
||||||
|
})
|
||||||
|
|
||||||
|
protectedAPI := app.Party("/protected")
|
||||||
|
// Register the verify middleware to allow access only to authorized clients.
|
||||||
|
protectedAPI.Use(verifyMiddleware)
|
||||||
|
// ^ or UseRouter(verifyMiddleware) to disallow unauthorized http error handlers too.
|
||||||
|
|
||||||
|
protectedAPI.Get("/", protected)
|
||||||
|
// Invalidate the token through server-side, even if it's not expired yet.
|
||||||
|
protectedAPI.Get("/logout", logout)
|
||||||
|
|
||||||
|
// http://localhost:8080
|
||||||
|
// http://localhost:8080/protected?token=$token (or Authorization: Bearer $token)
|
||||||
|
// http://localhost:8080/protected/logout?token=$token
|
||||||
|
// http://localhost:8080/protected?token=$token (401)
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateToken(signer *jwt.Signer) iris.Handler {
|
||||||
|
return func(ctx iris.Context) {
|
||||||
|
claims := fooClaims{Foo: "bar"}
|
||||||
|
|
||||||
|
token, err := signer.Sign(claims)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Write(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func protected(ctx iris.Context) {
|
||||||
|
// Get the verified and decoded claims.
|
||||||
|
claims := jwt.Get(ctx).(*fooClaims)
|
||||||
|
|
||||||
|
// Optionally, get token information if you want to work with them.
|
||||||
|
// Just an example on how you can retrieve all the standard claims (set by signer's max age, "exp").
|
||||||
|
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
|
||||||
|
expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
|
||||||
|
timeLeft := standardClaims.Timeleft()
|
||||||
|
|
||||||
|
ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logout(ctx iris.Context) {
|
||||||
|
err := ctx.Logout()
|
||||||
|
if err != nil {
|
||||||
|
ctx.WriteString(err.Error())
|
||||||
|
} else {
|
||||||
|
ctx.Writef("token invalidated, a new token is required to access the protected API")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,139 +1,183 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
"github.com/kataras/iris/v12/middleware/jwt"
|
"github.com/kataras/iris/v12/middleware/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserClaims a custom claims structure. You can just use jwt.Claims too.
|
const (
|
||||||
|
accessTokenMaxAge = 10 * time.Minute
|
||||||
|
refreshTokenMaxAge = time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
privateKey, publicKey = jwt.MustLoadRSA("rsa_private_key.pem", "rsa_public_key.pem")
|
||||||
|
|
||||||
|
signer = jwt.NewSigner(jwt.RS256, privateKey, accessTokenMaxAge)
|
||||||
|
verifier = jwt.NewVerifier(jwt.RS256, publicKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserClaims a custom access claims structure.
|
||||||
type UserClaims struct {
|
type UserClaims struct {
|
||||||
jwt.Claims
|
ID string `json:"user_id"`
|
||||||
Username string
|
// Do: `json:"username,required"` to have this field required
|
||||||
|
// or see the Validate method below instead.
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenPair holds the access token and refresh token response.
|
// GetID implements the partial context user's ID interface.
|
||||||
type TokenPair struct {
|
// Note that if claims were a map then the claims value converted to UserClaims
|
||||||
AccessToken string `json:"access_token"`
|
// and no need to implement any method.
|
||||||
RefreshToken string `json:"refresh_token"`
|
//
|
||||||
|
// This is useful when multiple auth methods are used (e.g. basic auth, jwt)
|
||||||
|
// but they all share a couple of methods.
|
||||||
|
func (u *UserClaims) GetID() string {
|
||||||
|
return u.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsername implements the partial context user's Username interface.
|
||||||
|
func (u *UserClaims) GetUsername() string {
|
||||||
|
return u.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate completes the middleware's custom ClaimsValidator.
|
||||||
|
// It will not accept a token which its claims missing the username field
|
||||||
|
// (useful to not accept refresh tokens generated by the same algorithm).
|
||||||
|
func (u *UserClaims) Validate() error {
|
||||||
|
if u.Username == "" {
|
||||||
|
return fmt.Errorf("username field is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For refresh token, we will just use the jwt.Claims
|
||||||
|
// structure which contains the standard JWT fields.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized)
|
||||||
|
|
||||||
// Access token, short-live.
|
app.Get("/authenticate", generateTokenPair)
|
||||||
accessJWT := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
app.Get("/refresh", refreshToken)
|
||||||
// Refresh token, long-live. Important: Give different secret keys(!)
|
|
||||||
refreshJWT := jwt.HMAC(1*time.Hour, "other secret", "other16bytesecre")
|
protectedAPI := app.Party("/protected")
|
||||||
// On refresh token, we extract it only from a request body
|
{
|
||||||
// of JSON, e.g. {"refresh_token": $token }.
|
verifyMiddleware := verifier.Verify(func() interface{} {
|
||||||
// You can also do it manually in the handler level though.
|
return new(UserClaims)
|
||||||
refreshJWT.Extractors = []jwt.TokenExtractor{
|
})
|
||||||
jwt.FromJSON("refresh_token"),
|
|
||||||
|
protectedAPI.Use(verifyMiddleware)
|
||||||
|
|
||||||
|
protectedAPI.Get("/", func(ctx iris.Context) {
|
||||||
|
// Access the claims through: jwt.Get:
|
||||||
|
// claims := jwt.Get(ctx).(*UserClaims)
|
||||||
|
// ctx.Writef("Username: %s\n", claims.Username)
|
||||||
|
//
|
||||||
|
// OR through context's user (if at least one method was implement by our UserClaims):
|
||||||
|
user := ctx.User()
|
||||||
|
id, _ := user.GetID()
|
||||||
|
username, _ := user.GetUsername()
|
||||||
|
ctx.Writef("ID: %s\nUsername: %s\n", id, username)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate access and refresh tokens and send to the client.
|
// http://localhost:8080/protected (401)
|
||||||
app.Get("/authenticate", func(ctx iris.Context) {
|
|
||||||
tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
|
|
||||||
if err != nil {
|
|
||||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(tokenPair)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Get("/refresh", func(ctx iris.Context) {
|
|
||||||
// Manual (if jwt.FromJSON missing):
|
|
||||||
// var payload = struct {
|
|
||||||
// RefreshToken string `json:"refresh_token"`
|
|
||||||
// }{}
|
|
||||||
//
|
|
||||||
// err := ctx.ReadJSON(&payload)
|
|
||||||
// if err != nil {
|
|
||||||
// ctx.StatusCode(iris.StatusBadRequest)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// j.VerifyTokenString(ctx, payload.RefreshToken, &claims)
|
|
||||||
|
|
||||||
var claims jwt.Claims
|
|
||||||
if err := refreshJWT.VerifyToken(ctx, &claims); err != nil {
|
|
||||||
ctx.Application().Logger().Warnf("verify refresh token: %v", err)
|
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := claims.Subject
|
|
||||||
if userID == "" {
|
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate a database call against our jwt subject.
|
|
||||||
if userID != "53afcf05-38a3-43c3-82af-8bbbe0e4a149" {
|
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// All OK, re-generate the new pair and send to client.
|
|
||||||
tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
|
|
||||||
if err != nil {
|
|
||||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(tokenPair)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Get("/", func(ctx iris.Context) {
|
|
||||||
var claims UserClaims
|
|
||||||
if err := accessJWT.VerifyToken(ctx, &claims); err != nil {
|
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, claims.Expiry.Time())
|
|
||||||
})
|
|
||||||
|
|
||||||
// http://localhost:8080 (401)
|
|
||||||
// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
|
// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
|
||||||
// http://localhost:8080?token={access_token} (200)
|
// http://localhost:8080/protected?token={access_token} (200)
|
||||||
// http://localhost:8080?token={refresh_token} (401)
|
// http://localhost:8080/protected?token={refresh_token} (401)
|
||||||
// http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
|
// http://localhost:8080/refresh?refresh_token={refresh_token}
|
||||||
|
// OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
|
||||||
|
// http://localhost:8080/refresh?refresh_token={access_token} (401)
|
||||||
app.Listen(":8080")
|
app.Listen(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateTokenPair(accessJWT, refreshJWT *jwt.JWT) (TokenPair, error) {
|
func generateTokenPair(ctx iris.Context) {
|
||||||
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
|
// Simulate a user...
|
||||||
|
userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||||
|
|
||||||
customClaims := UserClaims{
|
// Map the current user with the refresh token,
|
||||||
Claims: accessJWT.Expiry(standardClaims),
|
// so we make sure, on refresh route, that this refresh token owns
|
||||||
|
// to that user before re-generate.
|
||||||
|
refreshClaims := jwt.Claims{Subject: userID}
|
||||||
|
|
||||||
|
accessClaims := UserClaims{
|
||||||
|
ID: userID,
|
||||||
Username: "kataras",
|
Username: "kataras",
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, err := accessJWT.Token(customClaims)
|
// Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.
|
||||||
|
// First argument is the access claims,
|
||||||
|
// second argument is the refresh claims,
|
||||||
|
// third argument is the refresh max age.
|
||||||
|
tokenPair, err := signer.NewTokenPair(accessClaims, refreshClaims, refreshTokenMaxAge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return TokenPair{}, err
|
ctx.Application().Logger().Errorf("token pair: %v", err)
|
||||||
|
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// At refresh tokens you don't need any custom claims.
|
// Send the generated token pair to the client.
|
||||||
refreshClaims := refreshJWT.Expiry(jwt.Claims{
|
// The tokenPair looks like: {"access_token": $token, "refresh_token": $token}
|
||||||
ID: "refresh_kataras",
|
ctx.JSON(tokenPair)
|
||||||
// For example, the User ID,
|
}
|
||||||
// this is necessary to check against the database
|
|
||||||
// if the user still exist or has credentials to access our page.
|
// There are various methods of refresh token, depending on the application requirements.
|
||||||
Subject: "53afcf05-38a3-43c3-82af-8bbbe0e4a149",
|
// In this example we will accept a refresh token only, we will verify only a refresh token
|
||||||
})
|
// and we re-generate a whole new pair. An alternative would be to accept a token pair
|
||||||
|
// of both access and refresh tokens, verify the refresh, verify the access with a Leeway time
|
||||||
refreshToken, err := refreshJWT.Token(refreshClaims)
|
// and check if its going to expire soon, then generate a single access token.
|
||||||
if err != nil {
|
func refreshToken(ctx iris.Context) {
|
||||||
return TokenPair{}, err
|
// Assuming you have access to the current user, e.g. sessions.
|
||||||
}
|
//
|
||||||
|
// Simulate a database call against our jwt subject
|
||||||
return TokenPair{
|
// to make sure that this refresh token is a pair generated by this user.
|
||||||
AccessToken: accessToken,
|
// * Note: You can remove the ExpectSubject and do this validation later on by yourself.
|
||||||
RefreshToken: refreshToken,
|
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||||
}, nil
|
|
||||||
|
// Get the refresh token from ?refresh_token=$token OR
|
||||||
|
// the request body's JSON{"refresh_token": "$token"}.
|
||||||
|
refreshToken := []byte(ctx.URLParam("refresh_token"))
|
||||||
|
if len(refreshToken) == 0 {
|
||||||
|
// You can read the whole body with ctx.GetBody/ReadBody too.
|
||||||
|
var tokenPair jwt.TokenPair
|
||||||
|
if err := ctx.ReadJSON(&tokenPair); err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = tokenPair.RefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the refresh token, which its subject MUST match the "currentUserID".
|
||||||
|
_, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Application().Logger().Errorf("verify refresh token: %v", err)
|
||||||
|
ctx.StatusCode(iris.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom validation checks can be performed after Verify calls too:
|
||||||
|
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||||
|
userID := verifiedToken.StandardClaims.Subject
|
||||||
|
if userID != currentUserID {
|
||||||
|
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// All OK, re-generate the new pair and send to client,
|
||||||
|
// we could only generate an access token as well.
|
||||||
|
generateTokenPair(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUnauthorized(ctx iris.Context) {
|
||||||
|
if err := ctx.GetErr(); err != nil {
|
||||||
|
ctx.Application().Logger().Errorf("unauthorized: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.WriteString("Unauthorized")
|
||||||
}
|
}
|
||||||
|
|
27
_examples/auth/jwt/refresh-token/rsa_private_key.pem
Normal file
27
_examples/auth/jwt/refresh-token/rsa_private_key.pem
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEArwO0q8WbBvrplz3lTQjsWu66HC7M3mVAjmjLq8Wj/ipqVtiJ
|
||||||
|
MrUL9t/0q9PNO/KX9u+HayFNYM4TnYkXVZX3M5E31W8fPPy74D/XpqFwrwT7bAEw
|
||||||
|
pT51JJyxkoBAyOh08lmR2EYvpGF7qErra7qbkk4LGFbhoFCXdMLXguT4rPymkzFH
|
||||||
|
dQrmGYOBS+v9imSuJddCZpXyv6Ko7AKB4mhzg4RC5RJZO5GEHVUrSMHxZB0syF8c
|
||||||
|
U+28iL8A7SlGKTNZPZiHmCQVRqA6WlllL/YV/t6p24kaNZBUp9JGbAzOeKuVUv2u
|
||||||
|
vfNKwB/aBwnFKauM9I6RmC4bnI1nGHjETlNNWwIDAQABAoIBAHBPKHmybTGlgpET
|
||||||
|
nzo4J7SSzcuYHM/6mdrJVSn9wqcwAN2KR0DK/cqHHTPGz0VRAEPuojAVRtqAZAYM
|
||||||
|
G3VIr0HgRrwoextf9BCL549+uhkWUWGVwenIktPT2f/xXaGPyrxazkTDhX8vL3Nn
|
||||||
|
4HtZXMweWPBdkJyYGxlKj5Hn7czTpG3VKpvpHeFlY4caF+FT2as1jcQ1MjPnGslH
|
||||||
|
Ss+sYPBp/70w2T114Z4wlR4OryI1LeuFeje9obrn0HAmJd0ZKYM21awp/YWJ/y8J
|
||||||
|
wIH6XQ4AGR9iTRhuffK1XRM/Iec3K/YhOn4PtKdT7OsIujAKY7A9WcqSFif+/E1g
|
||||||
|
jom3eMECgYEAw5Zdqt2uZ19FuDlDTW4Kw8Z2NyXgWp33LkAXG1mJw7bqDhfPeB1c
|
||||||
|
xTPs4i4RubGuDusygxZ3GgJAO7tLGzNQfWNoi03mM7Q/BJGkA9VZr+U28zsSRQOQ
|
||||||
|
+J9xNsdgUMP1js7X/NNM2bxTC8zy9wEsWr9JwNo1C7uHTE9WXAumBI8CgYEA5RKV
|
||||||
|
niSbyko36W3Vi0ZnGBrRhy0Eiq85V2mhWzHN+txcv+8aISow2wioTUzrpR0aVZ4j
|
||||||
|
v9+siJENlALVzdUFihy0lPxHqLJT746Cixz95WRTLkdHeNllV0DMfOph2x3j1Hjd
|
||||||
|
3PgTv+jqb6npY0/2Vb2pp4t/zVikGaObsAalSHUCgYBne8B1bjMfqI3n6gxNBIMX
|
||||||
|
kILtrNGmwFuPEgPnyZkVf0sZR8nSwJ5cDJwyE7P3LyZr6E9igllj3nsD35Xef2j/
|
||||||
|
3r/qrL2275BEJ5bDHHgGk91eFgwVjcx/b0TkedrhAL2E4LXwpA/OSFEcNkT7IZjJ
|
||||||
|
Ltqj+hAE9CSi4HtN2i/tywKBgBotKn28zzSpkIQTMgDNVcCSZ/kbctZqOZI8lty1
|
||||||
|
70TIY6znJMQ/bv/ImHrk3FSs47J+9LTbWXrtoHCWdlokCpMCvrv7rDCh2Cea0F4X
|
||||||
|
PQg2k67JJGix5vu2guePXQlN/Bfui+PRUWhvtEJ4VxwrKgoYN0fXEA6mH3JymLrf
|
||||||
|
t4l1AoGBALk4o9swGjw7MnByYJmOidlJ0p9Wj1BWWJJYoYX2VfjIuvZj6BNxkEb0
|
||||||
|
aVmYRC+40e9L1rOyrlyaO/TiQaIPE4ljVs/AmMKGz8sIcVfwdyERH3nDrXxvlAav
|
||||||
|
lSvfKoYM3J+5c63CDuU45gztpmavNerzCczqYTLOEMx1eCLHOQlx
|
||||||
|
-----END PRIVATE KEY-----
|
9
_examples/auth/jwt/refresh-token/rsa_public_key.pem
Normal file
9
_examples/auth/jwt/refresh-token/rsa_public_key.pem
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwO0q8WbBvrplz3lTQjs
|
||||||
|
Wu66HC7M3mVAjmjLq8Wj/ipqVtiJMrUL9t/0q9PNO/KX9u+HayFNYM4TnYkXVZX3
|
||||||
|
M5E31W8fPPy74D/XpqFwrwT7bAEwpT51JJyxkoBAyOh08lmR2EYvpGF7qErra7qb
|
||||||
|
kk4LGFbhoFCXdMLXguT4rPymkzFHdQrmGYOBS+v9imSuJddCZpXyv6Ko7AKB4mhz
|
||||||
|
g4RC5RJZO5GEHVUrSMHxZB0syF8cU+28iL8A7SlGKTNZPZiHmCQVRqA6WlllL/YV
|
||||||
|
/t6p24kaNZBUp9JGbAzOeKuVUv2uvfNKwB/aBwnFKauM9I6RmC4bnI1nGHjETlNN
|
||||||
|
WwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -1,30 +0,0 @@
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
Proc-Type: 4,ENCRYPTED
|
|
||||||
DEK-Info: DES-EDE3-CBC,6B0BC214C94124FE
|
|
||||||
|
|
||||||
lAM48DEM/GdCDimr9Vhi+fSHLgduDb0l2BA4uhILgNby51jxY/4X3IqM6f3ImKX7
|
|
||||||
cEd9OBug+pwIugB0UW0L0f5Pd59Ovpiaz3xLci1/19ehYnMqsuP3YAnJm40hT5VP
|
|
||||||
p0gWRiR415PJ0fPeeJPFx5IsqvkTJ30LWZHUZX4EkdcL5L8PrVbmthGDbLh+OcMc
|
|
||||||
LzoP8eTglzlZF03nyvAol6+p2eZtvOJLu8nWG25q17kyBx6kEiCsWFcUBTX9G7sH
|
|
||||||
CM3naByDijqZXE/XXtmTMLSRRnlk7Q5WLxClroHlUP9y8BQFMo2TW4Z+vNjHUkc1
|
|
||||||
77ghabX1704bAlIE8LLZJKrm/C5+VKyV6117SVG/2bc4036Y5rStXpANbk1j4K0x
|
|
||||||
ADvpRhuTpifaogdvJP+8eXBdl841MQMRzWuZHp6UNYYQegoV9C+KHyJx4UPjZyzd
|
|
||||||
gblZmKgU+BsX3mV6MLhJtd6dheLZtpBsAlSstJxzmgwqz9faONYEGeItXO+NnxbA
|
|
||||||
mxAp/mI+Fz2jfgYlWjwkyPPzD4k/ZMMzB4XLkKKs9XaxUtTomiDkuYZfnABhxt73
|
|
||||||
xBy40V1rb/NyeW80pk1zEHM6Iy/48ETSp9U3k9sSOXjMhYbPXgxDtimV8w0qGFAo
|
|
||||||
2Tif7ZuaiuC38rOkoHK9C6vy2Dp8lQZ+QBnUKLeFsyhq9CaqSdnyUTMj3oEZXXf+
|
|
||||||
TqqeO+PTtl7JaNfGRq6/aMZqxACHkyVUvYvjZzx07CJ2fr+OtNqxallM6Oc/o9NZ
|
|
||||||
5u7lpgrYaKM/b67q0d2X/AoxR5zrZuM8eam3acD1PwHFQKbJWuFNmjWtnlZNuR3X
|
|
||||||
fZEmxIKwDlup8TxFcqbbZtPHuQA2mTMTqfRkf8oPSO+N6NNaUpb0ignYyA7Eu5GT
|
|
||||||
b02d/oNLETMikxUxntMSH7GhuOpfJyELz8krYTttbJ+a93h4wBeYW2+LyAr/cRLB
|
|
||||||
mbtKLtaN7f3FaOSnu8e0+zlJ7xglHPXqblRL9q6ZDM5UJtJD4rA7LPZHk/0Y1Kb6
|
|
||||||
hBh1qMDu0r3IV4X7MDacvxw7aa7D8TyXJiFSvxykVhds+ndjIe51Ics5908+lev3
|
|
||||||
nwE69PLMwyqe2vvE2oDwao4XJuBLCHjcv/VagRSz/XQGMbZqb3L6unyd3UPl8JjP
|
|
||||||
ovipNwM4rFnE54uiUUeki7TZGDYO72vQcSaLrmbeAWc2m202+rqLz0WMm6HpPmCv
|
|
||||||
IgexpX2MnIeHJ3+BlEjA2u+S6xNSD7qHGk2pb7DD8nRvUdSHAHeaQbrkEfEhhR2Q
|
|
||||||
Dw5gdw1JyQ0UKBl5ndn/1Ub2Asl016lZjpqHyMIVS4tFixACDsihEYMmq/zQmTj4
|
|
||||||
8oBZTU+fycN/KiGKZBsqxIwgYIeMz/GfvoyN5m57l6fwEZALVpveI1pP4fiZB/Z8
|
|
||||||
xLKa5JK6L10lAD1YHWc1dPhamf9Sb3JwN2CFtGvjOJ/YjAZu3jJoxi40DtRkE3Rh
|
|
||||||
HI8Cbx1OORzoo0kO0vy42rz5qunYyVmEzPKtOj+YjVEhVJ85yJZ9bTZtuyqMv8mH
|
|
||||||
cnwEeIFK8cmm9asbVzQGDwN/UGB4cO3LrMX1RYk4GRttTGlp0729BbmZmu00RnD/
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
62
_examples/auth/jwt/tutorial/README.md
Normal file
62
_examples/auth/jwt/tutorial/README.md
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Iris JWT Tutorial
|
||||||
|
|
||||||
|
This example show how to use JWT with domain-driven design pattern with Iris. There is also a simple Go client which describes how you can use Go to authorize a user and use the server's API.
|
||||||
|
|
||||||
|
## Run the server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authenticate, get the token
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl --location --request POST 'http://localhost:8080/signin' \
|
||||||
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode 'username=admin' \
|
||||||
|
--data-urlencode 'password=admin'
|
||||||
|
|
||||||
|
> $token
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get all TODOs for this User
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl --location --request GET 'http://localhost:8080/todos' \
|
||||||
|
--header 'Authorization: Bearer $token'
|
||||||
|
|
||||||
|
> $todos
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get a specific User's TODO
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl --location --request GET 'http://localhost:8080/todos/$id' \
|
||||||
|
--header 'Authorization: Bearer $token'
|
||||||
|
|
||||||
|
> $todo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get all TODOs for all Users (admin role)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl --location --request GET 'http://localhost:8080/admin/todos' \
|
||||||
|
--header 'Authorization: Bearer $token'
|
||||||
|
|
||||||
|
> $todos
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create a new TODO
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl --location --request POST 'http://localhost:8080/todos' \
|
||||||
|
--header 'Authorization: Bearer $token' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{
|
||||||
|
"title": "test titlte",
|
||||||
|
"body": "test body"
|
||||||
|
}'
|
||||||
|
|
||||||
|
> Status Created
|
||||||
|
> $todo
|
||||||
|
```
|
140
_examples/auth/jwt/tutorial/api/auth.go
Normal file
140
_examples/auth/jwt/tutorial/api/auth.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"myapp/domain/model"
|
||||||
|
"myapp/domain/repository"
|
||||||
|
"myapp/util"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/middleware/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultSecretKey = "sercrethatmaycontainch@r$32chars"
|
||||||
|
|
||||||
|
func getSecretKey() string {
|
||||||
|
secret := os.Getenv(util.AppName + "_SECRET")
|
||||||
|
if secret == "" {
|
||||||
|
return defaultSecretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserClaims represents the user token claims.
|
||||||
|
type UserClaims struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Roles []model.Role `json:"roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate implements the custom struct claims validator,
|
||||||
|
// this is totally optionally and maybe unnecessary but good to know how.
|
||||||
|
func (u *UserClaims) Validate() error {
|
||||||
|
if u.UserID == "" {
|
||||||
|
return fmt.Errorf("%w: %s", jwt.ErrMissingKey, "user_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify allows only authorized clients.
|
||||||
|
func Verify() iris.Handler {
|
||||||
|
secret := getSecretKey()
|
||||||
|
|
||||||
|
verifier := jwt.NewVerifier(jwt.HS256, []byte(secret), jwt.Expected{Issuer: util.AppName})
|
||||||
|
verifier.Extractors = []jwt.TokenExtractor{jwt.FromHeader} // extract token only from Authorization: Bearer $token
|
||||||
|
return verifier.Verify(func() interface{} {
|
||||||
|
return new(UserClaims)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowAdmin allows only authorized clients with "admin" access role.
|
||||||
|
// Should be registered after Verify.
|
||||||
|
func AllowAdmin(ctx iris.Context) {
|
||||||
|
if !IsAdmin(ctx) {
|
||||||
|
ctx.StopWithText(iris.StatusForbidden, "admin access required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignIn accepts the user form data and returns a token to authorize a client.
|
||||||
|
func SignIn(repo repository.UserRepository) iris.Handler {
|
||||||
|
secret := getSecretKey()
|
||||||
|
signer := jwt.NewSigner(jwt.HS256, []byte(secret), 15*time.Minute)
|
||||||
|
|
||||||
|
return func(ctx iris.Context) {
|
||||||
|
/*
|
||||||
|
type LoginForm struct {
|
||||||
|
Username string `form:"username"`
|
||||||
|
Password string `form:"password"`
|
||||||
|
}
|
||||||
|
and ctx.ReadForm OR use the ctx.FormValue(s) method.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var (
|
||||||
|
username = ctx.FormValue("username")
|
||||||
|
password = ctx.FormValue("password")
|
||||||
|
)
|
||||||
|
|
||||||
|
user, ok := repo.GetByUsernameAndPassword(username, password)
|
||||||
|
if !ok {
|
||||||
|
ctx.StopWithText(iris.StatusBadRequest, "wrong username or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := UserClaims{
|
||||||
|
UserID: user.ID,
|
||||||
|
Roles: user.Roles,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally, generate a JWT ID.
|
||||||
|
jti, err := util.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := signer.Sign(claims, jwt.Claims{
|
||||||
|
ID: jti,
|
||||||
|
Issuer: util.AppName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Write(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignOut invalidates a user from server-side using the jwt Blocklist.
|
||||||
|
func SignOut(ctx iris.Context) {
|
||||||
|
ctx.Logout() // this is automatically binded to a function which invalidates the current request token by the JWT Verifier above.
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClaims returns the current authorized client claims.
|
||||||
|
func GetClaims(ctx iris.Context) *UserClaims {
|
||||||
|
claims := jwt.Get(ctx).(*UserClaims)
|
||||||
|
return claims
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserID returns the current authorized client's user id extracted from claims.
|
||||||
|
func GetUserID(ctx iris.Context) string {
|
||||||
|
return GetClaims(ctx).UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdmin reports whether the current client has admin access.
|
||||||
|
func IsAdmin(ctx iris.Context) bool {
|
||||||
|
for _, role := range GetClaims(ctx).Roles {
|
||||||
|
if role == model.Admin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
119
_examples/auth/jwt/tutorial/api/todo.go
Normal file
119
_examples/auth/jwt/tutorial/api/todo.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"myapp/domain/repository"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TodoRequest represents a Todo HTTP request.
|
||||||
|
type TodoRequest struct {
|
||||||
|
Title string `json:"title" form:"title" url:"title"`
|
||||||
|
Body string `json:"body" form:"body" url:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTodo handles the creation of a Todo entry.
|
||||||
|
func CreateTodo(repo repository.TodoRepository) iris.Handler {
|
||||||
|
return func(ctx iris.Context) {
|
||||||
|
var req TodoRequest
|
||||||
|
err := ctx.ReadBody(&req) // will bind the "req" to a JSON, form or url query request data.
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := GetUserID(ctx)
|
||||||
|
todo, err := repo.Create(userID, req.Title, req.Body)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.StatusCode(iris.StatusCreated)
|
||||||
|
ctx.JSON(todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTodo lists all users todos.
|
||||||
|
// Parameter: {id}.
|
||||||
|
func GetTodo(repo repository.TodoRepository) iris.Handler {
|
||||||
|
return func(ctx iris.Context) {
|
||||||
|
id := ctx.Params().Get("id")
|
||||||
|
userID := GetUserID(ctx)
|
||||||
|
|
||||||
|
todo, err := repo.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
code := iris.StatusInternalServerError
|
||||||
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
|
code = iris.StatusNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.StopWithError(code, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsAdmin(ctx) { // admin can access any user's todos.
|
||||||
|
if todo.UserID != userID {
|
||||||
|
ctx.StopWithStatus(iris.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTodos lists todos of the current user.
|
||||||
|
func ListTodos(repo repository.TodoRepository) iris.Handler {
|
||||||
|
return func(ctx iris.Context) {
|
||||||
|
userID := GetUserID(ctx)
|
||||||
|
todos, err := repo.GetAllByUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if len(todos) == 0 {
|
||||||
|
// ctx.StopWithError(iris.StatusNotFound, fmt.Errorf("no entries found"))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// Or let the client decide what to do on empty list.
|
||||||
|
ctx.JSON(todos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllTodos lists all users todos.
|
||||||
|
// Access: admin.
|
||||||
|
// Middleware: AllowAdmin.
|
||||||
|
func ListAllTodos(repo repository.TodoRepository) iris.Handler {
|
||||||
|
return func(ctx iris.Context) {
|
||||||
|
todos, err := repo.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(todos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leave as exercise: use filtering instead...
|
||||||
|
|
||||||
|
// ListTodosByUser lists all todos by a specific user.
|
||||||
|
// Access: admin.
|
||||||
|
// Middleware: AllowAdmin.
|
||||||
|
// Parameter: {id}.
|
||||||
|
func ListTodosByUser(repo repository.TodoRepository) iris.Handler {
|
||||||
|
return func(ctx iris.Context) {
|
||||||
|
userID := ctx.Params().Get("id")
|
||||||
|
todos, err := repo.GetAllByUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(todos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
9
_examples/auth/jwt/tutorial/domain/model/role.go
Normal file
9
_examples/auth/jwt/tutorial/domain/model/role.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
// Role represents a role.
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Admin represents the Admin access role.
|
||||||
|
Admin Role = "admin"
|
||||||
|
)
|
10
_examples/auth/jwt/tutorial/domain/model/todo.go
Normal file
10
_examples/auth/jwt/tutorial/domain/model/todo.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
// Todo represents the Todo model.
|
||||||
|
type Todo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
CreatedAt int64 `json:"created_at"` // unix seconds.
|
||||||
|
}
|
9
_examples/auth/jwt/tutorial/domain/model/user.go
Normal file
9
_examples/auth/jwt/tutorial/domain/model/user.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
// User represents our User model.
|
||||||
|
type User struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
HashedPassword []byte `json:"-"`
|
||||||
|
Roles []Role `json:"roles"`
|
||||||
|
}
|
45
_examples/auth/jwt/tutorial/domain/repository/samples.go
Normal file
45
_examples/auth/jwt/tutorial/domain/repository/samples.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"myapp/domain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateSamples generates data samples.
|
||||||
|
func GenerateSamples(userRepo UserRepository, todoRepo TodoRepository) error {
|
||||||
|
// Create users.
|
||||||
|
for _, username := range []string{"vasiliki", "george", "kwstas"} {
|
||||||
|
// My grandmother.
|
||||||
|
// My young brother.
|
||||||
|
// My youngest brother.
|
||||||
|
password := fmt.Sprintf("%s_pass", username)
|
||||||
|
if _, err := userRepo.Create(username, password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a user with admin role.
|
||||||
|
if _, err := userRepo.Create("admin", "admin", model.Admin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create two todos per user.
|
||||||
|
users, err := userRepo.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, u := range users {
|
||||||
|
for j := 0; j < 2; j++ {
|
||||||
|
title := fmt.Sprintf("%s todo %d:%d title", u.Username, i, j)
|
||||||
|
body := fmt.Sprintf("%s todo %d:%d body", u.Username, i, j)
|
||||||
|
_, err := todoRepo.Create(u.ID, title, body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"myapp/domain/model"
|
||||||
|
"myapp/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNotFound indicates that an entry was not found.
|
||||||
|
// Usage: errors.Is(err, ErrNotFound)
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
|
// TodoRepository is responsible for Todo CRUD operations,
|
||||||
|
// however, for the sake of the example we only implement the Create and Read ones.
|
||||||
|
type TodoRepository interface {
|
||||||
|
Create(userID, title, body string) (model.Todo, error)
|
||||||
|
GetByID(id string) (model.Todo, error)
|
||||||
|
GetAll() ([]model.Todo, error)
|
||||||
|
GetAllByUser(userID string) ([]model.Todo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ TodoRepository = (*memoryTodoRepository)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryTodoRepository struct {
|
||||||
|
todos []model.Todo // map[string]model.Todo
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemoryTodoRepository returns the default in-memory todo repository.
|
||||||
|
func NewMemoryTodoRepository() TodoRepository {
|
||||||
|
r := new(memoryTodoRepository)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryTodoRepository) Create(userID, title, body string) (model.Todo, error) {
|
||||||
|
id, err := util.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return model.Todo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
todo := model.Todo{
|
||||||
|
ID: id,
|
||||||
|
UserID: userID,
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
CreatedAt: util.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
r.todos = append(r.todos, todo)
|
||||||
|
r.mu.Unlock()
|
||||||
|
|
||||||
|
return todo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryTodoRepository) GetByID(id string) (model.Todo, error) {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, todo := range r.todos {
|
||||||
|
if todo.ID == id {
|
||||||
|
return todo, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.Todo{}, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryTodoRepository) GetAll() ([]model.Todo, error) {
|
||||||
|
r.mu.RLock()
|
||||||
|
tmp := make([]model.Todo, len(r.todos))
|
||||||
|
copy(tmp, r.todos)
|
||||||
|
r.mu.RUnlock()
|
||||||
|
return tmp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryTodoRepository) GetAllByUser(userID string) ([]model.Todo, error) {
|
||||||
|
// initialize a slice, so we don't have "null" at empty response.
|
||||||
|
todos := make([]model.Todo, 0)
|
||||||
|
|
||||||
|
r.mu.RLock()
|
||||||
|
for _, todo := range r.todos {
|
||||||
|
if todo.UserID == userID {
|
||||||
|
todos = append(todos, todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.mu.RUnlock()
|
||||||
|
|
||||||
|
return todos, nil
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"myapp/domain/model"
|
||||||
|
"myapp/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserRepository is responsible for User CRUD operations,
|
||||||
|
// however, for the sake of the example we only implement the Read one.
|
||||||
|
type UserRepository interface {
|
||||||
|
Create(username, password string, roles ...model.Role) (model.User, error)
|
||||||
|
// GetByUsernameAndPassword should return a User based on the given input.
|
||||||
|
GetByUsernameAndPassword(username, password string) (model.User, bool)
|
||||||
|
GetAll() ([]model.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ UserRepository = (*memoryUserRepository)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryUserRepository struct {
|
||||||
|
// Users represents a user database.
|
||||||
|
// For the sake of the tutorial we use a simple slice of users.
|
||||||
|
users []model.User
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemoryUserRepository returns the default in-memory user repository.
|
||||||
|
func NewMemoryUserRepository() UserRepository {
|
||||||
|
r := new(memoryUserRepository)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryUserRepository) Create(username, password string, roles ...model.Role) (model.User, error) {
|
||||||
|
id, err := util.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return model.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := util.GeneratePassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return model.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := model.User{
|
||||||
|
ID: id,
|
||||||
|
Username: username,
|
||||||
|
HashedPassword: hashedPassword,
|
||||||
|
Roles: roles,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
r.users = append(r.users, user)
|
||||||
|
r.mu.Unlock()
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUsernameAndPassword returns a user from the storage based on the given "username" and "password".
|
||||||
|
func (r *memoryUserRepository) GetByUsernameAndPassword(username, password string) (model.User, bool) {
|
||||||
|
for _, u := range r.users { // our example uses a static slice.
|
||||||
|
if u.Username == username {
|
||||||
|
// we compare the user input and the stored hashed password.
|
||||||
|
ok := util.ValidatePassword(password, u.HashedPassword)
|
||||||
|
if ok {
|
||||||
|
return u, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.User{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryUserRepository) GetAll() ([]model.User, error) {
|
||||||
|
r.mu.RLock()
|
||||||
|
tmp := make([]model.User, len(r.users))
|
||||||
|
copy(tmp, r.users)
|
||||||
|
r.mu.RUnlock()
|
||||||
|
return tmp, nil
|
||||||
|
}
|
12
_examples/auth/jwt/tutorial/go-client/README.md
Normal file
12
_examples/auth/jwt/tutorial/go-client/README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Go Client
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
2020/11/04 21:08:40 Access Token:
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYTAwYzI3ZDEtYjVhYS00NjU0LWFmMTYtYjExNzNkZTY1NjI5Iiwicm9sZXMiOlsiYWRtaW4iXSwiaWF0IjoxNjA0NTE2OTIwLCJleHAiOjE2MDQ1MTc4MjAsImp0aSI6IjYzNmVmMDc0LTE2MzktNGJhZi1hNGNiLTQ4ZDM4NGMxMzliYSIsImlzcyI6Im15YXBwIn0.T9B0zG0AHShO5JfQgrMQBlToH33KHgp8nLMPFpN6QmM"
|
||||||
|
2020/11/04 21:08:40 Todo Created:
|
||||||
|
model.Todo{ID:"cfa38d7a-c556-4301-ae1f-fb90f705071c", UserID:"a00c27d1-b5aa-4654-af16-b1173de65629", Title:"test todo title", Body:"test todo body contents", CreatedAt:1604516920}
|
||||||
|
```
|
109
_examples/auth/jwt/tutorial/go-client/client.go
Normal file
109
_examples/auth/jwt/tutorial/go-client/client.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the default http client instance used by the following methods.
|
||||||
|
var Client = http.DefaultClient
|
||||||
|
|
||||||
|
// RequestOption is a function which can be used to modify
|
||||||
|
// a request instance before Do.
|
||||||
|
type RequestOption func(*http.Request) error
|
||||||
|
|
||||||
|
// WithAccessToken sets the given "token" to the authorization request header.
|
||||||
|
func WithAccessToken(token []byte) RequestOption {
|
||||||
|
bearer := "Bearer " + string(token)
|
||||||
|
return func(req *http.Request) error {
|
||||||
|
req.Header.Add("Authorization", bearer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContentType sets the content-type request header.
|
||||||
|
func WithContentType(cType string) RequestOption {
|
||||||
|
return func(req *http.Request) error {
|
||||||
|
req.Header.Set("Content-Type", cType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContentLength sets the content-length request header.
|
||||||
|
func WithContentLength(length int) RequestOption {
|
||||||
|
return func(req *http.Request) error {
|
||||||
|
req.Header.Set("Content-Length", strconv.Itoa(length))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do fires a request to the server.
|
||||||
|
func Do(method, url string, body io.Reader, opts ...RequestOption) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if err = opt(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON fires a request with "v" as client json data.
|
||||||
|
func JSON(method, url string, v interface{}, opts ...RequestOption) (*http.Response, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err := json.NewEncoder(buf).Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, WithContentType("application/json; charset=utf-8"))
|
||||||
|
return Do(method, url, buf, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form fires a request with "formData" as client form data.
|
||||||
|
func Form(method, url string, formData url.Values, opts ...RequestOption) (*http.Response, error) {
|
||||||
|
encoded := formData.Encode()
|
||||||
|
body := strings.NewReader(encoded)
|
||||||
|
|
||||||
|
opts = append([]RequestOption{
|
||||||
|
WithContentType("application/x-www-form-urlencoded"),
|
||||||
|
WithContentLength(len(encoded)),
|
||||||
|
}, opts...)
|
||||||
|
|
||||||
|
return Do(method, url, body, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindResponse binds a response body to the "dest" pointer and closes the body.
|
||||||
|
func BindResponse(resp *http.Response, dest interface{}) error {
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
if idx := strings.IndexRune(contentType, ';'); idx > 0 {
|
||||||
|
contentType = contentType[0:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case "application/json":
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return json.NewDecoder(resp.Body).Decode(dest)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported content type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawResponse simply returns the raw response body.
|
||||||
|
func RawResponse(resp *http.Response) ([]byte, error) {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return ioutil.ReadAll(resp.Body)
|
||||||
|
}
|
69
_examples/auth/jwt/tutorial/go-client/main.go
Normal file
69
_examples/auth/jwt/tutorial/go-client/main.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"myapp/api"
|
||||||
|
"myapp/domain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const base = "http://localhost:8080"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
accessToken, err := authenticate("admin", "admin")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Access Token:\n%q", accessToken)
|
||||||
|
|
||||||
|
todo, err := createTodo(accessToken, "test todo title", "test todo body contents")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Todo Created:\n%#+v", todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticate(username, password string) ([]byte, error) {
|
||||||
|
endpoint := base + "/signin"
|
||||||
|
|
||||||
|
data := make(url.Values)
|
||||||
|
data.Set("username", username)
|
||||||
|
data.Set("password", password)
|
||||||
|
|
||||||
|
resp, err := Form(http.MethodPost, endpoint, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := RawResponse(resp)
|
||||||
|
return accessToken, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTodo(accessToken []byte, title, body string) (model.Todo, error) {
|
||||||
|
var todo model.Todo
|
||||||
|
|
||||||
|
endpoint := base + "/todos"
|
||||||
|
|
||||||
|
req := api.TodoRequest{
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := JSON(http.MethodPost, endpoint, req, WithAccessToken(accessToken))
|
||||||
|
if err != nil {
|
||||||
|
return todo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
rawData, _ := RawResponse(resp)
|
||||||
|
return todo, fmt.Errorf("failed to create a todo: %s", string(rawData))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = BindResponse(resp, &todo)
|
||||||
|
return todo, err
|
||||||
|
}
|
11
_examples/auth/jwt/tutorial/go.mod
Normal file
11
_examples/auth/jwt/tutorial/go.mod
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module myapp
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.1.2
|
||||||
|
github.com/kataras/iris/v12 v12.2.0-alpha.0.20201031040657-23d4c411cadb
|
||||||
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/kataras/iris/v12 => ../../../../
|
40
_examples/auth/jwt/tutorial/main.go
Normal file
40
_examples/auth/jwt/tutorial/main.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"myapp/api"
|
||||||
|
"myapp/domain/repository"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
userRepository = repository.NewMemoryUserRepository()
|
||||||
|
todoRepository = repository.NewMemoryTodoRepository()
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := repository.GenerateSamples(userRepository, todoRepository); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
app.Post("/signin", api.SignIn(userRepository))
|
||||||
|
|
||||||
|
verify := api.Verify()
|
||||||
|
|
||||||
|
todosAPI := app.Party("/todos", verify)
|
||||||
|
todosAPI.Post("/", api.CreateTodo(todoRepository))
|
||||||
|
todosAPI.Get("/", api.ListTodos(todoRepository))
|
||||||
|
todosAPI.Get("/{id}", api.GetTodo(todoRepository))
|
||||||
|
|
||||||
|
adminAPI := app.Party("/admin", verify, api.AllowAdmin)
|
||||||
|
adminAPI.Get("/todos", api.ListAllTodos(todoRepository))
|
||||||
|
|
||||||
|
// POST http://localhost:8080/signin (Form: username, password)
|
||||||
|
// GET http://localhost:8080/todos
|
||||||
|
// GET http://localhost:8080/todos/{id}
|
||||||
|
// POST http://localhost:8080/todos (JSON, Form or URL: title, body)
|
||||||
|
// GET http://localhost:8080/admin/todos
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
7
_examples/auth/jwt/tutorial/util/app.go
Normal file
7
_examples/auth/jwt/tutorial/util/app.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
// Constants for the application.
|
||||||
|
const (
|
||||||
|
Version = "0.0.1"
|
||||||
|
AppName = "myapp"
|
||||||
|
)
|
7
_examples/auth/jwt/tutorial/util/clock.go
Normal file
7
_examples/auth/jwt/tutorial/util/clock.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Now is the default current time for the whole application.
|
||||||
|
// Can be modified for testing or custom timezone.
|
||||||
|
var Now = time.Now
|
25
_examples/auth/jwt/tutorial/util/password.go
Normal file
25
_examples/auth/jwt/tutorial/util/password.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
// MustGeneratePassword same as GeneratePassword but panics on errors.
|
||||||
|
func MustGeneratePassword(userPassword string) []byte {
|
||||||
|
hashed, err := GeneratePassword(userPassword)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashed
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePassword will generate a hashed password for us based on the
|
||||||
|
// user's input.
|
||||||
|
func GeneratePassword(userPassword string) ([]byte, error) {
|
||||||
|
return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePassword will check if passwords are matched.
|
||||||
|
func ValidatePassword(userPassword string, hashed []byte) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword))
|
||||||
|
return err == nil
|
||||||
|
}
|
23
_examples/auth/jwt/tutorial/util/uuid.go
Normal file
23
_examples/auth/jwt/tutorial/util/uuid.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
// MustGenerateUUID returns a new v4 UUID or panics.
|
||||||
|
func MustGenerateUUID() string {
|
||||||
|
id, err := GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateUUID returns a new v4 UUID.
|
||||||
|
func GenerateUUID() (string, error) {
|
||||||
|
id, err := uuid.NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id.String(), nil
|
||||||
|
}
|
|
@ -2,4 +2,6 @@ module github.com/kataras/iris/_examples/dependency-injection/jwt/contrib
|
||||||
|
|
||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
require github.com/iris-contrib/middleware/jwt v0.0.0-20200810001613-32cf668f999f
|
require (
|
||||||
|
github.com/iris-contrib/middleware/jwt v0.0.0-20201017024110-39b50ffeb885
|
||||||
|
)
|
||||||
|
|
|
@ -11,37 +11,57 @@ func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
app.ConfigureContainer(register)
|
app.ConfigureContainer(register)
|
||||||
|
|
||||||
|
// http://localhost:8080/authenticate
|
||||||
|
// http://localhost:8080/restricted (Header: Authorization = Bearer $token)
|
||||||
app.Listen(":8080")
|
app.Listen(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
func register(api *iris.APIContainer) {
|
var secret = []byte("secret")
|
||||||
j := jwt.HMAC(15*time.Minute, "secret", "secretforencrypt")
|
|
||||||
|
|
||||||
|
func register(api *iris.APIContainer) {
|
||||||
api.RegisterDependency(func(ctx iris.Context) (claims userClaims) {
|
api.RegisterDependency(func(ctx iris.Context) (claims userClaims) {
|
||||||
if err := j.VerifyToken(ctx, &claims); err != nil {
|
/* Using the middleware:
|
||||||
|
if ctx.Proceed(verify) {
|
||||||
|
// ^ the "verify" middleware will stop the execution if it's failed to verify the request.
|
||||||
|
// Map the input parameter of "restricted" function with the claims.
|
||||||
|
return jwt.Get(ctx).(*userClaims)
|
||||||
|
}*/
|
||||||
|
token := jwt.FromHeader(ctx)
|
||||||
|
if token == "" {
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, jwt.ErrMissing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedToken, err := jwt.Verify(jwt.HS256, secret, []byte(token))
|
||||||
|
if err != nil {
|
||||||
ctx.StopWithError(iris.StatusUnauthorized, err)
|
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verifiedToken.Claims(&claims)
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
api.Get("/authenticate", writeToken(j))
|
api.Get("/authenticate", writeToken)
|
||||||
api.Get("/restricted", restrictedPage)
|
api.Get("/restricted", restrictedPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
type userClaims struct {
|
type userClaims struct {
|
||||||
jwt.Claims
|
Username string `json:"username"`
|
||||||
Username string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeToken(j *jwt.JWT) iris.Handler {
|
func writeToken(ctx iris.Context) {
|
||||||
return func(ctx iris.Context) {
|
claims := userClaims{
|
||||||
j.WriteToken(ctx, userClaims{
|
Username: "kataras",
|
||||||
Claims: j.Expiry(jwt.Claims{Issuer: "an-issuer"}),
|
|
||||||
Username: "kataras",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token, err := jwt.Sign(jwt.HS256, secret, claims, jwt.MaxAge(1*time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Write(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restrictedPage(claims userClaims) string {
|
func restrictedPage(claims userClaims) string {
|
||||||
|
|
|
@ -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
_examples/request-body/read-url/main.go
Normal file
38
_examples/request-body/read-url/main.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// package main contains an example on how to use the ReadURL,
|
||||||
|
// same way you can do the ReadQuery, ReadParams, ReadJSON, ReadProtobuf and e.t.c.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
type myURL struct {
|
||||||
|
Name string `url:"name"` // or `param:"name"`
|
||||||
|
Age int `url:"age"` // >> >>
|
||||||
|
Tail []string `url:"tail"` // >> >>
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := newApp()
|
||||||
|
|
||||||
|
// http://localhost:8080/iris/web/framework?name=kataras&age=27
|
||||||
|
// myURL: main.myURL{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}}
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApp() *iris.Application {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
app.Get("/{tail:path}", func(ctx iris.Context) {
|
||||||
|
var u myURL
|
||||||
|
// ReadURL is a shortcut of ReadParams + ReadQuery.
|
||||||
|
if err := ctx.ReadURL(&u); err != nil {
|
||||||
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Writef("myURL: %#v", u)
|
||||||
|
})
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
16
_examples/request-body/read-url/main_test.go
Normal file
16
_examples/request-body/read-url/main_test.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/httptest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadURL(t *testing.T) {
|
||||||
|
app := newApp()
|
||||||
|
|
||||||
|
e := httptest.New(t, app)
|
||||||
|
|
||||||
|
expectedBody := `myURL: main.myURL{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}}`
|
||||||
|
e.GET("/iris/web/framework").WithQuery("name", "kataras").WithQuery("age", 27).Expect().Status(httptest.StatusOK).Body().Equal(expectedBody)
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
42
_examples/sessions/viewdata/main.go
Normal file
42
_examples/sessions/viewdata/main.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
app.RegisterView(iris.HTML("./views", ".html"))
|
||||||
|
|
||||||
|
sess := sessions.New(sessions.Config{Cookie: "session_cookie", AllowReclaim: true})
|
||||||
|
app.Use(sess.Handler())
|
||||||
|
// ^ use app.UseRouter instead to access sessions on HTTP errors too.
|
||||||
|
|
||||||
|
// Register our custom middleware, after the sessions middleware.
|
||||||
|
app.Use(setSessionViewData)
|
||||||
|
|
||||||
|
app.Get("/", index)
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSessionViewData(ctx iris.Context) {
|
||||||
|
session := sessions.Get(ctx)
|
||||||
|
ctx.ViewData("session", session)
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func index(ctx iris.Context) {
|
||||||
|
session := sessions.Get(ctx)
|
||||||
|
session.Set("username", "kataras")
|
||||||
|
ctx.View("index")
|
||||||
|
/* OR without middleware:
|
||||||
|
ctx.View("index", iris.Map{
|
||||||
|
"session": session,
|
||||||
|
// {{.session.Get "username"}}
|
||||||
|
// OR to pass only the 'username':
|
||||||
|
// "username": session.Get("username"),
|
||||||
|
// {{.username}}
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
}
|
11
_examples/sessions/viewdata/views/index.html
Normal file
11
_examples/sessions/viewdata/views/index.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sessions View Data</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Hello {{.session.Get "username"}}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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`.
|
||||||
|
|
|
@ -715,7 +715,7 @@ type Configuration struct {
|
||||||
// The body will not be changed and existing data before the
|
// The body will not be changed and existing data before the
|
||||||
// context.UnmarshalBody/ReadJSON/ReadXML will be not consumed.
|
// context.UnmarshalBody/ReadJSON/ReadXML will be not consumed.
|
||||||
//
|
//
|
||||||
// See `Context.RecordBody` method for the same feature, per-request.
|
// See `Context.RecordRequestBody` method for the same feature, per-request.
|
||||||
DisableBodyConsumptionOnUnmarshal bool `ini:"disable_body_consumption" json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"`
|
DisableBodyConsumptionOnUnmarshal bool `ini:"disable_body_consumption" json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"`
|
||||||
// FireEmptyFormError returns if set to tue true then the `context.ReadBody/ReadForm`
|
// FireEmptyFormError returns if set to tue true then the `context.ReadBody/ReadForm`
|
||||||
// will return an `iris.ErrEmptyForm` on empty request form data.
|
// will return an `iris.ErrEmptyForm` on empty request form data.
|
||||||
|
|
|
@ -714,9 +714,10 @@ func (ctx *Context) StopWithError(statusCode int, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetErr(err)
|
ctx.SetErr(err)
|
||||||
if IsErrPrivate(err) {
|
if _, ok := err.(ErrPrivate); ok {
|
||||||
// error is private, we can't render it, instead .
|
// error is private, we SHOULD not render it,
|
||||||
// let the error handler render the code text.
|
// leave the error handler alone to
|
||||||
|
// render the code's text instead.
|
||||||
ctx.StopWithStatus(statusCode)
|
ctx.StopWithStatus(statusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -2129,11 +2130,11 @@ func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
|
||||||
|
|
||||||
const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
|
const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
|
||||||
|
|
||||||
// RecordBody same as the Application's DisableBodyConsumptionOnUnmarshal configuration field
|
// RecordRequestBody same as the Application's DisableBodyConsumptionOnUnmarshal
|
||||||
// but acts for the current request.
|
// configuration field but acts only for the current request.
|
||||||
// It makes the request body readable more than once.
|
// It makes the request body readable more than once.
|
||||||
func (ctx *Context) RecordBody() {
|
func (ctx *Context) RecordRequestBody(b bool) {
|
||||||
ctx.values.Set(disableRequestBodyConsumptionContextKey, true)
|
ctx.values.Set(disableRequestBodyConsumptionContextKey, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRecordingBody reports whether the request body can be readen multiple times.
|
// IsRecordingBody reports whether the request body can be readen multiple times.
|
||||||
|
@ -2145,7 +2146,7 @@ func (ctx *Context) IsRecordingBody() bool {
|
||||||
// GetBody reads and returns the request body.
|
// GetBody reads and returns the request body.
|
||||||
// The default behavior for the http request reader is to consume the data readen
|
// The default behavior for the http request reader is to consume the data readen
|
||||||
// but you can change that behavior by passing the `WithoutBodyConsumptionOnUnmarshal` Iris option
|
// but you can change that behavior by passing the `WithoutBodyConsumptionOnUnmarshal` Iris option
|
||||||
// or by calling the `RecordBody` method.
|
// or by calling the `RecordRequestBody` method.
|
||||||
//
|
//
|
||||||
// However, whenever you can use the `ctx.Request().Body` instead.
|
// However, whenever you can use the `ctx.Request().Body` instead.
|
||||||
func (ctx *Context) GetBody() ([]byte, error) {
|
func (ctx *Context) GetBody() ([]byte, error) {
|
||||||
|
@ -2358,6 +2359,30 @@ func (ctx *Context) ReadParams(ptr interface{}) error {
|
||||||
return ctx.app.Validate(ptr)
|
return ctx.app.Validate(ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadURL is a shortcut of ReadParams and ReadQuery.
|
||||||
|
// It binds dynamic path parameters and URL query parameters
|
||||||
|
// to the "ptr" pointer struct value.
|
||||||
|
// The struct fields may contain "url" or "param" binding tags.
|
||||||
|
// If a validator exists then it validates the result too.
|
||||||
|
func (ctx *Context) ReadURL(ptr interface{}) error {
|
||||||
|
values := make(map[string][]string, ctx.params.Len())
|
||||||
|
ctx.params.Visit(func(key string, value string) {
|
||||||
|
values[key] = strings.Split(value, "/")
|
||||||
|
})
|
||||||
|
|
||||||
|
for k, v := range ctx.getQuery() {
|
||||||
|
values[k] = append(values[k], v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode using all available binding tags (url, header, param).
|
||||||
|
err := schema.Decode(values, ptr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.app.Validate(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
// ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error.
|
// ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error.
|
||||||
// Look `ReadJSONProtobuf` too.
|
// Look `ReadJSONProtobuf` too.
|
||||||
func (ctx *Context) ReadProtobuf(ptr proto.Message) error {
|
func (ctx *Context) ReadProtobuf(ptr proto.Message) error {
|
||||||
|
@ -2409,7 +2434,28 @@ func (ctx *Context) ReadMsgPack(ptr interface{}) error {
|
||||||
// If a GET method request then it reads from a form (or URL Query), otherwise
|
// If a GET method request then it reads from a form (or URL Query), otherwise
|
||||||
// it tries to match (depending on the request content-type) the data format e.g.
|
// it tries to match (depending on the request content-type) the data format e.g.
|
||||||
// JSON, Protobuf, MsgPack, XML, YAML, MultipartForm and binds the result to the "ptr".
|
// JSON, Protobuf, MsgPack, XML, YAML, MultipartForm and binds the result to the "ptr".
|
||||||
|
// As a special case if the "ptr" was a pointer to string or []byte
|
||||||
|
// then it will bind it to the request body as it is.
|
||||||
func (ctx *Context) ReadBody(ptr interface{}) error {
|
func (ctx *Context) ReadBody(ptr interface{}) error {
|
||||||
|
|
||||||
|
// If the ptr is string or byte, read the body as it's.
|
||||||
|
switch v := ptr.(type) {
|
||||||
|
case *string:
|
||||||
|
b, err := ctx.GetBody()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*v = string(b)
|
||||||
|
case *[]byte:
|
||||||
|
b, err := ctx.GetBody()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(*v, b)
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.Method() == http.MethodGet {
|
if ctx.Method() == http.MethodGet {
|
||||||
if ctx.Request().URL.RawQuery != "" {
|
if ctx.Request().URL.RawQuery != "" {
|
||||||
// try read from query.
|
// try read from query.
|
||||||
|
@ -5065,8 +5111,6 @@ func (ctx *Context) IsDebug() bool {
|
||||||
return ctx.app.IsDebug()
|
return ctx.app.IsDebug()
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorContextKey = "iris.context.error"
|
|
||||||
|
|
||||||
// SetErr is just a helper that sets an error value
|
// SetErr is just a helper that sets an error value
|
||||||
// as a context value, it does nothing more.
|
// as a context value, it does nothing more.
|
||||||
// Also, by-default this error's value is written to the client
|
// Also, by-default this error's value is written to the client
|
||||||
|
@ -5088,14 +5132,71 @@ func (ctx *Context) SetErr(err error) {
|
||||||
|
|
||||||
// GetErr is a helper which retrieves
|
// GetErr is a helper which retrieves
|
||||||
// the error value stored by `SetErr`.
|
// the error value stored by `SetErr`.
|
||||||
|
//
|
||||||
|
// Note that, if an error was stored by `SetErrPrivate`
|
||||||
|
// then it returns the underline/original error instead
|
||||||
|
// of the internal error wrapper.
|
||||||
func (ctx *Context) GetErr() error {
|
func (ctx *Context) GetErr() error {
|
||||||
|
_, err := ctx.GetErrPublic()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrPrivate if provided then the error saved in context
|
||||||
|
// should NOT be visible to the client no matter what.
|
||||||
|
type ErrPrivate interface {
|
||||||
|
error
|
||||||
|
IrisPrivateError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// An internal wrapper for the `SetErrPrivate` method.
|
||||||
|
type privateError struct{ error }
|
||||||
|
|
||||||
|
func (e privateError) IrisPrivateError() {}
|
||||||
|
|
||||||
|
// PrivateError accepts an error and returns a wrapped private one.
|
||||||
|
func PrivateError(err error) ErrPrivate {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errPrivate, ok := err.(ErrPrivate)
|
||||||
|
if !ok {
|
||||||
|
errPrivate = privateError{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errPrivate
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorContextKey = "iris.context.error"
|
||||||
|
|
||||||
|
// SetErrPrivate sets an error that it's only accessible through `GetErr`
|
||||||
|
// and it should never be sent to the client.
|
||||||
|
//
|
||||||
|
// Same as ctx.SetErr with an error that completes the `ErrPrivate` interface.
|
||||||
|
// See `GetErrPublic` too.
|
||||||
|
func (ctx *Context) SetErrPrivate(err error) {
|
||||||
|
ctx.SetErr(PrivateError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrPublic reports whether the stored error
|
||||||
|
// can be displayed to the client without risking
|
||||||
|
// to expose security server implementation to the client.
|
||||||
|
//
|
||||||
|
// If the error is not nil, it is always the original one.
|
||||||
|
func (ctx *Context) GetErrPublic() (bool, error) {
|
||||||
if v := ctx.values.Get(errorContextKey); v != nil {
|
if v := ctx.values.Get(errorContextKey); v != nil {
|
||||||
if err, ok := v.(error); ok {
|
switch err := v.(type) {
|
||||||
return err
|
case privateError:
|
||||||
|
// If it's an error set by SetErrPrivate then unwrap it.
|
||||||
|
return false, err.error
|
||||||
|
case ErrPrivate:
|
||||||
|
return false, err
|
||||||
|
case error:
|
||||||
|
return true, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrPanicRecovery may be returned from `Context` actions of a `Handler`
|
// ErrPanicRecovery may be returned from `Context` actions of a `Handler`
|
||||||
|
@ -5135,22 +5236,6 @@ func IsErrPanicRecovery(err error) (*ErrPanicRecovery, bool) {
|
||||||
return v, ok
|
return v, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrPrivate if provided then the error saved in context
|
|
||||||
// should NOT be visible to the client no matter what.
|
|
||||||
type ErrPrivate interface {
|
|
||||||
IrisPrivateError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrPrivate reports whether the given "err" is a private one.
|
|
||||||
func IsErrPrivate(err error) bool {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := err.(ErrPrivate)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRecovered reports whether this handler has been recovered
|
// IsRecovered reports whether this handler has been recovered
|
||||||
// by the Iris recover middleware.
|
// by the Iris recover middleware.
|
||||||
func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) {
|
func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) {
|
||||||
|
@ -5255,12 +5340,34 @@ func (ctx *Context) Logout(args ...interface{}) error {
|
||||||
|
|
||||||
const userContextKey = "iris.user"
|
const userContextKey = "iris.user"
|
||||||
|
|
||||||
// SetUser sets a User for this request.
|
// SetUser sets a value as a User for this request.
|
||||||
// It's used by auth middlewares as a common
|
// It's used by auth middlewares as a common
|
||||||
// method to provide user information to the
|
// method to provide user information to the
|
||||||
// next handlers in the chain.
|
// next handlers in the chain
|
||||||
func (ctx *Context) SetUser(u User) {
|
// Look the `User` method to retrieve it.
|
||||||
|
func (ctx *Context) SetUser(i interface{}) error {
|
||||||
|
if i == nil {
|
||||||
|
ctx.values.Remove(userContextKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, ok := i.(User)
|
||||||
|
if !ok {
|
||||||
|
if m, ok := i.(Map); ok { // it's a map, convert it to a User.
|
||||||
|
u = UserMap(m)
|
||||||
|
} else {
|
||||||
|
// It's a structure, wrap it and let
|
||||||
|
// runtime decide the features.
|
||||||
|
p := newUserPartial(i)
|
||||||
|
if p == nil {
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
||||||
|
u = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.values.Set(userContextKey, u)
|
ctx.values.Set(userContextKey, u)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// User returns the registered User of this request.
|
// User returns the registered User of this request.
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNotSupported is fired when a specific method is not implemented
|
// ErrNotSupported is fired when a specific method is not implemented
|
||||||
|
@ -21,124 +24,458 @@ var ErrNotSupported = errors.New("not supported")
|
||||||
//
|
//
|
||||||
// The caller is free to cast this with the implementation directly
|
// The caller is free to cast this with the implementation directly
|
||||||
// when special features are offered by the authorization system.
|
// when special features are offered by the authorization system.
|
||||||
|
//
|
||||||
|
// To make optional some of the fields you can just embed the User interface
|
||||||
|
// and implement whatever methods you want to support.
|
||||||
|
//
|
||||||
|
// There are three builtin implementations of the User interface:
|
||||||
|
// - SimpleUser
|
||||||
|
// - UserMap (a wrapper by SetUser)
|
||||||
|
// - UserPartial (a wrapper by SetUser)
|
||||||
type User interface {
|
type User interface {
|
||||||
// GetAuthorization should return the authorization method,
|
// GetAuthorization should return the authorization method,
|
||||||
// e.g. Basic Authentication.
|
// e.g. Basic Authentication.
|
||||||
GetAuthorization() string
|
GetAuthorization() (string, error)
|
||||||
// GetAuthorizedAt should return the exact time the
|
// GetAuthorizedAt should return the exact time the
|
||||||
// client has been authorized for the "first" time.
|
// client has been authorized for the "first" time.
|
||||||
GetAuthorizedAt() time.Time
|
GetAuthorizedAt() (time.Time, error)
|
||||||
|
// GetID should return the ID of the User.
|
||||||
|
GetID() (string, error)
|
||||||
// GetUsername should return the name of the User.
|
// GetUsername should return the name of the User.
|
||||||
GetUsername() string
|
GetUsername() (string, error)
|
||||||
// GetPassword should return the encoded or raw password
|
// GetPassword should return the encoded or raw password
|
||||||
// (depends on the implementation) of the User.
|
// (depends on the implementation) of the User.
|
||||||
GetPassword() string
|
GetPassword() (string, error)
|
||||||
// GetEmail should return the e-mail of the User.
|
// GetEmail should return the e-mail of the User.
|
||||||
GetEmail() string
|
GetEmail() (string, error)
|
||||||
}
|
// GetRoles should optionally return the specific user's roles.
|
||||||
|
// Returns `ErrNotSupported` if this method is not
|
||||||
|
// implemented by the User implementation.
|
||||||
|
GetRoles() ([]string, error)
|
||||||
|
// GetToken should optionally return a token used
|
||||||
|
// to authorize this User.
|
||||||
|
GetToken() ([]byte, error)
|
||||||
|
// GetField should optionally return a dynamic field
|
||||||
|
// based on its key. Useful for custom user fields.
|
||||||
|
// Keep in mind that these fields are encoded as a separate JSON key.
|
||||||
|
GetField(key string) (interface{}, error)
|
||||||
|
} /* Notes:
|
||||||
|
We could use a structure of User wrapper and separate interfaces for each of the methods
|
||||||
|
so they return ErrNotSupported if the implementation is missing it, so the `Features`
|
||||||
|
field and HasUserFeature can be omitted and
|
||||||
|
add a Raw() interface{} to return the underline User implementation too.
|
||||||
|
The advandages of the above idea is that we don't have to add new methods
|
||||||
|
for each of the builtin features and we can keep the (assumed) struct small.
|
||||||
|
But we dont as it has many disadvantages, unless is requested.
|
||||||
|
^ UPDATE: this is done through UserPartial.
|
||||||
|
|
||||||
// FeaturedUser optional interface that a User can implement.
|
The disadvantage of the current implementation is that the developer MUST
|
||||||
type FeaturedUser interface {
|
complete the whole interface in order to be a valid User and if we add
|
||||||
User
|
new methods in the future their implementation will break
|
||||||
// GetFeatures should optionally return a list of features
|
(unless they have a static interface implementation check as we have on SimpleUser).
|
||||||
// the User implementation offers.
|
We kind of by-pass this disadvantage by providing a SimpleUser which can be embedded (as pointer)
|
||||||
GetFeatures() []UserFeature
|
to the end-developer's custom implementations.
|
||||||
}
|
*/
|
||||||
|
|
||||||
// UserFeature a type which represents a user's optional feature.
|
|
||||||
// See `HasUserFeature` function for more.
|
|
||||||
type UserFeature uint32
|
|
||||||
|
|
||||||
// The list of standard UserFeatures.
|
|
||||||
const (
|
|
||||||
AuthorizedAtFeature UserFeature = iota
|
|
||||||
UsernameFeature
|
|
||||||
PasswordFeature
|
|
||||||
EmailFeature
|
|
||||||
)
|
|
||||||
|
|
||||||
// HasUserFeature reports whether the "u" User
|
|
||||||
// implements a specific "feature" User Feature.
|
|
||||||
//
|
|
||||||
// It returns ErrNotSupported if a user does not implement
|
|
||||||
// the FeaturedUser interface.
|
|
||||||
func HasUserFeature(user User, feature UserFeature) (bool, error) {
|
|
||||||
if u, ok := user.(FeaturedUser); ok {
|
|
||||||
for _, f := range u.GetFeatures() {
|
|
||||||
if f == feature {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimpleUser is a simple implementation of the User interface.
|
// SimpleUser is a simple implementation of the User interface.
|
||||||
type SimpleUser struct {
|
type SimpleUser struct {
|
||||||
Authorization string `json:"authorization"`
|
Authorization string `json:"authorization,omitempty"`
|
||||||
AuthorizedAt time.Time `json:"authorized_at"`
|
AuthorizedAt time.Time `json:"authorized_at,omitempty"`
|
||||||
Username string `json:"username"`
|
ID string `json:"id,omitempty"`
|
||||||
Password string `json:"-"`
|
Username string `json:"username,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Password string `json:"-"`
|
||||||
Features []UserFeature `json:"-"`
|
Email string `json:"email,omitempty"`
|
||||||
|
Roles []string `json:"roles,omitempty"`
|
||||||
|
Token json.RawMessage `json:"token,omitempty"`
|
||||||
|
Fields Map `json:"fields,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ User = (*SimpleUser)(nil)
|
var _ User = (*SimpleUser)(nil)
|
||||||
|
|
||||||
// GetAuthorization returns the authorization method,
|
// GetAuthorization returns the authorization method,
|
||||||
// e.g. Basic Authentication.
|
// e.g. Basic Authentication.
|
||||||
func (u *SimpleUser) GetAuthorization() string {
|
func (u *SimpleUser) GetAuthorization() (string, error) {
|
||||||
return u.Authorization
|
return u.Authorization, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthorizedAt returns the exact time the
|
// GetAuthorizedAt returns the exact time the
|
||||||
// client has been authorized for the "first" time.
|
// client has been authorized for the "first" time.
|
||||||
func (u *SimpleUser) GetAuthorizedAt() time.Time {
|
func (u *SimpleUser) GetAuthorizedAt() (time.Time, error) {
|
||||||
return u.AuthorizedAt
|
return u.AuthorizedAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the ID of the User.
|
||||||
|
func (u *SimpleUser) GetID() (string, error) {
|
||||||
|
return u.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsername returns the name of the User.
|
// GetUsername returns the name of the User.
|
||||||
func (u *SimpleUser) GetUsername() string {
|
func (u *SimpleUser) GetUsername() (string, error) {
|
||||||
return u.Username
|
return u.Username, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPassword returns the raw password of the User.
|
// GetPassword returns the raw password of the User.
|
||||||
func (u *SimpleUser) GetPassword() string {
|
func (u *SimpleUser) GetPassword() (string, error) {
|
||||||
return u.Password
|
return u.Password, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEmail returns the e-mail of the User.
|
// GetEmail returns the e-mail of (string,error) User.
|
||||||
func (u *SimpleUser) GetEmail() string {
|
func (u *SimpleUser) GetEmail() (string, error) {
|
||||||
return u.Email
|
return u.Email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFeatures returns a list of features
|
// GetRoles returns the specific user's roles.
|
||||||
// this User implementation offers.
|
// Returns with `ErrNotSupported` if the Roles field is not initialized.
|
||||||
func (u *SimpleUser) GetFeatures() []UserFeature {
|
func (u *SimpleUser) GetRoles() ([]string, error) {
|
||||||
if u.Features != nil {
|
if u.Roles == nil {
|
||||||
return u.Features
|
return nil, ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
var features []UserFeature
|
return u.Roles, nil
|
||||||
|
}
|
||||||
if !u.AuthorizedAt.IsZero() {
|
|
||||||
features = append(features, AuthorizedAtFeature)
|
// GetToken returns the token associated with this User.
|
||||||
}
|
// It may return empty if the User is not featured with a Token.
|
||||||
|
//
|
||||||
if u.Username != "" {
|
// The implementation can change that behavior.
|
||||||
features = append(features, UsernameFeature)
|
// Returns with `ErrNotSupported` if the Token field is empty.
|
||||||
}
|
func (u *SimpleUser) GetToken() ([]byte, error) {
|
||||||
|
if len(u.Token) == 0 {
|
||||||
if u.Password != "" {
|
return nil, ErrNotSupported
|
||||||
features = append(features, PasswordFeature)
|
}
|
||||||
}
|
|
||||||
|
return u.Token, nil
|
||||||
if u.Email != "" {
|
}
|
||||||
features = append(features, EmailFeature)
|
|
||||||
}
|
// GetField optionally returns a dynamic field from the `Fields` field
|
||||||
|
// based on its key.
|
||||||
return features
|
func (u *SimpleUser) GetField(key string) (interface{}, error) {
|
||||||
|
if u.Fields == nil {
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Fields[key], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserMap can be used to convert a common map[string]interface{} to a User.
|
||||||
|
// Usage:
|
||||||
|
// user := map[string]interface{}{
|
||||||
|
// "username": "kataras",
|
||||||
|
// "age" : 27,
|
||||||
|
// }
|
||||||
|
// ctx.SetUser(user)
|
||||||
|
// OR
|
||||||
|
// user := UserStruct{....}
|
||||||
|
// ctx.SetUser(user)
|
||||||
|
// [...]
|
||||||
|
// username, err := ctx.User().GetUsername()
|
||||||
|
// field,err := ctx.User().GetField("age")
|
||||||
|
// age := field.(int)
|
||||||
|
// OR cast it:
|
||||||
|
// user := ctx.User().(UserMap)
|
||||||
|
// username := user["username"].(string)
|
||||||
|
// age := user["age"].(int)
|
||||||
|
type UserMap Map
|
||||||
|
|
||||||
|
var _ User = UserMap{}
|
||||||
|
|
||||||
|
// GetAuthorization returns the authorization or Authorization value of the map.
|
||||||
|
func (u UserMap) GetAuthorization() (string, error) {
|
||||||
|
return u.str("authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorizedAt returns the authorized_at or Authorized_At value of the map.
|
||||||
|
func (u UserMap) GetAuthorizedAt() (time.Time, error) {
|
||||||
|
return u.time("authorized_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the id or Id or ID value of the map.
|
||||||
|
func (u UserMap) GetID() (string, error) {
|
||||||
|
return u.str("id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsername returns the username or Username value of the map.
|
||||||
|
func (u UserMap) GetUsername() (string, error) {
|
||||||
|
return u.str("username")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPassword returns the password or Password value of the map.
|
||||||
|
func (u UserMap) GetPassword() (string, error) {
|
||||||
|
return u.str("password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmail returns the email or Email value of the map.
|
||||||
|
func (u UserMap) GetEmail() (string, error) {
|
||||||
|
return u.str("email")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoles returns the roles or Roles value of the map.
|
||||||
|
func (u UserMap) GetRoles() ([]string, error) {
|
||||||
|
return u.strSlice("roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken returns the roles or Roles value of the map.
|
||||||
|
func (u UserMap) GetToken() ([]byte, error) {
|
||||||
|
return u.bytes("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetField returns the raw map's value based on its "key".
|
||||||
|
// It's not kind of useful here as you can just use the map.
|
||||||
|
func (u UserMap) GetField(key string) (interface{}, error) {
|
||||||
|
return u[key], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserMap) val(key string) interface{} {
|
||||||
|
isTitle := unicode.IsTitle(rune(key[0])) // if starts with uppercase.
|
||||||
|
if isTitle {
|
||||||
|
key = strings.ToLower(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserMap) bytes(key string) ([]byte, error) {
|
||||||
|
if v := u.val(key); v != nil {
|
||||||
|
switch s := v.(type) {
|
||||||
|
case []byte:
|
||||||
|
return s, nil
|
||||||
|
case string:
|
||||||
|
return []byte(s), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserMap) str(key string) (string, error) {
|
||||||
|
if v := u.val(key); v != nil {
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// exists or not we don't care, if it's invalid type we don't fill it.
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserMap) strSlice(key string) ([]string, error) {
|
||||||
|
if v := u.val(key); v != nil {
|
||||||
|
if s, ok := v.([]string); ok {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserMap) time(key string) (time.Time, error) {
|
||||||
|
if v := u.val(key); v != nil {
|
||||||
|
if t, ok := v.(time.Time); ok {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
userGetAuthorization interface {
|
||||||
|
GetAuthorization() string
|
||||||
|
}
|
||||||
|
|
||||||
|
userGetAuthorizedAt interface {
|
||||||
|
GetAuthorizedAt() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
userGetID interface {
|
||||||
|
GetID() string
|
||||||
|
}
|
||||||
|
|
||||||
|
userGetUsername interface {
|
||||||
|
GetUsername() string
|
||||||
|
}
|
||||||
|
|
||||||
|
userGetPassword interface {
|
||||||
|
GetPassword() string
|
||||||
|
}
|
||||||
|
|
||||||
|
userGetEmail interface {
|
||||||
|
GetEmail() string
|
||||||
|
}
|
||||||
|
|
||||||
|
userGetRoles interface {
|
||||||
|
GetRoles() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
userGetToken interface {
|
||||||
|
GetToken() []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
userGetField interface {
|
||||||
|
GetField(string) interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPartial is a User.
|
||||||
|
// It's a helper which wraps a struct value that
|
||||||
|
// may or may not complete the whole User interface.
|
||||||
|
UserPartial struct {
|
||||||
|
Raw interface{}
|
||||||
|
userGetAuthorization
|
||||||
|
userGetAuthorizedAt
|
||||||
|
userGetID
|
||||||
|
userGetUsername
|
||||||
|
userGetPassword
|
||||||
|
userGetEmail
|
||||||
|
userGetRoles
|
||||||
|
userGetToken
|
||||||
|
userGetField
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ User = (*UserPartial)(nil)
|
||||||
|
|
||||||
|
func newUserPartial(i interface{}) *UserPartial {
|
||||||
|
containsAtLeastOneMethod := false
|
||||||
|
p := &UserPartial{Raw: i}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetAuthorization); ok {
|
||||||
|
p.userGetAuthorization = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetAuthorizedAt); ok {
|
||||||
|
p.userGetAuthorizedAt = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetID); ok {
|
||||||
|
p.userGetID = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetUsername); ok {
|
||||||
|
p.userGetUsername = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetPassword); ok {
|
||||||
|
p.userGetPassword = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetEmail); ok {
|
||||||
|
p.userGetEmail = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetRoles); ok {
|
||||||
|
p.userGetRoles = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetToken); ok {
|
||||||
|
p.userGetToken = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := i.(userGetField); ok {
|
||||||
|
p.userGetField = u
|
||||||
|
containsAtLeastOneMethod = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !containsAtLeastOneMethod {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorization should return the authorization method,
|
||||||
|
// e.g. Basic Authentication.
|
||||||
|
func (u *UserPartial) GetAuthorization() (string, error) {
|
||||||
|
if v := u.userGetAuthorization; v != nil {
|
||||||
|
return v.GetAuthorization(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorizedAt should return the exact time the
|
||||||
|
// client has been authorized for the "first" time.
|
||||||
|
func (u *UserPartial) GetAuthorizedAt() (time.Time, error) {
|
||||||
|
if v := u.userGetAuthorizedAt; v != nil {
|
||||||
|
return v.GetAuthorizedAt(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID should return the ID of the User.
|
||||||
|
func (u *UserPartial) GetID() (string, error) {
|
||||||
|
if v := u.userGetID; v != nil {
|
||||||
|
return v.GetID(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsername should return the name of the User.
|
||||||
|
func (u *UserPartial) GetUsername() (string, error) {
|
||||||
|
if v := u.userGetUsername; v != nil {
|
||||||
|
return v.GetUsername(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPassword should return the encoded or raw password
|
||||||
|
// (depends on the implementation) of the User.
|
||||||
|
func (u *UserPartial) GetPassword() (string, error) {
|
||||||
|
if v := u.userGetPassword; v != nil {
|
||||||
|
return v.GetPassword(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmail should return the e-mail of the User.
|
||||||
|
func (u *UserPartial) GetEmail() (string, error) {
|
||||||
|
if v := u.userGetEmail; v != nil {
|
||||||
|
return v.GetEmail(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoles should optionally return the specific user's roles.
|
||||||
|
// Returns `ErrNotSupported` if this method is not
|
||||||
|
// implemented by the User implementation.
|
||||||
|
func (u *UserPartial) GetRoles() ([]string, error) {
|
||||||
|
if v := u.userGetRoles; v != nil {
|
||||||
|
return v.GetRoles(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken should optionally return a token used
|
||||||
|
// to authorize this User.
|
||||||
|
func (u *UserPartial) GetToken() ([]byte, error) {
|
||||||
|
if v := u.userGetToken; v != nil {
|
||||||
|
return v.GetToken(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetField should optionally return a dynamic field
|
||||||
|
// based on its key. Useful for custom user fields.
|
||||||
|
// Keep in mind that these fields are encoded as a separate JSON key.
|
||||||
|
func (u *UserPartial) GetField(key string) (interface{}, error) {
|
||||||
|
if v := u.userGetField; v != nil {
|
||||||
|
return v.GetField(key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,15 +129,14 @@ type RoutesProvider interface { // api builder
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultErrorHandler(ctx *context.Context) {
|
func defaultErrorHandler(ctx *context.Context) {
|
||||||
if err := ctx.GetErr(); err != nil {
|
if ok, err := ctx.GetErrPublic(); ok {
|
||||||
if !context.IsErrPrivate(err) {
|
// If an error is stored and it's not a private one
|
||||||
ctx.WriteString(err.Error())
|
// write it to the response body.
|
||||||
return
|
ctx.WriteString(err.Error())
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
// Otherwise, write the code's text instead.
|
||||||
ctx.WriteString(context.StatusText(ctx.GetStatusCode()))
|
ctx.WriteString(context.StatusText(ctx.GetStatusCode()))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *routerHandler) Build(provider RoutesProvider) error {
|
func (h *routerHandler) Build(provider RoutesProvider) error {
|
||||||
|
|
22
go.mod
22
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.1
|
||||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398
|
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398
|
||||||
github.com/andybalholm/brotli v1.0.1
|
github.com/andybalholm/brotli v1.0.1
|
||||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible
|
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible
|
||||||
|
@ -12,7 +12,7 @@ require (
|
||||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
|
||||||
github.com/fatih/structs v1.1.0
|
github.com/fatih/structs v1.1.0
|
||||||
github.com/flosch/pongo2/v4 v4.0.0
|
github.com/flosch/pongo2/v4 v4.0.0
|
||||||
github.com/go-redis/redis/v8 v8.2.3
|
github.com/go-redis/redis/v8 v8.3.3
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/hashicorp/go-version v1.2.1
|
github.com/hashicorp/go-version v1.2.1
|
||||||
github.com/iris-contrib/httpexpect/v2 v2.0.5
|
github.com/iris-contrib/httpexpect/v2 v2.0.5
|
||||||
|
@ -21,26 +21,26 @@ require (
|
||||||
github.com/json-iterator/go v1.1.10
|
github.com/json-iterator/go v1.1.10
|
||||||
github.com/kataras/blocks v0.0.4
|
github.com/kataras/blocks v0.0.4
|
||||||
github.com/kataras/golog v0.1.5
|
github.com/kataras/golog v0.1.5
|
||||||
|
github.com/kataras/jwt v0.0.5
|
||||||
github.com/kataras/neffos v0.0.16
|
github.com/kataras/neffos v0.0.16
|
||||||
github.com/kataras/pio v0.0.10
|
github.com/kataras/pio v0.0.10
|
||||||
github.com/kataras/sitemap v0.0.5
|
github.com/kataras/sitemap v0.0.5
|
||||||
github.com/kataras/tunnel v0.0.2
|
github.com/kataras/tunnel v0.0.2
|
||||||
github.com/klauspost/compress v1.11.1
|
github.com/klauspost/compress v1.11.2
|
||||||
github.com/mailru/easyjson v0.7.6
|
github.com/mailru/easyjson v0.7.6
|
||||||
github.com/microcosm-cc/bluemonday v1.0.4
|
github.com/microcosm-cc/bluemonday v1.0.4
|
||||||
github.com/russross/blackfriday/v2 v2.0.1
|
github.com/russross/blackfriday/v2 v2.0.1
|
||||||
github.com/schollz/closestmatch v2.1.0+incompatible
|
github.com/schollz/closestmatch v2.1.0+incompatible
|
||||||
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
|
github.com/tdewolff/minify/v2 v2.9.10
|
||||||
github.com/tdewolff/minify/v2 v2.9.7
|
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.9
|
||||||
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1
|
|
||||||
github.com/yosssi/ace v0.0.5
|
github.com/yosssi/ace v0.0.5
|
||||||
go.etcd.io/bbolt v1.3.5
|
go.etcd.io/bbolt v1.3.5
|
||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c
|
golang.org/x/net v0.0.0-20201027133719-8eef5233e2a1
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
|
golang.org/x/sys v0.0.0-20201028094953-708e7fb298ac
|
||||||
golang.org/x/text v0.3.3
|
golang.org/x/text v0.3.4
|
||||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
|
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
|
||||||
google.golang.org/protobuf v1.25.0
|
google.golang.org/protobuf v1.25.0
|
||||||
gopkg.in/ini.v1 v1.61.0
|
gopkg.in/ini.v1 v1.62.0
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -718,7 +718,7 @@ func (ac *AccessLog) Handler(ctx *context.Context) {
|
||||||
// Enable reading the request body
|
// Enable reading the request body
|
||||||
// multiple times (route handler and this middleware).
|
// multiple times (route handler and this middleware).
|
||||||
if ac.shouldReadRequestBody() {
|
if ac.shouldReadRequestBody() {
|
||||||
ctx.RecordBody()
|
ctx.RecordRequestBody(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the fields context value so they can be modified
|
// Set the fields context value so they can be modified
|
||||||
|
|
|
@ -26,7 +26,11 @@ func TestBasicAuthUseRouter(t *testing.T) {
|
||||||
|
|
||||||
app.Get("/user_string", func(ctx iris.Context) {
|
app.Get("/user_string", func(ctx iris.Context) {
|
||||||
user := ctx.User()
|
user := ctx.User()
|
||||||
ctx.Writef("%s\n%s\n%s", user.GetAuthorization(), user.GetUsername(), user.GetPassword())
|
|
||||||
|
authorization, _ := user.GetAuthorization()
|
||||||
|
username, _ := user.GetUsername()
|
||||||
|
password, _ := user.GetPassword()
|
||||||
|
ctx.Writef("%s\n%s\n%s", authorization, username, password)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Get("/", func(ctx iris.Context) {
|
app.Get("/", func(ctx iris.Context) {
|
||||||
|
|
126
middleware/jwt/aliases.go
Normal file
126
middleware/jwt/aliases.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import "github.com/kataras/jwt"
|
||||||
|
|
||||||
|
// Error values.
|
||||||
|
var (
|
||||||
|
ErrBlocked = jwt.ErrBlocked
|
||||||
|
ErrDecrypt = jwt.ErrDecrypt
|
||||||
|
ErrExpected = jwt.ErrExpected
|
||||||
|
ErrExpired = jwt.ErrExpired
|
||||||
|
ErrInvalidKey = jwt.ErrInvalidKey
|
||||||
|
ErrIssuedInTheFuture = jwt.ErrIssuedInTheFuture
|
||||||
|
ErrMissing = jwt.ErrMissing
|
||||||
|
ErrMissingKey = jwt.ErrMissingKey
|
||||||
|
ErrNotValidYet = jwt.ErrNotValidYet
|
||||||
|
ErrTokenAlg = jwt.ErrTokenAlg
|
||||||
|
ErrTokenForm = jwt.ErrTokenForm
|
||||||
|
ErrTokenSignature = jwt.ErrTokenSignature
|
||||||
|
)
|
||||||
|
|
||||||
|
// Signature algorithms.
|
||||||
|
var (
|
||||||
|
EdDSA = jwt.EdDSA
|
||||||
|
HS256 = jwt.HS256
|
||||||
|
HS384 = jwt.HS384
|
||||||
|
HS512 = jwt.HS512
|
||||||
|
RS256 = jwt.RS256
|
||||||
|
RS384 = jwt.RS384
|
||||||
|
RS512 = jwt.RS512
|
||||||
|
ES256 = jwt.ES256
|
||||||
|
ES384 = jwt.ES384
|
||||||
|
ES512 = jwt.ES512
|
||||||
|
PS256 = jwt.PS256
|
||||||
|
PS384 = jwt.PS384
|
||||||
|
PS512 = jwt.PS512
|
||||||
|
)
|
||||||
|
|
||||||
|
// Signature algorithm helpers.
|
||||||
|
var (
|
||||||
|
MustLoadHMAC = jwt.MustLoadHMAC
|
||||||
|
LoadHMAC = jwt.LoadHMAC
|
||||||
|
MustLoadRSA = jwt.MustLoadRSA
|
||||||
|
LoadPrivateKeyRSA = jwt.LoadPrivateKeyRSA
|
||||||
|
LoadPublicKeyRSA = jwt.LoadPublicKeyRSA
|
||||||
|
ParsePrivateKeyRSA = jwt.ParsePrivateKeyRSA
|
||||||
|
ParsePublicKeyRSA = jwt.ParsePublicKeyRSA
|
||||||
|
MustLoadECDSA = jwt.MustLoadECDSA
|
||||||
|
LoadPrivateKeyECDSA = jwt.LoadPrivateKeyECDSA
|
||||||
|
LoadPublicKeyECDSA = jwt.LoadPublicKeyECDSA
|
||||||
|
ParsePrivateKeyECDSA = jwt.ParsePrivateKeyECDSA
|
||||||
|
ParsePublicKeyECDSA = jwt.ParsePublicKeyECDSA
|
||||||
|
MustLoadEdDSA = jwt.MustLoadEdDSA
|
||||||
|
LoadPrivateKeyEdDSA = jwt.LoadPrivateKeyEdDSA
|
||||||
|
LoadPublicKeyEdDSA = jwt.LoadPublicKeyEdDSA
|
||||||
|
ParsePrivateKeyEdDSA = jwt.ParsePrivateKeyEdDSA
|
||||||
|
ParsePublicKeyEdDSA = jwt.ParsePublicKeyEdDSA
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type alises for the underline jwt package.
|
||||||
|
type (
|
||||||
|
// Alg is the signature algorithm interface alias.
|
||||||
|
Alg = jwt.Alg
|
||||||
|
// Claims represents the standard claim values (as specified in RFC 7519).
|
||||||
|
Claims = jwt.Claims
|
||||||
|
// Expected is a TokenValidator which performs simple checks
|
||||||
|
// between standard claims values.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// expecteed := jwt.Expected{
|
||||||
|
// Issuer: "my-app",
|
||||||
|
// }
|
||||||
|
// verifiedToken, err := verifier.Verify(..., expected)
|
||||||
|
Expected = jwt.Expected
|
||||||
|
|
||||||
|
// TokenValidator is the token validator interface alias.
|
||||||
|
TokenValidator = jwt.TokenValidator
|
||||||
|
// VerifiedToken is the type alias for the verfieid token type,
|
||||||
|
// the result of the VerifyToken function.
|
||||||
|
VerifiedToken = jwt.VerifiedToken
|
||||||
|
// SignOption used to set signing options at Sign function.
|
||||||
|
SignOption = jwt.SignOption
|
||||||
|
// TokenPair is just a helper structure which holds both access and refresh tokens.
|
||||||
|
TokenPair = jwt.TokenPair
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encryption algorithms.
|
||||||
|
var (
|
||||||
|
GCM = jwt.GCM
|
||||||
|
// Helper to generate random key,
|
||||||
|
// can be used to generate hmac signature key and GCM+AES for testing.
|
||||||
|
MustGenerateRandom = jwt.MustGenerateRandom
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Leeway adds validation for a leeway expiration time.
|
||||||
|
// If the token was not expired then a comparison between
|
||||||
|
// this "leeway" and the token's "exp" one is expected to pass instead (now+leeway > exp).
|
||||||
|
// Example of use case: disallow tokens that are going to be expired in 3 seconds from now,
|
||||||
|
// this is useful to make sure that the token is valid when the when the user fires a database call for example.
|
||||||
|
// Usage:
|
||||||
|
// verifiedToken, err := verifier.Verify(..., jwt.Leeway(5*time.Second))
|
||||||
|
Leeway = jwt.Leeway
|
||||||
|
// MaxAge is a SignOption to set the expiration "exp", "iat" JWT standard claims.
|
||||||
|
// Can be passed as last input argument of the `Sign` function.
|
||||||
|
//
|
||||||
|
// If maxAge > second then sets expiration to the token.
|
||||||
|
// It's a helper field to set the "exp" and "iat" claim values.
|
||||||
|
// Usage:
|
||||||
|
// signer.Sign(..., jwt.MaxAge(15*time.Minute))
|
||||||
|
MaxAge = jwt.MaxAge
|
||||||
|
|
||||||
|
// ID is a shurtcut to set jwt ID on Sign.
|
||||||
|
ID = func(id string) jwt.SignOptionFunc {
|
||||||
|
return func(c *Claims) {
|
||||||
|
c.ID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shortcuts for Signing and Verifying.
|
||||||
|
var (
|
||||||
|
Verify = jwt.Verify
|
||||||
|
VerifyEncryptedToken = jwt.VerifyEncrypted
|
||||||
|
Sign = jwt.Sign
|
||||||
|
SignEncrypted = jwt.SignEncrypted
|
||||||
|
)
|
|
@ -1,82 +0,0 @@
|
||||||
package jwt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/square/go-jose/v3"
|
|
||||||
"github.com/square/go-jose/v3/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// Claims represents public claim values (as specified in RFC 7519).
|
|
||||||
Claims = jwt.Claims
|
|
||||||
// Audience represents the recipients that the token is intended for.
|
|
||||||
Audience = jwt.Audience
|
|
||||||
// NumericDate represents date and time as the number of seconds since the
|
|
||||||
// epoch, including leap seconds. Non-integer values can be represented
|
|
||||||
// in the serialized format, but we round to the nearest second.
|
|
||||||
NumericDate = jwt.NumericDate
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// NewNumericDate constructs NumericDate from time.Time value.
|
|
||||||
NewNumericDate = jwt.NewNumericDate
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// KeyAlgorithm represents a key management algorithm.
|
|
||||||
KeyAlgorithm = jose.KeyAlgorithm
|
|
||||||
|
|
||||||
// SignatureAlgorithm represents a signature (or MAC) algorithm.
|
|
||||||
SignatureAlgorithm = jose.SignatureAlgorithm
|
|
||||||
|
|
||||||
// ContentEncryption represents a content encryption algorithm.
|
|
||||||
ContentEncryption = jose.ContentEncryption
|
|
||||||
)
|
|
||||||
|
|
||||||
// Key management algorithms.
|
|
||||||
const (
|
|
||||||
ED25519 = jose.ED25519
|
|
||||||
RSA15 = jose.RSA1_5
|
|
||||||
RSAOAEP = jose.RSA_OAEP
|
|
||||||
RSAOAEP256 = jose.RSA_OAEP_256
|
|
||||||
A128KW = jose.A128KW
|
|
||||||
A192KW = jose.A192KW
|
|
||||||
A256KW = jose.A256KW
|
|
||||||
DIRECT = jose.DIRECT
|
|
||||||
ECDHES = jose.ECDH_ES
|
|
||||||
ECDHESA128KW = jose.ECDH_ES_A128KW
|
|
||||||
ECDHESA192KW = jose.ECDH_ES_A192KW
|
|
||||||
ECDHESA256KW = jose.ECDH_ES_A256KW
|
|
||||||
A128GCMKW = jose.A128GCMKW
|
|
||||||
A192GCMKW = jose.A192GCMKW
|
|
||||||
A256GCMKW = jose.A256GCMKW
|
|
||||||
PBES2HS256A128KW = jose.PBES2_HS256_A128KW
|
|
||||||
PBES2HS384A192KW = jose.PBES2_HS384_A192KW
|
|
||||||
PBES2HS512A256KW = jose.PBES2_HS512_A256KW
|
|
||||||
)
|
|
||||||
|
|
||||||
// Signature algorithms.
|
|
||||||
const (
|
|
||||||
EdDSA = jose.EdDSA
|
|
||||||
HS256 = jose.HS256
|
|
||||||
HS384 = jose.HS384
|
|
||||||
HS512 = jose.HS512
|
|
||||||
RS256 = jose.RS256
|
|
||||||
RS384 = jose.RS384
|
|
||||||
RS512 = jose.RS512
|
|
||||||
ES256 = jose.ES256
|
|
||||||
ES384 = jose.ES384
|
|
||||||
ES512 = jose.ES512
|
|
||||||
PS256 = jose.PS256
|
|
||||||
PS384 = jose.PS384
|
|
||||||
PS512 = jose.PS512
|
|
||||||
)
|
|
||||||
|
|
||||||
// Content encryption algorithms.
|
|
||||||
const (
|
|
||||||
A128CBCHS256 = jose.A128CBC_HS256
|
|
||||||
A192CBCHS384 = jose.A192CBC_HS384
|
|
||||||
A256CBCHS512 = jose.A256CBC_HS512
|
|
||||||
A128GCM = jose.A128GCM
|
|
||||||
A192GCM = jose.A192GCM
|
|
||||||
A256GCM = jose.A256GCM
|
|
||||||
)
|
|
31
middleware/jwt/blocklist.go
Normal file
31
middleware/jwt/blocklist.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Blocklist should hold and manage invalidated-by-server tokens.
|
||||||
|
// The `NewBlocklist` and `NewBlocklistContext` functions
|
||||||
|
// returns a memory storage of tokens,
|
||||||
|
// it is the internal "blocklist" struct.
|
||||||
|
//
|
||||||
|
// The end-developer can implement her/his own blocklist,
|
||||||
|
// e.g. a redis one to keep persistence of invalidated tokens on server restarts.
|
||||||
|
// and bind to the JWT middleware's Blocklist field.
|
||||||
|
type Blocklist interface {
|
||||||
|
jwt.TokenValidator
|
||||||
|
|
||||||
|
// InvalidateToken should invalidate a verified JWT token.
|
||||||
|
InvalidateToken(token []byte, c Claims) error
|
||||||
|
// Del should remove a token from the storage.
|
||||||
|
Del(key string) error
|
||||||
|
// Has should report whether a specific token exists in the storage.
|
||||||
|
Has(key string) (bool, error)
|
||||||
|
// Count should return the total amount of tokens stored.
|
||||||
|
Count() (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type blocklistConnect interface {
|
||||||
|
Connect() error
|
||||||
|
IsConnected() bool
|
||||||
|
}
|
185
middleware/jwt/blocklist/redis/blocklist.go
Normal file
185
middleware/jwt/blocklist/redis/blocklist.go
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/core/host"
|
||||||
|
"github.com/kataras/iris/v12/middleware/jwt"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultContext = context.Background()
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Options is just a type alias for the go-redis Client Options.
|
||||||
|
Options = redis.Options
|
||||||
|
// ClusterOptions is just a type alias for the go-redis Cluster Client Options.
|
||||||
|
ClusterOptions = redis.ClusterOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the interface which both
|
||||||
|
// go-redis Client and Cluster Client implements.
|
||||||
|
type Client interface {
|
||||||
|
redis.Cmdable // Commands.
|
||||||
|
io.Closer // CloseConnection.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocklist is a jwt.Blocklist backed by Redis.
|
||||||
|
type Blocklist struct {
|
||||||
|
// GetKey is a function which can be used how to extract
|
||||||
|
// the unique identifier for a token.
|
||||||
|
// Required. By default the token key is extracted through the claims.ID ("jti").
|
||||||
|
GetKey func(token []byte, claims jwt.Claims) string
|
||||||
|
// Prefix the token key into the redis database.
|
||||||
|
// Note that if you can also select a different database
|
||||||
|
// through ClientOptions (or ClusterOptions).
|
||||||
|
// Defaults to empty string (no prefix).
|
||||||
|
Prefix string
|
||||||
|
// Both Client and ClusterClient implements this interface.
|
||||||
|
client Client
|
||||||
|
connected uint32
|
||||||
|
// Customize any go-redis fields manually
|
||||||
|
// before Connect.
|
||||||
|
ClientOptions Options
|
||||||
|
ClusterOptions ClusterOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jwt.Blocklist = (*Blocklist)(nil)
|
||||||
|
|
||||||
|
// NewBlocklist returns a new redis-based Blocklist.
|
||||||
|
// Modify its ClientOptions or ClusterOptions depending the application needs
|
||||||
|
// and call its Connect.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// blocklist := NewBlocklist()
|
||||||
|
// blocklist.ClientOptions.Addr = ...
|
||||||
|
// err := blocklist.Connect()
|
||||||
|
//
|
||||||
|
// And register it:
|
||||||
|
//
|
||||||
|
// verifier := jwt.NewVerifier(...)
|
||||||
|
// verifier.Blocklist = blocklist
|
||||||
|
func NewBlocklist() *Blocklist {
|
||||||
|
return &Blocklist{
|
||||||
|
GetKey: defaultGetKey,
|
||||||
|
Prefix: "",
|
||||||
|
ClientOptions: Options{
|
||||||
|
Addr: "127.0.0.1:6379",
|
||||||
|
// The rest are defaulted to good values already.
|
||||||
|
},
|
||||||
|
// If its Addrs > 0 before connect then cluster client is used instead.
|
||||||
|
ClusterOptions: ClusterOptions{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultGetKey(_ []byte, claims jwt.Claims) string {
|
||||||
|
return claims.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect prepares the redis client and fires a ping response to it.
|
||||||
|
func (b *Blocklist) Connect() error {
|
||||||
|
if b.Prefix != "" {
|
||||||
|
getKey := b.GetKey
|
||||||
|
b.GetKey = func(token []byte, claims jwt.Claims) string {
|
||||||
|
return b.Prefix + getKey(token, claims)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.ClusterOptions.Addrs) > 0 {
|
||||||
|
// Use cluster client.
|
||||||
|
b.client = redis.NewClusterClient(&b.ClusterOptions)
|
||||||
|
} else {
|
||||||
|
b.client = redis.NewClient(&b.ClientOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := b.client.Ping(defaultContext).Result()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
host.RegisterOnInterrupt(func() {
|
||||||
|
atomic.StoreUint32(&b.connected, 0)
|
||||||
|
b.client.Close()
|
||||||
|
})
|
||||||
|
atomic.StoreUint32(&b.connected, 1)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConnected reports whether the Connect function was called.
|
||||||
|
func (b *Blocklist) IsConnected() bool {
|
||||||
|
return atomic.LoadUint32(&b.connected) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateToken checks if the token exists and
|
||||||
|
func (b *Blocklist) ValidateToken(token []byte, c jwt.Claims, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
if err == jwt.ErrExpired {
|
||||||
|
b.Del(b.GetKey(token, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err // respect the previous error.
|
||||||
|
}
|
||||||
|
|
||||||
|
has, err := b.Has(b.GetKey(token, c))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if has {
|
||||||
|
return jwt.ErrBlocked
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateToken invalidates a verified JWT token.
|
||||||
|
func (b *Blocklist) InvalidateToken(token []byte, c jwt.Claims) error {
|
||||||
|
key := b.GetKey(token, c)
|
||||||
|
return b.client.SetEX(defaultContext, key, token, c.Timeleft()).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del removes a token from the storage.
|
||||||
|
func (b *Blocklist) Del(key string) error {
|
||||||
|
return b.client.Del(defaultContext, key).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has reports whether a specific token exists in the storage.
|
||||||
|
func (b *Blocklist) Has(key string) (bool, error) {
|
||||||
|
n, err := b.client.Exists(defaultContext, key).Result()
|
||||||
|
return n > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the total amount of tokens stored.
|
||||||
|
func (b *Blocklist) Count() (int64, error) {
|
||||||
|
if b.Prefix == "" {
|
||||||
|
return b.client.DBSize(defaultContext).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err := b.getKeys(0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(len(keys)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Blocklist) getKeys(cursor uint64) ([]string, error) {
|
||||||
|
keys, cursor, err := b.client.Scan(defaultContext, cursor, b.Prefix+"*", 300000).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor != 0 {
|
||||||
|
moreKeys, err := b.getKeys(cursor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keys = append(keys, moreKeys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
71
middleware/jwt/extractor.go
Normal file
71
middleware/jwt/extractor.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenExtractor is a function that takes a context as input and returns
|
||||||
|
// a token. An empty string should be returned if no token found
|
||||||
|
// without additional information.
|
||||||
|
type TokenExtractor func(*context.Context) string
|
||||||
|
|
||||||
|
// FromHeader is a token extractor.
|
||||||
|
// It reads the token from the Authorization request header of form:
|
||||||
|
// Authorization: "Bearer {token}".
|
||||||
|
func FromHeader(ctx *context.Context) string {
|
||||||
|
authHeader := ctx.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// pure check: authorization header format must be Bearer {token}
|
||||||
|
authHeaderParts := strings.Split(authHeader, " ")
|
||||||
|
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return authHeaderParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromQuery is a token extractor.
|
||||||
|
// It reads the token from the "token" url query parameter.
|
||||||
|
func FromQuery(ctx *context.Context) string {
|
||||||
|
return ctx.URLParam("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromJSON is a token extractor.
|
||||||
|
// Reads a json request body and extracts the json based on the given field.
|
||||||
|
// The request content-type should contain the: application/json header value, otherwise
|
||||||
|
// this method will not try to read and consume the body.
|
||||||
|
func FromJSON(jsonKey string) TokenExtractor {
|
||||||
|
return func(ctx *context.Context) string {
|
||||||
|
if ctx.GetContentTypeRequested() != context.ContentJSONHeaderValue {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var m context.Map
|
||||||
|
ctx.RecordRequestBody(true)
|
||||||
|
defer ctx.RecordRequestBody(false)
|
||||||
|
if err := ctx.ReadJSON(&m); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if m == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := m[jsonKey]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,613 +1,7 @@
|
||||||
package jwt
|
package jwt
|
||||||
|
|
||||||
import (
|
import "github.com/kataras/iris/v12/context"
|
||||||
"crypto"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
|
|
||||||
"github.com/square/go-jose/v3"
|
|
||||||
"github.com/square/go-jose/v3/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
context.SetHandlerName("iris/middleware/jwt.*", "iris.jwt")
|
context.SetHandlerName("iris/middleware/jwt.*", "iris.jwt")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenExtractor is a function that takes a context as input and returns
|
|
||||||
// a token. An empty string should be returned if no token found
|
|
||||||
// without additional information.
|
|
||||||
type TokenExtractor func(*context.Context) string
|
|
||||||
|
|
||||||
// FromHeader is a token extractor.
|
|
||||||
// It reads the token from the Authorization request header of form:
|
|
||||||
// Authorization: "Bearer {token}".
|
|
||||||
func FromHeader(ctx *context.Context) string {
|
|
||||||
authHeader := ctx.GetHeader("Authorization")
|
|
||||||
if authHeader == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// pure check: authorization header format must be Bearer {token}
|
|
||||||
authHeaderParts := strings.Split(authHeader, " ")
|
|
||||||
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return authHeaderParts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromQuery is a token extractor.
|
|
||||||
// It reads the token from the "token" url query parameter.
|
|
||||||
func FromQuery(ctx *context.Context) string {
|
|
||||||
return ctx.URLParam("token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromJSON is a token extractor.
|
|
||||||
// Reads a json request body and extracts the json based on the given field.
|
|
||||||
// The request content-type should contain the: application/json header value, otherwise
|
|
||||||
// this method will not try to read and consume the body.
|
|
||||||
func FromJSON(jsonKey string) TokenExtractor {
|
|
||||||
return func(ctx *context.Context) string {
|
|
||||||
if ctx.GetContentTypeRequested() != context.ContentJSONHeaderValue {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var m context.Map
|
|
||||||
if err := ctx.ReadJSON(&m); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if m == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := m[jsonKey]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
tok, ok := v.(string)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return tok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWT holds the necessary information the middleware need
|
|
||||||
// to sign and verify tokens.
|
|
||||||
//
|
|
||||||
// The `RSA(privateFile, publicFile, password)` package-level helper function
|
|
||||||
// can be used to decode the SignKey and VerifyKey.
|
|
||||||
type JWT struct {
|
|
||||||
// MaxAge is the expiration duration of the generated tokens.
|
|
||||||
MaxAge time.Duration
|
|
||||||
|
|
||||||
// Extractors are used to extract a raw token string value
|
|
||||||
// from the request.
|
|
||||||
// Builtin extractors:
|
|
||||||
// * FromHeader
|
|
||||||
// * FromQuery
|
|
||||||
// * FromJSON
|
|
||||||
// Defaults to a slice of `FromHeader` and `FromQuery`.
|
|
||||||
Extractors []TokenExtractor
|
|
||||||
|
|
||||||
// Signer is used to sign the token.
|
|
||||||
// It is set on `New` and `Default` package-level functions.
|
|
||||||
Signer jose.Signer
|
|
||||||
// VerificationKey is used to verify the token (public key).
|
|
||||||
VerificationKey interface{}
|
|
||||||
|
|
||||||
// Encrypter is used to, optionally, encrypt the token.
|
|
||||||
// It is set on `WithEncryption` method.
|
|
||||||
Encrypter jose.Encrypter
|
|
||||||
// DecriptionKey is used to decrypt the token (private key)
|
|
||||||
DecriptionKey interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type privateKey interface{ Public() crypto.PublicKey }
|
|
||||||
|
|
||||||
// New returns a new JWT instance.
|
|
||||||
// It accepts a maximum time duration for token expiration
|
|
||||||
// and the algorithm among with its key for signing and verification.
|
|
||||||
//
|
|
||||||
// See `WithEncryption` method to add token encryption too.
|
|
||||||
// Use `Token` method to generate a new token string
|
|
||||||
// and `VerifyToken` method to decrypt, verify and bind claims of an incoming request token.
|
|
||||||
// Token, by default, is extracted by "Authorization: Bearer {token}" request header and
|
|
||||||
// url query parameter of "token". Token extractors can be modified through the `Extractors` field.
|
|
||||||
//
|
|
||||||
// For example, if you want to sign and verify using RSA-256 key:
|
|
||||||
// 1. Generate key file, e.g:
|
|
||||||
// $ openssl genrsa -des3 -out private.pem 2048
|
|
||||||
// 2. Read file contents with io.ReadFile("./private.pem")
|
|
||||||
// 3. Pass the []byte result to the `ParseRSAPrivateKey(contents, password)` package-level helper
|
|
||||||
// 4. Use the result *rsa.PrivateKey as "key" input parameter of this `New` function.
|
|
||||||
//
|
|
||||||
// See aliases.go file for available algorithms.
|
|
||||||
func New(maxAge time.Duration, alg SignatureAlgorithm, key interface{}) (*JWT, error) {
|
|
||||||
sig, err := jose.NewSigner(jose.SigningKey{
|
|
||||||
Algorithm: alg,
|
|
||||||
Key: key,
|
|
||||||
}, (&jose.SignerOptions{}).WithType("JWT"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j := &JWT{
|
|
||||||
Signer: sig,
|
|
||||||
VerificationKey: key,
|
|
||||||
MaxAge: maxAge,
|
|
||||||
Extractors: []TokenExtractor{FromHeader, FromQuery},
|
|
||||||
}
|
|
||||||
|
|
||||||
if s, ok := key.(privateKey); ok {
|
|
||||||
j.VerificationKey = s.Public()
|
|
||||||
}
|
|
||||||
|
|
||||||
return j, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default key filenames for `RSA`.
|
|
||||||
const (
|
|
||||||
DefaultSignFilename = "jwt_sign.key"
|
|
||||||
DefaultEncFilename = "jwt_enc.key"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RSA returns a new `JWT` instance.
|
|
||||||
// It tries to parse RSA256 keys from "filenames[0]" (defaults to "jwt_sign.key") and
|
|
||||||
// "filenames[1]" (defaults to "jwt_enc.key") files or generates and exports new random keys.
|
|
||||||
//
|
|
||||||
// It panics on errors.
|
|
||||||
// Use the `New` package-level function instead for more options.
|
|
||||||
func RSA(maxAge time.Duration, filenames ...string) *JWT {
|
|
||||||
var (
|
|
||||||
signFilename = DefaultSignFilename
|
|
||||||
encFilename = DefaultEncFilename
|
|
||||||
)
|
|
||||||
|
|
||||||
switch len(filenames) {
|
|
||||||
case 1:
|
|
||||||
signFilename = filenames[0]
|
|
||||||
case 2:
|
|
||||||
encFilename = filenames[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not try to create or load enc key if only sign key already exists.
|
|
||||||
withEncryption := true
|
|
||||||
if fileExists(signFilename) {
|
|
||||||
withEncryption = fileExists(encFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
sigKey, err := LoadRSA(signFilename, 2048)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := New(maxAge, RS256, sigKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if withEncryption {
|
|
||||||
encKey, err := LoadRSA(encFilename, 2048)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = j.WithEncryption(A128CBCHS256, RSA15, encKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
signEnv = "JWT_SECRET"
|
|
||||||
encEnv = "JWT_SECRET_ENC"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getenv(key string, def string) string {
|
|
||||||
v := os.Getenv(key)
|
|
||||||
if v == "" {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMAC returns a new `JWT` instance.
|
|
||||||
// It tries to read hmac256 secret keys from system environment variables:
|
|
||||||
// * JWT_SECRET for signing and verification key and
|
|
||||||
// * JWT_SECRET_ENC for encryption and decryption key
|
|
||||||
// and defaults them to the given "keys" respectfully.
|
|
||||||
//
|
|
||||||
// It panics on errors.
|
|
||||||
// Use the `New` package-level function instead for more options.
|
|
||||||
func HMAC(maxAge time.Duration, keys ...string) *JWT {
|
|
||||||
var defaultSignSecret, defaultEncSecret string
|
|
||||||
|
|
||||||
switch len(keys) {
|
|
||||||
case 1:
|
|
||||||
defaultSignSecret = keys[0]
|
|
||||||
case 2:
|
|
||||||
defaultEncSecret = keys[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
signSecret := getenv(signEnv, defaultSignSecret)
|
|
||||||
encSecret := getenv(encEnv, defaultEncSecret)
|
|
||||||
|
|
||||||
j, err := New(maxAge, HS256, []byte(signSecret))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if encSecret != "" {
|
|
||||||
err = j.WithEncryption(A128GCM, DIRECT, []byte(encSecret))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithEncryption method enables encryption and decryption of the token.
|
|
||||||
// It sets an appropriate encrypter(`Encrypter` and the `DecriptionKey` fields) based on the key type.
|
|
||||||
func (j *JWT) WithEncryption(contentEncryption ContentEncryption, alg KeyAlgorithm, key interface{}) error {
|
|
||||||
var publicKey interface{} = key
|
|
||||||
if s, ok := key.(privateKey); ok {
|
|
||||||
publicKey = s.Public()
|
|
||||||
}
|
|
||||||
|
|
||||||
enc, err := jose.NewEncrypter(contentEncryption, jose.Recipient{
|
|
||||||
Algorithm: alg,
|
|
||||||
Key: publicKey,
|
|
||||||
},
|
|
||||||
(&jose.EncrypterOptions{}).WithType("JWT").WithContentType("JWT"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
j.Encrypter = enc
|
|
||||||
j.DecriptionKey = key
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expiry returns a new standard Claims with
|
|
||||||
// the `Expiry` and `IssuedAt` fields of the "claims" filled
|
|
||||||
// based on the given "maxAge" duration.
|
|
||||||
//
|
|
||||||
// See the `JWT.Expiry` method too.
|
|
||||||
func Expiry(maxAge time.Duration, claims Claims) Claims {
|
|
||||||
now := time.Now()
|
|
||||||
claims.Expiry = NewNumericDate(now.Add(maxAge))
|
|
||||||
claims.IssuedAt = NewNumericDate(now)
|
|
||||||
return claims
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expiry method same as `Expiry` package-level function,
|
|
||||||
// it returns a Claims with the expiration fields of the "claims"
|
|
||||||
// filled based on the JWT's `MaxAge` field.
|
|
||||||
// Only use it when this standard "claims"
|
|
||||||
// is embedded on a custom claims structure.
|
|
||||||
// Usage:
|
|
||||||
// type UserClaims struct {
|
|
||||||
// jwt.Claims
|
|
||||||
// Username string
|
|
||||||
// }
|
|
||||||
// [...]
|
|
||||||
// standardClaims := j.Expiry(jwt.Claims{...})
|
|
||||||
// customClaims := UserClaims{
|
|
||||||
// Claims: standardClaims,
|
|
||||||
// Username: "kataras",
|
|
||||||
// }
|
|
||||||
// j.WriteToken(ctx, customClaims)
|
|
||||||
func (j *JWT) Expiry(claims Claims) Claims {
|
|
||||||
return Expiry(j.MaxAge, claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token generates and returns a new token string.
|
|
||||||
// See `VerifyToken` too.
|
|
||||||
func (j *JWT) Token(claims interface{}) (string, error) {
|
|
||||||
// switch c := claims.(type) {
|
|
||||||
// case Claims:
|
|
||||||
// claims = Expiry(j.MaxAge, c)
|
|
||||||
// case map[string]interface{}: let's not support map.
|
|
||||||
// now := time.Now()
|
|
||||||
// c["iat"] = now.Unix()
|
|
||||||
// c["exp"] = now.Add(j.MaxAge).Unix()
|
|
||||||
// }
|
|
||||||
if c, ok := claims.(Claims); ok {
|
|
||||||
claims = Expiry(j.MaxAge, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
token string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
// jwt.Builder and jwt.NestedBuilder contain same methods but they are not the same.
|
|
||||||
if j.DecriptionKey != nil {
|
|
||||||
token, err = jwt.SignedAndEncrypted(j.Signer, j.Encrypter).Claims(claims).CompactSerialize()
|
|
||||||
} else {
|
|
||||||
token, err = jwt.Signed(j.Signer).Claims(claims).CompactSerialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Let's no support maps, typed claim is the way to go.
|
|
||||||
// validateMapClaims validates claims of map type.
|
|
||||||
func validateMapClaims(m map[string]interface{}, e jwt.Expected, leeway time.Duration) error {
|
|
||||||
if !e.Time.IsZero() {
|
|
||||||
if v, ok := m["nbf"]; ok {
|
|
||||||
if notBefore, ok := v.(NumericDate); ok {
|
|
||||||
if e.Time.Add(leeway).Before(notBefore.Time()) {
|
|
||||||
return ErrNotValidYet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := m["exp"]; ok {
|
|
||||||
if exp, ok := v.(int64); ok {
|
|
||||||
if e.Time.Add(-leeway).Before(time.Unix(exp, 0)) {
|
|
||||||
return ErrExpired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := m["iat"]; ok {
|
|
||||||
if issuedAt, ok := v.(int64); ok {
|
|
||||||
if e.Time.Add(leeway).Before(time.Unix(issuedAt, 0)) {
|
|
||||||
return ErrIssuedInTheFuture
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// WriteToken is a helper which just generates(calls the `Token` method) and writes
|
|
||||||
// a new token to the client in plain text format.
|
|
||||||
//
|
|
||||||
// Use the `Token` method to get a new generated token raw string value.
|
|
||||||
func (j *JWT) WriteToken(ctx *context.Context, claims interface{}) error {
|
|
||||||
token, err := j.Token(claims)
|
|
||||||
if err != nil {
|
|
||||||
ctx.StatusCode(500)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = ctx.WriteString(token)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrMissing when token cannot be extracted from the request.
|
|
||||||
ErrMissing = errors.New("token is missing")
|
|
||||||
// ErrExpired indicates that token is used after expiry time indicated in exp claim.
|
|
||||||
ErrExpired = errors.New("token is expired (exp)")
|
|
||||||
// ErrNotValidYet indicates that token is used before time indicated in nbf claim.
|
|
||||||
ErrNotValidYet = errors.New("token not valid yet (nbf)")
|
|
||||||
// ErrIssuedInTheFuture indicates that the iat field is in the future.
|
|
||||||
ErrIssuedInTheFuture = errors.New("token issued in the future (iat)")
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
claimsValidator interface {
|
|
||||||
ValidateWithLeeway(e jwt.Expected, leeway time.Duration) error
|
|
||||||
}
|
|
||||||
claimsAlternativeValidator interface { // to keep iris-contrib/jwt MapClaims compatible.
|
|
||||||
Validate() error
|
|
||||||
}
|
|
||||||
claimsContextValidator interface {
|
|
||||||
Validate(ctx *context.Context) error
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsValidated reports whether a token is already validated through
|
|
||||||
// `VerifyToken`. It returns true when the claims are compatible
|
|
||||||
// validators: a `Claims` value or a value that implements the `Validate() error` method.
|
|
||||||
func IsValidated(ctx *context.Context) bool { // see the `ReadClaims`.
|
|
||||||
return ctx.Values().Get(needsValidationContextKey) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateClaims(ctx *context.Context, claims interface{}) (err error) {
|
|
||||||
switch c := claims.(type) {
|
|
||||||
case claimsValidator:
|
|
||||||
err = c.ValidateWithLeeway(jwt.Expected{Time: time.Now()}, 0)
|
|
||||||
case claimsAlternativeValidator:
|
|
||||||
err = c.Validate()
|
|
||||||
case claimsContextValidator:
|
|
||||||
err = c.Validate(ctx)
|
|
||||||
case *json.RawMessage:
|
|
||||||
// if the data type is raw message (json []byte)
|
|
||||||
// then it should contain exp (and iat and nbf) keys.
|
|
||||||
// Unmarshal raw message to validate against.
|
|
||||||
v := new(Claims)
|
|
||||||
err = json.Unmarshal(*c, v)
|
|
||||||
if err == nil {
|
|
||||||
return validateClaims(ctx, v)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
ctx.Values().Set(needsValidationContextKey, struct{}{})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
switch err {
|
|
||||||
case jwt.ErrExpired:
|
|
||||||
return ErrExpired
|
|
||||||
case jwt.ErrNotValidYet:
|
|
||||||
return ErrNotValidYet
|
|
||||||
case jwt.ErrIssuedInTheFuture:
|
|
||||||
return ErrIssuedInTheFuture
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyToken verifies (and decrypts) the request token,
|
|
||||||
// it also validates and binds the parsed token's claims to the "claimsPtr" (destination).
|
|
||||||
// It does return a nil error on success.
|
|
||||||
func (j *JWT) VerifyToken(ctx *context.Context, claimsPtr interface{}) error {
|
|
||||||
var token string
|
|
||||||
|
|
||||||
for _, extract := range j.Extractors {
|
|
||||||
if token = extract(ctx); token != "" {
|
|
||||||
break // ok we found it.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return j.VerifyTokenString(ctx, token, claimsPtr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyTokenString verifies and unmarshals an extracted token to "claimsPtr" destination.
|
|
||||||
// The Context is required when the claims validator needs it, otherwise can be nil.
|
|
||||||
func (j *JWT) VerifyTokenString(ctx *context.Context, token string, claimsPtr interface{}) error {
|
|
||||||
if token == "" {
|
|
||||||
return ErrMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
parsedToken *jwt.JSONWebToken
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if j.DecriptionKey != nil {
|
|
||||||
t, cerr := jwt.ParseSignedAndEncrypted(token)
|
|
||||||
if cerr != nil {
|
|
||||||
return cerr
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedToken, err = t.Decrypt(j.DecriptionKey)
|
|
||||||
} else {
|
|
||||||
parsedToken, err = jwt.ParseSigned(token)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = parsedToken.Claims(j.VerificationKey, claimsPtr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return validateClaims(ctx, claimsPtr)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ClaimsContextKey is the context key which the jwt claims are stored from the `Verify` method.
|
|
||||||
ClaimsContextKey = "iris.jwt.claims"
|
|
||||||
needsValidationContextKey = "iris.jwt.claims.unvalidated"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify is a middleware. It verifies and optionally decrypts an incoming request token.
|
|
||||||
// It does write a 401 unauthorized status code if verification or decryption failed.
|
|
||||||
// It calls the `ctx.Next` on verified requests.
|
|
||||||
//
|
|
||||||
// See `VerifyToken` instead to verify, decrypt, validate and acquire the claims at once.
|
|
||||||
//
|
|
||||||
// A call of `ReadClaims` is required to validate and acquire the jwt claims
|
|
||||||
// on the next request.
|
|
||||||
func (j *JWT) Verify(ctx *context.Context) {
|
|
||||||
var raw json.RawMessage
|
|
||||||
if err := j.VerifyToken(ctx, &raw); err != nil {
|
|
||||||
ctx.StopWithStatus(401)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Values().Set(ClaimsContextKey, raw)
|
|
||||||
ctx.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadClaims binds the "claimsPtr" (destination)
|
|
||||||
// to the verified (and decrypted) claims.
|
|
||||||
// The `Verify` method should be called first (registered as middleware).
|
|
||||||
func ReadClaims(ctx *context.Context, claimsPtr interface{}) error {
|
|
||||||
v := ctx.Values().Get(ClaimsContextKey)
|
|
||||||
if v == nil {
|
|
||||||
return ErrMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, ok := v.(json.RawMessage)
|
|
||||||
if !ok {
|
|
||||||
return ErrMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.Unmarshal(raw, claimsPtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsValidated(ctx) {
|
|
||||||
// If already validated on `Verify/VerifyToken`
|
|
||||||
// then no need to perform the check again.
|
|
||||||
ctx.Values().Remove(needsValidationContextKey)
|
|
||||||
return validateClaims(ctx, claimsPtr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns and validates (if not already) the claims
|
|
||||||
// stored on request context's values storage.
|
|
||||||
//
|
|
||||||
// Should be used instead of the `ReadClaims` method when
|
|
||||||
// a custom verification middleware was registered (see the `Verify` method for an example).
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// j := jwt.New(...)
|
|
||||||
// [...]
|
|
||||||
// app.Use(func(ctx iris.Context) {
|
|
||||||
// var claims CustomClaims_or_jwt.Claims
|
|
||||||
// if err := j.VerifyToken(ctx, &claims); err != nil {
|
|
||||||
// ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// ctx.Values().Set(jwt.ClaimsContextKey, claims)
|
|
||||||
// ctx.Next()
|
|
||||||
// })
|
|
||||||
// [...]
|
|
||||||
// app.Post("/restricted", func(ctx iris.Context){
|
|
||||||
// v, err := jwt.Get(ctx)
|
|
||||||
// [handle error...]
|
|
||||||
// claims,ok := v.(CustomClaims_or_jwt.Claims)
|
|
||||||
// if !ok {
|
|
||||||
// [do you support more than one type of claims? Handle here]
|
|
||||||
// }
|
|
||||||
// [use claims...]
|
|
||||||
// })
|
|
||||||
func Get(ctx *context.Context) (interface{}, error) {
|
|
||||||
claims := ctx.Values().Get(ClaimsContextKey)
|
|
||||||
if claims == nil {
|
|
||||||
return nil, ErrMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsValidated(ctx) {
|
|
||||||
ctx.Values().Remove(needsValidationContextKey)
|
|
||||||
err := validateClaims(ctx, claims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// Package jwt_test contains simple Iris jwt tests. Most of the jwt functionality is already tested inside the jose package itself.
|
|
||||||
package jwt_test
|
package jwt_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -11,129 +10,56 @@ import (
|
||||||
"github.com/kataras/iris/v12/middleware/jwt"
|
"github.com/kataras/iris/v12/middleware/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type userClaims struct {
|
var testAlg, testSecret = jwt.HS256, []byte("sercrethatmaycontainch@r$")
|
||||||
jwt.Claims
|
|
||||||
Username string
|
type fooClaims struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const testMaxAge = 3 * time.Second
|
// The actual tests are inside the kataras/jwt repository.
|
||||||
|
// This runs simple checks of just the middleware part.
|
||||||
// Random RSA verification and encryption.
|
func TestJWT(t *testing.T) {
|
||||||
func TestRSA(t *testing.T) {
|
|
||||||
j := jwt.RSA(testMaxAge)
|
|
||||||
t.Cleanup(func() {
|
|
||||||
os.Remove(jwt.DefaultSignFilename)
|
|
||||||
os.Remove(jwt.DefaultEncFilename)
|
|
||||||
})
|
|
||||||
testWriteVerifyToken(t, j)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMAC verification and encryption.
|
|
||||||
func TestHMAC(t *testing.T) {
|
|
||||||
j := jwt.HMAC(testMaxAge, "secret", "itsa16bytesecret")
|
|
||||||
testWriteVerifyToken(t, j)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNew_HMAC(t *testing.T) {
|
|
||||||
j, err := jwt.New(testMaxAge, jwt.HS256, []byte("secret"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = j.WithEncryption(jwt.A128GCM, jwt.DIRECT, []byte("itsa16bytesecret"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testWriteVerifyToken(t, j)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMAC verification only (unecrypted).
|
|
||||||
func TestVerify(t *testing.T) {
|
|
||||||
j, err := jwt.New(testMaxAge, jwt.HS256, []byte("another secret"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
testWriteVerifyToken(t, j)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testWriteVerifyToken(t *testing.T, j *jwt.JWT) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
j.Extractors = append(j.Extractors, jwt.FromJSON("access_token"))
|
|
||||||
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
|
|
||||||
expectedClaims := userClaims{
|
|
||||||
Claims: j.Expiry(standardClaims),
|
|
||||||
Username: "kataras",
|
|
||||||
}
|
|
||||||
|
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
app.Get("/auth", func(ctx iris.Context) {
|
|
||||||
j.WriteToken(ctx, expectedClaims)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Post("/restricted", func(ctx iris.Context) {
|
signer := jwt.NewSigner(testAlg, testSecret, 3*time.Second)
|
||||||
var claims userClaims
|
app.Get("/", func(ctx iris.Context) {
|
||||||
if err := j.VerifyToken(ctx, &claims); err != nil {
|
claims := fooClaims{Foo: "bar"}
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
token, err := signer.Sign(claims)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(claims)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Post("/restricted_middleware_readclaims", j.Verify, func(ctx iris.Context) {
|
|
||||||
var claims userClaims
|
|
||||||
if err := jwt.ReadClaims(ctx, &claims); err != nil {
|
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(claims)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Post("/restricted_middleware_get", j.Verify, func(ctx iris.Context) {
|
|
||||||
claims, err := jwt.Get(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.Write(token)
|
||||||
|
})
|
||||||
|
|
||||||
ctx.JSON(claims)
|
verifier := jwt.NewVerifier(testAlg, testSecret)
|
||||||
|
verifier.ErrorHandler = func(ctx iris.Context, err error) { // app.OnErrorCode(401, ...)
|
||||||
|
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
|
}
|
||||||
|
middleware := verifier.Verify(func() interface{} { return new(fooClaims) })
|
||||||
|
app.Get("/protected", middleware, func(ctx iris.Context) {
|
||||||
|
claims := jwt.Get(ctx).(*fooClaims)
|
||||||
|
ctx.WriteString(claims.Foo)
|
||||||
})
|
})
|
||||||
|
|
||||||
e := httptest.New(t, app)
|
e := httptest.New(t, app)
|
||||||
|
|
||||||
// Get token.
|
// Get generated token.
|
||||||
rawToken := e.GET("/auth").Expect().Status(httptest.StatusOK).Body().Raw()
|
token := e.GET("/").Expect().Status(iris.StatusOK).Body().Raw()
|
||||||
if rawToken == "" {
|
// Test Header.
|
||||||
t.Fatalf("empty token")
|
headerValue := fmt.Sprintf("Bearer %s", token)
|
||||||
}
|
e.GET("/protected").WithHeader("Authorization", headerValue).Expect().
|
||||||
|
Status(iris.StatusOK).Body().Equal("bar")
|
||||||
|
// Test URL query.
|
||||||
|
e.GET("/protected").WithQuery("token", token).Expect().
|
||||||
|
Status(iris.StatusOK).Body().Equal("bar")
|
||||||
|
|
||||||
restrictedPaths := [...]string{"/restricted", "/restricted_middleware_readclaims", "/restricted_middleware_get"}
|
// Test unauthorized.
|
||||||
|
e.GET("/protected").Expect().Status(iris.StatusUnauthorized)
|
||||||
now := time.Now()
|
e.GET("/protected").WithHeader("Authorization", "missing bearer").Expect().Status(iris.StatusUnauthorized)
|
||||||
for _, path := range restrictedPaths {
|
e.GET("/protected").WithQuery("token", "invalid_token").Expect().Status(iris.StatusUnauthorized)
|
||||||
// Authorization Header.
|
// Test expired (note checks happen based on second round).
|
||||||
e.POST(path).WithHeader("Authorization", "Bearer "+rawToken).Expect().
|
time.Sleep(5 * time.Second)
|
||||||
Status(httptest.StatusOK).JSON().Equal(expectedClaims)
|
e.GET("/protected").WithHeader("Authorization", headerValue).Expect().
|
||||||
|
Status(iris.StatusUnauthorized).Body().Equal("token expired")
|
||||||
// URL Query.
|
|
||||||
e.POST(path).WithQuery("token", rawToken).Expect().
|
|
||||||
Status(httptest.StatusOK).JSON().Equal(expectedClaims)
|
|
||||||
|
|
||||||
// JSON Body.
|
|
||||||
e.POST(path).WithJSON(iris.Map{"access_token": rawToken}).Expect().
|
|
||||||
Status(httptest.StatusOK).JSON().Equal(expectedClaims)
|
|
||||||
|
|
||||||
// Missing "Bearer".
|
|
||||||
e.POST(path).WithHeader("Authorization", rawToken).Expect().
|
|
||||||
Status(httptest.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
expireRemDur := testMaxAge - time.Since(now)
|
|
||||||
|
|
||||||
// Expiration.
|
|
||||||
time.Sleep(expireRemDur /* -end */)
|
|
||||||
for _, path := range restrictedPaths {
|
|
||||||
e.POST(path).WithQuery("token", rawToken).Expect().Status(httptest.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
package jwt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoadRSA tries to read RSA Private Key from "fname" system file,
|
|
||||||
// if does not exist then it generates a new random one based on "bits" (e.g. 2048, 4096)
|
|
||||||
// and exports it to a new "fname" file.
|
|
||||||
func LoadRSA(fname string, bits int) (key *rsa.PrivateKey, err error) {
|
|
||||||
exists := fileExists(fname)
|
|
||||||
if exists {
|
|
||||||
key, err = importFromFile(fname)
|
|
||||||
} else {
|
|
||||||
key, err = rsa.GenerateKey(rand.Reader, bits)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
err = exportToFile(key, fname)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func exportToFile(key *rsa.PrivateKey, filename string) error {
|
|
||||||
b := x509.MarshalPKCS1PrivateKey(key)
|
|
||||||
encoded := pem.EncodeToMemory(
|
|
||||||
&pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: b,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return ioutil.WriteFile(filename, encoded, 0600)
|
|
||||||
}
|
|
||||||
|
|
||||||
func importFromFile(filename string) (*rsa.PrivateKey, error) {
|
|
||||||
b, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseRSAPrivateKey(b, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileExists(filename string) bool {
|
|
||||||
info, err := os.Stat(filename)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !info.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNotPEM is an error type of the `ParseXXX` function(s) fired
|
|
||||||
// when the data are not PEM-encoded.
|
|
||||||
ErrNotPEM = errors.New("key must be PEM encoded")
|
|
||||||
// ErrInvalidKey is an error type of the `ParseXXX` function(s) fired
|
|
||||||
// when the contents are not type of rsa private key.
|
|
||||||
ErrInvalidKey = errors.New("key is not of type *rsa.PrivateKey")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseRSAPrivateKey encodes a PEM-encoded PKCS1 or PKCS8 private key protected with a password.
|
|
||||||
func ParseRSAPrivateKey(key, password []byte) (*rsa.PrivateKey, error) {
|
|
||||||
block, _ := pem.Decode(key)
|
|
||||||
if block == nil {
|
|
||||||
return nil, ErrNotPEM
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
parsedKey interface{}
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
var blockDecrypted []byte
|
|
||||||
if len(password) > 0 {
|
|
||||||
if blockDecrypted, err = x509.DecryptPEMBlock(block, password); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
blockDecrypted = block.Bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedKey, err = x509.ParsePKCS1PrivateKey(blockDecrypted); err != nil {
|
|
||||||
if parsedKey, err = x509.ParsePKCS8PrivateKey(blockDecrypted); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey, ok := parsedKey.(*rsa.PrivateKey)
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrInvalidKey
|
|
||||||
}
|
|
||||||
|
|
||||||
return privateKey, nil
|
|
||||||
}
|
|
99
middleware/jwt/signer.go
Normal file
99
middleware/jwt/signer.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Signer holds common options to sign and generate a token.
|
||||||
|
// Its Sign method can be used to generate a token which can be sent to the client.
|
||||||
|
// Its NewTokenPair can be used to construct a token pair (access_token, refresh_token).
|
||||||
|
//
|
||||||
|
// It does not support JWE, JWK.
|
||||||
|
type Signer struct {
|
||||||
|
Alg Alg
|
||||||
|
Key interface{}
|
||||||
|
|
||||||
|
// MaxAge to set "exp" and "iat".
|
||||||
|
// Recommended value for access tokens: 15 minutes.
|
||||||
|
// Defaults to 0, no limit.
|
||||||
|
MaxAge time.Duration
|
||||||
|
Options []SignOption
|
||||||
|
|
||||||
|
Encrypt func([]byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigner accepts the signature algorithm among with its (private or shared) key
|
||||||
|
// and the max life time duration of generated tokens and returns a JWT signer.
|
||||||
|
// See its Sign method.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// signer := NewSigner(HS256, secret, 15*time.Minute)
|
||||||
|
// token, err := signer.Sign(userClaims{Username: "kataras"})
|
||||||
|
func NewSigner(signatureAlg Alg, signatureKey interface{}, maxAge time.Duration) *Signer {
|
||||||
|
if signatureAlg == HS256 {
|
||||||
|
// A tiny helper if the end-developer uses string instead of []byte for hmac keys.
|
||||||
|
if k, ok := signatureKey.(string); ok {
|
||||||
|
signatureKey = []byte(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Signer{
|
||||||
|
Alg: signatureAlg,
|
||||||
|
Key: signatureKey,
|
||||||
|
MaxAge: maxAge,
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxAge > 0 {
|
||||||
|
s.Options = []SignOption{MaxAge(maxAge)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEncryption enables AES-GCM payload-only decryption.
|
||||||
|
func (s *Signer) WithEncryption(key, additionalData []byte) *Signer {
|
||||||
|
encrypt, _, err := jwt.GCM(key, additionalData)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // important error before serve, stop everything.
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Encrypt = encrypt
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign generates a new token based on the given "claims" which is valid up to "s.MaxAge".
|
||||||
|
func (s *Signer) Sign(claims interface{}, opts ...SignOption) ([]byte, error) {
|
||||||
|
if len(opts) > 0 {
|
||||||
|
opts = append(opts, s.Options...)
|
||||||
|
} else {
|
||||||
|
opts = s.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignEncrypted(s.Alg, s.Key, s.Encrypt, claims, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTokenPair accepts the access and refresh claims plus the life time duration for the refresh token
|
||||||
|
// and generates a new token pair which can be sent to the client.
|
||||||
|
// The same token pair can be json-decoded.
|
||||||
|
func (s *Signer) NewTokenPair(accessClaims interface{}, refreshClaims interface{}, refreshMaxAge time.Duration, accessOpts ...SignOption) (TokenPair, error) {
|
||||||
|
if refreshMaxAge <= s.MaxAge {
|
||||||
|
return TokenPair{}, fmt.Errorf("refresh max age should be bigger than access token's one[%d - %d]", refreshMaxAge, s.MaxAge)
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := s.Sign(accessClaims, accessOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return TokenPair{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := Sign(s.Alg, s.Key, refreshClaims, MaxAge(refreshMaxAge))
|
||||||
|
if err != nil {
|
||||||
|
return TokenPair{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenPair := jwt.NewTokenPair(accessToken, refreshToken)
|
||||||
|
return tokenPair, nil
|
||||||
|
}
|
263
middleware/jwt/verifier.go
Normal file
263
middleware/jwt/verifier.go
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
|
||||||
|
"github.com/kataras/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
claimsContextKey = "iris.jwt.claims"
|
||||||
|
verifiedTokenContextKey = "iris.jwt.token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get returns the claims decoded by a verifier.
|
||||||
|
func Get(ctx *context.Context) interface{} {
|
||||||
|
if v := ctx.Values().Get(claimsContextKey); v != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerifiedToken returns the verified token structure
|
||||||
|
// which holds information about the decoded token
|
||||||
|
// and its standard claims.
|
||||||
|
func GetVerifiedToken(ctx *context.Context) *VerifiedToken {
|
||||||
|
if v := ctx.Values().Get(verifiedTokenContextKey); v != nil {
|
||||||
|
if tok, ok := v.(*VerifiedToken); ok {
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifier holds common options to verify an incoming token.
|
||||||
|
// Its Verify method can be used as a middleware to allow authorized clients to access an API.
|
||||||
|
//
|
||||||
|
// It does not support JWE, JWK.
|
||||||
|
type Verifier struct {
|
||||||
|
Alg Alg
|
||||||
|
Key interface{}
|
||||||
|
|
||||||
|
Decrypt func([]byte) ([]byte, error)
|
||||||
|
|
||||||
|
Extractors []TokenExtractor
|
||||||
|
Blocklist Blocklist
|
||||||
|
Validators []TokenValidator
|
||||||
|
|
||||||
|
ErrorHandler func(ctx *context.Context, err error)
|
||||||
|
// DisableContextUser disables the registration of the claims as context User.
|
||||||
|
DisableContextUser bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVerifier accepts the algorithm for the token's signature among with its (public) key
|
||||||
|
// and optionally some token validators for all verify middlewares that may initialized under this Verifier.
|
||||||
|
// See its Verify method.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// verifier := NewVerifier(HS256, secret)
|
||||||
|
// OR
|
||||||
|
// verifier := NewVerifier(HS256, secret, Expected{Issuer: "my-app"})
|
||||||
|
//
|
||||||
|
// claimsGetter := func() interface{} { return new(userClaims) }
|
||||||
|
// middleware := verifier.Verify(claimsGetter)
|
||||||
|
// OR
|
||||||
|
// middleware := verifier.Verify(claimsGetter, Expected{Issuer: "my-app"})
|
||||||
|
//
|
||||||
|
// Register the middleware, e.g.
|
||||||
|
// app.Use(middleware)
|
||||||
|
//
|
||||||
|
// Get the claims:
|
||||||
|
// claims := jwt.Get(ctx).(*userClaims)
|
||||||
|
// username := claims.Username
|
||||||
|
//
|
||||||
|
// Get the context user:
|
||||||
|
// username, err := ctx.User().GetUsername()
|
||||||
|
func NewVerifier(signatureAlg Alg, signatureKey interface{}, validators ...TokenValidator) *Verifier {
|
||||||
|
if signatureAlg == HS256 {
|
||||||
|
// A tiny helper if the end-developer uses string instead of []byte for hmac keys.
|
||||||
|
if k, ok := signatureKey.(string); ok {
|
||||||
|
signatureKey = []byte(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Verifier{
|
||||||
|
Alg: signatureAlg,
|
||||||
|
Key: signatureKey,
|
||||||
|
Extractors: []TokenExtractor{FromHeader, FromQuery},
|
||||||
|
ErrorHandler: func(ctx *context.Context, err error) {
|
||||||
|
ctx.StopWithError(401, context.PrivateError(err))
|
||||||
|
},
|
||||||
|
Validators: validators,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDecryption enables AES-GCM payload-only encryption.
|
||||||
|
func (v *Verifier) WithDecryption(key, additionalData []byte) *Verifier {
|
||||||
|
_, decrypt, err := jwt.GCM(key, additionalData)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // important error before serve, stop everything.
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Decrypt = decrypt
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDefaultBlocklist attaches an in-memory blocklist storage
|
||||||
|
// to invalidate tokens through server-side.
|
||||||
|
// To invalidate a token simply call the Context.Logout method.
|
||||||
|
func (v *Verifier) WithDefaultBlocklist() *Verifier {
|
||||||
|
v.Blocklist = jwt.NewBlocklist(30 * time.Minute)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Verifier) invalidate(ctx *context.Context) {
|
||||||
|
if verifiedToken := GetVerifiedToken(ctx); verifiedToken != nil {
|
||||||
|
v.Blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
|
||||||
|
ctx.Values().Remove(claimsContextKey)
|
||||||
|
ctx.Values().Remove(verifiedTokenContextKey)
|
||||||
|
ctx.SetUser(nil)
|
||||||
|
ctx.SetLogoutFunc(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestToken extracts the token from the request.
|
||||||
|
func (v *Verifier) RequestToken(ctx *context.Context) (token string) {
|
||||||
|
for _, extract := range v.Extractors {
|
||||||
|
if token = extract(ctx); token != "" {
|
||||||
|
break // ok we found it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ClaimsValidator is a special interface which, if the destination claims
|
||||||
|
// implements it then the verifier runs its Validate method before return.
|
||||||
|
ClaimsValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimsContextValidator same as ClaimsValidator but it accepts
|
||||||
|
// a request context which can be used for further checks before
|
||||||
|
// validating the incoming token's claims.
|
||||||
|
ClaimsContextValidator interface {
|
||||||
|
Validate(*context.Context) error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifyToken simply verifies the given "token" and validates its standard claims (such as expiration).
|
||||||
|
// Returns a structure which holds the token's information. See the Verify method instead.
|
||||||
|
func (v *Verifier) VerifyToken(token []byte, validators ...TokenValidator) (*VerifiedToken, error) {
|
||||||
|
return jwt.VerifyEncrypted(v.Alg, v.Key, v.Decrypt, token, validators...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify is the most important piece of code inside the Verifier.
|
||||||
|
// It accepts the "claimsType" function which should return a pointer to a custom structure
|
||||||
|
// which the token's decode claims valuee will be binded and validated to.
|
||||||
|
// Returns a common Iris handler which can be used as a middleware to protect an API
|
||||||
|
// from unauthorized client requests. After this, the route handlers can access the claims
|
||||||
|
// through the jwt.Get package-level function.
|
||||||
|
//
|
||||||
|
// By default it extracts the token from Authorization: Bearer $token header and ?token URL Query parameter,
|
||||||
|
// to change that behavior modify its Extractors field.
|
||||||
|
//
|
||||||
|
// By default a 401 status code with a generic message will be sent to the client on
|
||||||
|
// a token verification or claims validation failure, to change that behavior
|
||||||
|
// modify its ErrorHandler field or register OnErrorCode(401, errorHandler) and
|
||||||
|
// retrieve the error through Context.GetErr method.
|
||||||
|
//
|
||||||
|
// If the "claimsType" is nil then only the jwt.GetVerifiedToken is available
|
||||||
|
// and the handler should unmarshal the payload to extract the claims by itself.
|
||||||
|
func (v *Verifier) Verify(claimsType func() interface{}, validators ...TokenValidator) context.Handler {
|
||||||
|
unmarshal := jwt.Unmarshal
|
||||||
|
if claimsType != nil {
|
||||||
|
c := claimsType()
|
||||||
|
if hasRequired(c) {
|
||||||
|
unmarshal = jwt.UnmarshalWithRequired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Blocklist != nil {
|
||||||
|
// If blocklist implements the connect interface,
|
||||||
|
// try to connect if it's not already connected manually by developer,
|
||||||
|
// if errored then just return a handler which will fire this error every single time.
|
||||||
|
if bc, ok := v.Blocklist.(blocklistConnect); ok {
|
||||||
|
if !bc.IsConnected() {
|
||||||
|
if err := bc.Connect(); err != nil {
|
||||||
|
return func(ctx *context.Context) {
|
||||||
|
v.ErrorHandler(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validators = append([]TokenValidator{v.Blocklist}, append(v.Validators, validators...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(ctx *context.Context) {
|
||||||
|
token := []byte(v.RequestToken(ctx))
|
||||||
|
verifiedToken, err := v.VerifyToken(token, validators...)
|
||||||
|
if err != nil {
|
||||||
|
v.ErrorHandler(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claimsType != nil {
|
||||||
|
dest := claimsType()
|
||||||
|
if err = unmarshal(verifiedToken.Payload, dest); err != nil {
|
||||||
|
v.ErrorHandler(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validator, ok := dest.(ClaimsValidator); ok {
|
||||||
|
if err = validator.Validate(); err != nil {
|
||||||
|
v.ErrorHandler(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if contextValidator, ok := dest.(ClaimsContextValidator); ok {
|
||||||
|
if err = contextValidator.Validate(ctx); err != nil {
|
||||||
|
v.ErrorHandler(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !v.DisableContextUser {
|
||||||
|
ctx.SetUser(dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Values().Set(claimsContextKey, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Blocklist != nil {
|
||||||
|
ctx.SetLogoutFunc(v.invalidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Values().Set(verifiedTokenContextKey, verifiedToken)
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasRequired(i interface{}) bool {
|
||||||
|
val := reflect.Indirect(reflect.ValueOf(i))
|
||||||
|
typ := val.Type()
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
if jwt.HasRequiredJSONTag(field) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ const (
|
||||||
DefaultRedisNetwork = "tcp"
|
DefaultRedisNetwork = "tcp"
|
||||||
// DefaultRedisAddr the redis address option, "127.0.0.1:6379".
|
// DefaultRedisAddr the redis address option, "127.0.0.1:6379".
|
||||||
DefaultRedisAddr = "127.0.0.1:6379"
|
DefaultRedisAddr = "127.0.0.1:6379"
|
||||||
// DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second
|
// DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second.
|
||||||
DefaultRedisTimeout = time.Duration(30) * time.Second
|
DefaultRedisTimeout = time.Duration(30) * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user