2020-05-27 11:02:17 +02:00
|
|
|
package jwt
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto"
|
2020-10-17 14:22:42 +02:00
|
|
|
"encoding/json"
|
2020-10-18 13:42:19 +02:00
|
|
|
"fmt"
|
2020-05-31 16:57:30 +02:00
|
|
|
"os"
|
2020-05-27 11:02:17 +02:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/kataras/iris/v12/context"
|
|
|
|
|
|
|
|
"github.com/square/go-jose/v3"
|
|
|
|
"github.com/square/go-jose/v3/jwt"
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
context.SetHandlerName("iris/middleware/jwt.*", "iris.jwt")
|
|
|
|
}
|
|
|
|
|
|
|
|
// TokenExtractor is a function that takes a context as input and returns
|
|
|
|
// a token. An empty string should be returned if no token found
|
|
|
|
// without additional information.
|
2020-07-10 22:21:09 +02:00
|
|
|
type TokenExtractor func(*context.Context) string
|
2020-05-27 11:02:17 +02:00
|
|
|
|
|
|
|
// FromHeader is a token extractor.
|
|
|
|
// It reads the token from the Authorization request header of form:
|
|
|
|
// Authorization: "Bearer {token}".
|
2020-07-10 22:21:09 +02:00
|
|
|
func FromHeader(ctx *context.Context) string {
|
2020-05-27 11:02:17 +02:00
|
|
|
authHeader := ctx.GetHeader("Authorization")
|
|
|
|
if authHeader == "" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// pure check: authorization header format must be Bearer {token}
|
|
|
|
authHeaderParts := strings.Split(authHeader, " ")
|
|
|
|
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return authHeaderParts[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromQuery is a token extractor.
|
|
|
|
// It reads the token from the "token" url query parameter.
|
2020-07-10 22:21:09 +02:00
|
|
|
func FromQuery(ctx *context.Context) string {
|
2020-05-27 11:02:17 +02:00
|
|
|
return ctx.URLParam("token")
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromJSON is a token extractor.
|
|
|
|
// Reads a json request body and extracts the json based on the given field.
|
|
|
|
// The request content-type should contain the: application/json header value, otherwise
|
|
|
|
// this method will not try to read and consume the body.
|
|
|
|
func FromJSON(jsonKey string) TokenExtractor {
|
2020-07-10 22:21:09 +02:00
|
|
|
return func(ctx *context.Context) string {
|
2020-05-27 11:02:17 +02:00
|
|
|
if ctx.GetContentTypeRequested() != context.ContentJSONHeaderValue {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
var m context.Map
|
|
|
|
if err := ctx.ReadJSON(&m); err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
if m == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
v, ok := m[jsonKey]
|
|
|
|
if !ok {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
tok, ok := v.(string)
|
|
|
|
if !ok {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return tok
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// JWT holds the necessary information the middleware need
|
|
|
|
// to sign and verify tokens.
|
|
|
|
//
|
|
|
|
// The `RSA(privateFile, publicFile, password)` package-level helper function
|
|
|
|
// can be used to decode the SignKey and VerifyKey.
|
2020-10-17 05:40:17 +02:00
|
|
|
//
|
|
|
|
// For an easy use look the `HMAC` package-level function
|
|
|
|
// and the its `NewUser` and `VerifyUser` methods.
|
2020-05-27 11:02:17 +02:00
|
|
|
type JWT struct {
|
|
|
|
// MaxAge is the expiration duration of the generated tokens.
|
|
|
|
MaxAge time.Duration
|
|
|
|
|
|
|
|
// Extractors are used to extract a raw token string value
|
|
|
|
// from the request.
|
|
|
|
// Builtin extractors:
|
|
|
|
// * FromHeader
|
|
|
|
// * FromQuery
|
|
|
|
// * FromJSON
|
|
|
|
// Defaults to a slice of `FromHeader` and `FromQuery`.
|
|
|
|
Extractors []TokenExtractor
|
|
|
|
|
|
|
|
// Signer is used to sign the token.
|
|
|
|
// It is set on `New` and `Default` package-level functions.
|
|
|
|
Signer jose.Signer
|
|
|
|
// VerificationKey is used to verify the token (public key).
|
|
|
|
VerificationKey interface{}
|
|
|
|
|
|
|
|
// Encrypter is used to, optionally, encrypt the token.
|
2020-05-28 15:20:58 +02:00
|
|
|
// It is set on `WithEncryption` method.
|
2020-05-27 11:02:17 +02:00
|
|
|
Encrypter jose.Encrypter
|
|
|
|
// DecriptionKey is used to decrypt the token (private key)
|
|
|
|
DecriptionKey interface{}
|
2020-10-17 05:40:17 +02:00
|
|
|
|
|
|
|
// Blocklist holds the invalidated-by-server tokens (that are not yet expired).
|
|
|
|
// It is not initialized by default.
|
|
|
|
// Initialization Usage:
|
|
|
|
// j.UseBlocklist()
|
|
|
|
// OR
|
|
|
|
// j.Blocklist = jwt.NewBlocklist(gcEveryDuration)
|
|
|
|
// Usage:
|
|
|
|
// - ctx.Logout()
|
|
|
|
// - j.Invalidate(ctx)
|
|
|
|
Blocklist *Blocklist
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type privateKey interface{ Public() crypto.PublicKey }
|
|
|
|
|
|
|
|
// New returns a new JWT instance.
|
|
|
|
// It accepts a maximum time duration for token expiration
|
|
|
|
// and the algorithm among with its key for signing and verification.
|
|
|
|
//
|
|
|
|
// See `WithEncryption` method to add token encryption too.
|
|
|
|
// Use `Token` method to generate a new token string
|
|
|
|
// and `VerifyToken` method to decrypt, verify and bind claims of an incoming request token.
|
|
|
|
// Token, by default, is extracted by "Authorization: Bearer {token}" request header and
|
|
|
|
// url query parameter of "token". Token extractors can be modified through the `Extractors` field.
|
|
|
|
//
|
|
|
|
// For example, if you want to sign and verify using RSA-256 key:
|
|
|
|
// 1. Generate key file, e.g:
|
|
|
|
// $ openssl genrsa -des3 -out private.pem 2048
|
|
|
|
// 2. Read file contents with io.ReadFile("./private.pem")
|
2020-05-31 16:57:30 +02:00
|
|
|
// 3. Pass the []byte result to the `ParseRSAPrivateKey(contents, password)` package-level helper
|
2020-05-27 11:02:17 +02:00
|
|
|
// 4. Use the result *rsa.PrivateKey as "key" input parameter of this `New` function.
|
|
|
|
//
|
|
|
|
// See aliases.go file for available algorithms.
|
|
|
|
func New(maxAge time.Duration, alg SignatureAlgorithm, key interface{}) (*JWT, error) {
|
|
|
|
sig, err := jose.NewSigner(jose.SigningKey{
|
|
|
|
Algorithm: alg,
|
|
|
|
Key: key,
|
|
|
|
}, (&jose.SignerOptions{}).WithType("JWT"))
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
j := &JWT{
|
|
|
|
Signer: sig,
|
|
|
|
VerificationKey: key,
|
|
|
|
MaxAge: maxAge,
|
|
|
|
Extractors: []TokenExtractor{FromHeader, FromQuery},
|
|
|
|
}
|
|
|
|
|
|
|
|
if s, ok := key.(privateKey); ok {
|
|
|
|
j.VerificationKey = s.Public()
|
|
|
|
}
|
|
|
|
|
|
|
|
return j, nil
|
|
|
|
}
|
|
|
|
|
2020-06-07 14:26:06 +02:00
|
|
|
// Default key filenames for `RSA`.
|
2020-05-31 16:57:30 +02:00
|
|
|
const (
|
2020-06-07 14:26:06 +02:00
|
|
|
DefaultSignFilename = "jwt_sign.key"
|
|
|
|
DefaultEncFilename = "jwt_enc.key"
|
2020-05-31 16:57:30 +02:00
|
|
|
)
|
|
|
|
|
2020-06-07 14:26:06 +02:00
|
|
|
// RSA returns a new `JWT` instance.
|
|
|
|
// It tries to parse RSA256 keys from "filenames[0]" (defaults to "jwt_sign.key") and
|
|
|
|
// "filenames[1]" (defaults to "jwt_enc.key") files or generates and exports new random keys.
|
2020-05-31 16:57:30 +02:00
|
|
|
//
|
|
|
|
// It panics on errors.
|
|
|
|
// Use the `New` package-level function instead for more options.
|
2020-06-07 14:26:06 +02:00
|
|
|
func RSA(maxAge time.Duration, filenames ...string) *JWT {
|
|
|
|
var (
|
|
|
|
signFilename = DefaultSignFilename
|
|
|
|
encFilename = DefaultEncFilename
|
|
|
|
)
|
|
|
|
|
|
|
|
switch len(filenames) {
|
|
|
|
case 1:
|
|
|
|
signFilename = filenames[0]
|
|
|
|
case 2:
|
|
|
|
encFilename = filenames[1]
|
|
|
|
}
|
|
|
|
|
2020-05-31 16:57:30 +02:00
|
|
|
// Do not try to create or load enc key if only sign key already exists.
|
|
|
|
withEncryption := true
|
2020-06-07 14:26:06 +02:00
|
|
|
if fileExists(signFilename) {
|
|
|
|
withEncryption = fileExists(encFilename)
|
2020-05-31 16:57:30 +02:00
|
|
|
}
|
|
|
|
|
2020-06-07 14:26:06 +02:00
|
|
|
sigKey, err := LoadRSA(signFilename, 2048)
|
2020-05-31 16:57:30 +02:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
j, err := New(maxAge, RS256, sigKey)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if withEncryption {
|
2020-06-07 14:26:06 +02:00
|
|
|
encKey, err := LoadRSA(encFilename, 2048)
|
2020-05-31 16:57:30 +02:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
err = j.WithEncryption(A128CBCHS256, RSA15, encKey)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return j
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
signEnv = "JWT_SECRET"
|
|
|
|
encEnv = "JWT_SECRET_ENC"
|
|
|
|
)
|
|
|
|
|
|
|
|
func getenv(key string, def string) string {
|
|
|
|
v := os.Getenv(key)
|
|
|
|
if v == "" {
|
|
|
|
return def
|
|
|
|
}
|
|
|
|
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
2020-06-07 14:26:06 +02:00
|
|
|
// HMAC returns a new `JWT` instance.
|
2020-05-31 16:57:30 +02:00
|
|
|
// It tries to read hmac256 secret keys from system environment variables:
|
|
|
|
// * JWT_SECRET for signing and verification key and
|
|
|
|
// * JWT_SECRET_ENC for encryption and decryption key
|
|
|
|
// and defaults them to the given "keys" respectfully.
|
|
|
|
//
|
|
|
|
// It panics on errors.
|
|
|
|
// Use the `New` package-level function instead for more options.
|
2020-06-07 14:26:06 +02:00
|
|
|
func HMAC(maxAge time.Duration, keys ...string) *JWT {
|
2020-05-31 16:57:30 +02:00
|
|
|
var defaultSignSecret, defaultEncSecret string
|
|
|
|
|
|
|
|
switch len(keys) {
|
|
|
|
case 1:
|
|
|
|
defaultSignSecret = keys[0]
|
|
|
|
case 2:
|
|
|
|
defaultEncSecret = keys[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
signSecret := getenv(signEnv, defaultSignSecret)
|
|
|
|
encSecret := getenv(encEnv, defaultEncSecret)
|
|
|
|
|
|
|
|
j, err := New(maxAge, HS256, []byte(signSecret))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if encSecret != "" {
|
|
|
|
err = j.WithEncryption(A128GCM, DIRECT, []byte(encSecret))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return j
|
|
|
|
}
|
|
|
|
|
2020-05-27 11:02:17 +02:00
|
|
|
// WithEncryption method enables encryption and decryption of the token.
|
|
|
|
// It sets an appropriate encrypter(`Encrypter` and the `DecriptionKey` fields) based on the key type.
|
|
|
|
func (j *JWT) WithEncryption(contentEncryption ContentEncryption, alg KeyAlgorithm, key interface{}) error {
|
|
|
|
var publicKey interface{} = key
|
|
|
|
if s, ok := key.(privateKey); ok {
|
|
|
|
publicKey = s.Public()
|
|
|
|
}
|
|
|
|
|
|
|
|
enc, err := jose.NewEncrypter(contentEncryption, jose.Recipient{
|
|
|
|
Algorithm: alg,
|
|
|
|
Key: publicKey,
|
|
|
|
},
|
|
|
|
(&jose.EncrypterOptions{}).WithType("JWT").WithContentType("JWT"),
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
j.Encrypter = enc
|
|
|
|
j.DecriptionKey = key
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// UseBlocklist initializes the Blocklist.
|
|
|
|
// Should be called on jwt middleware creation-time,
|
|
|
|
// after this, the developer can use the Context.Logout method
|
|
|
|
// to invalidate a verified token by the server-side.
|
|
|
|
func (j *JWT) UseBlocklist() {
|
|
|
|
gcEvery := 30 * time.Minute
|
|
|
|
if j.MaxAge > 0 {
|
|
|
|
gcEvery = j.MaxAge
|
|
|
|
}
|
|
|
|
j.Blocklist = NewBlocklist(gcEvery)
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// ExpiryMap adds the expiration based on the "maxAge" to the "claims" map.
|
|
|
|
// It's called automatically on `Token` method.
|
|
|
|
func ExpiryMap(maxAge time.Duration, claims context.Map) {
|
|
|
|
now := time.Now()
|
|
|
|
if claims["exp"] == nil {
|
|
|
|
claims["exp"] = NewNumericDate(now.Add(maxAge))
|
|
|
|
}
|
|
|
|
|
|
|
|
if claims["iat"] == nil {
|
|
|
|
claims["iat"] = NewNumericDate(now)
|
|
|
|
}
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Token generates and returns a new token string.
|
|
|
|
// See `VerifyToken` too.
|
|
|
|
func (j *JWT) Token(claims interface{}) (string, error) {
|
2020-10-17 05:40:17 +02:00
|
|
|
return j.token(j.MaxAge, claims)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (j *JWT) token(maxAge time.Duration, claims interface{}) (string, error) {
|
|
|
|
if claims == nil {
|
|
|
|
return "", ErrInvalidKey
|
|
|
|
}
|
|
|
|
|
|
|
|
c, nErr := normalize(claims)
|
|
|
|
if nErr != nil {
|
|
|
|
return "", nErr
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
ExpiryMap(maxAge, c)
|
|
|
|
|
2020-05-27 11:02:17 +02:00
|
|
|
var (
|
|
|
|
token string
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
// jwt.Builder and jwt.NestedBuilder contain same methods but they are not the same.
|
2020-10-17 05:40:17 +02:00
|
|
|
//
|
|
|
|
// Note that the .Claims method there, converts a Struct to a map under the hoods.
|
|
|
|
// That means that we will not have any performance cost
|
|
|
|
// if we do it by ourselves and pass always a Map there.
|
|
|
|
// That gives us the option to allow user to pass ANY go struct
|
|
|
|
// and we can add the "exp", "nbf", "iat" map values by ourselves
|
|
|
|
// based on the j.MaxAge.
|
|
|
|
// (^ done, see normalize, all methods are
|
|
|
|
// changed to accept totally custom types, no need to embed the standard Claims anymore).
|
2020-05-27 11:02:17 +02:00
|
|
|
if j.DecriptionKey != nil {
|
2020-10-17 05:40:17 +02:00
|
|
|
token, err = jwt.SignedAndEncrypted(j.Signer, j.Encrypter).Claims(c).CompactSerialize()
|
2020-05-27 11:02:17 +02:00
|
|
|
} else {
|
2020-10-17 05:40:17 +02:00
|
|
|
token, err = jwt.Signed(j.Signer).Claims(c).CompactSerialize()
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return token, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteToken is a helper which just generates(calls the `Token` method) and writes
|
|
|
|
// a new token to the client in plain text format.
|
|
|
|
//
|
|
|
|
// Use the `Token` method to get a new generated token raw string value.
|
2020-07-10 22:21:09 +02:00
|
|
|
func (j *JWT) WriteToken(ctx *context.Context, claims interface{}) error {
|
2020-05-27 11:02:17 +02:00
|
|
|
token, err := j.Token(claims)
|
|
|
|
if err != nil {
|
|
|
|
ctx.StatusCode(500)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = ctx.WriteString(token)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// VerifyToken verifies (and decrypts) the request token,
|
|
|
|
// it also validates and binds the parsed token's claims to the "claimsPtr" (destination).
|
|
|
|
//
|
|
|
|
// The last, variadic, input argument is optionally, if provided then the
|
|
|
|
// parsed claims must match the expectations;
|
|
|
|
// e.g. Audience, Issuer, ID, Subject.
|
|
|
|
// See `ExpectXXX` package-functions for details.
|
|
|
|
func (j *JWT) VerifyToken(ctx *context.Context, claimsPtr interface{}, expectations ...Expectation) (*TokenInfo, error) {
|
|
|
|
token := j.RequestToken(ctx)
|
|
|
|
return j.VerifyTokenString(ctx, token, claimsPtr, expectations...)
|
|
|
|
}
|
2020-05-27 11:02:17 +02:00
|
|
|
|
2020-10-18 16:15:29 +02:00
|
|
|
// VerifyRefreshToken like the `VerifyToken` but it verifies a refresh token one instead.
|
|
|
|
// If the implementation does not fill the application's requirements,
|
|
|
|
// you can ignore this method and still use the `VerifyToken` for refresh tokens too.
|
|
|
|
//
|
|
|
|
// This method adds the ExpectRefreshToken expectation and it
|
|
|
|
// tries to read the refresh token from raw body or,
|
|
|
|
// if content type was application/json, then it extracts the token
|
|
|
|
// from the JSON request body's {"refresh_token": "$token"} key.
|
|
|
|
func (j *JWT) VerifyRefreshToken(ctx *context.Context, claimsPtr interface{}, expectations ...Expectation) (*TokenInfo, error) {
|
|
|
|
token := j.RequestToken(ctx)
|
|
|
|
if token == "" {
|
|
|
|
var tokenPair TokenPair // read "refresh_token" from JSON.
|
|
|
|
if ctx.GetContentTypeRequested() == context.ContentJSONHeaderValue {
|
|
|
|
ctx.ReadJSON(&tokenPair) // ignore error.
|
|
|
|
token = tokenPair.RefreshToken
|
|
|
|
if token == "" {
|
|
|
|
return nil, ErrMissing
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ctx.ReadBody(&token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return j.VerifyTokenString(ctx, token, claimsPtr, append(expectations, ExpectRefreshToken)...)
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// RequestToken extracts the token from the request.
|
|
|
|
func (j *JWT) RequestToken(ctx *context.Context) (token string) {
|
|
|
|
for _, extract := range j.Extractors {
|
|
|
|
if token = extract(ctx); token != "" {
|
|
|
|
break // ok we found it.
|
|
|
|
}
|
2020-06-07 14:26:06 +02:00
|
|
|
}
|
2020-05-27 11:02:17 +02:00
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TokenSetter is an interface which if implemented
|
|
|
|
// the extracted, verified, token is stored to the object.
|
|
|
|
type TokenSetter interface {
|
|
|
|
SetToken(token string)
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// TokenInfo holds the standard token information may required
|
|
|
|
// for further actions.
|
|
|
|
// This structure is mostly useful when the developer's go structure
|
|
|
|
// does not hold the standard jwt fields (e.g. "exp")
|
|
|
|
// but want access to the parsed token which contains those fields.
|
|
|
|
// Inside the middleware, it is used to invalidate tokens through server-side, see `Invalidate`.
|
|
|
|
type TokenInfo struct {
|
|
|
|
RequestToken string // The request token.
|
|
|
|
Claims Claims // The standard JWT parsed fields from the request Token.
|
|
|
|
Value interface{} // The pointer to the end-developer's custom claims structure (see `Get`).
|
|
|
|
}
|
|
|
|
|
|
|
|
const tokenInfoContextKey = "iris.jwt.token"
|
|
|
|
|
|
|
|
// Get returns the verified developer token claims.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Usage:
|
|
|
|
// j := jwt.New(...)
|
|
|
|
// app.Use(j.Verify(func() interface{} { return new(CustomClaims) }))
|
|
|
|
// app.Post("/restricted", func(ctx iris.Context){
|
|
|
|
// claims := jwt.Get(ctx).(*CustomClaims)
|
|
|
|
// [use claims...]
|
|
|
|
// })
|
|
|
|
//
|
|
|
|
// Note that there is one exception, if the value was a pointer
|
|
|
|
// to a map[string]interface{}, it returns the map itself so it can be
|
|
|
|
// accessible directly without the requirement of unwrapping it, e.g.
|
|
|
|
// j.Verify(func() interface{} {
|
|
|
|
// return &iris.Map{}
|
|
|
|
// }
|
|
|
|
// [...]
|
|
|
|
// claims := jwt.Get(ctx).(iris.Map)
|
|
|
|
func Get(ctx *context.Context) interface{} {
|
|
|
|
if tok := GetTokenInfo(ctx); tok != nil {
|
|
|
|
switch v := tok.Value.(type) {
|
|
|
|
case *context.Map:
|
|
|
|
return *v
|
2020-10-17 14:22:42 +02:00
|
|
|
case *json.RawMessage:
|
|
|
|
// This is useful when we can accept more than one
|
|
|
|
// type of JWT token in the same request path,
|
|
|
|
// but we also want to keep type safety.
|
|
|
|
// Usage:
|
|
|
|
// type myClaims struct { Roles []string `json:"roles"`}
|
|
|
|
// v := jwt.Get(ctx)
|
|
|
|
// var claims myClaims
|
|
|
|
// jwt.Unmarshal(v, &claims)
|
|
|
|
// [...claims.Roles]
|
|
|
|
return *v
|
2020-10-17 05:40:17 +02:00
|
|
|
default:
|
|
|
|
return v
|
2020-06-07 14:26:06 +02:00
|
|
|
}
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetTokenInfo returns the verified token's information.
|
|
|
|
func GetTokenInfo(ctx *context.Context) *TokenInfo {
|
|
|
|
if v := ctx.Values().Get(tokenInfoContextKey); v != nil {
|
|
|
|
if t, ok := v.(*TokenInfo); ok {
|
|
|
|
return t
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
return nil
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// Invalidate invalidates a verified JWT token.
|
|
|
|
// It adds the request token, retrieved by Verify methods, to the block list.
|
|
|
|
// Next request will be blocked, even if the token was not yet expired.
|
|
|
|
// This method can be used when the client-side does not clear the token
|
|
|
|
// on a user logout operation.
|
|
|
|
//
|
|
|
|
// Note: the Blocklist should be initialized before serve-time: j.UseBlocklist().
|
|
|
|
func (j *JWT) Invalidate(ctx *context.Context) {
|
|
|
|
if j.Blocklist == nil {
|
|
|
|
ctx.Application().Logger().Debug("jwt.Invalidate: Blocklist is nil")
|
|
|
|
return
|
|
|
|
}
|
2020-05-27 11:02:17 +02:00
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
tokenInfo := GetTokenInfo(ctx)
|
|
|
|
if tokenInfo == nil {
|
|
|
|
return
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
j.Blocklist.Set(tokenInfo.RequestToken, tokenInfo.Claims.Expiry.Time())
|
2020-09-16 12:57:11 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// VerifyTokenString verifies and unmarshals an extracted request token to "dest" destination.
|
|
|
|
// The last variadic input indicates any further validations against the verified token claims.
|
|
|
|
// If the given "dest" is a valid context.User then ctx.User() will return it.
|
|
|
|
// If the token is missing an `ErrMissing` is returned.
|
|
|
|
// If the incoming token was expired an `ErrExpired` is returned.
|
|
|
|
// If the incoming token was blocked by the server an `ErrBlocked` is returned.
|
|
|
|
func (j *JWT) VerifyTokenString(ctx *context.Context, token string, dest interface{}, expectations ...Expectation) (*TokenInfo, error) {
|
2020-05-27 11:02:17 +02:00
|
|
|
if token == "" {
|
2020-10-17 05:40:17 +02:00
|
|
|
return nil, ErrMissing
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
parsedToken *jwt.JSONWebToken
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
if j.DecriptionKey != nil {
|
|
|
|
t, cerr := jwt.ParseSignedAndEncrypted(token)
|
|
|
|
if cerr != nil {
|
2020-10-17 05:40:17 +02:00
|
|
|
return nil, cerr
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
parsedToken, err = t.Decrypt(j.DecriptionKey)
|
|
|
|
} else {
|
|
|
|
parsedToken, err = jwt.ParseSigned(token)
|
|
|
|
}
|
|
|
|
if err != nil {
|
2020-10-17 05:40:17 +02:00
|
|
|
return nil, err
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-18 13:42:19 +02:00
|
|
|
var (
|
|
|
|
claims Claims
|
|
|
|
tokenMaxAger tokenWithMaxAge
|
|
|
|
)
|
|
|
|
|
2020-10-18 16:15:29 +02:00
|
|
|
var (
|
|
|
|
ignoreDest = dest == nil
|
|
|
|
ignoreVarClaims bool
|
|
|
|
)
|
|
|
|
if !ignoreDest { // if dest was not nil, check if the dest is already a standard claims pointer.
|
|
|
|
_, ignoreVarClaims = dest.(*Claims)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure read the standard claims one if dest was Claims or was nil.
|
|
|
|
// (it wont break anything if we unmarshal them twice though, we just do it for performance reasons).
|
|
|
|
var pointers = []interface{}{&tokenMaxAger}
|
|
|
|
if !ignoreDest {
|
|
|
|
pointers = append(pointers, dest)
|
|
|
|
}
|
|
|
|
if !ignoreVarClaims {
|
|
|
|
pointers = append(pointers, &claims)
|
|
|
|
}
|
|
|
|
if err = parsedToken.Claims(j.VerificationKey, pointers...); err != nil {
|
2020-10-17 05:40:17 +02:00
|
|
|
return nil, err
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-18 16:15:29 +02:00
|
|
|
// Set the std claims, if missing from receiver so the expectations and validation still work.
|
|
|
|
if ignoreVarClaims {
|
|
|
|
claims = *dest.(*Claims)
|
|
|
|
} else if ignoreDest {
|
|
|
|
dest = &claims
|
|
|
|
}
|
|
|
|
|
2020-10-18 13:42:19 +02:00
|
|
|
expectMaxAge := j.MaxAge
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// Build the Expected value.
|
|
|
|
expected := Expected{}
|
|
|
|
for _, e := range expectations {
|
|
|
|
if e != nil {
|
|
|
|
// expection can be used as a field validation too (see MeetRequirements).
|
|
|
|
if err = e(&expected, dest); err != nil {
|
2020-10-18 13:42:19 +02:00
|
|
|
if err == ErrExpectRefreshToken {
|
|
|
|
if tokenMaxAger.MaxAge > 0 {
|
|
|
|
// If max age exists, grab it and compare it later.
|
|
|
|
// Otherwise fire the ErrExpectRefreshToken.
|
|
|
|
expectMaxAge = tokenMaxAger.MaxAge
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2020-10-17 05:40:17 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-05-27 11:02:17 +02:00
|
|
|
|
2020-10-18 13:42:19 +02:00
|
|
|
gotMaxAge := getMaxAge(claims)
|
|
|
|
if !compareMaxAge(expectMaxAge, gotMaxAge) {
|
|
|
|
// Additional check to automatically invalidate
|
|
|
|
// any previous jwt maxAge setting change.
|
|
|
|
// In-short, if the time.Now().Add j.MaxAge
|
|
|
|
// does not match the "iat" (issued at) then we invalidate the token.
|
|
|
|
return nil, ErrInvalidMaxAge
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// For other standard JWT claims fields such as "exp"
|
|
|
|
// The developer can just add a field of Expiry *NumericDate `json:"exp"`
|
|
|
|
// and will be filled by the parsed token automatically.
|
|
|
|
// No need for more interfaces.
|
2020-05-27 11:02:17 +02:00
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
err = validateClaims(ctx, dest, claims, expected)
|
|
|
|
if err != nil {
|
|
|
|
if err == ErrExpired {
|
|
|
|
// If token was expired remove it from the block list.
|
|
|
|
if j.Blocklist != nil {
|
|
|
|
j.Blocklist.Del(token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, err
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
if j.Blocklist != nil {
|
|
|
|
// If token exists in the block list, then stop here.
|
|
|
|
if j.Blocklist.Has(token) {
|
|
|
|
return nil, ErrBlocked
|
|
|
|
}
|
|
|
|
}
|
2020-05-27 11:02:17 +02:00
|
|
|
|
2020-10-18 16:15:29 +02:00
|
|
|
if !ignoreDest {
|
|
|
|
if ut, ok := dest.(TokenSetter); ok {
|
|
|
|
// The u.Token is empty even if we set it and export it on JSON structure.
|
|
|
|
// Set it manually.
|
|
|
|
ut.SetToken(token)
|
|
|
|
}
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// Set the information.
|
|
|
|
tokenInfo := &TokenInfo{
|
|
|
|
RequestToken: token,
|
|
|
|
Claims: claims,
|
|
|
|
Value: dest,
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
return tokenInfo, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TokenPair holds the access token and refresh token response.
|
|
|
|
type TokenPair struct {
|
|
|
|
AccessToken string `json:"access_token"`
|
|
|
|
RefreshToken string `json:"refresh_token"`
|
|
|
|
}
|
|
|
|
|
2020-10-18 13:42:19 +02:00
|
|
|
type tokenWithMaxAge struct {
|
|
|
|
// Useful to separate access from refresh tokens.
|
|
|
|
// Can be used to by-pass the internal check of expected
|
|
|
|
// MaxAge setting to match the token's received max age too.
|
|
|
|
MaxAge time.Duration `json:"tokenMaxAge"`
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// TokenPair generates a token pair of access and refresh tokens.
|
|
|
|
// The first two arguments required for the refresh token
|
|
|
|
// and the last one is the claims for the access token one.
|
|
|
|
func (j *JWT) TokenPair(refreshMaxAge time.Duration, refreshClaims interface{}, accessClaims interface{}) (TokenPair, error) {
|
2020-10-18 13:42:19 +02:00
|
|
|
if refreshMaxAge <= j.MaxAge {
|
|
|
|
return TokenPair{}, fmt.Errorf("refresh max age should be bigger than access token's one[%d - %d]", refreshMaxAge, j.MaxAge)
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
accessToken, err := j.Token(accessClaims)
|
2020-05-27 11:02:17 +02:00
|
|
|
if err != nil {
|
2020-10-17 05:40:17 +02:00
|
|
|
return TokenPair{}, err
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-18 13:42:19 +02:00
|
|
|
c, err := normalize(refreshClaims)
|
|
|
|
if err != nil {
|
|
|
|
return TokenPair{}, err
|
|
|
|
}
|
|
|
|
if c == nil {
|
|
|
|
c = make(context.Map)
|
|
|
|
}
|
|
|
|
// need to validate against its value instead of the setting's one (see `VerifyTokenString`).
|
|
|
|
c["tokenMaxAge"] = refreshMaxAge
|
|
|
|
|
|
|
|
refreshToken, err := j.token(refreshMaxAge, c)
|
2020-10-17 05:40:17 +02:00
|
|
|
if err != nil {
|
|
|
|
return TokenPair{}, nil
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
pair := TokenPair{
|
|
|
|
AccessToken: accessToken,
|
|
|
|
RefreshToken: refreshToken,
|
|
|
|
}
|
|
|
|
|
|
|
|
return pair, nil
|
2020-05-27 11:02:17 +02:00
|
|
|
}
|
2020-06-01 05:57:52 +02:00
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// Verify returns a middleware which
|
|
|
|
// decrypts an incoming request token to the result of the given "newPtr".
|
|
|
|
// It does write a 401 unauthorized status code if verification or decryption failed.
|
|
|
|
// It calls the `ctx.Next` on verified requests.
|
2020-06-01 05:57:52 +02:00
|
|
|
//
|
2020-10-17 05:40:17 +02:00
|
|
|
// Iit unmarshals the token to the specific type returned from the given "newPtr" function.
|
|
|
|
// It sets the Context User and User's Token too. So the next handler(s)
|
|
|
|
// of the same chain can access the User through a `Context.User()` call.
|
2020-06-01 05:57:52 +02:00
|
|
|
//
|
2020-10-17 05:40:17 +02:00
|
|
|
// Note unlike `VerifyToken`, this method automatically protects
|
|
|
|
// the claims with JSON required tags (see `MeetRequirements` Expection).
|
2020-06-01 05:57:52 +02:00
|
|
|
//
|
2020-10-17 05:40:17 +02:00
|
|
|
// On verified tokens:
|
|
|
|
// - The information can be retrieved through `Get` and `GetTokenInfo` functions.
|
|
|
|
// - User is set if the newPtr returns a valid Context User
|
|
|
|
// - The Context Logout method is set if Blocklist was initialized
|
|
|
|
// Any error is captured to the Context,
|
|
|
|
// which can be retrieved by a `ctx.GetErr()` call.
|
2020-10-17 14:22:42 +02:00
|
|
|
//
|
|
|
|
// See `VerifyJSON` too.
|
2020-10-17 05:40:17 +02:00
|
|
|
func (j *JWT) Verify(newPtr func() interface{}, expections ...Expectation) context.Handler {
|
2020-10-17 14:22:42 +02:00
|
|
|
if newPtr == nil {
|
|
|
|
newPtr = func() interface{} {
|
|
|
|
// Return a map here as the default type one,
|
|
|
|
// as it does allow .Get callers to access its fields with ease
|
|
|
|
// (although, I always recommend using structs for type-safety and
|
|
|
|
// also they can accept a required tag option too).
|
|
|
|
return &context.Map{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
expections = append(expections, MeetRequirements(newPtr()))
|
|
|
|
|
|
|
|
return func(ctx *context.Context) {
|
|
|
|
ptr := newPtr()
|
|
|
|
|
|
|
|
tokenInfo, err := j.VerifyToken(ctx, ptr, expections...)
|
2020-06-01 05:57:52 +02:00
|
|
|
if err != nil {
|
2020-10-17 05:40:17 +02:00
|
|
|
ctx.Application().Logger().Debugf("iris.jwt.Verify: %v", err)
|
|
|
|
ctx.StopWithError(401, context.PrivateError(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if u, ok := ptr.(context.User); ok {
|
|
|
|
ctx.SetUser(u)
|
|
|
|
}
|
|
|
|
|
|
|
|
if j.Blocklist != nil {
|
|
|
|
ctx.SetLogoutFunc(j.Invalidate)
|
2020-06-01 05:57:52 +02:00
|
|
|
}
|
2020-10-17 05:40:17 +02:00
|
|
|
|
|
|
|
ctx.Values().Set(tokenInfoContextKey, tokenInfo)
|
|
|
|
ctx.Next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-17 14:22:42 +02:00
|
|
|
// VerifyJSON works like `Verify` but instead it
|
|
|
|
// binds its "newPtr" function to return a raw JSON message.
|
|
|
|
// This allows the caller to bind this JSON message to any Go structure (or map).
|
|
|
|
// This is useful when we can accept more than one
|
|
|
|
// type of JWT token in the same request path,
|
|
|
|
// but we also want to keep type safety.
|
|
|
|
// Usage:
|
|
|
|
// app.Use(jwt.VerifyJSON())
|
|
|
|
// Inside a route Handler:
|
|
|
|
// claims := struct { Roles []string `json:"roles"`}{}
|
|
|
|
// jwt.ReadJSON(ctx, &claims)
|
|
|
|
// ...access to claims.Roles as []string
|
|
|
|
func (j *JWT) VerifyJSON(expections ...Expectation) context.Handler {
|
|
|
|
return j.Verify(func() interface{} {
|
|
|
|
return new(json.RawMessage)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReadJSON is a helper which binds "claimsPtr" to the
|
|
|
|
// raw JSON token claims.
|
|
|
|
// Use inside the handlers when `VerifyJSON()` middleware was registered.
|
|
|
|
func ReadJSON(ctx *context.Context, claimsPtr interface{}) error {
|
|
|
|
v := Get(ctx)
|
|
|
|
if v == nil {
|
|
|
|
return ErrMissing
|
|
|
|
}
|
|
|
|
data, ok := v.(json.RawMessage)
|
|
|
|
if !ok {
|
|
|
|
return ErrMissing
|
|
|
|
}
|
|
|
|
return Unmarshal(data, claimsPtr)
|
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
// NewUser returns a new User based on the given "opts".
|
|
|
|
// The caller can modify the User until its `GetToken` is called.
|
|
|
|
func (j *JWT) NewUser(opts ...UserOption) *User {
|
|
|
|
u := &User{
|
|
|
|
j: j,
|
|
|
|
SimpleUser: &context.SimpleUser{
|
|
|
|
Authorization: "IRIS_JWT_USER", // Used to separate a refresh token with a user/access one too.
|
|
|
|
Features: []context.UserFeature{
|
|
|
|
context.TokenFeature,
|
|
|
|
},
|
|
|
|
},
|
2020-06-01 05:57:52 +02:00
|
|
|
}
|
|
|
|
|
2020-10-17 05:40:17 +02:00
|
|
|
for _, opt := range opts {
|
|
|
|
opt(u)
|
|
|
|
}
|
|
|
|
|
|
|
|
return u
|
|
|
|
}
|
|
|
|
|
|
|
|
// VerifyUser works like the `Verify` method but instead
|
|
|
|
// it unmarshals the token to the specific User type.
|
|
|
|
// It sets the Context User too. So the next handler(s)
|
|
|
|
// of the same chain can access the User through a `Context.User()` call.
|
|
|
|
func (j *JWT) VerifyUser() context.Handler {
|
|
|
|
return j.Verify(func() interface{} {
|
|
|
|
return new(User)
|
|
|
|
})
|
2020-06-01 05:57:52 +02:00
|
|
|
}
|