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:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user