Skip to content

Commit

Permalink
Merge main (#708)
Browse files Browse the repository at this point in the history
* Setup cron system (#691)

* Setup cron system

* removed retry events schedule

* add support for jwt authentication (#690)

* add support for jwt authentication

* feat: add support for blacklisting tokens and logout endpoint

* chore: update route

* add test for user repository

* add user service test

* add user integration test

* add jwt test

* add jwt realm test

* chore: fix failing test

* chore: add docs

* chore: refactor jwt config to be standalone

* update routes

* chore: add duration comment

* add ReplayAppEvent endpoint (#672)

* feat: add ReplayAppEvent endpoint

* tests: add tests

* fix: specify wantErr

* fix: change url to /events/{eventID/replay

* tests: fix testdb.SeedEvent call

Co-authored-by: Subomi Oluwalana <subomioluwalana71@gmail.com>

* add crud for organisations (#685)

* feat: add crud for organisations

* feat: implement crud for organisations

* feat: implement crud for organisations in BadgerDB

* tests: add tests

* tests: add integration build tags

* tests: fix sorting

* tests: fix sorting

* tests: fix TestDeleteOrganisation

* tests: fix tests

* tests: fix tests

* tests: add tests for organisation_service.go

* tests: add organisation_integration_test.go

* tests: add integration build tag

* fix: add requireOrganisation to organisation sub route

* tests: fix tests
tests: add Test_UpdateOrganisation_EmptyOrganisationName & Test_CreateOrganisation_EmptyOrganisationName

* tests: fix tests

* fix: implement suggestions

* tests: fix urls

* fix initRealmChain call

* docs: regenerate docs

* Add missing comma to convoy.json.example (#700)

* chore: fixed tests and build

Co-authored-by: Subomi Oluwalana <subomioluwalana71@gmail.com>
Co-authored-by: Dotun Jolaoso <dotunjolaosho@gmail.com>
Co-authored-by: Daniel Oluojomu <danvixent@gmail.com>
Co-authored-by: Daniel Perrefort <djperrefort@pitt.edu>
  • Loading branch information
5 people committed Jun 6, 2022
1 parent 3f0c87c commit 6981526
Show file tree
Hide file tree
Showing 65 changed files with 9,036 additions and 1,618 deletions.
2 changes: 2 additions & 0 deletions auth/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Credential struct {
Username string `json:"username"`
Password string `json:"password"`
APIKey string `json:"api_key"`
Token string `json:"token"`
}

func (c *Credential) String() string {
Expand All @@ -28,6 +29,7 @@ type CredentialType string
const (
CredentialTypeBasic = CredentialType("BASIC")
CredentialTypeAPIKey = CredentialType("BEARER")
CredentialTypeJWT = CredentialType("JWT")
)

func (c CredentialType) String() string {
Expand Down
201 changes: 201 additions & 0 deletions auth/realm/jwt/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package jwt

import (
"context"
"encoding/base64"
"errors"
"fmt"
"time"

"github.com/frain-dev/convoy"
"github.com/frain-dev/convoy/cache"
"github.com/frain-dev/convoy/config"
"github.com/frain-dev/convoy/datastore"
"github.com/frain-dev/convoy/util"
"github.com/golang-jwt/jwt"
)

var (
ErrInvalidToken = errors.New("invalid token")
ErrTokenExpired = errors.New("expired token")
)

type Token struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}

type VerifiedToken struct {
UserID string
Expiry int64
}

const (
JwtDefaultSecret string = "convoy-jwt"
JwtDefaultRefreshSecret string = "convoy-refresh-jwt"
JwtDefaultExpiry int = 1800 //seconds
JwtDefaultRefreshExpiry int = 86400 //seconds
)

type Jwt struct {
Secret string
Expiry int
RefreshSecret string
RefreshExpiry int
cache cache.Cache
}

func NewJwt(opts *config.JwtRealmOptions, cache cache.Cache) *Jwt {

j := &Jwt{
Secret: opts.Secret,
Expiry: opts.Expiry,
RefreshSecret: opts.RefreshSecret,
RefreshExpiry: opts.RefreshExpiry,
cache: cache,
}

if util.IsStringEmpty(j.Secret) {
j.Secret = JwtDefaultSecret
}

if util.IsStringEmpty(j.RefreshSecret) {
j.RefreshSecret = JwtDefaultRefreshSecret
}

if j.Expiry == 0 {
j.Expiry = JwtDefaultExpiry
}

if j.RefreshExpiry == 0 {
j.RefreshExpiry = JwtDefaultRefreshExpiry
}

return j
}

func (j *Jwt) GenerateToken(user *datastore.User) (Token, error) {
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.UID,
"exp": time.Now().Add(time.Second * time.Duration(j.Expiry)).Unix(),
})

token := Token{}

accessToken, err := tok.SignedString([]byte(j.Secret))
if err != nil {
return token, err
}

refreshToken, err := j.generateRefreshToken(user)
if err != nil {
return token, err
}

token.AccessToken = accessToken
token.RefreshToken = refreshToken

return token, nil

}

func (j *Jwt) ValidateAccessToken(accessToken string) (*VerifiedToken, error) {
return j.validateToken(accessToken, j.Secret)
}

func (j *Jwt) ValidateRefreshToken(refreshToken string) (*VerifiedToken, error) {
return j.validateToken(refreshToken, j.RefreshSecret)
}

// A token is considered blacklisted if the base64 encoding
// of the token exists as a key within the cache
func (j *Jwt) isTokenBlacklisted(token string) (bool, error) {
var exists *string

key := convoy.TokenCacheKey.Get(j.EncodeToken(token)).String()
err := j.cache.Get(context.Background(), key, &exists)

if err != nil {
return false, err
}

if exists == nil {
return false, nil
}

return true, nil

}

func (j *Jwt) BlacklistToken(verified *VerifiedToken, token string) error {
// Calculate the remaining valid time for the token
ttl := time.Until(time.Unix(verified.Expiry, 0))
key := convoy.TokenCacheKey.Get(j.EncodeToken(token)).String()
err := j.cache.Set(context.Background(), key, &verified.UserID, ttl)

if err != nil {
return err
}

return nil
}

func (j *Jwt) EncodeToken(token string) string {
return base64.StdEncoding.EncodeToString([]byte(token))
}

func (j *Jwt) generateRefreshToken(user *datastore.User) (string, error) {
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.UID,
"exp": time.Now().Add(time.Second * time.Duration(j.RefreshExpiry)).Unix(),
})

return refreshToken.SignedString([]byte(j.RefreshSecret))
}

func (j *Jwt) validateToken(accessToken, secret string) (*VerifiedToken, error) {
var userId string
var expiry float64

isBlacklisted, err := j.isTokenBlacklisted(accessToken)
if err != nil {
return nil, err
}

if isBlacklisted {
return nil, ErrInvalidToken
}

token, err := jwt.Parse(accessToken, func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}

return []byte(secret), nil
})

if err != nil {
v, ok := err.(*jwt.ValidationError)
if ok && v.Errors == jwt.ValidationErrorExpired {
if payload, ok := token.Claims.(jwt.MapClaims); ok {
expiry = payload["exp"].(float64)
}

return &VerifiedToken{Expiry: int64(expiry)}, ErrTokenExpired
}

return nil, err
}

payload, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
userId = payload["sub"].(string)
expiry = payload["exp"].(float64)

v := &VerifiedToken{UserID: userId, Expiry: int64(expiry)}
return v, nil
}

return nil, err
}
49 changes: 49 additions & 0 deletions auth/realm/jwt/jwt_realm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package jwt

import (
"context"
"fmt"

"github.com/frain-dev/convoy/auth"
"github.com/frain-dev/convoy/cache"
"github.com/frain-dev/convoy/config"
"github.com/frain-dev/convoy/datastore"
)

type JwtRealm struct {
userRepo datastore.UserRepository
jwt *Jwt
}

func NewJwtRealm(userRepo datastore.UserRepository, opts *config.JwtRealmOptions, cache cache.Cache) *JwtRealm {
return &JwtRealm{userRepo: userRepo, jwt: NewJwt(opts, cache)}
}

func (j *JwtRealm) Authenticate(ctx context.Context, cred *auth.Credential) (*auth.AuthenticatedUser, error) {
if cred.Type != auth.CredentialTypeJWT {
return nil, fmt.Errorf("%s only authenticates credential type %s", j.GetName(), auth.CredentialTypeJWT.String())
}

verified, err := j.jwt.ValidateAccessToken(cred.Token)
if err != nil {
return nil, ErrInvalidToken
}

user, err := j.userRepo.FindUserByID(ctx, verified.UserID)
if err != nil {
return nil, ErrInvalidToken
}

authUser := &auth.AuthenticatedUser{
AuthenticatedByRealm: j.GetName(),
Credential: *cred,
Role: user.Role,
}

return authUser, nil

}

func (j *JwtRealm) GetName() string {
return "jwt"
}

0 comments on commit 6981526

Please sign in to comment.