1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 02:17:05 +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:
Gerasimos (Makis) Maropoulos
2020-10-30 22:12:16 +02:00
parent d517f36a29
commit 8eea0296a7
21 changed files with 750 additions and 2431 deletions

View File

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