mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
184 lines
5.9 KiB
Go
184 lines
5.9 KiB
Go
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 {
|
|
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"`
|
|
}
|
|
|
|
// GetID implements the partial context user's ID interface.
|
|
// Note that if claims were a map then the claims value converted to UserClaims
|
|
// and no need to implement any method.
|
|
//
|
|
// This is useful when multiple auth methods are used (e.g. basic auth, jwt)
|
|
// but they all share a couple of methods.
|
|
func (u *UserClaims) GetID() string {
|
|
return u.ID
|
|
}
|
|
|
|
// GetUsername implements the partial context user's Username interface.
|
|
func (u *UserClaims) GetUsername() string {
|
|
return u.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)
|
|
|
|
app.Get("/authenticate", generateTokenPair)
|
|
app.Get("/refresh", refreshToken)
|
|
|
|
protectedAPI := app.Party("/protected")
|
|
{
|
|
verifyMiddleware := verifier.Verify(func() interface{} {
|
|
return new(UserClaims)
|
|
})
|
|
|
|
protectedAPI.Use(verifyMiddleware)
|
|
|
|
protectedAPI.Get("/", func(ctx iris.Context) {
|
|
// Access the claims through: jwt.Get:
|
|
// claims := jwt.Get(ctx).(*UserClaims)
|
|
// ctx.Writef("Username: %s\n", claims.Username)
|
|
//
|
|
// OR through context's user (if at least one method was implement by our UserClaims):
|
|
user := ctx.User()
|
|
id, _ := user.GetID()
|
|
username, _ := user.GetUsername()
|
|
ctx.Writef("ID: %s\nUsername: %s\n", id, username)
|
|
})
|
|
}
|
|
|
|
// http://localhost:8080/protected (401)
|
|
// 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?refresh_token={refresh_token}
|
|
// OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
|
|
// http://localhost:8080/refresh?refresh_token={access_token} (401)
|
|
app.Listen(":8080")
|
|
}
|
|
|
|
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.
|
|
refreshClaims := jwt.Claims{Subject: userID}
|
|
|
|
accessClaims := UserClaims{
|
|
ID: userID,
|
|
Username: "kataras",
|
|
}
|
|
|
|
// Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.
|
|
// 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().Errorf("token pair: %v", err)
|
|
ctx.StopWithStatus(iris.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Send the generated token pair to the client.
|
|
// The tokenPair looks like: {"access_token": $token, "refresh_token": $token}
|
|
ctx.JSON(tokenPair)
|
|
}
|
|
|
|
// 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
|
|
// to make sure that this refresh token is a pair generated by this user.
|
|
// * 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 := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})
|
|
if err != nil {
|
|
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 := verifiedToken.StandardClaims.Subject
|
|
if userID != currentUserID {
|
|
ctx.StopWithStatus(iris.StatusUnauthorized)
|
|
return
|
|
}
|
|
*/
|
|
|
|
// 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")
|
|
}
|