jwt

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2021 License: MIT Imports: 18 Imported by: 1

README

JSON Web Token

Go Reference

import "github.com/mdigger/jwt"

// create a pattern and describe in it the things that we would like to
// include all the tokens
conf := jwt.Config{
	Issuer:  "me.mdigger.test",
	Expires:  time.Hour,
	Created: true,
	Key:     `top secret`,
}
// describe additional fields of token (structure)
data := jwt.JSON{
	"sub": "34529345",
	"email": "dmitrys@example.com",
	"name": "Dmitry Sedykh",
	"birthday": jwt.Time{time.Date(1971, time.December, 24, 0, 0, 0, 0, time.Local)},
}

// create and sign the token
token, err := conf.Token(data)
if err != nil {
	log.Fatalln("Error creating:", err)
}

// ---------------------------------------------------

// parse a token and get data
// if the token is not valid, then return an error
claim := make(jwt.JSON)
if err := jwt.Decode(token, &claim); err != nil {
	log.Fatalln("Error parsing:", err)
}

Documentation

Overview

Package jwt предоставляет возможности для удобного создания и проверки токенов в формате JWT.

Поддерживаются алгоритмы HS256, RS256 и ES256.

Делалось исключительно для себя и подход принципиально отличается от большинства существующих библиотек для работы с JWT: в первую очередь я пытался облегчить работу с токенами по типичному сценарию. Специально для генерации большого количества однотипных токенов сделан класс jwt.Config, который позволяет описать основные поля и временные метки токена, а потом быстро создавать токены, передавая только дополнительные данные.

Кроме того, мне очень не понравился подход в других библиотеках по созданию разных объектов для подписи разными алгоритмами, а особенно их "многословность". Здесь все просто: ключ абстрактного формата, а при генерации подписи автоматически используется подходящий алгоритм. Благодаря этому подходу все вспомогательные классы просто скрыты и не пугают выбором и настройкой.

Так же в библиотеку добавлены некоторые вспомогательные функции, которые могут пригодиться при работе с токенами:

- Time для разбора и представления времени в виде числа, а не строки;

- JSON для быстрого описания полей токена, когда не хочется создавать специально для этого структуру с описанием полей;

- NewRS256Key() и NewES256Key() для быстрой генерации ключей в формате RSA и ECDSA;

- Nonce() для генерации случайных строковых последовательностей заданной длины;

- Keys для работы со списками публичных ключей в формате JWKS.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrEmptySignKey    = errors.New("empty token sign key")
	ErrInvalid         = errors.New("invalid token")
	ErrBadType         = errors.New("bad token type")
	ErrNotSigned       = errors.New("token not signed")
	ErrCreatedAfterNow = errors.New("token created after now")
	ErrNotBeforeNow    = errors.New("token not before now")
	ErrExpired         = errors.New("token expired")
	ErrBadHashFunc     = errors.New("hash function for key is not available")
)

Ошибки создания и верификации токенов.

View Source
var RSAKeyBits = 2048

RSAKeyBits содержит длину ключа для генерации RSA. Используется в функции NewRS256Key.

Functions

func Decode

func Decode(token string, claimset interface{}) error

Decode декодирует содержимое токена в claimset. По ходу распаковки проверяются основные временные поля токена, но не подпись токена.

Example
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/mdigger/jwt"
)

var secret = "my secret sign key"

func GenerateTestToken() string {
	claimset := jwt.JSON{
		"iss":      "http://service.example.com/",
		"sub":      "2934852845",
		"iat":      jwt.Time{Time: time.Now()},
		"exp":      jwt.Time{Time: time.Now().Add(time.Hour)},
		"name":     "Dmitry Sedykh",
		"email":    "dmitrys@example.com",
		"birthday": jwt.Time{Time: time.Date(1971, time.December, 24, 8, 43, 0, 0, time.UTC)},
		"nonce":    jwt.Nonce(8)(),
	}

	var err error
	token, err := jwt.Encode(claimset, secret)
	if err != nil {
		panic(err)
	}

	return token
}

func main() {
	token := GenerateTestToken()

	// описываем структуру с данными, которые хотим распаковать из токена
	var claimset struct {
		Issuer   string   `json:"iss"`
		Subject  string   `json:"sub"`
		Created  jwt.Time `json:"iat"`
		Expired  jwt.Time `json:"exp"`
		Name     string
		Email    string
		Birthday jwt.Time // время представлено числовом виде
		Nonce    string
	}

	// извлекаем данные из токена
	if err := jwt.Decode(token, &claimset); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", claimset)
}
Output:

func Encode

func Encode(claimset, key interface{}) (string, error)

Encode возвращает подписанный с помощью ключа key токен из claimset. Если ключ не указан, то токен не будет подписан. Key может быть представлен в виде строки или ключа для RSA или ECDSA. А может быть представлен в виде функции, которая возвращает нужный ключ. Поддерживаются следующие форматы ключа:

*rsa.PrivateKey
*ecdsa.PrivateKey
string
[]byte
fmt.Stringer

Так же поддерживаются следующие форматы функции для передачи ключа:

func() interface{}
func() string, interface{}

В последних случаях, кроме ключа, так же возвращается его идентификатор.

Example
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/mdigger/jwt"
)

var secret = "my secret sign key"

func main() {
	// описываем данные токена (не обязательно в JSON)
	claimset := jwt.JSON{
		"iss":      "http://service.example.com/",
		"sub":      "2934852845",
		"iat":      jwt.Time{Time: time.Now()},
		"exp":      jwt.Time{Time: time.Now().Add(time.Hour)},
		"name":     "Dmitry Sedykh",
		"email":    "dmitrys@example.com",
		"birthday": jwt.Time{Time: time.Date(1971, time.December, 24, 0, 0, 0, 0, time.Local)},
		"nonce":    jwt.Nonce(8)(),
	}

	// создаем токен с подписью HS256 с секретным ключом
	token, err := jwt.Encode(claimset, secret)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("token:", token)
}
Output:

func NewES256Key

func NewES256Key() *ecdsa.PrivateKey

NewES256Key возвращает новый ключ для подписи в формате ESxxx. По умолчанию возвращается ES256 ключ, но это можно изменить через переменную ECDSACurve.

Вызывает panic в случае ошибки создания.

func NewHS256Key

func NewHS256Key(length int) []byte

NewHS256Key возвращает новый ключ для подписи в формате HS256 указанной длины.

Вызывает panic в случае ошибки создания.

func NewRS256Key

func NewRS256Key() *rsa.PrivateKey

NewRS256Key возвращает новый ключ для подписи в формате RS256. Длина ключа задается переменной RSAKeyBits.

Вызывает panic в случае ошибки создания.

func Nonce

func Nonce(length uint8) func() string

Nonce возвращает функцию, которая генерирует случайные псевдо-уникальные строки заданного размера. Можно использовать ее для создания уникальных значений поля nonce токенов.

Example
package main

import (
	"fmt"
	"log"

	"github.com/mdigger/jwt"
)

func main() {
	conf := jwt.Config{
		Issuer:   "http://service.example.com/",
		UniqueID: jwt.Nonce(8), // задаем функцию генерации случайного nonce
	}

	token, err := conf.Token(jwt.JSON{"sub": "9394203942934"})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(token)
}
Output:

func Verify

func Verify(token string, key interface{}) (claim []byte, err error)

Verify проверяет подпись токена. В качестве параметра передается ключ для проверки подписи или функция, принимающая одно или два строковых значения (алгоритм и идентификатор ключа) и возвращающая для них ключ.

Сам ключ может быть в указан в следующих форматах:

*rsa.PrivateKey
*rsa.PublicKey
*ecdsa.PrivateKey
*ecdsa.PublicKey
string
[]byte
fmt.Stringer

Так же поддерживаются следующие форматы функции для передачи ключа:

func(keyID string, alg string) interface{}
func(keyID string) interface{}

Кроме проверки подписи, проверяются основные даты токена, что он актуален на данный момент.

Возвращается неразобранное содержимое токена.

Example
package main

import (
	"log"
	"time"

	"github.com/mdigger/jwt"
)

var secret = "my secret sign key"

func GenerateTestToken() string {
	claimset := jwt.JSON{
		"iss":      "http://service.example.com/",
		"sub":      "2934852845",
		"iat":      jwt.Time{Time: time.Now()},
		"exp":      jwt.Time{Time: time.Now().Add(time.Hour)},
		"name":     "Dmitry Sedykh",
		"email":    "dmitrys@example.com",
		"birthday": jwt.Time{Time: time.Date(1971, time.December, 24, 8, 43, 0, 0, time.UTC)},
		"nonce":    jwt.Nonce(8)(),
	}

	var err error
	token, err := jwt.Encode(claimset, secret)
	if err != nil {
		panic(err)
	}

	return token
}

func main() {
	token := GenerateTestToken()

	// проверка подписи токена с простым ключом
	if _, err := jwt.Verify(token, secret); err != nil {
		log.Fatal(err)
	}

	// описываем функцию, которая будет возвращать наш ключ в зависимости от
	// алгоритма и идентификатора ключа
	getMyKey := func(keyID, alg string) []byte {
		if alg != "HS256" || keyID != "" {
			return nil
		}
		return []byte(secret)
	}

	// вызываем проверку подписи с вызовом функции получения ключа
	if _, err := jwt.Verify(token, getMyKey); err != nil {
		log.Fatal(err)
	}
}
Output:

Types

type Config

type Config struct {
	Issuer    string        // iss - идентификатор выпускающего
	Created   bool          // iat - добавлять время создания
	Expires   time.Duration // exp - добавлять время жизни
	NotBefore time.Duration // nbf - добавлять время начала действия
	Type      string        // typ - тип токена
	UniqueID  func() string // nonce - генератор случайной строки
	Private   JSON          // дополнительные именованные поля

	Key interface{} // ключ для подписи токена или функция его возвращающая
}

Config описывает шаблон для генерации токена. Поля, указанные в нем, будут автоматически добавляться при генерации токена. Любые общие дополнительные поля для токена можно указать в Private. Если задана функция Nonce, то в каждый токен будет добавляться уникальная строка, возвращаемая этой функцией.

Для автоматической подписи токена необходимо указать ключ. В зависимости от его типа будут использоваться разные алгоритмы для генерации подписи. Для *rsa.PrivateKey будет создаваться токен с алгоритмом подписи RS256. Для *ecdsa.PrivateKey - ES256, ES384 или ES512, в зависимости от параметров созданного ключа. Для string, []byte или любого другого объекта, который поддерживает строковое представление (fmt.Stringer) - HS256.

Чтобы указать в заголовке токена идентификатор ключа, используемого для подписи, нужно чтобы функция ключа возвращала два значения: первым будет KeyID, а вторым - сам ключ для подписи.

Example
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/mdigger/jwt"
)

func main() {
	// инициализируем шаблон с описанием основных полей токена
	conf := jwt.Config{
		Issuer:   "http://test.example.com/auth",
		Created:  true,                   // добавлять дату создания
		Expires:  time.Hour,              // время жизни
		UniqueID: jwt.Nonce(8),           // добавлять nonce
		Private:  jwt.JSON{"temp": true}, // дополнительные поля
		Key:      jwt.NewRS256Key(),      // ключ для подписи
	}

	// создаем токен с указанными полями
	token, err := conf.Token(jwt.JSON{
		"sub":      "938102384109384",
		"email":    "user@example.com",
		"name":     "Test User",
		"birthday": time.Date(1971, time.December, 24, 0, 0, 0, 0, time.Local),
		"temp":     false, // переопределяем поля в шаблоне
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(token)
}
Output:

func (Config) Token

func (c Config) Token(claimset interface{}) (string, error)

Token возвращает сгенерированный токен на основании шаблона и предоставленных данных. В качестве payload можно указать map[string]interface{} или собственный объект. Так же принимается строка: в этом случае считается, что она задает единственное значение "sub".

Есть несколько отличий от стандартной сериализации в формат JSON, которые заложены в этой функции. Во-первых, все поля, представленные в виде time.Time кодируются в числовом виде, а не строками, как это стандартно происходит в Go. А не заданные значение time.Time автоматически игнорируются. Во-вторых, в структурах для именования полей могут использоваться теги "json" и "jwt". Последний имеет чуть больший приоритет, поэтому вы можете специально для токенов указывать другие имена. Если имя поля не определено в теге, то используется само имя поля, но первая его буква при этом становится строчной, что больше соответствует формату JSON токена.

type JSON

type JSON = map[string]interface{}

JSON является синонимом для быстрого создания map в формате JSON.

Example
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/mdigger/jwt"
)

func main() {
	// определяем содержимое токена
	claimset := jwt.JSON{
		"iss":      "http://service.example.com/",
		"sub":      "2934852845",
		"iat":      jwt.Time{Time: time.Now()},
		"exp":      jwt.Time{Time: time.Now().Add(time.Hour)},
		"name":     "Dmitry Sedykh",
		"email":    "dmitrys@example.com",
		"birthday": jwt.Time{Time: time.Date(1971, time.December, 24, 0, 0, 0, 0, time.Local)},
		"nonce":    jwt.Nonce(8)(),
	}

	// создаем токен без подписи
	token, err := jwt.Encode(claimset, nil)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("token:", token)
}
Output:

type JWK

type JWK struct {
	// The "kty" (key type) parameter identifies the cryptographic algorithm
	// family used with the key, such as "RSA" or "EC".
	Type string `json:"kty"`
	// The "use" (public key use) parameter identifies the intended use of
	// the public key.  The "use" parameter is employed to indicate whether
	// a public key is used for encrypting data or verifying the signature
	// on data.
	//
	// Values defined by this specification are:
	// 	- "sig" (signature)
	// 	- "enc" (encryption)
	Usage string `json:"use,omitempty"`
	// The "key_ops" (key operations) parameter identifies the operation(s)
	// for which the key is intended to be used.  The "key_ops" parameter is
	// intended for use cases in which public, private, or symmetric keys
	// may be present.
	//
	// Its value is an array of key operation values.  Values defined by
	// this specification are:
	// 	- "sign" (compute digital signature or MAC)
	// 	- "verify" (verify digital signature or MAC)
	// 	- "encrypt" (encrypt content)
	// 	- "decrypt" (decrypt content and validate decryption, if applicable)
	// 	- "wrapKey" (encrypt key)
	// 	- "unwrapKey" (decrypt key and validate decryption, if applicable)
	// 	- "deriveKey" (derive key)
	// 	- "deriveBits" (derive bits not to be used as a key)
	KeyOps []string `json:"key_ops,omitempty"`
	// The "alg" (algorithm) parameter identifies the algorithm intended for
	// use with the key.
	Algorithm string `json:"alg"`
	// The "kid" (key ID) parameter is used to match a specific key.  This
	// is used, for instance, to choose among a set of keys within a JWK Set
	// during key rollover.
	ID string `json:"kid,omitempty"`
	// ECDSA Public
	Curve string `json:"crv,omitempty"` // Curve
	X     string `json:"x,omitempty"`   // X Coordinate
	Y     string `json:"y,omitempty"`   // Y Coordinate
	// RSA Public
	N string `json:"n,omitempty"` // Modulus
	E string `json:"e,omitempty"` // Exponent
	// Private RSA & ECDSA
	D string `json:"d,omitempty"` // ECC Private Key or RSA Private Exponent
	// RSA Private
	P   string   `json:"p,omitempty"`   // First Prime Factor
	Q   string   `json:"q,omitempty"`   // Second Prime Factor
	DP  string   `json:"dp,omitempty"`  // First Factor CRT Exponent
	DQ  string   `json:"dq,omitempty"`  // Second Factor CRT Exponent
	QI  string   `json:"qi,omitempty"`  // First CRT Coefficient
	OTH []rsaCtr `json:"oth,omitempty"` // Other Primes Info
	// HS
	K string `json:"k,omitempty"`
}

JWK описывает формат данных ключа.

The "use" and "key_ops" JWK members SHOULD NOT be used together. Mimetype: application/jwk+json (application/jwk-set+json)

func JWKEncode

func JWKEncode(key interface{}, keyID string) (jwk *JWK, err error)

JWKEncode возвращает представление ключа в формате JWK.

https://tools.ietf.org/html/rfc7517

func (*JWK) Decode

func (key *JWK) Decode() (interface{}, error)

Decode декодирует описание в ключ.

type Time

type Time struct {
	time.Time
}

Time подменяет собой стандартное time.Time, но переопределяет для него формат представления и распаковки из JSON в виде числа. Во всем остальном ведет себя как стандартный time.Time.

Example
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/mdigger/jwt"
)

var secret = "my secret sign key"

func GenerateTestToken() string {
	claimset := jwt.JSON{
		"iss":      "http://service.example.com/",
		"sub":      "2934852845",
		"iat":      jwt.Time{Time: time.Now()},
		"exp":      jwt.Time{Time: time.Now().Add(time.Hour)},
		"name":     "Dmitry Sedykh",
		"email":    "dmitrys@example.com",
		"birthday": jwt.Time{Time: time.Date(1971, time.December, 24, 8, 43, 0, 0, time.UTC)},
		"nonce":    jwt.Nonce(8)(),
	}

	var err error
	token, err := jwt.Encode(claimset, secret)
	if err != nil {
		panic(err)
	}

	return token
}

func main() {
	token := GenerateTestToken()

	// описываем структуру с данными, которые хотим распаковать из токена
	var claimset struct {
		Issuer   string   `json:"iss"`
		Subject  string   `json:"sub"`
		Created  jwt.Time `json:"iat"`
		Expired  jwt.Time `json:"exp"`
		Name     string
		Email    string
		Birthday jwt.Time // время представлено числовом виде
		Nonce    string
	}

	// извлекаем данные из токена
	if err := jwt.Decode(token, &claimset); err != nil {
		log.Fatal(err)
	}

	fmt.Println(claimset.Birthday.UTC())
}
Output:

1971-12-24 08:43:00 +0000 UTC

func (Time) MarshalJSON

func (t Time) MarshalJSON() ([]byte, error)

MarshalJSON представляет время в формате JSON в виде числа.

func (*Time) UnmarshalJSON

func (t *Time) UnmarshalJSON(data []byte) error

UnmarshalJSON восстанавливает время, представленное в формате JSON в виде числа.

Jump to

Keyboard shortcuts

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