Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
76
pkg/jwt/jwt_parser.go
Normal file
76
pkg/jwt/jwt_parser.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// AuthToken token json
|
||||
type AuthToken struct {
|
||||
Token string `json:"token" validate:"jwt"`
|
||||
}
|
||||
|
||||
// GetPayload decodes the token payload
|
||||
func GetPayload(token string) (map[string]interface{}, error) {
|
||||
payload := map[string]interface{}{}
|
||||
|
||||
data, err := parsePayload(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &payload)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetAuthorizationHeader parses auth token from Authorization header
|
||||
func GetAuthorizationHeader(r *http.Request) (AuthToken, error) {
|
||||
auth := AuthToken{}
|
||||
header := r.Header.Get(AuthenticationHeader)
|
||||
if header == "" {
|
||||
return auth, errors.New("no authorization header")
|
||||
}
|
||||
if !strings.Contains(header, "Bearer ") {
|
||||
return auth, errors.New("missing Bearer")
|
||||
}
|
||||
auth.Token = ParseJWTToken(header)
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func ParseJWTToken(token string) string {
|
||||
return strings.ReplaceAll(token, "Bearer ", "")
|
||||
}
|
||||
|
||||
func parsePayload(token string) ([]byte, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) < 3 {
|
||||
return nil, errors.New("unable to parse token")
|
||||
}
|
||||
raw, err := decodeJWT(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func decodeJWT(src string) ([]byte, error) {
|
||||
if l := len(src) % 4; l > 0 {
|
||||
src += strings.Repeat("=", 4-l)
|
||||
}
|
||||
|
||||
decoded, err := base64.URLEncoding.DecodeString(src)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
119
pkg/jwt/jwt_test.go
Normal file
119
pkg/jwt/jwt_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
const expiredToken = "eyJraWQiOiJlUTNuZFJLaUVcL084VUZ5RHFsYjN0S1RzWG00SzVPMlc4NXd3VWkzT2tNZz0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiUGFqSzVNX0d0M3lta0ZOTjhOMUJydyIsInN1YiI6IjJkZDZmZWQ5LWU1ODItNDUxYi1hOTNiLTViOTQxMGRmYmM0MyIsImNvZ25pdG86Z3JvdXBzIjpbInVzLXdlc3QtMl9BV3dqTFh5bTJfQXp1cmVBRCJdLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX0FXd2pMWHltMiIsImNvZ25pdG86dXNlcm5hbWUiOiJhenVyZWFkX2p3dUBmaXNrZXJpbmMuY29tIiwiYXVkIjoiN2NrMnRmb3FhdmM3MmM0NWhoN3RnZTQya2QiLCJpZGVudGl0aWVzIjpbeyJ1c2VySWQiOiJqd3VAZmlza2VyaW5jLmNvbSIsInByb3ZpZGVyTmFtZSI6IkF6dXJlQUQiLCJwcm92aWRlclR5cGUiOiJTQU1MIiwiaXNzdWVyIjoiaHR0cHM6XC9cL3N0cy53aW5kb3dzLm5ldFwvNWFhNGI2NDAtYzlmYy00YTliLWIzYTMtZDRhN2QwMDhmYjVlXC8iLCJwcmltYXJ5IjoidHJ1ZSIsImRhdGVDcmVhdGVkIjoiMTYxMjkwMjQxMzM4MyJ9XSwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE2MTMxNTkzNDAsImV4cCI6MTYxMzE3OTk2MywiaWF0IjoxNjEzMTc2MzYzLCJlbWFpbCI6Imp3dUBmaXNrZXJpbmMuY29tIn0.lMIMjTaG11Y-Ft6wbuE9J3ic4EWmK-VgDXbcO583r8sckgKfWgpTI9Qy3zkkhmN0btDtQP4EqKI5afHKbDVu02wZk2y_y1adgWBxLtOJX3yCifxK99mCQUAjMvyBQ3_YbhLUexv3kvh047w0Fe3VjdPftfRwpfbmQsIYjWhF-MzDjdZJPXnXm3GjbtW6g3eKqA9AHg05ghBC4seatrDhHWKVSYS8DzmfJlsJCcdbdzZQ3fVLnYsVOU8-LK6B-IbpmpTUaobcF-acAwFaNPD56mGxI3xpnvExc9sM8ZBQD2NNhnHqY04p7mjaK2Wf4p73yLtI3SdW5SWy-w1reiaElQ"
|
||||
const invalidToken = "eyJraWQiOiJlUTNuZFJLaUVcL084VUZ5RHFsYjN0S1RzWG00SzVPMlc4NXd3VWkzT2tNZz0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiUGFqSzVNX0d0M3lta0ZOTjhOMUJydyIsInN1YiI6IjJkZDZmZWQ5LWU1ODItNDUxYi1hOTNiLTViOTQxMGRmYmM0MyIsImNvZ25pdG86Z3JvdXBzIjpbInVzLXdlc3QtMl9BV3dqTFh5bTJfQXp1cmVBRCJdLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX0FXd2pMWHltMiIsImNvZ25pdG86dXNlcm5hbWUiOiJhenVyZWFkX2p3dUBmaXNrZXJpbmMuY29tIiwiYXVkIjoiN2NrMnRmb3FhdmM3MmM0NWhoN3RnZTQya2QiLCJpZGVudGl0aWVzIjpbeyJ1c2VySWQiOiJqd3VAZmlza2VyaW5jLmNvbSIsInByb3ZpZGVyTmFtZSI6IkF6dXJlQUQiLCJwcm92aWRlclR5cGUiOiJTQU1MIiwiaXNzdWVyIjoiaHR0cHM6XC9cL3N0cy53aW5kb3dzLm5ldFwvNWFhNGI2NDAtYzlmYy00YTliLWIzYTMtZDRhN2QwMDhmYjVlXC8iLCJwcmltYXJ5IjoidHJ1ZSIsImRhdGVDcmVhdGVkIjoiMTYxMjkwMjQxMzM4MyJ9XSwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE2MTMxNTkzNDAsImV4cCI6MTYxMzE3OTk2MywiaWF0IjoxNjEzMTc2MzYzLCJlbWFpbCI6Imp3dUBmaXNrZXJpbmMuY29tIn0.lMIMjTaG11Y-Ft6wbuE9J3ic4EWmK-VgDXbcO583r8sckgKfWgpTI9Qy3zkkhmN0btDtQP4EqKI5afHKbDVu02wZk2y_y1adgWBxLtOJX3yCifxK99mCQUAjMvyBQ3_YbhLUexv3kvh047w0Fe3VjdPftfRwpfbmQsIYjWhF-MzDjdZJPXnXm3GjbtW6g3eKqA9AHg05ghBC4seatrDhHWKVSYS8DzmfJlsJCcdbdzZQ3fVLnYsVOU8-LK6B-IbpmpTUaobcF-acAwFaNPD56mGxI3xpnvExc9sM8ZBQD2NNhnHqY04p7mjaK2Wf4p73yLtI3SdW5SWy-w1reiaEl"
|
||||
|
||||
func init() {
|
||||
os.Setenv("JWK_URL", "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_AWwjLXym2/.well-known/jwks.json")
|
||||
}
|
||||
|
||||
func TestValidation(t *testing.T) {
|
||||
validator := NewJWTValidator("")
|
||||
type testCase struct {
|
||||
Name string
|
||||
Token string
|
||||
ExpectedError string
|
||||
DisableExpireCheck bool
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
Name: "Expired",
|
||||
Token: expiredToken,
|
||||
ExpectedError: "token expired",
|
||||
},
|
||||
{
|
||||
Name: "Invalid",
|
||||
Token: invalidToken,
|
||||
ExpectedError: "invalid token",
|
||||
},
|
||||
{
|
||||
Name: "Expired Disabled",
|
||||
Token: expiredToken,
|
||||
DisableExpireCheck: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
validator.DisableExpireCheck(test.DisableExpireCheck)
|
||||
_, err := validator.ValidateToken(test.Token)
|
||||
if err != nil && err.Error() != test.ExpectedError {
|
||||
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedError, err.Error())
|
||||
}
|
||||
if test.ExpectedError == "" && err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedError, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPayload(t *testing.T) {
|
||||
payload, err := GetPayload(expiredToken)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Payload", "No error", err)
|
||||
}
|
||||
if payload == nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Payload", "Not nil", payload)
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Payload", "Has data", len(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAuthorizationHeader(t *testing.T) {
|
||||
type testCase struct {
|
||||
Name string
|
||||
Request *http.Request
|
||||
ExpectedToken string
|
||||
ExpectedError string
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
Name: "No header",
|
||||
Request: testhelper.MakeTestRequestWithHeaders(http.MethodGet, "/", map[string]string{}, nil),
|
||||
ExpectedError: "no authorization header",
|
||||
},
|
||||
{
|
||||
Name: "Blank header",
|
||||
Request: testhelper.MakeTestRequestWithHeaders(http.MethodGet, "/", map[string]string{
|
||||
"Authorization": "",
|
||||
}, nil),
|
||||
ExpectedError: "no authorization header",
|
||||
},
|
||||
{
|
||||
Name: "No Bearer",
|
||||
Request: testhelper.MakeTestRequestWithHeaders(http.MethodGet, "/", map[string]string{
|
||||
"Authorization": "XXXXXXXXXXX",
|
||||
}, nil),
|
||||
ExpectedError: "missing Bearer",
|
||||
},
|
||||
{
|
||||
Name: "Good header",
|
||||
Request: testhelper.MakeTestRequestWithHeaders(http.MethodGet, "/", map[string]string{
|
||||
"Authorization": "Bearer XXXXXXXXXXX",
|
||||
}, nil),
|
||||
ExpectedToken: "XXXXXXXXXXX",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
auth, err := GetAuthorizationHeader(test.Request)
|
||||
if err != nil && err.Error() != test.ExpectedError {
|
||||
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedError, err.Error())
|
||||
}
|
||||
if test.ExpectedError == "" && err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedError, err.Error())
|
||||
}
|
||||
if auth.Token != test.ExpectedToken {
|
||||
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedToken, auth.Token)
|
||||
}
|
||||
}
|
||||
}
|
||||
101
pkg/jwt/jwt_validator.go
Normal file
101
pkg/jwt/jwt_validator.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
|
||||
"github.com/lestrrat-go/jwx/jwk"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const AuthenticationHeader = "Authorization"
|
||||
const invalidTokenError = "invalid token"
|
||||
const expiredTokenError = "token expired"
|
||||
|
||||
// default jwk for https://cognito-idp.us-west-2.amazonaws.com/us-west-2_AWwjLXym2/.well-known/jwks.json
|
||||
var DefaultJWK = `{"keys":[{"alg":"RS256","e":"AQAB","kid":"eQ3ndRKiE/O8UFyDqlb3tKTsXm4K5O2W85wwUi3OkMg=","kty":"RSA","n":"pVeGfTzlCvcnzUE4f7LsVDhzsZbGdAn6q1LH3DSwqFF6Xw-c6z8AGV744_qvxRrDlmQs85cXPJHh2AVKJQnWBipp6EUWO5TEdMS_0cgoTk1Gr3CagUnYBZwm53HIUC8bMuWx0C6FQWcnmleNQbWR_k-zipsPbZw2sYAtSWRVGfjG6Gwo4wZx0spBk9hq3ovG5mVxnItnKJYWyx3V_ZKKa5r5ImItJa1AwaxoZxsO13NMOPTed89iSbK_IR_Db8pX6STgl6pa6YYSvI1-phBt_PLjTz2gusRj897sHxJYga5KfNgbvNkeHdaDljwilT4IKDZq1hzIrmaPrUKApb0e9w","use":"sig"},{"alg":"RS256","e":"AQAB","kid":"jIz0QTcsKCT+hxGz2S0+ChPyN7w8riP/l6mqzAXRl6o=","kty":"RSA","n":"yDqFnw52wraJImOT5rCPL2www0pRglnSS-GPG6kZMqos7KHqcO5pVD020_5g2OefK6Gs0ndUI3eDOeBwASKeZuoezAgu9D9whFHJI6-_oIiz2af3ahodRISnhFAbwcvU4i8_M6OWATVaTU5aODAcM_8q1aS-Rfp6zY9rrlaJ6RmCdYeVNue4nvS97bOrpTXmFBB2fAzbhWSq0axmWZWBFyMO12FFMvT_dCaL1dzBOEzNQU03tKsUa0WEqNs169utuo9TydX9hhjpnDtqYjIEvyOFTAnU8IldX_iiWbnR1-8BHeyqomMQFIjQCTRkLReKYDAyrVF4cFah-BDYQiluCw","use":"sig"}]}`
|
||||
|
||||
type JWTValidatorInterface interface {
|
||||
ValidateToken(token string) (map[string]interface{}, error)
|
||||
ValidateError(token string) (err error)
|
||||
SetKeys(data string)
|
||||
DisableExpireCheck(disable bool)
|
||||
}
|
||||
|
||||
func NewJWTValidator(jwkUrl string) JWTValidatorInterface {
|
||||
return &JWTValidator{jwkUrl: jwkUrl}
|
||||
}
|
||||
|
||||
type JWTValidator struct {
|
||||
disableExpireCheck bool
|
||||
jwkUrl string
|
||||
keys jwk.Set
|
||||
option jwt.ParseOption
|
||||
}
|
||||
|
||||
// ValidateToken validates a token
|
||||
func (v *JWTValidator) ValidateToken(token string) (map[string]interface{}, error) {
|
||||
result, err := jwt.ParseString(token, v.getKeySetOption(), jwt.InferAlgorithmFromKey(true))
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Send()
|
||||
return nil, errors.New(invalidTokenError)
|
||||
}
|
||||
|
||||
if !v.disableExpireCheck && time.Now().After(result.Expiration()) {
|
||||
return nil, errors.New(expiredTokenError)
|
||||
}
|
||||
|
||||
return result.PrivateClaims(), nil
|
||||
}
|
||||
|
||||
// returns the original validation error with all the tech details
|
||||
func (v *JWTValidator) ValidateError(token string) (err error) {
|
||||
_, err = jwt.ParseString(token, v.getKeySetOption())
|
||||
return
|
||||
}
|
||||
|
||||
func (v *JWTValidator) getKeySetOption() jwt.ParseOption {
|
||||
if v.option == nil {
|
||||
v.option = jwt.WithKeySet(v.getKeys())
|
||||
logger.Info().Msgf("getKeySetOption %v", v.option)
|
||||
}
|
||||
|
||||
return v.option
|
||||
}
|
||||
|
||||
func (v *JWTValidator) getKeys() jwk.Set {
|
||||
if v.keys == nil {
|
||||
var err error
|
||||
if len(v.jwkUrl) == 0 {
|
||||
v.jwkUrl = envtool.GetEnv("JWK_URL", "NOT_SET")
|
||||
}
|
||||
if v.jwkUrl == "NOT_SET" {
|
||||
logger.Info().Msg("getKeys no jwk url, using default")
|
||||
v.SetKeys(DefaultJWK)
|
||||
} else {
|
||||
v.keys, err = jwk.Fetch(context.Background(), v.jwkUrl)
|
||||
if err != nil {
|
||||
logger.Error().Err(errors.WithStack(err)).Send()
|
||||
}
|
||||
logger.Info().Msgf("getKeys %v", v.keys)
|
||||
}
|
||||
}
|
||||
|
||||
return v.keys
|
||||
}
|
||||
|
||||
// Only use this for unit tests to disable expire check on token
|
||||
func (v *JWTValidator) DisableExpireCheck(disable bool) {
|
||||
v.disableExpireCheck = disable
|
||||
}
|
||||
|
||||
// SetKeys sets JWK keys from JSON data
|
||||
func (v *JWTValidator) SetKeys(data string) {
|
||||
v.keys = jwk.NewSet()
|
||||
json.Unmarshal([]byte(data), &v.keys)
|
||||
}
|
||||
Reference in New Issue
Block a user