- Notifications
You must be signed in to change notification settings - Fork20
A fast and simple JWT implementation for Go
License
kataras/jwt
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Zero-dependency lighweight, fast and simpleJWT & JWKS implementation written inGo. This package was designed with security, performance and simplicity in mind, it protects your tokens fromcritical vulnerabilities that you may find in other libraries.
Pleasestar this open source project to attract more developers so that together we can improve it even more!
The only requirement is theGo Programming Language.
$ go get github.com/kataras/jwt@latest
Import asimport "github.com/kataras/jwt"
and use it asjwt.XXX
.
- Getting started
- Sign a Token
- Verify a Token
- Block a Token
- Token Pair
- JSON Web Algorithms
- Encryption
- Benchmarks
- Examples
- References
- License
Sign and generate a token with theSign
method, returns the token in compact form. Optionally set an expiration, if"exp"
is missing from the payload use thejwt.MaxAge
helper. Verify the token with theVerify
method, returns aVerifiedToken
value. Decode the custom claims with theVerifiedToken.Claims
method. Extremely easy!
package mainimport ("time""github.com/kataras/jwt")// Keep it secret.varsharedKey= []byte("sercrethatmaycontainch@r$32chars")typeFooClaimsstruct {Foostring`json:"foo"`}funcmain() {// Generate a token which expires at 15 minutes from now:myClaims:=FooClaims{Foo:"bar",}// can be a map too.token,err:=jwt.Sign(jwt.HS256,sharedKey,myClaims,jwt.MaxAge(15*time.Minute))iferr!=nil {panic(err)}// Verify and extract claims from a token:verifiedToken,err:=jwt.Verify(jwt.HS256,sharedKey,token)iferr!=nil {panic(err)}varclaimsFooClaimserr=verifiedToken.Claims(&claims)iferr!=nil {panic(err)}print(claims.Foo)}
The package contains comments on each one of its exported functions, structures and variables, therefore, for a more detailed technical documentation please refer togodocs.
Signing and Verifying a token is an extremely easy process.
Signing a Token is done through theSign
package-level function.
varsharedKey= []byte("sercrethatmaycontainch@r$32chars")
typeUserstruct {Usernamestring`json:"username"`}
userClaims:=User {Username:"kataras",}token,err:=jwt.Sign(jwt.HS256,sharedkey,userClaims,jwt.MaxAge(15*time.Minute))
[1]
The first argument is the signingalgorithm to create the signature part.[2]
The second argument is the private key (or shared key, when symmetric algorithm was chosen) will be used to create the signature.[3]
The third argument is the JWT claims. The JWT claims is the payload part and it depends on your application's requirements, there you can set custom fields (and expiration) that you can extract to another request of the same authorized client later on. Note that the claims can beany Go type, including customstruct
,map
and raw[]byte
.[4]
The last variadic argument is a type ofSignOption
(MaxAge
function andClaims
struct are both valid sign options), can be used to merge custom claims with the standard ones.Returns
the encoded token, ready to be sent and stored to the client.
Thejwt.MaxAge
is a helper which sets thejwt.Claims.Expiry
andjwt.Claims.IssuedAt
for you.
Example Code to manually set all claims using a standardmap
:
now:=time.Now()claims:=map[string]any{"iat":now.Unix(),"exp":now.Add(15*time.Minute).Unix(),"foo":"bar",}token,err:=jwt.Sign(jwt.HS256,sharedKey,claims)
See
SignWithHeader
too.
Example Code to merge map claims with standard claims:
customClaims:= jwt.Map{"foo":"bar"}now:=time.Now()standardClaims:= jwt.Claims{Expiry:now.Add(15*time.Minute).Unix(),IssuedAt:now.Unix(),Issuer:"my-app",}token,err:=jwt.Sign(jwt.HS256,sharedKey,customClaims,standardClaims)
The
jwt.Map
is just atype alias, ashortcut, ofmap[string]any
.
At all cases, theiat(IssuedAt)
andexp(Expiry/MaxAge)
(andnbf(NotBefore)
) values will be validated automatically on theVerify
method.
Example Code to Sign & Verify a non-JSON payload:
token,err:=jwt.Sign(jwt.HS256,sharedkey, []byte("raw payload - no json here"))
If the payload is not a JSON one, then merging with standard claims is not possible, therefore options like
jwt.MaxAge
are not available.
verifiedToken,err:=jwt.Verify(jwt.HS256,sharedKey,token,jwt.Plain)// verifiedToken.Payload == raw contents
Again, if the received payload is not a JSON one, options like
jwt.Expected
orjwt.NewBlocklist
are not available as well.
Thejwt.Claims
we've shown above, looks like this:
typeClaimsstruct {// The opposite of the exp claim. A number representing a specific// date and time in the format “seconds since epoch” as defined by POSIX.// This claim sets the exact moment from which this JWT is considered valid.// The current time (see `Clock` package-level variable)// must be equal to or later than this date and time.NotBeforeint64`json:"nbf,omitempty"`// A number representing a specific date and time (in the same// format as exp and nbf) at which this JWT was issued.IssuedAtint64`json:"iat,omitempty"`// A number representing a specific date and time in the// format “seconds since epoch” as defined by POSIX6.// This claims sets the exact moment from which// this JWT is considered invalid. This implementation// allow for a certain skew between clocks// (by considering this JWT to be valid for a few minutes// after the expiration date, modify the `Clock` variable).Expiryint64`json:"exp,omitempty"`// A string representing a unique identifier for this JWT.// This claim may be used to differentiate JWTs with// other similar content (preventing replays, for instance).IDstring`json:"jti,omitempty"`// A string or URI that uniquely identifies the party// that issued the JWT.// Its interpretation is application specific// (there is no central authority managing issuers).Issuerstring`json:"iss,omitempty"`// A string or URI that uniquely identifies the party// that this JWT carries information about.// In other words, the claims contained in this JWT// are statements about this party.// The JWT spec specifies that this claim must be unique in// the context of the issuer or,// in cases where that is not possible, globally unique. Handling of// this claim is application specific.Subjectstring`json:"sub,omitempty"`// Either a single string or URI or an array of such// values that uniquely identify the intended recipients of this JWT.// In other words, when this claim is present, the party reading// the data in this JWT must find itself in the aud claim or// disregard the data contained in the JWT.// As in the case of the iss and sub claims, this claim is// application specific.Audience []string`json:"aud,omitempty"`}
Verifying a Token is done through theVerify
package-level function.
verifiedToken,err:=jwt.Verify(jwt.HS256,sharedKey,token)
See
VerifyWithHeaderValidator
too.
TheVerifiedToken
carries the token decoded information:
typeVerifiedTokenstruct {Token []byte// The original token.Header []byte// The header (decoded) part.Payload []byte// The payload (decoded) part.Signature []byte// The signature (decoded) part.StandardClaimsClaims// Standard claims extracted from the payload.}
To extract any custom claims, given on theSign
method, we use the result of theVerify
method, which is aVerifiedToken
pointer. This VerifiedToken has a single method, theClaims(dest any) error
one, which can be used to decode the claims (payload part) to a value of our choice. Again, that value can be amap
or anystruct
.
varclaims=struct {Foostring`json:"foo"`}{}// or a map.err:=verifiedToken.Claims(&claims)
By default expiration set and validation is done throughtime.Now()
. You can change that behavior through thejwt.Clock
variable, e.g.
jwt.Clock=time.Now().UTC
When more than one token with different claims can be generated based on the same algorithm and key, somehow you need to invalidate a token if its payload misses one or more fields of your custom claims structure. Although it's not recommended to use the same algorithm and key for generating two different types of tokens, you can do it, and to avoid invalid claims to be retrieved by your application's route handler this package offers the JSON,required
tag field. It checks if the claims extracted from the token's payload meet the requirements of the expectedstruct value.
The first thing we have to do is to change the defaultjwt.Unmarshal
variable to thejwt.UnmarshalWithRequired
, once at the init of the application:
funcinit() {jwt.Unmarshal=jwt.UnmarshalWithRequired}
The second thing, is to add the,required
json tag field to our struct, e.g.
typeuserClaimsstruct {Usernamestring`json:"username,required"`}
That's all, theVerifiedToken.Claims
method will throw anErrMissingKey
if the given token's payload does not meet the requirements.
A more performance-wise alternative tojson:"XXX,required"
is to add validators to check the standard claims values through aTokenValidator
or to check the custom claims manually after theVerifiedToken.Claims
method.
TheTokenValidator
interface looks like this:
typeTokenValidatorinterface {ValidateToken(token []byte,standardClaimsClaims,errerror)error}
The last argument ofVerify
/VerifyEncrypted
optionally accepts one or moreTokenValidator
. Available builtin validators:
Leeway(time.Duration)
Expected
Blocklist
TheLeeway
adds validation for a leeway expiration time.If the token was not expired then a comparison betweenthis "leeway" and the token's "exp" one is expected to pass instead (now+leeway > exp).Example of use case: disallow tokens that are going to be expired in 3 seconds from now,this is useful to make sure that the token is valid when the when the user fires a database call:
verifiedToken,err:=jwt.Verify(jwt.HS256,sharedKey,token,jwt.Leeway(3*time.Second))iferr!=nil {// err == jwt.ErrExpired}
TheExpected
performs simple checks between standard claims values. For example, disallow tokens that their"iss"
claim does not match the"my-app"
value:
verifiedToken,err:=jwt.Verify(jwt.HS256,sharedKey,token, jwt.Expected{Issuer:"my-app",})iferr!=nil {// errors.Is(jwt.ErrExpected, err)}
When a user logs out, the client app should delete the token from its memory. This would stop the client from being able to make authorized requests. But if the token is still valid and somebody else has access to it, the token could still be used. Therefore, a server-side invalidation is indeed useful for cases like that. When the server receives a logout request, take the token from the request and store it to theBlocklist
through itsInvalidateToken
method. For each authorized request thejwt.Verify
will check theBlocklist
to see if the token has been invalidated. To keep the search space small, the expired tokens are automatically removed from the Blocklist's in-memory storage.
Enable blocklist by following the three simple steps below.
1. Initialize a blocklist instance, clean unused and expired tokens every 1 hour.
blocklist:=jwt.NewBlocklist(1*time.Hour)
2. Add theblocklist
instance to thejwt.Verify
's last argument, to disallow blocked entries.
verifiedToken,err:=jwt.Verify(jwt.HS256,sharedKey,token,blocklist)// [err == jwt.ErrBlocked when the token is valid but was blocked]
3. Call theblocklist.InvalidateToken
whenever you want to block a specific authorized token. The method accepts the token and the expiration time should be removed from the blocklist.
blocklist.InvalidateToken(verifiedToken.Token,verifiedToken.StandardClaims)
By default the unique identifier is retrieved through the"jti"
(Claims{ID}
) and if that it's empty then the raw token is used as the map key instead. To change that behavior simply modify theblocklist.GetKey
field before theInvalidateToken
method.
A Token pair helps us to handle refresh tokens. It is a structure which holds both Access Token and Refresh Token. Refresh Token is long-live and access token is short-live. The server sends both of them at the first contact. The client uses the access token to access an API. The client can renew its access token by hitting a special REST endpoint to the server. The server verifies the refresh token andoptionally the access token which should returnErrExpired
, if it's expired or going to be expired in some time from now (Leeway
), and renders a new generated token to the client. There are countless resources online and different kind of methods for using a refresh token. Thisjwt
package offers just a helper structure which holds both the access and refresh tokens and it's ready to be sent and received to and from a client.
typeClientClaimsstruct {ClientIDstring`json:"client_id"`}
accessClaims:=ClientClaims{ClientID:"client-id"}accessToken,err:=jwt.Sign(alg,secret,accessClaims,jwt.MaxAge(10*time.Minute))iferr!=nil {// [handle error...]}refreshClaims:= jwt.Claims{Subject:"client",Issuer:"my-app"}refreshToken,err:=jwt.Sign(alg,secret,refreshClaims,jwt.MaxAge(time.Hour))iferr!=nil {// [handle error...]}tokenPair:=jwt.NewTokenPair(accessToken,refreshToken)
ThetokenPair
is JSON-compatible value, you can render it to a client and read it from a client HTTP request.
There are several types of signing algorithms available according to the JWA(JSON Web Algorithms) spec. The specification requires a single algorithm to be supported by all conforming implementations:
- HMAC using SHA-256, called
HS256
in the JWA spec.
The specification also defines a series of recommended algorithms:
- RSASSA PKCS1 v1.5 using SHA-256, called
RS256
in the JWA spec. - ECDSA using P-256 and SHA-256, called
ES256
in the JWA spec.
The implementation supportsall of the above plusRSA-PSS
and the newEd25519
. Navigate to thealg.go source file for details. In-short:
Choosing the best algorithm for your application needs is up to you, however, my recommendations follows.
- Already work with RSA public and private keys? Choose RSA(RS256/RS384/RS512/PS256/PS384/PS512) (length of produced token characters is bigger).
- If you need the separation between public and private key, choose ECDSA(ES256/ES384/ES512) or EdDSA. ECDSA and EdDSA produce smaller tokens than RSA.
- If you need performance and well-tested algorithm, choose HMAC(HS256/HS384/HS512) -the most common method.
The basic difference between symmetric and an asymmetric algorithmis that symmetric uses one shared key for both signing and verifying a token,and the asymmetric uses private key for signing and a public key for verifying.In general, asymmetric data is more secure because it uses different keysfor the signing and verifying process but it's slower than symmetric ones.
If you ever need to use your own JSON Web algorithm, just implement theAlg interface. Pass it onjwt.Sign
andjwt.Verify
functions and you're ready to GO.
Keys can be generated viaOpenSSL or through Go's standard library.
import ("crypto/rand""crypto/rsa""crypto/elliptic""crypto/ed25519")
// Generate HMACsharedKey:=make([]byte,32)_,_=rand.Read(sharedKey)// Generate RSAbitSize:=2048privateKey,_:=rsa.GenerateKey(rand.Reader,bitSize)publicKey:=&privateKey.PublicKey// Generace ECDSAc:=elliptic.P256()privateKey,_:=ecdsa.GenerateKey(c,rand.Reader)publicKey:=&privateKey.PublicKey// Generate EdDSApublicKey,privateKey,_:=ed25519.GenerateKey(rand.Reader)
Converting keys to PEM files is kind of easy task using the Go Programming Language, take a quick look at thePEM example for ed25519.
This package contains all the helpers you need to load and parse PEM-formatted keys.
All the available helpers:
// HMACMustLoadHMAC(filenameOrRawstring) []byte
// RSAMustLoadRSA(privFile,pubFilestring) (*rsa.PrivateKey,*rsa.PublicKey)LoadPrivateKeyRSA(filenamestring) (*rsa.PrivateKey,error)LoadPublicKeyRSA(filenamestring) (*rsa.PublicKey,error)ParsePrivateKeyRSA(key []byte) (*rsa.PrivateKey,error)ParsePublicKeyRSA(key []byte) (*rsa.PublicKey,error)
// ECDSAMustLoadECDSA(privFile,pubFilestring) (*ecdsa.PrivateKey,*ecdsa.PublicKey)LoadPrivateKeyECDSA(filenamestring) (*ecdsa.PrivateKey,error)LoadPublicKeyECDSA(filenamestring) (*ecdsa.PublicKey,error)ParsePrivateKeyECDSA(key []byte) (*ecdsa.PrivateKey,error)ParsePublicKeyECDSA(key []byte) (*ecdsa.PublicKey,error)
// EdDSAMustLoadEdDSA(privFile,pubFilestring) (ed25519.PrivateKey,ed25519.PublicKey)LoadPrivateKeyEdDSA(filenamestring) (ed25519.PrivateKey,error)LoadPublicKeyEdDSA(filenamestring) (ed25519.PublicKey,error)ParsePrivateKeyEdDSA(key []byte) (ed25519.PrivateKey,error)ParsePublicKeyEdDSA(key []byte) (ed25519.PublicKey,error)
Example Code:
import"github.com/kataras/jwt"
privateKey,publicKey:=jwt.MustLoadEdDSA("./private_key.pem","./public_key.pem")
claims:= jwt.Map{"foo":"bar"}maxAge:=jwt.MaxAge(15*time.Minute)token,err:=jwt.Sign(jwt.EdDSA,privateKey,claims,maxAge)
verifiedToken,err:=Verify(EdDSA,publicKey,token)
Embedded keys? No problem, just integrate the
jwt.ReadFile
variable which is just a type offunc(filename string) ([]byte, error)
.
JWE (encrypted JWTs) is outside the scope of this package, a wire encryption of the token's payload is offered to secure the data instead. If the application requires to transmit a token which holds private data then it needs to encrypt the data on Sign and decrypt on Verify. TheSignEncrypted
andVerifyEncrypted
package-level functions can be called to apply any type of encryption.
The package offers one of the most popular and common way to secure data; theGCM
mode + AES cipher. We follow theencrypt-then-sign
flow which most researchers recommend (it's safer as it preventspadding oracle attacks).
In-short, you need to call thejwt.GCM
and pass its result to thejwt.SignEncrypted
andjwt.VerifyEncrypted
:
// Replace with your own keys and keep them secret.// The "encKey" is used for the encryption and// the "sigKey" is used for the selected JSON Web Algorithm// (shared/symmetric HMAC in that case).var (encKey=MustGenerateRandom(32)sigKey=MustGenerateRandom(32))funcmain(){encrypt,decrypt,err:=GCM(encKey,nil)iferr!=nil {// [handle error...] }// Encrypt and Sign the claims:token,err:=SignEncrypted(jwt.HS256,sigKey,encrypt,claims,jwt.MaxAge(15*time.Minute))// [...]// Verify and decrypt the claims:verifiedToken,err:=VerifyEncrypted(jwt.HS256,sigKey,decrypt,token)// [...]}
Read more about GCM at:https://en.wikipedia.org/wiki/Galois/Counter_Mode
Here is what helped me to implement JWT in Go:
- The JWT RFC:https://tools.ietf.org/html/rfc7519
- The JWK RFC: Seehttps://tools.ietf.org/html/rfc7517#section-5
- The official JWT book, all you need to learn:https://auth0.com/resources/ebooks/jwt-handbook
- Create Your JWTs From Scratch (PHP):https://dzone.com/articles/create-your-jwts-from-scratch
- How to make your own JWT (Javascript):https://medium.com/code-wave/how-to-make-your-own-jwt-c1a32b5c3898
- Encode and Decode keys:https://golang.org/src/crypto/x509/x509_test.go (and its variants)
- The inspiration behind the "Blacklist" feature (I prefer to chose the word "Blocklist" instead):https://blog.indrek.io/articles/invalidate-jwt/
- We need JWT in the modern web:https://medium.com/swlh/why-do-we-need-the-json-web-token-jwt-in-the-modern-web-8490a7284482
- Best Practices of using JWT with GraphQL:https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/
This software is licensed under theMIT License.
About
A fast and simple JWT implementation for Go