feat(pass): tokens (#24)

feat(pass): tokens

Add token signing and verification to be used by pass.
This commit is contained in:
Krzysztof Dryś 2024-01-19 15:34:02 +01:00 committed by GitHub
parent dbd4245b6f
commit 17fb204680
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 584 additions and 11 deletions

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ licenses-errors
data/
.env.testing

View File

@ -93,6 +93,18 @@ services:
env_file:
- .env
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
image: localstack/localstack
ports:
- "127.0.0.1:4566:4566"
environment:
- DEBUG=1
volumes:
- "./tests/localstack_init.sh:/etc/localstack/init/ready.d/localstack_init.sh" # ready hook
- "./data/localstack:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
volumes:
go-modules:
# shared-volume is used to share volume between api and admin. On producition AWS S3 is used,

11
go.mod
View File

@ -12,15 +12,18 @@ require (
github.com/go-playground/validator/v10 v10.15.5
github.com/go-redis/redis_rate/v10 v10.0.1
github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.3.1
github.com/gorilla/websocket v1.5.0
github.com/jaswdr/faker v1.16.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/pkg/errors v0.9.1
github.com/pressly/goose/v3 v3.15.1
github.com/redis/go-redis/v9 v9.3.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.8.4
golang.org/x/sync v0.4.0
google.golang.org/api v0.147.0
gorm.io/datatypes v1.2.0
gorm.io/driver/mysql v1.5.2
@ -60,7 +63,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
@ -82,13 +84,12 @@ require (
go.opencensus.io v0.24.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect

14
go.sum
View File

@ -144,6 +144,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@ -384,8 +386,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -529,8 +531,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -544,8 +546,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

62
internal/pass/sign/lib.go Normal file
View File

@ -0,0 +1,62 @@
package sign
import (
"crypto"
"crypto/ecdsa"
"crypto/x509"
"fmt"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/golang-jwt/jwt/v5"
)
const (
awsKeySpec = "ECC_NIST_P256"
awsSigningAlgorithm = "ECDSA_SHA_256"
jwtSigningAlgorithm = "ES256"
// since we control both signature and verification, and we always use the same
// algorithm, jwt header part (first segment) is always the same.
// we can skip it (as in not send it) to save bytes in QR code.
// note: header has only key type, not key id.
jwtHeader = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9."
)
type Service struct {
publicKey *ecdsa.PublicKey
signingMethod jwt.SigningMethod
}
func NewService(keyID string, client *kms.KMS) (*Service, error) {
resp, err := client.GetPublicKey(&kms.GetPublicKeyInput{
KeyId: &keyID,
})
if err != nil {
return nil, fmt.Errorf("failed to fetch key for %q: %w", keyID, err)
}
if *resp.KeySpec != awsKeySpec {
return nil, fmt.Errorf("the only supported key spec is %q, received: %q", awsKeySpec, *resp.KeySpec)
}
key, err := x509.ParsePKIXPublicKey(resp.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to parse response from KMSas public key: %w", err)
}
return &Service{
publicKey: key.(*ecdsa.PublicKey),
signingMethod: kmsSigningMethod{
client: client,
keyID: keyID,
hash: crypto.SHA256,
},
}, nil
}
type ConnectionType string
const (
ConnectionTypeBrowserExtensionWait ConnectionType = "be/wait"
ConnectionTypeBrowserExtensionProxy ConnectionType = "be/proxy"
ConnectionTypeMobileProxy ConnectionType = "mobile/proxy"
)

View File

@ -0,0 +1,169 @@
package sign
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type ecdsaSigningMethodWithStaticKey struct {
privateKey *ecdsa.PrivateKey
}
func (e ecdsaSigningMethodWithStaticKey) Verify(signingString string, sig []byte, key interface{}) error {
panic("not needed")
}
func (e ecdsaSigningMethodWithStaticKey) Sign(signingString string, key interface{}) ([]byte, error) {
return jwt.SigningMethodES256.Sign(signingString, e.privateKey)
}
func (e ecdsaSigningMethodWithStaticKey) Alg() string {
return jwt.SigningMethodES256.Alg()
}
func TestSignAndVerifyHappyPath(t *testing.T) {
srv := createTestService(t)
now := time.Now()
token, err := srv.SignAndEncode(Message{
ConnectionID: uuid.New().String(),
ExpiresAt: now.Add(time.Hour),
ConnectionType: ConnectionTypeBrowserExtensionProxy,
})
if err != nil {
t.Fatal(err)
}
if err := srv.CanI(token, ConnectionTypeBrowserExtensionProxy); err != nil {
t.Fatal(err)
}
}
func createTestService(t *testing.T) Service {
t.Helper()
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
srv := Service{
publicKey: &pk.PublicKey,
signingMethod: ecdsaSigningMethodWithStaticKey{
privateKey: pk,
},
}
return srv
}
func TestSignAndVerify(t *testing.T) {
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
Credentials: credentials.NewStaticCredentials("test", "test", ""),
S3ForcePathStyle: aws.Bool(true),
Endpoint: aws.String("http://localhost:4566"),
})
if err != nil {
t.Fatal(err)
}
kmsClient := kms.New(sess)
srv := createTestService(t)
now := time.Now()
tests := []struct {
name string
tokenFn func() string
expectedError error
}{
{
name: "not even jwt token",
tokenFn: func() string {
return "xxx"
},
expectedError: jwt.ErrTokenMalformed,
},
{
name: "token is expired",
tokenFn: func() string {
token, err := srv.SignAndEncode(Message{
ConnectionID: uuid.New().String(),
ExpiresAt: now.Add(-time.Hour),
ConnectionType: ConnectionTypeBrowserExtensionProxy,
})
if err != nil {
t.Fatal(err)
}
return token
},
expectedError: jwt.ErrTokenExpired,
},
{
name: "invalid claim",
tokenFn: func() string {
token, err := srv.SignAndEncode(Message{
ConnectionID: uuid.New().String(),
ExpiresAt: now.Add(time.Hour),
ConnectionType: ConnectionTypeBrowserExtensionWait,
})
if err != nil {
t.Fatal(err)
}
return token
},
expectedError: ErrInvalidClaims,
},
{
name: "invalid signature",
tokenFn: func() string {
resp, err := kmsClient.CreateKey(&kms.CreateKeyInput{
KeySpec: aws.String("ECC_NIST_P256"),
KeyUsage: aws.String("SIGN_VERIFY"),
})
if err != nil {
t.Fatal(err)
}
serviceWithAnotherKey, err := NewService(*resp.KeyMetadata.KeyId, kmsClient)
if err != nil {
t.Fatal(err)
}
token, err := serviceWithAnotherKey.SignAndEncode(Message{
ConnectionID: uuid.New().String(),
ExpiresAt: now.Add(-time.Hour),
ConnectionType: ConnectionTypeBrowserExtensionProxy,
})
if err != nil {
t.Fatal(err)
}
return token
},
expectedError: jwt.ErrTokenSignatureInvalid,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
token := tc.tokenFn()
err := srv.CanI(token, ConnectionTypeBrowserExtensionProxy)
if err == nil {
t.Fatalf("Expected error %v, got nil", tc.expectedError)
}
if !errors.Is(err, tc.expectedError) {
t.Fatalf("Expected error %v, got %v", tc.expectedError, err)
}
})
}
}

111
internal/pass/sign/sign.go Normal file
View File

@ -0,0 +1,111 @@
package sign
import (
"crypto"
"encoding/asn1"
"fmt"
"math/big"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/golang-jwt/jwt/v5"
)
type Message struct {
ConnectionID string
ExpiresAt time.Time
ConnectionType ConnectionType
}
// SignAndEncode information in the message. The result
// is second and third part of jwt token. Since the first
// part is constant it is omitted.
func (s Service) SignAndEncode(m Message) (string, error) {
token := jwt.NewWithClaims(s.signingMethod, jwt.MapClaims{
"exp": m.ExpiresAt.Unix(),
"aud": []string{string(m.ConnectionType)},
"c_id": m.ConnectionID,
})
// no key is needed, as we use custom signing method.
signed, err := token.SignedString(nil)
if err != nil {
return "", fmt.Errorf("failed to sign jwt: %w", err)
}
if !strings.HasPrefix(signed, jwtHeader) {
return "", fmt.Errorf("unpexpected signed string format")
}
return strings.TrimPrefix(signed, jwtHeader), nil
}
type kmsSigningMethod struct {
client *kms.KMS
keyID string
hash crypto.Hash
}
// Verify implements jwt.SigningMethod#Method. Because we
// provide key to jwt library, this is never called.
func (s kmsSigningMethod) Verify(signingString string, sig []byte, key interface{}) error {
panic("should never be called")
}
// Sign implements jwt.SigningMethod#Sign method.
func (s kmsSigningMethod) Sign(signingString string, key interface{}) ([]byte, error) {
messageType := "DIGEST"
hasher := s.hash.New()
if _, err := hasher.Write([]byte(signingString)); err != nil {
return nil, fmt.Errorf("failed to hash input")
}
hashedSigningString := hasher.Sum(nil)
resp, err := s.client.Sign(&kms.SignInput{
KeyId: &s.keyID,
Message: hashedSigningString,
MessageType: &messageType,
SigningAlgorithm: aws.String(awsSigningAlgorithm),
})
if err != nil {
return nil, fmt.Errorf("failed to sign the message: %w", err)
}
// We are using encryption method with SHA_256 digest. Hence, key has 256/8=32 bytes.
keySizeInBytes := 256 / 8
return formatKMSSignatureForJWT(keySizeInBytes, resp.Signature)
}
// Alg implements jwt.SigningMethod#Method.
func (s kmsSigningMethod) Alg() string {
return jwtSigningAlgorithm
}
// formatKMSSignatureForJWT translates asn1 encoded signature (returned by AWS)
// to format expected by JWT standard.
// It is an algorithm I found on the internet
// (here: https://github.com/twofas/2fas-server/pull/24/files/4f68cc2e611dca18b9787942e5cf12fc16518dd4#r1452702669 )
// It should be tested using e2e tests.
func formatKMSSignatureForJWT(keyBytes int, sig []byte) ([]byte, error) {
p := struct {
R *big.Int
S *big.Int
}{}
_, err := asn1.Unmarshal(sig, &p)
if err != nil {
return nil, err
}
rBytes := p.R.Bytes()
rBytesPadded := make([]byte, keyBytes)
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
sBytes := p.S.Bytes()
sBytesPadded := make([]byte, keyBytes)
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
out := append(rBytesPadded, sBytesPadded...)
return out, nil
}

View File

@ -0,0 +1,45 @@
package sign
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
)
var ErrInvalidClaims = errors.New("invalid claims")
// CanI establish connection with type tp given claims in token.
func (s Service) CanI(tokenString string, ct ConnectionType) error {
cl := jwt.MapClaims{}
// In Sign we removed `jwtHeader` from JWT before returning it.
// We need to add it again before doing the verification.
tokenString = jwtHeader + tokenString
token, err := jwt.ParseWithClaims(
tokenString,
&cl,
func(token *jwt.Token) (interface{}, error) {
return s.publicKey, nil
},
jwt.WithValidMethods([]string{"ES256"}),
jwt.WithExpirationRequired(),
)
if err != nil {
return fmt.Errorf("failed to parse token: %w", err)
}
claims, err := token.Claims.GetAudience()
if err != nil {
return fmt.Errorf("failed to get claims: %w", err)
}
for _, aud := range claims {
if aud == string(ct) {
return nil
}
}
return fmt.Errorf("%w: claim %q not found in claims", ErrInvalidClaims, ct)
}

18
tests/localstack_init.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/sh
apt install --assume-yes jq
AWS_REGION=us-east-1
KEY_ALIAS=pass_service
response=$(awslocal kms create-key \
--region $AWS_REGION \
--key-usage SIGN_VERIFY \
--customer-master-key-spec ECC_NIST_P256)
key_id=$(echo "${response}" | jq -r '.KeyMetadata.KeyId')
awslocal kms create-alias \
--region $AWS_REGION \
--alias-name "alias/$KEY_ALIAS" \
--target-key-id "${key_id}"

152
tests/pass/kms_test.go Normal file
View File

@ -0,0 +1,152 @@
package pass
import (
"errors"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/twofas/2fas-server/internal/pass/sign"
)
func TestSignAndVerifyHappyPath(t *testing.T) {
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
Credentials: credentials.NewStaticCredentials("test", "test", ""),
S3ForcePathStyle: aws.Bool(true),
Endpoint: aws.String("http://localhost:4566"),
})
if err != nil {
t.Fatal(err)
}
kmsClient := kms.New(sess)
srv, err := sign.NewService("alias/pass_service", kmsClient)
if err != nil {
t.Fatal(err)
}
now := time.Now()
token, err := srv.SignAndEncode(sign.Message{
ConnectionID: uuid.New().String(),
ExpiresAt: now.Add(time.Hour),
ConnectionType: sign.ConnectionTypeBrowserExtensionProxy,
})
if err != nil {
t.Fatal(err)
}
t.Log(token)
t.Log("Length of the token is", len(token))
if err := srv.CanI(token, sign.ConnectionTypeBrowserExtensionProxy); err != nil {
t.Fatal(err)
}
}
func TestSignAndVerify(t *testing.T) {
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
Credentials: credentials.NewStaticCredentials("test", "test", ""),
S3ForcePathStyle: aws.Bool(true),
Endpoint: aws.String("http://localhost:4566"),
})
if err != nil {
t.Fatal(err)
}
kmsClient := kms.New(sess)
srv, err := sign.NewService("alias/pass_service", kmsClient)
if err != nil {
t.Fatal(err)
}
now := time.Now()
tests := []struct {
name string
tokenFn func() string
expectedError error
}{
{
name: "not even jwt token",
tokenFn: func() string {
return "xxx"
},
expectedError: jwt.ErrTokenMalformed,
},
{
name: "token is expired",
tokenFn: func() string {
token, err := srv.SignAndEncode(sign.Message{
ConnectionID: uuid.New().String(),
ExpiresAt: now.Add(-time.Hour),
ConnectionType: sign.ConnectionTypeBrowserExtensionProxy,
})
if err != nil {
t.Fatal(err)
}
return token
},
expectedError: jwt.ErrTokenExpired,
},
{
name: "invalid claim",
tokenFn: func() string {
token, err := srv.SignAndEncode(sign.Message{
ConnectionID: uuid.New().String(),
ExpiresAt: now.Add(time.Hour),
ConnectionType: sign.ConnectionTypeBrowserExtensionWait,
})
if err != nil {
t.Fatal(err)
}
return token
},
expectedError: sign.ErrInvalidClaims,
},
{
name: "invalid signature",
tokenFn: func() string {
resp, err := kmsClient.CreateKey(&kms.CreateKeyInput{
KeySpec: aws.String("ECC_NIST_P256"),
KeyUsage: aws.String("SIGN_VERIFY"),
})
if err != nil {
t.Fatal(err)
}
serviceWithAnotherKey, err := sign.NewService(*resp.KeyMetadata.KeyId, kmsClient)
if err != nil {
t.Fatal(err)
}
token, err := serviceWithAnotherKey.SignAndEncode(sign.Message{
ConnectionID: uuid.New().String(),
ExpiresAt: now.Add(-time.Hour),
ConnectionType: sign.ConnectionTypeBrowserExtensionProxy,
})
if err != nil {
t.Fatal(err)
}
return token
},
expectedError: jwt.ErrTokenSignatureInvalid,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
token := tc.tokenFn()
err := srv.CanI(token, sign.ConnectionTypeBrowserExtensionProxy)
if err == nil {
t.Fatalf("Expected error %v, got nil", tc.expectedError)
}
if !errors.Is(err, tc.expectedError) {
t.Fatalf("Expected error %v, got %v", tc.expectedError, err)
}
})
}
}