mirror of
https://github.com/kataras/iris.git
synced 2026-01-09 13:05:56 +00:00
As noticed in my previous commit, the existing jwt libraries added a lot of performance cost between jwt-featured requests and simple requests. That's why a new custom JWT parser was created. This commit adds our custom jwt parser as the underline token signer and verifier
This commit is contained in:
@@ -4,41 +4,75 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
"github.com/kataras/jwt"
|
||||
)
|
||||
|
||||
/*
|
||||
Learn how to use any JWT 3rd-party package with Iris.
|
||||
In this example we use the kataras/jwt one.
|
||||
|
||||
Install with:
|
||||
go get -u github.com/kataras/jwt
|
||||
|
||||
Documentation:
|
||||
https://github.com/kataras/jwt#table-of-contents
|
||||
*/
|
||||
|
||||
// Replace with your own key and keep them secret.
|
||||
// The "signatureSharedKey" is used for the HMAC(HS256) signature algorithm.
|
||||
var signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
// With AES-GCM (128) encryption:
|
||||
// j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
||||
// Without extra encryption, just the sign key:
|
||||
j := jwt.HMAC(15*time.Minute, "secret")
|
||||
|
||||
app.Get("/", generateToken(j))
|
||||
app.Get("/protected", j.VerifyMap(), protected)
|
||||
app.Get("/", generateToken)
|
||||
app.Get("/protected", protected)
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func generateToken(j *jwt.JWT) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
token, err := j.Token(iris.Map{
|
||||
"foo": "bar",
|
||||
})
|
||||
if err != nil {
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
type fooClaims struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
|
||||
ctx.HTML(`Token: ` + token + `<br/><br/>
|
||||
<a href="/protected?token=` + token + `">/secured?token=` + token + `</a>`)
|
||||
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")
|
||||
|
||||
claims := jwt.Get(ctx).(iris.Map)
|
||||
// Decode the custom claims.
|
||||
var claims fooClaims
|
||||
verifiedToken.Claims(&claims)
|
||||
|
||||
ctx.Writef("foo=%s\n", claims["foo"])
|
||||
// Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp").
|
||||
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
|
||||
expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
|
||||
timeLeft := standardClaims.Timeleft()
|
||||
|
||||
ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
|
||||
}
|
||||
|
||||
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.WithGCM(encKey, nil)
|
||||
app.Get("/", generateToken(signer))
|
||||
|
||||
verifier := jwt.NewVerifier(jwt.HS256, sigKey)
|
||||
// Enable server-side token block feature (even before its expiration time):
|
||||
verifier.WithDefaultBlocklist()
|
||||
// Enable payload decryption with:
|
||||
// verifier.WithGCM(encKey, nil)
|
||||
verifyMiddleware := verifier.Verify(func() interface{} {
|
||||
return new(fooClaims)
|
||||
})
|
||||
|
||||
protectedAPI := app.Party("/protected")
|
||||
// Register the verify middleware to allow access only to authorized clients.
|
||||
protectedAPI.Use(verifyMiddleware)
|
||||
// ^ or UseRouter(verifyMiddleware) to disallow unauthorized http error handlers too.
|
||||
|
||||
protectedAPI.Get("/", protected)
|
||||
// Invalidate the token through server-side, even if it's not expired yet.
|
||||
protectedAPI.Get("/logout", logout)
|
||||
|
||||
// http://localhost:8080
|
||||
// http://localhost:8080/protected?token=$token (or Authorization: Bearer $token)
|
||||
// http://localhost:8080/protected/logout?token=$token
|
||||
// http://localhost:8080/protected?token=$token (401)
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func generateToken(signer *jwt.Signer) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
claims := fooClaims{Foo: "bar"}
|
||||
|
||||
token, err := signer.Sign(claims)
|
||||
if err != nil {
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Write(token)
|
||||
}
|
||||
}
|
||||
|
||||
func protected(ctx iris.Context) {
|
||||
// Get the verified and decoded claims.
|
||||
claims := jwt.Get(ctx).(*fooClaims)
|
||||
|
||||
// Optionally, get token information if you want to work with them.
|
||||
// Just an example on how you can retrieve all the standard claims (set by signer's max age, "exp").
|
||||
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
|
||||
expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
|
||||
timeLeft := standardClaims.Timeleft()
|
||||
|
||||
ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
|
||||
}
|
||||
|
||||
func logout(ctx iris.Context) {
|
||||
err := ctx.Logout()
|
||||
if err != nil {
|
||||
ctx.WriteString(err.Error())
|
||||
} else {
|
||||
ctx.Writef("token invalidated, a new token is required to access the protected API")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -1,368 +0,0 @@
|
||||
// Package main shows how you can use the Iris unique JWT middleware.
|
||||
// The file contains different kind of examples that all do the same job but,
|
||||
// depending on your code style and your application's requirements, you may choose one over other.
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
)
|
||||
|
||||
// Claims a custom claims structure.
|
||||
type Claims struct {
|
||||
// Optionally define JWT's "iss" (Issuer),
|
||||
// "sub" (Subject) and "aud" (Audience) for issuer and subject.
|
||||
// The JWT's "exp" (expiration) and "iat" (issued at) are automatically
|
||||
// set by the middleware.
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Audience []string `json:"aud"`
|
||||
/*
|
||||
Note that the above fields can be also extracted via:
|
||||
jwt.GetTokenInfo(ctx).Claims
|
||||
But in that example, we just showcase how these info can be embedded
|
||||
inside your own Go structure.
|
||||
*/
|
||||
|
||||
// Optionally define a "exp" (Expiry),
|
||||
// unlike the rest, this is unset on creation
|
||||
// (unless you want to override the middleware's max age option),
|
||||
// it's filled automatically by the JWT middleware
|
||||
// when the request token is verified.
|
||||
// See the POST /user route.
|
||||
Expiry *jwt.NumericDate `json:"exp"`
|
||||
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Get keys from system's environment variables
|
||||
// JWT_SECRET (for signing and verification) and JWT_SECRET_ENC(for encryption and decryption),
|
||||
// or defaults to "secret" and "itsa16bytesecret" respectfully.
|
||||
//
|
||||
// Use the `jwt.New` instead for more flexibility, if necessary.
|
||||
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
||||
|
||||
/*
|
||||
By default it extracts the token from url parameter "token={token}"
|
||||
and the Authorization Bearer {token} header.
|
||||
You can also take token from JSON body:
|
||||
j.Extractors = append(j.Extractors, jwt.FromJSON)
|
||||
*/
|
||||
|
||||
/* Optionally, enable block list to force-invalidate
|
||||
verified tokens even before their expiration time.
|
||||
This is useful when the client doesn't clear
|
||||
the token on a user logout by itself.
|
||||
|
||||
The duration argument clears any expired token on each every tick.
|
||||
There is a GC() method that can be manually called to clear expired blocked tokens
|
||||
from the memory.
|
||||
|
||||
j.Blocklist = jwt.NewBlocklist(30*time.Minute)
|
||||
OR NewBlocklistContext(stdContext, 30*time.Minute)
|
||||
|
||||
|
||||
To invalidate a verified token just call:
|
||||
j.Invalidate(ctx) inside a route handler.
|
||||
*/
|
||||
|
||||
app := iris.New()
|
||||
app.Logger().SetLevel("debug")
|
||||
|
||||
app.OnErrorCode(iris.StatusUnauthorized, func(ctx iris.Context) {
|
||||
// Note that, any error stored by an authentication
|
||||
// method in Iris is an iris.ErrPrivate.
|
||||
// Available jwt errors:
|
||||
// - ErrMissing
|
||||
// - ErrMissingKey
|
||||
// - ErrExpired
|
||||
// - ErrNotValidYet
|
||||
// - ErrIssuedInTheFuture
|
||||
// - ErrBlocked
|
||||
// An iris.ErrPrivate SHOULD never be displayed to the client as it is;
|
||||
// because it may contain critical security information about the server.
|
||||
//
|
||||
// Also keep in mind that JWT middleware logs verification errors to the
|
||||
// application's logger ("debug") so, normally you don't have to
|
||||
// bother showing the verification error to the browser/client.
|
||||
// However, you can retrieve that error and do what ever you feel right:
|
||||
if err := ctx.GetErr(); err != nil {
|
||||
// If we have an error stored,
|
||||
// (JWT middleware stores any verification errors to the Context),
|
||||
// set the error as response body,
|
||||
// which is the default behavior if that
|
||||
// wasn't an authentication error (as explained above)
|
||||
ctx.WriteString(err.Error())
|
||||
} else {
|
||||
// Else, the default behavior when no error was occured;
|
||||
// write the status text of the status code:
|
||||
ctx.WriteString(iris.StatusText(iris.StatusUnauthorized))
|
||||
}
|
||||
})
|
||||
|
||||
app.Get("/authenticate", func(ctx iris.Context) {
|
||||
claims := &Claims{
|
||||
Issuer: "server",
|
||||
Audience: []string{"user"},
|
||||
Username: "kataras",
|
||||
}
|
||||
|
||||
// WriteToken generates and sends the token to the client.
|
||||
// To generate a token use: tok, err := j.Token(claims)
|
||||
// then you can write it in any form you'd like.
|
||||
// The expiration JWT fields are automatically
|
||||
// set by the middleware, that means that your claims value
|
||||
// only needs to fill fields that your application specifically requires.
|
||||
j.WriteToken(ctx, claims)
|
||||
})
|
||||
|
||||
// Middleware + type-safe method,
|
||||
// useful in 99% of the cases, when your application
|
||||
// requires token verification under a whole path prefix, e.g. /protected:
|
||||
protectedAPI := app.Party("/protected")
|
||||
{
|
||||
protectedAPI.Use(j.Verify(func() interface{} {
|
||||
// Must return a pointer to a type.
|
||||
//
|
||||
// The Iris JWT implementation is very sophisticated.
|
||||
// We keep our claims in type-safe form.
|
||||
// However, you are free to use raw Go maps
|
||||
// (map[string]interface{} or iris.Map) too (example later on).
|
||||
//
|
||||
// Note that you can use the same "j" JWT instance
|
||||
// to serve different types of claims on other group of routes,
|
||||
// e.g. postRouter.Use(j.Verify(... return new(Post))).
|
||||
return new(Claims)
|
||||
}))
|
||||
|
||||
protectedAPI.Get("/", func(ctx iris.Context) {
|
||||
claims := jwt.Get(ctx).(*Claims)
|
||||
// All fields parsed from token are set to the claims,
|
||||
// including the Expiry (if defined).
|
||||
ctx.Writef("Username: %s\nExpires at: %s\nAudience: %s",
|
||||
claims.Username, claims.Expiry.Time(), claims.Audience)
|
||||
})
|
||||
}
|
||||
|
||||
// Verify token inside a handler method,
|
||||
// useful when you just need to verify a token on a single spot:
|
||||
app.Get("/inline", func(ctx iris.Context) {
|
||||
var claims Claims
|
||||
_, err := j.VerifyToken(ctx, &claims)
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef("Username: %s\nExpires at: %s\n",
|
||||
claims.Username, claims.Expiry.Time())
|
||||
})
|
||||
|
||||
// Use a common map as claims method,
|
||||
// not recommended, as we support typed claims but
|
||||
// you can do it:
|
||||
app.Get("/map/authenticate", func(ctx iris.Context) {
|
||||
claims := map[string]interface{}{ // or iris.Map for shortcut.
|
||||
"username": "kataras",
|
||||
}
|
||||
|
||||
j.WriteToken(ctx, claims)
|
||||
})
|
||||
|
||||
app.Get("/map/verify/middleware", j.Verify(func() interface{} {
|
||||
return &iris.Map{} // or &map[string]interface{}{}
|
||||
}), func(ctx iris.Context) {
|
||||
claims := jwt.Get(ctx).(iris.Map)
|
||||
// The Get method will unwrap the *iris.Map for you,
|
||||
// so its values are directly accessible:
|
||||
ctx.Writef("Username: %s\nExpires at: %s\n",
|
||||
claims["username"], claims["exp"].(*jwt.NumericDate).Time())
|
||||
})
|
||||
|
||||
app.Get("/map/verify", func(ctx iris.Context) {
|
||||
claims := make(iris.Map) // or make(map[string]interface{})
|
||||
|
||||
tokenInfo, err := j.VerifyToken(ctx, &claims)
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef("Username: %s\nExpires at: %s\n",
|
||||
claims["username"], tokenInfo.Claims.Expiry.Time()) /* the claims["exp"] is also set. */
|
||||
})
|
||||
|
||||
// Use the new Context.User() to retrieve the verified client method:
|
||||
// 1. Create a go stuct that implements the context.User interface:
|
||||
app.Get("/users/authenticate", func(ctx iris.Context) {
|
||||
user := &User{Username: "kataras"}
|
||||
j.WriteToken(ctx, user)
|
||||
})
|
||||
usersAPI := app.Party("/users")
|
||||
{
|
||||
usersAPI.Use(j.Verify(func() interface{} {
|
||||
return new(User)
|
||||
}))
|
||||
|
||||
usersAPI.Get("/", func(ctx iris.Context) {
|
||||
user := ctx.User()
|
||||
userToken, _ := user.GetToken()
|
||||
/*
|
||||
You can also cast it to the underline implementation
|
||||
and work with its fields:
|
||||
expires := user.(*User).Expiry.Time()
|
||||
*/
|
||||
// OR use the GetTokenInfo to get the parsed token information:
|
||||
expires := jwt.GetTokenInfo(ctx).Claims.Expiry.Time()
|
||||
lifetime := expires.Sub(time.Now()) // remeaning time to be expired.
|
||||
|
||||
ctx.Writef("Username: %s\nAuthenticated at: %s\nLifetime: %s\nToken: %s\n",
|
||||
user.GetUsername(), user.GetAuthorizedAt(), lifetime, userToken)
|
||||
})
|
||||
}
|
||||
|
||||
// http://localhost:8080/authenticate
|
||||
// http://localhost:8080/protected?token={token}
|
||||
// http://localhost:8080/inline?token={token}
|
||||
//
|
||||
// http://localhost:8080/map/authenticate
|
||||
// http://localhost:8080/map/verify?token={token}
|
||||
// http://localhost:8080/map/verify/middleware?token={token}
|
||||
//
|
||||
// http://localhost:8080/users/authenticate
|
||||
// http://localhost:8080/users?token={token}
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
// User is a custom implementation of the Iris Context User interface.
|
||||
// Optionally, for JWT, you can also implement
|
||||
// the SetToken(tok string) and
|
||||
// Validate(ctx iris.Context, claims jwt.Claims, e jwt.Expected) error
|
||||
// methods to set a token and add custom validation
|
||||
// to a User value parsed from a token.
|
||||
type User struct {
|
||||
iris.User
|
||||
Username string `json:"username"`
|
||||
|
||||
// Optionally, declare some JWT fields,
|
||||
// they are automatically filled by the middleware itself.
|
||||
IssuedAt *jwt.NumericDate `json:"iat"`
|
||||
Expiry *jwt.NumericDate `json:"exp"`
|
||||
Token string `json:"-"`
|
||||
}
|
||||
|
||||
// GetUsername returns the Username.
|
||||
// Look the iris/context.SimpleUser type
|
||||
// for all the methods you can implement.
|
||||
func (u *User) GetUsername() string {
|
||||
return u.Username
|
||||
}
|
||||
|
||||
// GetAuthorizedAt returns the IssuedAt time.
|
||||
// This and the Get/SetToken methods showcase how you can map JWT standard fields
|
||||
// to an Iris Context User.
|
||||
func (u *User) GetAuthorizedAt() time.Time {
|
||||
return u.IssuedAt.Time()
|
||||
}
|
||||
|
||||
// GetToken is a User interface method.
|
||||
func (u *User) GetToken() (string, error) {
|
||||
return u.Token, nil
|
||||
}
|
||||
|
||||
// SetToken is a special jwt.TokenSetter interface which is
|
||||
// called automatically when a token is parsed to this User value.
|
||||
func (u *User) SetToken(tok string) {
|
||||
u.Token = tok
|
||||
}
|
||||
|
||||
/*
|
||||
func default_RSA_Example() {
|
||||
j := jwt.RSA(15*time.Minute)
|
||||
}
|
||||
|
||||
Same as:
|
||||
|
||||
func load_File_Or_Generate_RSA_Example() {
|
||||
signKey, err := jwt.LoadRSA("jwt_sign.key", 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
j, err := jwt.New(15*time.Minute, jwt.RS256, signKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
encKey, err := jwt.LoadRSA("jwt_enc.key", 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = j.WithEncryption(jwt.A128CBCHS256, jwt.RSA15, encKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func hmac_Example() {
|
||||
// hmac
|
||||
key := []byte("secret")
|
||||
j, err := jwt.New(15*time.Minute, jwt.HS256, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// OPTIONAL encryption:
|
||||
encryptionKey := []byte("itsa16bytesecret")
|
||||
err = j.WithEncryption(jwt.A128GCM, jwt.DIRECT, encryptionKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func load_From_File_With_Password_Example() {
|
||||
b, err := ioutil.ReadFile("./rsa_password_protected.key")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
signKey,err := jwt.ParseRSAPrivateKey(b, []byte("pass"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
j, err := jwt.New(15*time.Minute, jwt.RS256, signKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func generate_RSA_Example() {
|
||||
signKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
encryptionKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
j, err := jwt.New(15*time.Minute, jwt.RS512, signKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = j.WithEncryption(jwt.A128CBCHS256, jwt.RSA15, encryptionKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -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-----
|
||||
@@ -1,57 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
)
|
||||
|
||||
const (
|
||||
accessTokenMaxAge = 10 * time.Minute
|
||||
refreshTokenMaxAge = time.Hour
|
||||
)
|
||||
|
||||
var (
|
||||
privateKey, publicKey = jwt.MustLoadRSA("rsa_private_key.pem", "rsa_public_key.pem")
|
||||
|
||||
signer = jwt.NewSigner(jwt.RS256, privateKey, accessTokenMaxAge)
|
||||
verifier = jwt.NewVerifier(jwt.RS256, publicKey)
|
||||
)
|
||||
|
||||
// UserClaims a custom access claims structure.
|
||||
type UserClaims struct {
|
||||
// In order to separate refresh and access tokens on validation level:
|
||||
// - Set a different Issuer, with a field of: Issuer string `json:"iss"`
|
||||
// - Set the Iris JWT's json tag option "required" on an access token field,
|
||||
// e.g. Username string `json:"username,required"`
|
||||
// - Let the middleware validate the correct one based on the given MaxAge,
|
||||
// which should be different between refresh and max age (refersh should be bigger)
|
||||
// by setting the `jwt.ExpectRefreshToken` on Verify/VerifyToken/VerifyTokenString
|
||||
// (see `refreshToken` function below)
|
||||
ID string `json:"user_id"`
|
||||
ID string `json:"user_id"`
|
||||
// Do: `json:"username,required"` to have this field required
|
||||
// or see the Validate method below instead.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// Validate completes the middleware's custom ClaimsValidator.
|
||||
// It will not accept a token which its claims missing the username field
|
||||
// (useful to not accept refresh tokens generated by the same algorithm).
|
||||
func (u *UserClaims) Validate() error {
|
||||
if u.Username == "" {
|
||||
return fmt.Errorf("username field is missing")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// For refresh token, we will just use the jwt.Claims
|
||||
// structure which contains the standard JWT fields.
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized)
|
||||
|
||||
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
||||
|
||||
app.Get("/authenticate", func(ctx iris.Context) {
|
||||
generateTokenPair(ctx, j)
|
||||
})
|
||||
|
||||
app.Get("/refresh", func(ctx iris.Context) {
|
||||
refreshToken(ctx, j)
|
||||
})
|
||||
app.Get("/authenticate", generateTokenPair)
|
||||
app.Get("/refresh", refreshToken)
|
||||
|
||||
protectedAPI := app.Party("/protected")
|
||||
{
|
||||
protectedAPI.Use(j.Verify(func() interface{} {
|
||||
verifyMiddleware := verifier.Verify(func() interface{} {
|
||||
return new(UserClaims)
|
||||
})) // OR j.VerifyToken(ctx, &claims, jwt.MeetRequirements(&UserClaims{}))
|
||||
})
|
||||
|
||||
protectedAPI.Use(verifyMiddleware)
|
||||
|
||||
protectedAPI.Get("/", func(ctx iris.Context) {
|
||||
// Get token info, even if our UserClaims does not embed those
|
||||
// through GetTokenInfo:
|
||||
expiresAt := jwt.GetTokenInfo(ctx).Claims.Expiry.Time()
|
||||
// Get your custom JWT claims through Get,
|
||||
// which is a shortcut of GetTokenInfo(ctx).Value:
|
||||
claims := jwt.Get(ctx).(*UserClaims)
|
||||
|
||||
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, expiresAt)
|
||||
ctx.Writef("Username: %s\n", claims.Username)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,33 +67,33 @@ func main() {
|
||||
// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
|
||||
// http://localhost:8080/protected?token={access_token} (200)
|
||||
// http://localhost:8080/protected?token={refresh_token} (401)
|
||||
// http://localhost:8080/refresh?token={refresh_token}
|
||||
// http://localhost:8080/refresh?refresh_token={refresh_token}
|
||||
// OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
|
||||
// OR http://localhost:8080/refresh (request PLAIN TEXT of {refresh_token}) (200) (response JSON {access_token, refresh_token})
|
||||
// http://localhost:8080/refresh?token={access_token} (401)
|
||||
// http://localhost:8080/refresh?refresh_token={access_token} (401)
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func generateTokenPair(ctx iris.Context, j *jwt.JWT) {
|
||||
func generateTokenPair(ctx iris.Context) {
|
||||
// Simulate a user...
|
||||
userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||
|
||||
// Map the current user with the refresh token,
|
||||
// so we make sure, on refresh route, that this refresh token owns
|
||||
// to that user before re-generate.
|
||||
refresh := jwt.Claims{Subject: userID}
|
||||
refreshClaims := jwt.Claims{Subject: userID}
|
||||
|
||||
access := UserClaims{
|
||||
accessClaims := UserClaims{
|
||||
ID: userID,
|
||||
Username: "kataras",
|
||||
}
|
||||
|
||||
// Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.
|
||||
// Second argument is the refresh claims and,
|
||||
// the last one is the access token's claims.
|
||||
tokenPair, err := j.TokenPair(1*time.Hour, refresh, access)
|
||||
// First argument is the access claims,
|
||||
// second argument is the refresh claims,
|
||||
// third argument is the refresh max age.
|
||||
tokenPair, err := signer.NewTokenPair(accessClaims, refreshClaims, refreshTokenMaxAge)
|
||||
if err != nil {
|
||||
ctx.Application().Logger().Debugf("token pair: %v", err)
|
||||
ctx.Application().Logger().Errorf("token pair: %v", err)
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -95,14 +103,12 @@ func generateTokenPair(ctx iris.Context, j *jwt.JWT) {
|
||||
ctx.JSON(tokenPair)
|
||||
}
|
||||
|
||||
func refreshToken(ctx iris.Context, j *jwt.JWT) {
|
||||
/*
|
||||
We could pass a jwt.Claims pointer as the second argument,
|
||||
but we don't have to because the method already returns
|
||||
the standard JWT claims information back to us:
|
||||
refresh, err := VerifyRefreshToken(ctx, nil)
|
||||
*/
|
||||
|
||||
// There are various methods of refresh token, depending on the application requirements.
|
||||
// In this example we will accept a refresh token only, we will verify only a refresh token
|
||||
// and we re-generate a whole new pair. An alternative would be to accept a token pair
|
||||
// of both access and refresh tokens, verify the refresh, verify the access with a Leeway time
|
||||
// and check if its going to expire soon, then generate a single access token.
|
||||
func refreshToken(ctx iris.Context) {
|
||||
// Assuming you have access to the current user, e.g. sessions.
|
||||
//
|
||||
// Simulate a database call against our jwt subject
|
||||
@@ -110,23 +116,46 @@ func refreshToken(ctx iris.Context, j *jwt.JWT) {
|
||||
// * Note: You can remove the ExpectSubject and do this validation later on by yourself.
|
||||
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||
|
||||
// Get the refresh token from ?refresh_token=$token OR
|
||||
// the request body's JSON{"refresh_token": "$token"}.
|
||||
refreshToken := []byte(ctx.URLParam("refresh_token"))
|
||||
if len(refreshToken) == 0 {
|
||||
// You can read the whole body with ctx.GetBody/ReadBody too.
|
||||
var tokenPair jwt.TokenPair
|
||||
if err := ctx.ReadJSON(&tokenPair); err != nil {
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
refreshToken = tokenPair.RefreshToken
|
||||
}
|
||||
|
||||
// Verify the refresh token, which its subject MUST match the "currentUserID".
|
||||
_, err := j.VerifyRefreshToken(ctx, nil, jwt.ExpectSubject(currentUserID))
|
||||
_, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})
|
||||
if err != nil {
|
||||
ctx.Application().Logger().Debugf("verify refresh token: %v", err)
|
||||
ctx.Application().Logger().Errorf("verify refresh token: %v", err)
|
||||
ctx.StatusCode(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
/* Custom validation checks can be performed after Verify calls too:
|
||||
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||
userID := refresh.Claims.Subject
|
||||
userID := verifiedToken.StandardClaims.Subject
|
||||
if userID != currentUserID {
|
||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
// All OK, re-generate the new pair and send to client.
|
||||
generateTokenPair(ctx, j)
|
||||
// All OK, re-generate the new pair and send to client,
|
||||
// we could only generate an access token as well.
|
||||
generateTokenPair(ctx)
|
||||
}
|
||||
|
||||
func handleUnauthorized(ctx iris.Context) {
|
||||
if err := ctx.GetErr(); err != nil {
|
||||
ctx.Application().Logger().Errorf("unauthorized: %v", err)
|
||||
}
|
||||
|
||||
ctx.WriteString("Unauthorized")
|
||||
}
|
||||
|
||||
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-----
|
||||
Reference in New Issue
Block a user