1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-20 03:17:04 +00:00

New JWT features and changes (examples updated). Improvements on the Context User and Private Error features

TODO: Write the new e-book JWT section and the HISTORY entry of the chnages and  add a simple example on site docs
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-10-17 06:40:17 +03:00
parent b816156e77
commit 1864f99145
19 changed files with 1749 additions and 493 deletions

View File

@@ -1,3 +1,6 @@
// Package main shows how you can use the Iris unique JWT middleware.
// The file contains different kind of examples that all do the same job but,
// depending on your code style and your application's requirements, you may choose one over other.
package main
import (
@@ -7,10 +10,31 @@ import (
"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
// 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() {
@@ -20,56 +44,241 @@ func main() {
//
// 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)
/*
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) {
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),
claims := &Claims{
Issuer: "server",
Audience: []string{"user"},
Username: "kataras",
}
j.WriteToken(ctx, customClaims)
// WriteToken generates and sends the token to the client.
// To generate a token use: tok, err := j.Token(claims)
// then you can write it in any form you'd like.
// The expiration JWT fields are automatically
// set by the middleware, that means that your claims value
// only needs to fill fields that your application specifically requires.
j.WriteToken(ctx, claims)
})
userRouter := app.Party("/user")
// Middleware + type-safe method,
// useful in 99% of the cases, when your application
// requires token verification under a whole path prefix, e.g. /protected:
protectedAPI := app.Party("/protected")
{
// userRouter.Use(j.Verify)
// 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
}
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)
}))
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, claims.Expiry.Time())
protectedAPI.Get("/", func(ctx iris.Context) {
claims := jwt.Get(ctx).(*Claims)
// All fields parsed from token are set to the claims,
// including the Expiry (if defined).
ctx.Writef("Username: %s\nExpires at: %s\nAudience: %s",
claims.Username, claims.Expiry.Time(), claims.Audience)
})
}
// Verify token inside a handler method,
// useful when you just need to verify a token on a single spot:
app.Get("/inline", func(ctx iris.Context) {
var claims Claims
_, err := j.VerifyToken(ctx, &claims)
if err != nil {
ctx.StopWithError(iris.StatusUnauthorized, err)
return
}
ctx.Writef("Username: %s\nExpires at: %s\n",
claims.Username, claims.Expiry.Time())
})
// Use a common map as claims method,
// not recommended, as we support typed claims but
// you can do it:
app.Get("/map/authenticate", func(ctx iris.Context) {
claims := map[string]interface{}{ // or iris.Map for shortcut.
"username": "kataras",
}
j.WriteToken(ctx, claims)
})
app.Get("/map/verify/middleware", j.Verify(func() interface{} {
return &iris.Map{} // or &map[string]interface{}{}
}), func(ctx iris.Context) {
claims := jwt.Get(ctx).(iris.Map)
// The Get method will unwrap the *iris.Map for you,
// so its values are directly accessible:
ctx.Writef("Username: %s\nExpires at: %s\n",
claims["username"], claims["exp"].(*jwt.NumericDate).Time())
})
app.Get("/map/verify", func(ctx iris.Context) {
claims := make(iris.Map) // or make(map[string]interface{})
tokenInfo, err := j.VerifyToken(ctx, &claims)
if err != nil {
ctx.StopWithError(iris.StatusUnauthorized, err)
return
}
ctx.Writef("Username: %s\nExpires at: %s\n",
claims["username"], tokenInfo.Claims.Expiry.Time()) /* the claims["exp"] is also set. */
})
// Use the new Context.User() to retrieve the verified client method:
// 1. Create a go stuct that implements the context.User interface:
app.Get("/users/authenticate", func(ctx iris.Context) {
user := &User{Username: "kataras"}
j.WriteToken(ctx, user)
})
usersAPI := app.Party("/users")
{
usersAPI.Use(j.Verify(func() interface{} {
return new(User)
}))
usersAPI.Get("/", func(ctx iris.Context) {
user := ctx.User()
userToken, _ := user.GetToken()
/*
You can also cast it to the underline implementation
and work with its fields:
expires := user.(*User).Expiry.Time()
*/
// OR use the GetTokenInfo to get the parsed token information:
expires := jwt.GetTokenInfo(ctx).Claims.Expiry.Time()
lifetime := expires.Sub(time.Now()) // remeaning time to be expired.
ctx.Writef("Username: %s\nAuthenticated at: %s\nLifetime: %s\nToken: %s\n",
user.GetUsername(), user.GetAuthorizedAt(), lifetime, userToken)
})
}
// http://localhost:8080/authenticate
// http://localhost:8080/protected?token={token}
// http://localhost:8080/inline?token={token}
//
// http://localhost:8080/map/authenticate
// http://localhost:8080/map/verify?token={token}
// http://localhost:8080/map/verify/middleware?token={token}
//
// http://localhost:8080/users/authenticate
// http://localhost:8080/users?token={token}
app.Listen(":8080")
}
// 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)

View File

@@ -7,133 +7,122 @@ import (
"github.com/kataras/iris/v12/middleware/jwt"
)
// UserClaims a custom claims structure. You can just use jwt.Claims too.
// UserClaims a custom access claims structure.
type UserClaims struct {
jwt.Claims
Username string
// We could that JWT field to separate the access and refresh token:
// Issuer string `json:"iss"`
// But let's cover the "required" feature too, see below:
ID string `json:"user_id,required"`
Username string `json:"username,required"`
}
// TokenPair holds the access token and refresh token response.
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// For refresh token, we will just use the jwt.Claims
// structure which contains the standard JWT fields.
func main() {
app := iris.New()
// Access token, short-live.
accessJWT := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
// Refresh token, long-live. Important: Give different secret keys(!)
refreshJWT := jwt.HMAC(1*time.Hour, "other secret", "other16bytesecre")
// On refresh token, we extract it only from a request body
// of JSON, e.g. {"refresh_token": $token }.
// You can also do it manually in the handler level though.
refreshJWT.Extractors = []jwt.TokenExtractor{
jwt.FromJSON("refresh_token"),
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
app.Get("/authenticate", func(ctx iris.Context) {
generateTokenPair(ctx, j)
})
app.Get("/refresh_json", func(ctx iris.Context) {
refreshTokenFromJSON(ctx, j)
})
protectedAPI := app.Party("/protected")
{
protectedAPI.Use(j.Verify(func() interface{} {
return new(UserClaims)
})) // OR j.VerifyToken(ctx, &claims, jwt.MeetRequirements(&UserClaims{}))
protectedAPI.Get("/", func(ctx iris.Context) {
// Get token info, even if our UserClaims does not embed those
// through GetTokenInfo:
expiresAt := jwt.GetTokenInfo(ctx).Claims.Expiry.Time()
// Get your custom JWT claims through Get,
// which is a shortcut of GetTokenInfo(ctx).Value:
claims := jwt.Get(ctx).(*UserClaims)
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, expiresAt)
})
}
// Generate access and refresh tokens and send to the client.
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/protected (401)
// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
// http://localhost:8080?token={access_token} (200)
// http://localhost:8080?token={refresh_token} (401)
// http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
// http://localhost:8080/protected?token={access_token} (200)
// http://localhost:8080/protected?token={refresh_token} (401)
// http://localhost:8080/refresh_json (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
app.Listen(":8080")
}
func generateTokenPair(accessJWT, refreshJWT *jwt.JWT) (TokenPair, error) {
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
func generateTokenPair(ctx iris.Context, j *jwt.JWT) {
// Simulate a user...
userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
customClaims := UserClaims{
Claims: accessJWT.Expiry(standardClaims),
// 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}
access := UserClaims{
ID: userID,
Username: "kataras",
}
accessToken, err := accessJWT.Token(customClaims)
// Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.
// Second argument is the refresh claims and,
// the last one is the access token's claims.
tokenPair, err := j.TokenPair(1*time.Hour, refresh, access)
if err != nil {
return TokenPair{}, err
ctx.Application().Logger().Debugf("token pair: %v", err)
ctx.StopWithStatus(iris.StatusInternalServerError)
return
}
// At refresh tokens you don't need any custom claims.
refreshClaims := refreshJWT.Expiry(jwt.Claims{
ID: "refresh_kataras",
// For example, the User ID,
// this is necessary to check against the database
// if the user still exist or has credentials to access our page.
Subject: "53afcf05-38a3-43c3-82af-8bbbe0e4a149",
})
refreshToken, err := refreshJWT.Token(refreshClaims)
if err != nil {
return TokenPair{}, err
}
return TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
}, nil
// Send the generated token pair to the client.
// The tokenPair looks like: {"access_token": $token, "refresh_token": $token}
ctx.JSON(tokenPair)
}
func refreshTokenFromJSON(ctx iris.Context, j *jwt.JWT) {
var tokenPair jwt.TokenPair
// Grab the refresh token from a JSON body (you can let it fetch by URL parameter too but
// it's common practice that you read it from a json body as
// it may contain the access token too (the same response we sent on generateTokenPair)).
err := ctx.ReadJSON(&tokenPair)
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
return
}
var refreshClaims jwt.Claims
err = j.VerifyTokenString(ctx, tokenPair.RefreshToken, &refreshClaims)
if err != nil {
ctx.Application().Logger().Debugf("verify refresh token: %v", err)
ctx.StatusCode(iris.StatusUnauthorized)
return
}
// Assuming you have access to the current user, e.g. sessions.
//
// Simulate a database call against our jwt subject
// to make sure that this refresh token is a pair generated by this user.
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
userID := refreshClaims.Subject
if userID != currentUserID {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
//
// Otherwise, the request must contain the (old) access token too,
// even if it's invalid, we can still fetch its fields, such as the user id.
// [...leave it for you]
// All OK, re-generate the new pair and send to client.
generateTokenPair(ctx, j)
}