auth API

auth

package

API reference for the auth package.

S
struct
Implements: Claims

Payload

Payload holds the claims for a simple token.

pkg/auth/token.go:30-33
type Payload struct

Methods

Valid
Method

Returns

error
func (Payload) Valid() error
{
	if time.Now().Unix() > p.Exp {
		return ErrExpiredToken
	}
	return nil
}

Fields

Name Type Description
Sub string json:"sub"
Exp int64 json:"exp"
S
struct
Implements: Claims

StandardClaims

StandardClaims holds common JWT-like claims.

pkg/auth/token.go:36-43
type StandardClaims struct

Methods

Valid
Method

Returns

error
func (StandardClaims) Valid() error
{
	if time.Now().Unix() > c.Exp {
		return ErrExpiredToken
	}
	return nil
}

Fields

Name Type Description
Sub string json:"sub"
Exp int64 json:"exp"
Iat int64 json:"iat,omitempty"
Jti string json:"jti,omitempty"
Iss string json:"iss,omitempty"
Aud string json:"aud,omitempty"
I
interface

Claims

Claims validates token claims.

pkg/auth/token.go:46-48
type Claims interface

Methods

Valid
Method

Returns

error
func Valid(...)
T
type

Algorithm

Algorithm identifies a signing algorithm.

pkg/auth/token.go:65-65
type Algorithm string
F
function

SignToken

SignToken signs a payload with HMAC-SHA256.

Parameters

secret
[]byte

Returns

string
error
pkg/auth/token.go:79-81
func SignToken(p Payload, secret []byte) (string, error)

{
	return sign(p, secret, AlgHMACSHA256)
}
F
function

VerifyToken

VerifyToken verifies and decodes a HMAC-SHA256 token.

Parameters

token
string
secret
[]byte

Returns

error
pkg/auth/token.go:84-86
func VerifyToken(token string, secret []byte) (Payload, error)

{
	return verify[Payload](token, secret, AlgHMACSHA256)
}
S
struct

Key

Key holds signing key material.

pkg/auth/token.go:89-95
type Key struct

Fields

Name Type Description
ID string
Secret []byte
Algorithm Algorithm
Private crypto.PrivateKey
Public crypto.PublicKey
S
struct

SignedToken

SignedToken holds a token string with its key ID and raw payload.

pkg/auth/token.go:98-103
type SignedToken struct

Fields

Name Type Description
Token string
KeyID string
Payload []byte
Raw string
I
interface

TokenService

TokenService signs and verifies tokens.

pkg/auth/token.go:106-109
type TokenService interface

Methods

Sign
Method

Parameters

claims Claims

Returns

error
func Sign(...)
Verify
Method

Parameters

token string

Returns

error
func Verify(...)
S
struct
Implements: TokenService

multiKeyService

pkg/auth/token.go:111-113
type multiKeyService struct

Methods

Sign
Method

Parameters

claims Claims

Returns

error
func (*multiKeyService) Sign(claims Claims) (*SignedToken, error)
{
	if len(s.keys) == 0 {
		return nil, errors.New("auth: no keys configured")
	}
	k := s.keys[0]
	return signWithKey(claims, k)
}
Verify
Method

Parameters

token string

Returns

error
func (*multiKeyService) Verify(token string) (Claims, error)
{
	for _, k := range s.keys {
		claims, err := verifyWithKey[StandardClaims](token, k)
		if err == nil {
			return claims, nil
		}
		if errors.Is(err, ErrUnknownKeyID) {
			continue
		}
	}
	return nil, ErrInvalidSignature
}

Fields

Name Type Description
keys []Key
F
function

NewService

NewService creates a TokenService with one or more keys.

Parameters

keys
...Key

Returns

pkg/auth/token.go:116-118
func NewService(keys ...Key) TokenService

{
	return &multiKeyService{keys: keys}
}
F
function

signWithKey

Parameters

claims
key

Returns

error
pkg/auth/token.go:141-175
func signWithKey(claims Claims, key Key) (*SignedToken, error)

{
	data, err := json.Marshal(claims)
	if err != nil {
		return nil, err
	}

	var sig []byte
	switch key.Algorithm {
	case AlgHMACSHA256:
		sig, err = signHMAC(data, key.Secret)
	case AlgRS256:
		sig, err = signRSA(data, key.Private.(*rsa.PrivateKey))
	case AlgES256:
		sig, err = signECDSA(data, key.Private.(*ecdsa.PrivateKey))
	case AlgEdDSA:
		sig, err = signEd25519(data, key.Private.(ed25519.PrivateKey))
	default:
		return nil, fmt.Errorf("auth: unsupported algorithm: %s", key.Algorithm)
	}
	if err != nil {
		return nil, err
	}

	kid := base64.RawURLEncoding.EncodeToString([]byte(key.ID))
	payload := base64.RawURLEncoding.EncodeToString(data)
	signature := base64.RawURLEncoding.EncodeToString(sig)
	token := kid + "." + payload + "." + signature

	return &SignedToken{
		Token:   token,
		KeyID:   key.ID,
		Payload: data,
		Raw:     token,
	}, nil
}
F
function

verifyWithKey

Parameters

token
string
key

Returns

T
error
pkg/auth/token.go:177-234
func verifyWithKey[T Claims](token string, key Key) (T, error)

{
	var zero T

	parts := splitToken(token)
	if len(parts) != 3 {
		return zero, ErrInvalidToken
	}

	kidBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		return zero, ErrInvalidToken
	}
	if string(kidBytes) != key.ID {
		return zero, ErrUnknownKeyID
	}

	payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return zero, ErrInvalidToken
	}

	sig, err := base64.RawURLEncoding.DecodeString(parts[2])
	if err != nil {
		return zero, ErrInvalidToken
	}

	switch key.Algorithm {
	case AlgRS256:
		if err := verifyRSA(payloadBytes, sig, key.Public.(*rsa.PublicKey)); err != nil {
			return zero, err
		}
	case AlgHMACSHA256:
		if !verifyHMAC(payloadBytes, sig, key.Secret) {
			return zero, ErrInvalidSignature
		}
	case AlgES256:
		if !verifyECDSA(payloadBytes, sig, key.Public.(*ecdsa.PublicKey)) {
			return zero, ErrInvalidSignature
		}
	case AlgEdDSA:
		if !verifyEd25519(payloadBytes, sig, key.Public.(ed25519.PublicKey)) {
			return zero, ErrInvalidSignature
		}
	default:
		return zero, fmt.Errorf("auth: unsupported algorithm: %s", key.Algorithm)
	}

	var claims T
	if err := json.Unmarshal(payloadBytes, &claims); err != nil {
		return zero, err
	}
	if c, ok := any(&claims).(Claims); ok {
		if err := c.Valid(); err != nil {
			return zero, err
		}
	}
	return claims, nil
}
F
function

sign

Parameters

p
T
secret
[]byte
alg

Returns

string
error
pkg/auth/token.go:236-251
func sign[T any](p T, secret []byte, alg Algorithm) (string, error)

{
	data, err := json.Marshal(p)
	if err != nil {
		return "", err
	}

	var sig []byte
	switch alg {
	case AlgHMACSHA256:
		sig = computeSig(data, secret)
	default:
		return "", fmt.Errorf("auth: unsupported algorithm: %s", alg)
	}

	return base64.RawURLEncoding.EncodeToString(data) + "." + base64.RawURLEncoding.EncodeToString(sig), nil
}
F
function

verify

Parameters

token
string
secret
[]byte
alg

Returns

T
error
pkg/auth/token.go:253-301
func verify[T any](token string, secret []byte, alg Algorithm) (T, error)

{
	var zero T

	idx := -1
	for i := 0; i < len(token); i++ {
		if token[i] == '.' {
			idx = i
			break
		}
	}
	if idx < 0 {
		return zero, ErrInvalidToken
	}

	payloadBytes, err := base64.RawURLEncoding.DecodeString(token[:idx])
	if err != nil {
		return zero, ErrInvalidToken
	}

	sig, err := base64.RawURLEncoding.DecodeString(token[idx+1:])
	if err != nil {
		return zero, ErrInvalidToken
	}

	var valid bool
	switch alg {
	case AlgHMACSHA256:
		expected := computeSig(payloadBytes, secret)
		valid = hmac.Equal(sig, expected)
	default:
		return zero, fmt.Errorf("auth: unsupported algorithm: %s", alg)
	}

	if !valid {
		return zero, ErrInvalidSignature
	}

	if err := json.Unmarshal(payloadBytes, &zero); err != nil {
		return zero, err
	}

	if p, ok := any(zero).(Claims); ok {
		if err := p.Valid(); err != nil {
			return zero, err
		}
	}

	return zero, nil
}
F
function

signHMAC

Parameters

data
[]byte
secret
[]byte

Returns

[]byte
error
pkg/auth/token.go:303-305
func signHMAC(data, secret []byte) ([]byte, error)

{
	return computeSig(data, secret), nil
}
F
function

signRSA

Parameters

data
[]byte

Returns

[]byte
error
pkg/auth/token.go:307-310
func signRSA(data []byte, key *rsa.PrivateKey) ([]byte, error)

{
	hash := sha256.Sum256(data)
	return rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, hash[:])
}
F
function

signECDSA

Parameters

data
[]byte

Returns

[]byte
error
pkg/auth/token.go:312-315
func signECDSA(data []byte, key *ecdsa.PrivateKey) ([]byte, error)

{
	hash := sha256.Sum256(data)
	return ecdsa.SignASN1(rand.Reader, key, hash[:])
}
F
function

signEd25519

Parameters

data
[]byte

Returns

[]byte
error
pkg/auth/token.go:317-319
func signEd25519(data []byte, key ed25519.PrivateKey) ([]byte, error)

{
	return ed25519.Sign(key, data), nil
}
F
function

verifyRSA

Parameters

data
[]byte
sig
[]byte

Returns

error
pkg/auth/token.go:321-324
func verifyRSA(data, sig []byte, key *rsa.PublicKey) error

{
	hash := sha256.Sum256(data)
	return rsa.VerifyPKCS1v15(key, crypto.SHA256, hash[:], sig)
}
F
function

verifyECDSA

Parameters

data
[]byte
sig
[]byte

Returns

bool
pkg/auth/token.go:326-329
func verifyECDSA(data, sig []byte, key *ecdsa.PublicKey) bool

{
	hash := sha256.Sum256(data)
	return ecdsa.VerifyASN1(key, hash[:], sig)
}
F
function

verifyEd25519

Parameters

data
[]byte
sig
[]byte

Returns

bool
pkg/auth/token.go:331-333
func verifyEd25519(data, sig []byte, key ed25519.PublicKey) bool

{
	return ed25519.Verify(key, data, sig)
}
F
function

verifyHMAC

Parameters

data
[]byte
sig
[]byte
secret
[]byte

Returns

bool
pkg/auth/token.go:335-338
func verifyHMAC(data, sig, secret []byte) bool

{
	expected := computeSig(data, secret)
	return hmac.Equal(sig, expected)
}
F
function

computeSig

Parameters

payload
[]byte
secret
[]byte

Returns

[]byte
pkg/auth/token.go:340-344
func computeSig(payload, secret []byte) []byte

{
	h := hmac.New(sha256.New, secret)
	_, _ = h.Write(payload)
	return h.Sum(nil)
}
F
function

splitToken

Parameters

token
string

Returns

[]string
pkg/auth/token.go:346-359
func splitToken(token string) []string

{
	var parts []string
	start := 0
	for i := 0; i < len(token); i++ {
		if token[i] == '.' {
			parts = append(parts, token[start:i])
			start = i + 1
		}
	}
	if start < len(token) {
		parts = append(parts, token[start:])
	}
	return parts
}
F
function

TestSignTokenAndVerifyToken

Parameters

pkg/auth/token_test.go:15-28
func TestSignTokenAndVerifyToken(t *testing.T)

{
	secret := []byte("s3cr3t")
	token, err := SignToken(Payload{Sub: "u1", Exp: time.Now().Add(time.Minute).Unix()}, secret)
	if err != nil {
		t.Fatalf("SignToken() error = %v", err)
	}
	payload, err := VerifyToken(token, secret)
	if err != nil {
		t.Fatalf("VerifyToken() error = %v", err)
	}
	if payload.Sub != "u1" {
		t.Fatalf("VerifyToken() subject = %q, want %q", payload.Sub, "u1")
	}
}
F
function

TestVerifyTokenRejectsMalformedToken

Parameters

pkg/auth/token_test.go:30-35
func TestVerifyTokenRejectsMalformedToken(t *testing.T)

{
	_, err := VerifyToken("invalid", []byte("s3cr3t"))
	if !errors.Is(err, ErrInvalidToken) {
		t.Fatalf("VerifyToken() error = %v, want ErrInvalidToken", err)
	}
}
F
function

TestVerifyTokenRejectsInvalidSignature

Parameters

pkg/auth/token_test.go:37-52
func TestVerifyTokenRejectsInvalidSignature(t *testing.T)

{
	secret := []byte("s3cr3t")
	token, err := SignToken(Payload{Sub: "u1", Exp: time.Now().Add(time.Minute).Unix()}, secret)
	if err != nil {
		t.Fatalf("SignToken() error = %v", err)
	}
	parts := splitToken(token)
	if len(parts) != 2 {
		t.Fatal("expected 2 parts")
	}
	tampered := parts[0] + "." + base64.RawURLEncoding.EncodeToString([]byte("tampered"))
	_, err = VerifyToken(tampered, secret)
	if !errors.Is(err, ErrInvalidSignature) {
		t.Fatalf("VerifyToken() error = %v, want ErrInvalidSignature", err)
	}
}
F
function

TestVerifyTokenRejectsExpiredToken

Parameters

pkg/auth/token_test.go:54-64
func TestVerifyTokenRejectsExpiredToken(t *testing.T)

{
	secret := []byte("s3cr3t")
	token, err := SignToken(Payload{Sub: "u1", Exp: time.Now().Add(-time.Minute).Unix()}, secret)
	if err != nil {
		t.Fatalf("SignToken() error = %v", err)
	}
	_, err = VerifyToken(token, secret)
	if !errors.Is(err, ErrExpiredToken) {
		t.Fatalf("VerifyToken() error = %v, want ErrExpiredToken", err)
	}
}
F
function

TestNewService_KeyRotation

Parameters

pkg/auth/token_test.go:66-85
func TestNewService_KeyRotation(t *testing.T)

{
	oldKey := Key{ID: "old", Secret: []byte("old-secret"), Algorithm: AlgHMACSHA256}
	newKey := Key{ID: "new", Secret: []byte("new-secret"), Algorithm: AlgHMACSHA256}
	svc := NewService(newKey, oldKey)
	token, err := svc.Sign(&StandardClaims{Sub: "user1", Exp: time.Now().Add(time.Hour).Unix()})
	if err != nil {
		t.Fatalf("Sign failed: %v", err)
	}
	if token.KeyID != "new" {
		t.Errorf("got key id %q, want %q", token.KeyID, "new")
	}
	claims, err := svc.Verify(token.Token)
	if err != nil {
		t.Fatalf("Verify failed: %v", err)
	}
	c := claims.(StandardClaims)
	if c.Sub != "user1" {
		t.Errorf("got sub %s, want user1", c.Sub)
	}
}
F
function

TestNewService_KeyRotationOldKeyStillWorks

Parameters

pkg/auth/token_test.go:87-101
func TestNewService_KeyRotationOldKeyStillWorks(t *testing.T)

{
	oldKey := Key{ID: "old", Secret: []byte("old-secret"), Algorithm: AlgHMACSHA256}
	newKey := Key{ID: "new", Secret: []byte("new-secret"), Algorithm: AlgHMACSHA256}
	svcOld := NewService(oldKey)
	token, _ := svcOld.Sign(&StandardClaims{Sub: "user1", Exp: time.Now().Add(time.Hour).Unix()})
	svcNew := NewService(newKey, oldKey)
	claims, err := svcNew.Verify(token.Token)
	if err != nil {
		t.Fatalf("Verify with old key should still work: %v", err)
	}
	c := claims.(StandardClaims)
	if c.Sub != "user1" {
		t.Errorf("got sub %s, want user1", c.Sub)
	}
}
F
function

TestNewService_WrongKey

Parameters

pkg/auth/token_test.go:103-113
func TestNewService_WrongKey(t *testing.T)

{
	key1 := Key{ID: "k1", Secret: []byte("secret1"), Algorithm: AlgHMACSHA256}
	key2 := Key{ID: "k2", Secret: []byte("secret2"), Algorithm: AlgHMACSHA256}
	svc1 := NewService(key1)
	token, _ := svc1.Sign(&StandardClaims{Sub: "u", Exp: time.Now().Add(time.Hour).Unix()})
	svc2 := NewService(key2)
	_, err := svc2.Verify(token.Token)
	if err == nil {
		t.Error("expected error when verifying with wrong key")
	}
}
F
function

TestNewService_RS256

Parameters

pkg/auth/token_test.go:115-130
func TestNewService_RS256(t *testing.T)

{
	priv, _ := rsa.GenerateKey(rand.Reader, 2048)
	svc := NewService(Key{ID: "rsa1", Private: priv, Public: &priv.PublicKey, Algorithm: AlgRS256})
	token, err := svc.Sign(&StandardClaims{Sub: "user_rsa", Exp: time.Now().Add(time.Hour).Unix()})
	if err != nil {
		t.Fatalf("Sign with RSA failed: %v", err)
	}
	claims, err := svc.Verify(token.Token)
	if err != nil {
		t.Fatalf("Verify with RSA failed: %v", err)
	}
	c := claims.(StandardClaims)
	if c.Sub != "user_rsa" {
		t.Errorf("got sub %s, want user_rsa", c.Sub)
	}
}
F
function

TestNewService_EdDSA

Parameters

pkg/auth/token_test.go:132-150
func TestNewService_EdDSA(t *testing.T)

{
	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("GenerateKey failed: %v", err)
	}
	svc := NewService(Key{ID: "ed1", Private: priv, Public: pub, Algorithm: AlgEdDSA})
	token, err := svc.Sign(&StandardClaims{Sub: "user_ed", Exp: time.Now().Add(time.Hour).Unix()})
	if err != nil {
		t.Fatalf("Sign with EdDSA failed: %v", err)
	}
	claims, err := svc.Verify(token.Token)
	if err != nil {
		t.Fatalf("Verify with EdDSA failed: %v", err)
	}
	c := claims.(StandardClaims)
	if c.Sub != "user_ed" {
		t.Errorf("got sub %s, want user_ed", c.Sub)
	}
}
F
function

TestNewService_ECDSA

Parameters

pkg/auth/token_test.go:152-170
func TestNewService_ECDSA(t *testing.T)

{
	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("GenerateKey failed: %v", err)
	}
	svc := NewService(Key{ID: "ec1", Private: priv, Public: &priv.PublicKey, Algorithm: AlgES256})
	token, err := svc.Sign(&StandardClaims{Sub: "user_ec", Exp: time.Now().Add(time.Hour).Unix()})
	if err != nil {
		t.Fatalf("Sign with ECDSA failed: %v", err)
	}
	claims, err := svc.Verify(token.Token)
	if err != nil {
		t.Fatalf("Verify with ECDSA failed: %v", err)
	}
	c := claims.(StandardClaims)
	if c.Sub != "user_ec" {
		t.Errorf("got sub %s, want user_ec", c.Sub)
	}
}
F
function

TestStandardClaims_Valid_Expired

Parameters

pkg/auth/token_test.go:172-177
func TestStandardClaims_Valid_Expired(t *testing.T)

{
	c := StandardClaims{Sub: "u", Exp: time.Now().Add(-time.Hour).Unix()}
	if err := c.Valid(); err == nil {
		t.Error("expected error for expired claims")
	}
}
F
function

TestStandardClaims_Valid_NotExpired

Parameters

pkg/auth/token_test.go:179-184
func TestStandardClaims_Valid_NotExpired(t *testing.T)

{
	c := StandardClaims{Sub: "u", Exp: time.Now().Add(time.Hour).Unix()}
	if err := c.Valid(); err != nil {
		t.Errorf("unexpected error: %v", err)
	}
}
F
function

TestNewService_NoKeys

Parameters

pkg/auth/token_test.go:186-192
func TestNewService_NoKeys(t *testing.T)

{
	svc := NewService()
	_, err := svc.Sign(&StandardClaims{Sub: "u", Exp: 0})
	if err == nil {
		t.Error("expected error with no keys")
	}
}