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 }