177 lines
4.3 KiB
Go
177 lines
4.3 KiB
Go
package tmobtokengen
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-jose/go-jose/v4"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
"github.com/youmark/pkcs8"
|
|
)
|
|
|
|
func buildClientSecret(clientID, secret string) string {
|
|
return base64.StdEncoding.EncodeToString([]byte(clientID + ":" + secret))
|
|
}
|
|
|
|
var (
|
|
ErrFailedToDecodePem = errors.New("failed to decode privateKey")
|
|
ErrNilSigner = errors.New("signer cannot be nil")
|
|
)
|
|
|
|
func ParsePrivateKey(pemFileBytes []byte, pwd []byte) (any, error) {
|
|
block, _ := pem.Decode(pemFileBytes)
|
|
if block == nil {
|
|
return nil, ErrFailedToDecodePem
|
|
}
|
|
key, _, err := pkcs8.ParsePrivateKey(block.Bytes, pwd)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse pkcs8 private key")
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
type jwtClaims struct {
|
|
EDTS string `json:"edts,omitempty"`
|
|
V string `json:"v,omitempty"`
|
|
Exp int64 `json:"exp,omitempty"`
|
|
EHTS string `json:"ehts,omitempty"`
|
|
IAT int64 `json:"iat,omitempty"`
|
|
UniqueStr string `json:"jti,omitempty"`
|
|
}
|
|
|
|
func buildClaims(
|
|
ehts map[EhtsKey]string,
|
|
curTime time.Time,
|
|
expDuration time.Duration,
|
|
uuidFunc func() string,
|
|
) ([]byte, error) {
|
|
curTime = curTime.UTC()
|
|
ehtsKeys, ehtsValues := ehtsToString(ehts)
|
|
encoded := b64EncodeEHTS([]byte(ehtsValues))
|
|
expTime := curTime.Add(expDuration)
|
|
uniqStr := uuidFunc()
|
|
|
|
objClaims := jwtClaims{
|
|
EDTS: string(encoded),
|
|
V: "1",
|
|
Exp: expTime.Unix(),
|
|
EHTS: ehtsKeys,
|
|
IAT: curTime.Unix(),
|
|
UniqueStr: uniqStr,
|
|
}
|
|
|
|
c, err := json.Marshal(objClaims)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to marshal claims during build")
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func sign(signer jose.Signer, claims []byte) (string, error) {
|
|
if signer == nil {
|
|
return "", ErrNilSigner
|
|
}
|
|
|
|
signature, err := signer.Sign(claims)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to sign claims during pop token build")
|
|
}
|
|
|
|
signed, err := signature.CompactSerialize()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to sign claims during pop token build")
|
|
}
|
|
|
|
return signed, nil
|
|
}
|
|
|
|
type Generator interface {
|
|
Generate(ehts EHTSMap) (string, error)
|
|
ClientSecretAsAuthVal() string
|
|
}
|
|
|
|
type PopTokenGenerator struct {
|
|
signer jose.Signer
|
|
clientSecret string // client secret composed from client id and secret via base64.StdEncoding
|
|
expDuration time.Duration // token expiration duration
|
|
genUuid func() string // uniq string is set as generator field for testing purposes
|
|
}
|
|
|
|
func GenerateUUID() string {
|
|
return uuid.New().String()
|
|
}
|
|
|
|
// NewTokenGenerator creates a new PopTokenGenerator.
|
|
func NewTokenGenerator(
|
|
clientID string,
|
|
secret string,
|
|
expDuration time.Duration,
|
|
pkValue string,
|
|
pkPwd ...[]byte,
|
|
) (*PopTokenGenerator, error) {
|
|
// Removing the read from file. We read our key from the environment
|
|
pkValue = strings.ReplaceAll(pkValue, "\\n", "\n")
|
|
keyBytes := pkValue
|
|
|
|
var pwd []byte
|
|
if len(pkPwd) != 0 {
|
|
pwd = pkPwd[0]
|
|
}
|
|
|
|
pk, err := ParsePrivateKey([]byte(keyBytes), pwd)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse private key file")
|
|
}
|
|
|
|
opts := new(jose.SignerOptions)
|
|
opts.WithType("JWT")
|
|
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: pk}, opts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create signer")
|
|
}
|
|
|
|
return &PopTokenGenerator{
|
|
signer: signer,
|
|
clientSecret: buildClientSecret(clientID, secret),
|
|
expDuration: expDuration,
|
|
genUuid: GenerateUUID,
|
|
}, nil
|
|
}
|
|
|
|
func (g *PopTokenGenerator) ClientSecretAsAuthVal() string {
|
|
return "Basic " + g.clientSecret
|
|
}
|
|
|
|
// Generate generates a pop token.
|
|
// It expects EHTSMap to be populated with the following keys, order is changed intrinsically, so don't worry:
|
|
// - ContentType
|
|
// - Authorization
|
|
// - URI
|
|
// - HTTPMethod
|
|
// - Body
|
|
func (g *PopTokenGenerator) Generate(ehts EHTSMap) (string, error) {
|
|
return g.generate(ehts, time.Now())
|
|
}
|
|
|
|
// generate generates a pop token.
|
|
// It takes the current time as parameter for testing.
|
|
func (g *PopTokenGenerator) generate(ehts EHTSMap, curTime time.Time) (string, error) {
|
|
c, err := buildClaims(ehts, curTime, g.expDuration, g.genUuid)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to build claims")
|
|
}
|
|
|
|
signed, err := sign(g.signer, c)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to sign claims")
|
|
}
|
|
|
|
return signed, nil
|
|
}
|