diff --git a/config/config.go b/config/config.go index ada6ca0c..db4a3004 100644 --- a/config/config.go +++ b/config/config.go @@ -56,6 +56,8 @@ const ( PvtKeyForMetering = "pvt_key_for_metering" NotificationServiceEndpoint = "notification_svc_end_point" ServiceHeartbeatType = "service_heartbeat_type" + TokenExpiryInSeconds = "token_expiry_in_seconds" + TokenSecretKey = "token_secret_key" //none|grpc|http //This defaultConfigJson will eventually be replaced by DefaultDaemonConfigurationSchema defaultConfigJson string = ` diff --git a/snetd/cmd/components.go b/snetd/cmd/components.go index 01daf0ee..682c54dc 100644 --- a/snetd/cmd/components.go +++ b/snetd/cmd/components.go @@ -9,6 +9,7 @@ import ( "github.com/singnet/snet-daemon/configuration_service" "github.com/singnet/snet-daemon/metrics" "github.com/singnet/snet-daemon/pricing" + "github.com/singnet/snet-daemon/token" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -50,6 +51,7 @@ type Components struct { freeCallUserService escrow.FreeCallUserService freeCallUserStorage *escrow.FreeCallUserStorage freeCallLockerStorage *escrow.PrefixedAtomicStorage + tokenService token.Service } func InitComponents(cmd *cobra.Command) (components *Components) { @@ -505,3 +507,13 @@ func (components *Components) ConfigurationService() *configuration_service.Conf return components.configurationService } + +func (components *Components) TokenService() token.Service { + if components.tokenService != nil { + return components.tokenService + } + + components.tokenService = token.NewJWTTokenService(*components.OrganizationMetaData()) + + return components.tokenService +} diff --git a/token/jwttoken.go b/token/jwttoken.go new file mode 100644 index 00000000..a653fe3b --- /dev/null +++ b/token/jwttoken.go @@ -0,0 +1,70 @@ +package token + +import ( + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/singnet/snet-daemon/blockchain" + "github.com/singnet/snet-daemon/config" + "strings" + "time" +) + +type customJWTokenServiceImpl struct { + getGroupId func() string +} + +//This will be used in components as a service to Create and Validate tokens +func NewJWTTokenService(data blockchain.OrganizationMetaData) Service { + return &customJWTokenServiceImpl{ + getGroupId: func() string { + return data.GetGroupIdString() + }, + } +} + +func (service customJWTokenServiceImpl) CreateToken(payLoad PayLoad) (CustomToken, error) { + atClaims := jwt.MapClaims{} + atClaims["payload"] = fmt.Sprintf("%v", payLoad) + atClaims["orgId"] = config.GetString(config.OrganizationId) + atClaims["groupId"] = service.getGroupId() + //set the Expiry of the Token generated + atClaims["exp"] = time.Now().UTC(). + Add(time.Second * time.Duration(config.GetInt(config.TokenExpiryInSeconds))).Unix() + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) + return jwtToken.SignedString([]byte(config.GetString(config.TokenSecretKey))) +} + +func (service customJWTokenServiceImpl) VerifyToken(receivedToken CustomToken, payLoad PayLoad) (err error) { + tokenString := fmt.Sprintf("%v", receivedToken) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method : %v", token.Header["alg"]) + } + return []byte(config.GetString(config.TokenSecretKey)), nil + }) + if err != nil { + return err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + if err = service.checkJwtTokenClaims(claims, payLoad); err != nil { + return err + } + } + return nil +} + +func (service customJWTokenServiceImpl) checkJwtTokenClaims(claims jwt.MapClaims, payload PayLoad) (err error) { + if strings.Compare(fmt.Sprintf("%v", claims["payload"]), fmt.Sprintf("%v", payload)) != 0 { + return fmt.Errorf("payload %v used to generate the Token doesnt match expected values", claims["payload"]) + } + + if strings.Compare(fmt.Sprintf("%v", claims["orgId"]), config.GetString(config.OrganizationId)) != 0 { + return fmt.Errorf("organization %v is not associated with this Daemon", claims["orgId"]) + } + + if strings.Compare(fmt.Sprintf("%v", claims["groupId"]), service.getGroupId()) != 0 { + return fmt.Errorf("groupId %v is not associated with this Daemon", claims["groupId"]) + } + return nil +} diff --git a/token/jwttoken_test.go b/token/jwttoken_test.go new file mode 100644 index 00000000..8bf75c26 --- /dev/null +++ b/token/jwttoken_test.go @@ -0,0 +1,51 @@ +package token + +import ( + "testing" + "time" + + "github.com/singnet/snet-daemon/config" + "github.com/stretchr/testify/assert" +) + +func Test_customJWTokenClaimsImpl_CreateToken(t *testing.T) { + tokenImpl := &customJWTokenServiceImpl{ + getGroupId: func() string { + return "GroupID" + }, + } + token, err := tokenImpl.CreateToken("any struct") + assert.Nil(t, err) + assert.NotNil(t, token) + err = tokenImpl.VerifyToken(token, "any struct") + config.Vip().Set(config.TokenExpiryInSeconds, 1) + token, err = tokenImpl.CreateToken("any struct") + time.Sleep(time.Second * 5) + assert.Nil(t, err) + err = tokenImpl.VerifyToken(token, "any struct") + assert.Equal(t, "Token is expired", err.Error()) + +} + +func Test_customJWTokenClaimsImpl_checkJwtTokenClaims(t *testing.T) { + tokenImpl := &customJWTokenServiceImpl{ + getGroupId: func() string { + return "GroupID" + }, + } + config.Vip().Set(config.TokenExpiryInSeconds, 50) + token, err := tokenImpl.CreateToken("any struct") + err = tokenImpl.VerifyToken(token, "different struct") + assert.Equal(t, "payload any struct used to generate the Token doesnt match expected values", err.Error()) + config.Vip().Set(config.OrganizationId, "differentOrganization") + err = tokenImpl.VerifyToken(token, "any struct") + assert.Equal(t, "organization ExampleOrganizationId is not associated with this Daemon", err.Error()) + config.Vip().Set(config.OrganizationId, "ExampleOrganizationId") + tokenImpl2 := &customJWTokenServiceImpl{ + getGroupId: func() string { + return "GroupID2" + }, + } + err = tokenImpl2.VerifyToken(token, "any struct") + assert.Equal(t, "groupId GroupID is not associated with this Daemon", err.Error()) +} diff --git a/token/token_service_api.go b/token/token_service_api.go new file mode 100644 index 00000000..b1131882 --- /dev/null +++ b/token/token_service_api.go @@ -0,0 +1,10 @@ +package token + +type PayLoad interface{} +type CustomToken interface{} + +// Token.Service interface is an API for creating and verifying tokens +type Service interface { + CreateToken(key PayLoad) (token CustomToken, err error) + VerifyToken(token CustomToken, key PayLoad) (err error) +}