From 85b5453ae11f97430aeea0f544ae877783c78441 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 16 Sep 2020 13:57:11 +0300 Subject: [PATCH] add auth/jwt/refresh-token example as requested at #1635 --- _examples/README.md | 1 + _examples/auth/jwt/refresh-token/main.go | 139 +++++++++++++++++++++++ middleware/jwt/jwt.go | 6 + 3 files changed, 146 insertions(+) create mode 100644 _examples/auth/jwt/refresh-token/main.go diff --git a/_examples/README.md b/_examples/README.md index ec2a9628..dcdad0f7 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -181,6 +181,7 @@ * [Basic Authentication](auth/basicauth/main.go) * [CORS](auth/cors) * [JWT](auth/jwt/main.go) + * [Refresh Token](auth/jwt/refresh-token/main.go) * [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go) * [OAUth2](auth/goth/main.go) * [Manage Permissions](auth/permissions/main.go) diff --git a/_examples/auth/jwt/refresh-token/main.go b/_examples/auth/jwt/refresh-token/main.go new file mode 100644 index 00000000..8b7b4c62 --- /dev/null +++ b/_examples/auth/jwt/refresh-token/main.go @@ -0,0 +1,139 @@ +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 +} + +// TokenPair holds the access token and refresh token response. +type TokenPair struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +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"), + } + + // 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/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}) + app.Listen(":8080") +} + +func generateTokenPair(accessJWT, refreshJWT *jwt.JWT) (TokenPair, error) { + standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}} + + customClaims := UserClaims{ + Claims: accessJWT.Expiry(standardClaims), + Username: "kataras", + } + + accessToken, err := accessJWT.Token(customClaims) + if err != nil { + return TokenPair{}, err + } + + // 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 nessecary 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 +} diff --git a/middleware/jwt/jwt.go b/middleware/jwt/jwt.go index 00df6663..196acd11 100644 --- a/middleware/jwt/jwt.go +++ b/middleware/jwt/jwt.go @@ -476,6 +476,12 @@ func (j *JWT) VerifyToken(ctx *context.Context, claimsPtr interface{}) error { } } + 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 }