Files
2026-05-26 19:59:34 +02:00

152 lines
3.8 KiB
Go

package jwt
import (
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"fmt"
"hash"
"strings"
)
// Decode splits a JWT and returns pretty-printed header and payload JSON.
func Decode(token string) (header, payload string, err error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return "", "", fmt.Errorf("expected 3 parts, got %d", len(parts))
}
hdrBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return "", "", fmt.Errorf("header: %w", err)
}
plBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return "", "", fmt.Errorf("payload: %w", err)
}
var hdrObj any
if err := json.Unmarshal(hdrBytes, &hdrObj); err != nil {
return "", "", fmt.Errorf("header JSON: %w", err)
}
var plObj any
if err := json.Unmarshal(plBytes, &plObj); err != nil {
return "", "", fmt.Errorf("payload JSON: %w", err)
}
hdrPretty, _ := json.MarshalIndent(hdrObj, "", " ")
plPretty, _ := json.MarshalIndent(plObj, "", " ")
return string(hdrPretty), string(plPretty), nil
}
// Encode builds and signs a JWT from raw JSON header and payload strings.
func Encode(header, payload, secret string) (string, error) {
var hdrObj map[string]any
if err := json.Unmarshal([]byte(header), &hdrObj); err != nil {
return "", fmt.Errorf("header JSON: %w", err)
}
var plObj any
if err := json.Unmarshal([]byte(payload), &plObj); err != nil {
return "", fmt.Errorf("payload JSON: %w", err)
}
hdrCompact, _ := json.Marshal(hdrObj)
plCompact, _ := json.Marshal(plObj)
hdrB64 := base64.RawURLEncoding.EncodeToString(hdrCompact)
plB64 := base64.RawURLEncoding.EncodeToString(plCompact)
signingInput := hdrB64 + "." + plB64
alg, _ := hdrObj["alg"].(string)
h, err := hashForAlg(alg)
if err != nil {
return signingInput + ".", fmt.Errorf("%w", err)
}
if h == nil {
return signingInput + ".", nil
}
mac := hmac.New(h, []byte(secret))
mac.Write([]byte(signingInput))
sig := mac.Sum(nil)
return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig), nil
}
// Verify checks whether the JWT signature is valid for the given secret.
// Returns (false, nil) for an invalid signature, (true, nil) for valid.
func Verify(token, secret string) (bool, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return false, fmt.Errorf("expected 3 parts, got %d", len(parts))
}
hdrBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return false, fmt.Errorf("header encoding: %w", err)
}
var hdrObj map[string]any
if err := json.Unmarshal(hdrBytes, &hdrObj); err != nil {
return false, fmt.Errorf("header JSON: %w", err)
}
alg, _ := hdrObj["alg"].(string)
h, err := hashForAlg(alg)
if err != nil {
return false, err
}
if h == nil {
return parts[2] == "", nil
}
signingInput := parts[0] + "." + parts[1]
mac := hmac.New(h, []byte(secret))
mac.Write([]byte(signingInput))
expected := mac.Sum(nil)
actual, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return false, fmt.Errorf("signature encoding: %w", err)
}
return hmac.Equal(actual, expected), nil
}
// Algorithm returns the "alg" claim from the JWT header, or "" if unreadable.
func Algorithm(token string) string {
parts := strings.SplitN(token, ".", 3)
if len(parts) < 1 {
return ""
}
hdrBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return ""
}
var hdrObj map[string]any
if err := json.Unmarshal(hdrBytes, &hdrObj); err != nil {
return ""
}
alg, _ := hdrObj["alg"].(string)
return alg
}
func hashForAlg(alg string) (func() hash.Hash, error) {
switch strings.ToUpper(alg) {
case "HS256":
return sha256.New, nil
case "HS384":
return sha512.New384, nil
case "HS512":
return sha512.New, nil
case "NONE", "":
return nil, nil
default:
return nil, fmt.Errorf("unsupported algorithm: %s", alg)
}
}