jwit

package module
v0.1.0-beta Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 2, 2020 License: Apache-2.0 Imports: 18 Imported by: 1

README

JWIT

JWIT is a tiny Go library built around go-jose that brings JSON Web Tokens (JWTs) and JSON Web Key Sets (JWKS) into your apps.

JWIT features:

  • A high-level API to sign and verify your asymmetric JWTs.
  • A high-level API to publish your public JWKS.

💡 As JWIT sticks to the standards and is not tight to any framework, you can actually pick which features you want to use. You can use it to just sign JWTs, just verify JWTs you get from a third-party and your own servers, or just expose your public JWKS.

One neat use-case:

  1. Your authorization server uses JWIT to sign new JWTs.
  2. Your authorization server uses JWIT to expose its public keys as a JWKS (usually at /.well-known/jwks.json).
  3. Your resource server uses JWIT to unmarshal incoming JWTs and validate them against your authorization server's JWKS.

🤯 JWIT will automatically catch changes to the JWKS. Rotating your secrets has never been so easy.

Installation

go get github.com/gilbsgilbs/jwit

Overview

This section shows a few basic examples that'll give you a sneak peak of how simple it is to work with JWIT. For more in-depth examples (such as working with private claims, loading keys from PEM, …), please head to the godoc page.

Create a signed JWT
// 1. Create a signer from a JSON Web Key Set (JWKS). The JWKS payload will typically reside your
//    authorization server's config or in a secure vault.
signer, err := jwit.NewSigner([]byte(`{"keys": [ ... ]}`))

// 2. Create a JWT that expires in one hour.
rawJWT, err := signer.SignJWT(jwit.C{Duration: 1 * time.Hour})

// 3. That's it, simple as that. rawJWT is a signed JWT token that is ready to serve.
fmt.Println(rawJWT)
Verify a JWT
// 1. Create a verifier
verifier, err := jwit.NewVerifier(
    // Recommended: specify an URL to the issuer's public JWKS.
    //              this will allow JWIT to catch changes to the JWKS.
    &jwit.Issuer{
        // This should correspond to the "iss" claims of the JWTs
        Name: "myVeryOwnIssuer",

        // This is an HTTP(S) URL where the authorization server publishes its public keys.
        // It will be queried the first time a JWT is verified and then periodically.
        JWKSURL: "https://my-very-own-issuer.com/.well-known/jwks.json",

        // You can specify how long the issuer's public keys should be kept in cache.
        // Passed that delay, the JWKS will be re-fetched once asynchronously.
        // Defaults to 24 hours.
        TTL: 10 * time.Hour,
    },
    // Alternatively: pass in public keys directly.
    &jwit.Issuer{
        Name: "myOtherIssuer",
        PublicKeys: []interface{}{
            // using Go's crypto types
            rsaPublicKey,
            ecdsaPublicKey,
            // or using a marshalled JWKS JSON
            []byte(`{"keys": [ … your JWKS … ]}`),
            // or using marshalled PEM blocks
            []byte(`-----BEGIN RSA PUBLIC KEY----- ... -----END RSA PUBLIC KEY-----`),
        },
    },
    // ... you can specify as many issuer as you want
)

// 2. Verify the JWT using its "iss" claim.
isValid, err := verifier.VerifyJWT(rawJWT)
Expose the public JWKS
http.HandleFunc(
    "/.well-known/jwks.json",
    func (w http.ResponseWriter, req *http.Request) {
        // Just get the public JWKS from the signer.
        jwks, err := signer.DumpPublicJWKS()
        if err != nil {
            panic(err)
        }

        // And write it to the response body
        w.Write(jwks)
    },
)

🔒 Security

If you found a security vulnerability in JWIT itslef, do not reveal it publicly and adopt a responsible disclosure. You may open a GitHub issue stating that you found a vulnerability and specifying a safe way to get in touch with you.

Note that JWIT is not another JWT/JWKS implementation by any mean. JWIT relies on go-jose, a popular JWx implementation by Square. On top of that, go-jose and Go's stdlib are the only dependencies to this library. This greatly reduces the attack surface of JWIT. If you found a security vulnerability in go-jose , plese refer to their bug bounty program.

Documentation

Overview

JWIT makes it easy to work with JWKS and asymmetric JWTs in your apps.

Checkout https://github.com/gilbsgilbs/jwit for a quick overview.

Example (ExposeJWKS)

This example shows how you can expose your public JWKS to the world.

package main

import (
	"fmt"
	"net/http"

	"github.com/gilbsgilbs/jwit"
)

func main() {
	// All JWKS listed in NewSigner() will be merged into one JWKS when calling "DumpPublicJWKS()".
	signer, _ := jwit.NewSigner(
		// The first argument must contain your (private) signing keys. If it contains more than one
		// signing keys, one will be picked at random each time you call `signJWT()`.
		[]byte(`{"keys": [ ... some private JSON Web Keys ... ]}`),

		// Following (variadic) arguments are optional and can be private or public keys.
		//
		// They won't be used to sign your JWTs, but will show as public keys in "DumpPublicJWKS()".
		// This is useful when you want to renew your signing keys as you need to make sure resource servers
		// still consider previously created tokens as valid (until they expire). Consequently, you'll usually
		// want to set these to your "old" signing keys, and eventually remove them.
		//
		// Refer to the example dedicated to signing keys renewal for details.
		[]byte(`{"keys": [ ... some JSON Web Keys ... ]}`),
		[]byte(`{"keys": [ ... other JSON Web Keys ... ]}`),
		[]byte(`{"keys": [ ... and so on ... ]}`),
	)

	privateJWKS, _ := signer.DumpSigningJWKS()
	fmt.Println("my private signing JWKS (to keep secret): ", string(privateJWKS))

	otherJWKS, _ := signer.DumpOtherJWKS()
	fmt.Println("my other JWKS (to keep secret):", string(otherJWKS))

	http.HandleFunc(
		"/.well-known/jwks.json",
		func(w http.ResponseWriter, req *http.Request) {
			// This function exposes all the keys as public keys.
			jwks, err := signer.DumpPublicJWKS()
			if err != nil {
				panic(err)
			}
			_, _ = w.Write(jwks)
		},
	)

	_ = http.ListenAndServe(":8080", nil)
}
Output:

Example (SignJWT)

This is a simple example of how to sign a JWT from a JWKS containing private keys.

package main

import (
	"fmt"
	"time"

	"github.com/gilbsgilbs/jwit"
)

func main() {
	// Create a signer from a JSON Web Key Set (JWKS) containing private keys.
	// The JWKS payload will typically reside your authorization server's config or in a secure vault.
	signer, _ := jwit.NewSigner([]byte(`{"keys": [ ... private JSON Web Keys ... ]}`))

	// You can optionnaly assign default claims to this signer.
	signer.DefaultClaims.Issuer = "My Authorization Server"
	signer.DefaultClaims.Duration = 1 * time.Hour // Shorthand for "Expiry: time.Now().Add(1 * time.Hour)"

	// Create a JWT that expires in one hour
	rawJWT, _ := signer.SignJWT(jwit.C{
		// You can optionnaly override default claims here
		// Duration: 1 * time.Hour,
	})

	// And that's all!
	fmt.Println("JWT:", rawJWT)
}
Output:

Example (SignJWTWithPrivateClaims)

This example shows how to sign a JWT with private claims .

package main

import (
	"fmt"
	"time"

	"github.com/gilbsgilbs/jwit"
)

func main() {
	type MyCustomClaims struct {
		Payload string `json:"payload"`
		IsAdmin bool   `json:"is_admin"`
	}

	signer, _ := jwit.NewSigner([]byte(`{"keys": [ ... ]}`))
	signer.DefaultClaims.Duration = 1 * time.Hour

	// Create a JWT with two custom claims
	rawJWT, _ := signer.SignJWT(
		jwit.C{},
		MyCustomClaims{
			Payload: "custom data",
			IsAdmin: false,
		},
	)

	// Done!
	fmt.Println("JWT:", rawJWT)
}
Output:

Example (SignerFromGoCrypto)

This example shows how to create a new signer using private keys from go's crypto package.

package main

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/rsa"
	"fmt"

	"github.com/gilbsgilbs/jwit"
)

func main() {
	var ecdsaPrivateKey *ecdsa.PrivateKey
	var rsaPrivateKey *rsa.PrivateKey

	// Just create the signer
	signer, err := jwit.NewSignerFromCryptoKeys(
		[]crypto.PrivateKey{
			ecdsaPrivateKey,
			rsaPrivateKey,
			// ... and so on
		},
	)
	if err != nil {
		panic(err)
	}

	// you can then use your signer normally
	rawJWT, _ := signer.SignJWT(jwit.C{})

	// and serve this token.
	fmt.Println(rawJWT)
}
Output:

Example (SignerFromPEM)

This shows how you can create a new signer using a PEM file as signing keys. Note however that it is recommended you used JWKS instead.

package main

import (
	"fmt"
	"io/ioutil"
	"path"

	"github.com/gilbsgilbs/jwit"
)

func main() {
	// Read a standard PEM file that may contain multiple private keys.
	// If the file contains multiple keys, one will be picked at random each time you sign a token.
	pemBytes, _ := ioutil.ReadFile(path.Join("myPrivateKeys.pem"))

	// Signer will detect PEM data vs JWKS data, so you can just do:
	signer, err := jwit.NewSigner(pemBytes)
	// Or if you prefer being explicit:
	// signer, err := jwit.NewSignerFromPEM(pemBytes)
	if err != nil {
		panic(err)
	}

	// you can then use your signer normally
	rawJWT, _ := signer.SignJWT(jwit.C{})

	// and serve this token.
	fmt.Println(rawJWT)
}
Output:

Example (SignerGracefullyRenewSigningKeys)

This example explains step-by-step how to gracefully renew signing keys.

package main

import (
	"fmt"

	"github.com/gilbsgilbs/jwit"
)

func main() {
	// Let's say your authorization server uses this signer:
	signer, _ := jwit.NewSigner(
		[]byte(`{"keys": [ ... old signing keys ... ]}`),
	)

	// Your authorization server exposes the public keys corresponding to this signer
	// at "/.well-known/jwks.json". Your resource servers use this public JWKS (tied to your
	// old signing keys) to verify the JWTs:
	publicJwks, _ := signer.DumpPublicJWKS()
	fmt.Println("/.well-known/jwks.json => ", string(publicJwks))

	// Now, you can safely replace your signer with this one:
	signer, _ = jwit.NewSigner(
		[]byte(`{"keys": [ ... old signing keys ... ]}`),
		[]byte(`{"keys": [ ... new signing keys ... ]}`),
	)

	// this signer will still sign the JWTs with the same keys as before, but declare the new
	// public signing keys at /.well-known/jwks.json.

	// So this should list the old signing keys along with the new ones.
	publicJwks, _ = signer.DumpPublicJWKS()
	fmt.Println("/.well-known/jwks.json => ", string(publicJwks))

	// You then need to wait for the new public keys to propagate across all your resource servers.
	// How long you need to wait depends on the TTL each resource server has defined.

	// ...

	// Once all resource servers have refreshed the JWKS, you can sign the JWTs with your new keys:
	signer, _ = jwit.NewSigner(
		[]byte(`{"keys": [ ... new signing keys ... ]}`),
		[]byte(`{"keys": [ ... old signing keys ... ]}`),
	)

	// This new signer will sign the JWTs with the new signing keys, but keep the old keys declared
	// in the public JWKS.

	// So this should list should not have changed compared to the previous time:
	publicJwks, _ = signer.DumpPublicJWKS()
	fmt.Println("/.well-known/jwks.json => ", string(publicJwks))

	// Now you need to wait again for all your tokens signed with the old signing keys to expire.
	// How long you need to wait depends on the value of the "exp" claim for each token.

	// ...

	// Finally, you can revoke the old signing keys:
	signer, _ = jwit.NewSigner(
		[]byte(`{"keys": [ ... new signing keys ... ]}`),
	)

	// And this will only list the new public signing keys:
	publicJwks, _ = signer.DumpPublicJWKS()
	fmt.Println("/.well-known/jwks.json => ", string(publicJwks))

	// 👏👏👏👏👏👏👏👏👏👏👏👏👏
}
Output:

Example (VerifyJWT)
package main

import (
	"crypto/ecdsa"
	"crypto/rsa"
	"time"

	"github.com/gilbsgilbs/jwit"
)

func main() {
	var rsaPublicKey *rsa.PublicKey
	var ecdsaPublicKey *ecdsa.PublicKey

	// Create a verifier
	verifier, _ := jwit.NewVerifier(
		// Recommended: specify an URL to the issuer's public JWKS.
		//              this will allow JWIT to catch changes to the JWKS.
		&jwit.Issuer{
			// This should correspond to the "iss" claims of the JWTs
			Name: "myVeryOwnIssuer",

			// This is an HTTP(S) URL where the authorization server publishes its public keys.
			// It will be queried the first time a JWT is verified and then periodically.
			JWKSURL: "https://my-very-own-issuer.com/.well-known/jwks.json",

			// You can specify how long the issuer's public keys should be kept in cache.
			// Passed that delay, the JWKS will be re-fetched once asynchronously.
			// Defaults to 24 hours.
			TTL: 10 * time.Hour,
		},
		// Alternatively: pass in public keys directly.
		&jwit.Issuer{
			Name: "myOtherIssuer",
			PublicKeys: []interface{}{
				// using Go's crypto types
				rsaPublicKey,
				ecdsaPublicKey,
				// or using a marshalled JWKS JSON
				[]byte(`{"keys": [ … your JWKS … ]}`),
				// or using marshalled PEM blocks
				[]byte(`-----BEGIN RSA PUBLIC KEY----- ... -----END RSA PUBLIC KEY-----`),
			},
		},
		// ... you can specify as many issuer as you want
	)

	// You typically get this from a Cookie or Authorization header.
	rawJWT := "ey[...]pX.ey[...]DI.Sf[...]5c"

	// Verify the JWT using its "iss" claim
	isValid, _ := verifier.VerifyJWT(rawJWT)

	if isValid {
		// do stuff
	}
}
Output:

Example (VerifyJWTUnmarshalPrivateClaims)

This example shows how to unmarshal private claims from a JWT.

package main

import (
	"github.com/gilbsgilbs/jwit"
)

func main() {
	type MyCustomClaims struct {
		Payload string `json:"payload"`
		IsAdmin bool   `json:"is_admin"`
	}

	verifier, _ := jwit.NewVerifier()
	rawJWT := "ey[...]pX.ey[...]DI.Sf[...]5c"

	var myCustomClaims MyCustomClaims
	// if the JWT is valid, the claims will be unmarshaled into myCustomClaims
	isValid, _ := verifier.VerifyJWT(rawJWT, &myCustomClaims)

	if isValid {
		// do stuff
	}
}
Output:

Example (VerifyJWTWithGoCryptoKeys)

Verifying a JWT against a specific set of keys can sometimes be useful (for example if the JWT issuer doesn't provide an "iss" claim or if you don't know the issuer's public key in advance). This example demonstrates how you can validate a JWT using your own set of public keys on an existing verifier.

package main

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/rsa"

	"github.com/gilbsgilbs/jwit"
)

func main() {
	var someECDSAPublicKey *ecdsa.PublicKey
	var someRSAPublicKey *rsa.PublicKey

	// Create a new empty verifyer
	verifier, _ := jwit.NewVerifier()

	// You'll typically get this from a Cookie or Authorization header.
	rawJWT := "ey[...]pX.ey[...]DI.Sf[...]5c"

	// If any of the RSA or ECDSA key was used to sign the JWT, isValid will be set to true.
	isValid, _ := verifier.VerifyJWTWithKeys(
		rawJWT,
		[]crypto.PublicKey{someECDSAPublicKey, someRSAPublicKey},
	)

	if isValid {
		// do stuff
	}
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnknownIssuer indicates that token is not issued by a known issuer.
	ErrUnknownIssuer = errors.New("jwit: the JWT issuer is unknown")

	// ErrUnexpectedSignature indicates that none of the issuer's JWK was used to sign the token.
	ErrUnexpectedSignature = errors.New("jwit: the JWT wasn't signed with a known signature")

	// ErrJWKSFetchFailed indicates that we got a non-2xx HTTP status whil fetching the JWKS.
	ErrJWKSFetchFailed = errors.New("jwit: couldn't fetch JWKS from server")
)

Verifier errors

View Source
var (
	// ErrUnknownKeyType indicates that the provided public/private key type is unknown.
	ErrUnknownKeyType = errors.New("jwit: provided public/private key type is unknown")
)

Signer errors

Functions

This section is empty.

Types

type C

type C = RegisteredClaims

C is a shortcut for RegisteredClaims.

type Issuer

type Issuer struct {
	sync.Mutex

	// Name is the name of the issuer (corresponding to a "iss" claim)
	Name string

	// PublicKeys is a set of known public keys for this issuer.
	PublicKeys []interface{}

	// JWKSURL is an URL where the issuer publishes its JWKS.
	JWKSURL string

	// TTL defines how long a JWKS are considered "fresh". Past that TTL, jwit will try
	// to refresh the JWKS asynchronously.
	TTL time.Duration
	// contains filtered or unexported fields
}

Issuer is a third-party that publishes a set of public keys.

type RegisteredClaims

type RegisteredClaims struct {
	// Duration is provided as an alternative to the Expiration Time ("exp") claim.
	Duration time.Duration

	Issuer    string    // iss
	Subject   string    // sub
	Audience  []string  // aud
	Expiry    time.Time // exp
	NotBefore time.Time // nbf
	IssuedAt  time.Time // iat
	ID        string    // jit
}

RegisteredClaims as per RFC7919. See https://tools.ietf.org/html/rfc7519#section-4.1

type Signer

type Signer struct {
	DefaultClaims RegisteredClaims
	// contains filtered or unexported fields
}

Signer will help you sign your JWTs and expose your public JWKS.

func NewSigner

func NewSigner(signingKeysBytes []byte, otherKeysBytes ...[]byte) (*Signer, error)

NewSigner creates a new signer from marshalled JWKS or PEM payloads. signingBytes are the JWKS or PEM private keys that will be used to sign the JWTs. otherBytes can contains public or private JWKs or PEMs that will be used in addition to signing keys to expose your public JWKS.

func NewSignerFromCryptoKeys

func NewSignerFromCryptoKeys(signingKeys []crypto.PrivateKey, otherKeys ...interface{}) (*Signer, error)

NewSignerFromCrytoKeys creates a new Signer from go crypto keys. Signing keys are private keys that will be picked at random to sign new JWTs. Other keys can be public or private keys and will be used in addition to signing keys to expose your public JWKS.

func NewSignerFromJWKS

func NewSignerFromJWKS(signingJWKSBytes []byte, otherJWKSBytes ...[]byte) (*Signer, error)

NewSignerFromJWKS creates a new signer from marshalled JWKS payloads. Signing JWKS are the JWKS that will be used to sign the JWTs. Other JWKS can contains public or private JWKs that will be used in addition to signing keys to expose your public JWKS.

func NewSignerFromPEM

func NewSignerFromPEM(signingPEMBytes []byte, otherPEMBytes ...[]byte) (*Signer, error)

NewSignerFromPEM creates a new signer from PEM-encoded data. Signing PEMs are the private keys that will be used to sign the JWTs. Other PEMs can contain public or private keys that will be used in addition to signing keys to expose your public JWKS.

func (*Signer) DumpOtherJWKS

func (signer *Signer) DumpOtherJWKS() ([]byte, error)

DumpOtherJWKS returns the other JWKS for this signer.

/!\ Do not make this key public /!\

func (*Signer) DumpPublicJWKS

func (signer *Signer) DumpPublicJWKS() ([]byte, error)

DumpPublicJWKS returns the JWKS corresponding to the public keys of this signer. The return value of this function is safe to expose publicly (usually at /.well-known/jwks.json)

func (*Signer) DumpSigningJWKS

func (signer *Signer) DumpSigningJWKS() ([]byte, error)

DumpSigningJWKS returns the signing JWKS for this signer.

/!\ Do not make this key public /!\

func (*Signer) SignJWT

func (signer *Signer) SignJWT(registeredClaims C, privateClaims ...interface{}) (string, error)

SignJWT returns a signed JWT with one of signer's signing keys picked at random.

type Verifier

type Verifier struct {
	// Issuers a set of trusted issuers, mapped by name (corresponding to the "iss" claim).
	Issuers map[string]*Issuer

	// The HTTP client used to fetch issuer's JWKS. By default, doesn't follow any redirections.
	HttpClient *http.Client
}

Verifier can verify a JWT validity. Don't create verifiers directly, use jwit.NewVerifier* helpers instead.

func NewVerifier

func NewVerifier(issuers ...*Issuer) (*Verifier, error)

New creates a new JWIT Verifier given a set of truster issuers.

func (*Verifier) VerifyJWT

func (verifier *Verifier) VerifyJWT(rawJWT string, dest ...interface{}) (bool, error)

VerifyJWT verifies whether the provided raw JWT is valid and was signed using any of the public keys that are known from the JWT issuer. If the JWT is valid (i.e. comes from a known issuer, was signed by any of the known issuer's public keys and is under its validity period), true is returned. If it is valid and dest is non-nil, the JWT claims are unmarshalled into dest. If the JWT doesn't come from a known issuer, ErrUnknownIssuer is returned.

func (*Verifier) VerifyJWTWithKeys

func (verifier *Verifier) VerifyJWTWithKeys(
	rawJWT string,
	publicKeys []crypto.PublicKey,
	dest ...interface{},
) (bool, error)

VerifyJWTWithKeys verifies whether the provided raw JWT is valid and was signed using any of the provided public keys. If the JWT is valid (i.e. comes from a known issuer, was signed by any of the provided public keys and is under its validity period), true is returned. If it is valid and dest is non-nil, the JWT claims are unmarshalled into dest.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL