diff --git a/environment/environmentmiddleware.go b/environment/environmentmiddleware.go index c3ed457..e8ba756 100644 --- a/environment/environmentmiddleware.go +++ b/environment/environmentmiddleware.go @@ -63,4 +63,4 @@ func Get(ctx context.Context) string { return "" } return value -} \ No newline at end of file +} diff --git a/environment/environmentmiddleware_test.go b/environment/environmentmiddleware_test.go index 6069a7b..e021865 100644 --- a/environment/environmentmiddleware_test.go +++ b/environment/environmentmiddleware_test.go @@ -1,10 +1,11 @@ package environment_test import ( - "github.com/d-velop/dvelop-sdk-go/environment" "net/http" "net/http/httptest" "testing" + + "github.com/d-velop/dvelop-sdk-go/environment" ) func TestRequestWithEnvironmentFunction_UsesReturnedEnvironment(t *testing.T) { diff --git a/idp/authmiddleware_test.go b/idp/authmiddleware_test.go index e28660a..4f20f30 100644 --- a/idp/authmiddleware_test.go +++ b/idp/authmiddleware_test.go @@ -10,6 +10,7 @@ import ( "net/url" "reflect" "testing" + "time" "github.com/d-velop/dvelop-sdk-go/idp" diff --git a/idp/test/testing.go b/idp/test/testing.go index f6a69da..b4fc88e 100644 --- a/idp/test/testing.go +++ b/idp/test/testing.go @@ -2,10 +2,11 @@ package test import ( "encoding/json" - "github.com/d-velop/dvelop-sdk-go/idp/scim" "net/http" "net/http/httptest" "regexp" + + "github.com/d-velop/dvelop-sdk-go/idp/scim" ) var bearerTokenRegex = regexp.MustCompile("^(?i)bearer (.*)$") diff --git a/lambda/environment.go b/lambda/environment.go index a641eb7..3b063f9 100644 --- a/lambda/environment.go +++ b/lambda/environment.go @@ -1,9 +1,10 @@ package lambda import ( - "github.com/aws/aws-lambda-go/lambdacontext" "net/http" "strings" + + "github.com/aws/aws-lambda-go/lambdacontext" ) func GetAliasFromRequest(req http.Request) string { @@ -18,4 +19,4 @@ func GetAliasFromRequest(req http.Request) string { } return "" -} \ No newline at end of file +} diff --git a/lambda/environment_test.go b/lambda/environment_test.go index abe5180..b4ed7f2 100644 --- a/lambda/environment_test.go +++ b/lambda/environment_test.go @@ -2,10 +2,11 @@ package lambda_test import ( "context" - "github.com/aws/aws-lambda-go/lambdacontext" - "github.com/d-velop/dvelop-sdk-go/lambda" "net/http" "testing" + + "github.com/aws/aws-lambda-go/lambdacontext" + "github.com/d-velop/dvelop-sdk-go/lambda" ) func TestGetAliasFromRequest_LambdaArnHasNamedLambdaAlias_ReturnsAliasFromArn(t *testing.T) { diff --git a/requestsignature/go.mod b/requestsignature/go.mod new file mode 100644 index 0000000..8a430cd --- /dev/null +++ b/requestsignature/go.mod @@ -0,0 +1,3 @@ +module github.com/d-velop/dvelop-sdk-go/requestsignature + +go 1.13 diff --git a/requestsignature/requestsignature.go b/requestsignature/requestsignature.go new file mode 100644 index 0000000..2920c45 --- /dev/null +++ b/requestsignature/requestsignature.go @@ -0,0 +1,341 @@ +package requestsignature + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "fmt" + "io/ioutil" + "net/http" + "path" + "reflect" + "regexp" + "sort" + "strings" + "time" +) + +type RequestSigner interface { + ValidateSignedRequest(req *http.Request) error +} + +type loggerFunc func(ctx context.Context, message string) + +type Dto struct { + EventType string `json:"type"` + TenantId string `json:"tenantId"` + BaseUri string `json:"baseUri"` +} + +// the DvelopLifeCycleEventPath is path of an app endpoint, that apps must be provide +const DvelopLifeCycleEventPath = "dvelop-cloud-lifecycle-event" + +// header who contains relevant headers for signature +const signatureHeaderKey = "x-dv-signature-headers" + +// header who contains timestamp of request +const timestampHeaderKey = "x-dv-signature-timestamp" + +// allowed content-type header value +const validContentTypeHeaderValue = "application/json" + +// valid time differenz of request +const timeDiff = 5 * time.Minute + +// Validate signature of request i.e. for validate HTTP events from cloud center +// The middleware "HandleCloudSignatureMiddleware" checks the signature of incoming requests. This is important for +// cloud center to app authentication. The cloudcenter make an POST request to app with a signature. The middleware +// checks if request is a POST request and the content-type header is set to "application/json". +// If the requested signature is valid, then your handler is invoke to handle the request. If the +// signature is invalid, the middleware returns the HTTP error 403 "Forbidden" and log the reason to your application log. +// +// The parameter for the "appSecret" is the base64 decoded app secret string of your app as byte array. +// +// More information about signature algorithm please visit the following documentation: +// https://developer.d-velop.de/documentation/ccapi/en/cloud-center-api-199623589.html +// +// Example: +// func main() { +// // replace `Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=` with your app secret (base64-string) +// myAppSecret, err := base64.StdEncoding.DecodeString(`Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=`) +// if err != nil { +// panic(err) +// } +// mux := http.NewServeMux() +// // the path must a ressource for dvelop-cloud-lifecycle-event +// path := "/app/dvelop-cloud-lifecycle-event" +// mux.Handle(path, requestsignatur.HandleSignaturValidation(myAppSecret, time.Now)(eventHandler())) +// } +// +// func eventHandler() http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { +// eventDto := new(requestsignature.RequestSignatureDto) +// err := json.NewDecoder(req.Body).Decode(eventDto) +// if err != nil { +// log.Print(err) +// http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +// return +// } +// doSomeStuff(eventDto) +// }) +// } + +func HandleCloudSignatureMiddleware(appSecret []byte, timeNow func() time.Time, logError, logInfo loggerFunc) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + if appSecret == nil { + logError(ctx, "validation signed request failed because app secret has not been configured") + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if req.Method != http.MethodPost { + logError(ctx, fmt.Sprintf("only POST request can be signed. Got method %v", req.Method)) + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + if pathBase := path.Base(req.URL.Path); pathBase != DvelopLifeCycleEventPath { + logError(ctx, fmt.Sprintf("path %v is not life cycle path. Life cycle path must %v", req.URL.Path, DvelopLifeCycleEventPath)) + http.Error(rw, fmt.Sprintf("wrong life cylce path: got %v", req.URL.Path), http.StatusBadRequest) + return + } + + if accept := req.Header.Get("content-type"); accept != validContentTypeHeaderValue { + logError(ctx, fmt.Sprintf("wrong content-type header found. Got %v want %v", accept, validContentTypeHeaderValue)) + http.Error(rw, fmt.Sprintf("%s: please use content-type '%s'", http.StatusText(http.StatusBadRequest), validContentTypeHeaderValue), http.StatusNotAcceptable) + return + } + + signer := NewRequestSigner(appSecret, timeNow, logInfo) + err := signer.ValidateSignedRequest(req) + if err != nil { + logError(ctx, fmt.Sprintf("validate signed request failed: %v", err)) + http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + next.ServeHTTP(rw, req) + }) + } +} + +// Validate signature of request as function i.e. for validate HTTP events from cloud center +// The function "ValidateSignedRequest" in "RequestSignatureValidator" checks the signature of a requests. +// This is important for cloud center to app authentication. The cloudcenter make an POST request to app with a signature. +// It checks if request is a POST request and the content-type header is set to "application/json". Then an own signature +// will be calculated by information from header "dv-signature-headers" and a hash of request body. If the calculcated +// signature is equals to signature of Authorization-header, the signature in request is valid. If signature is valid, +// no error is returned from the function. Otherwise it returns an error and you must abort the request by returning +// HTTP error 403 "Forbidden". +// +// The parameter for the "appSecret" is the base64 decoded app secret string of your app as byte array. +// +// More information about signature algorithm please visit the following documentation on +// https://developer.d-velop.de/documentation/ccapi/en/cloud-center-api-199623589.html +// +// Example: +// func eventHandler() http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { +// // replace `Zm9vYmFy` with your app secret (base64-string) +// myAppSecret, err := base64.StdEncoding.DecodeString(`Zm9vYmFy`) +// if err != nil { +// panic(err) +// } +// signatureValidator := NewRequestSignaturValidator(myAppSecret, time.Now) +// err = signatureValidator.ValidateSignedRequest(req) +// if err != nil { +// log.Print(err) +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// +// eventDto := &struct { +// EventType string `json:"type"` +// TenantId string `json:"tenantId"` +// BaseUri string `json:"baseUri"` +// }{} +// err = json.NewDecoder(req.Body).Decode(eventDto) +// if err != nil { +// log.Print(err) +// http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +// return +// } +// doSomeStuff(eventDto) +// }) +//} + +type requestSigner struct { + appSecret []byte + now func() time.Time + logInfo loggerFunc +} + +func NewRequestSigner(appSecret []byte, timeNow func() time.Time, logInfo loggerFunc) RequestSigner { + return &requestSigner{ + appSecret, + timeNow, + logInfo, + } +} + +// validate signed request as function +func (signer *requestSigner) ValidateSignedRequest(req *http.Request) error { + if len(signer.appSecret) == 0 { + return fmt.Errorf("app secret has not been configured") + } + + if contentType := req.Header.Get("content-type"); contentType != validContentTypeHeaderValue { + return fmt.Errorf("wrong accept header found. Got %v want %v", contentType, validContentTypeHeaderValue) + } + + authorizationHeaderValue, err := signer.isAuthorizationHeaderValid(req) + if err != nil { + return err + } + + err = signer.isTimestampValid(req) + if err != nil { + return err + } + + normalizedRequestHash, err := signer.getHexHashForNormalizedHeaders(req) + if err != nil { + return err + } + + hmacHexValue := signer.getHmacHash(normalizedRequestHash) + + if !signer.isAuthorizationHeaderEqualsToCalculatedHmacHex(authorizationHeaderValue, hmacHexValue) { + return fmt.Errorf("wrong signature in authorization header. Got %v want %v", authorizationHeaderValue, hmacHexValue) + } + + signer.logInfo(req.Context(), "received signature is valid") + + return nil +} + +func (signer *requestSigner) isAuthorizationHeaderEqualsToCalculatedHmacHex(authorizationHeaderValue string, hmacHexValue string) bool { + return reflect.DeepEqual(authorizationHeaderValue, hmacHexValue) +} + +func (signer *requestSigner) isAuthorizationHeaderValid(req *http.Request) (string, error) { + bearerRegex := regexp.MustCompile(`(?m)^(Bearer [[:xdigit:]]+)$`) + authorizationHeaderValue := req.Header.Get("Authorization") + if authorizationHeaderValue == "" { + return "", fmt.Errorf("authorization header missing") + } + if !bearerRegex.MatchString(authorizationHeaderValue) { + return "", fmt.Errorf("found authorization header is not a valid Bearer token. Got %v", authorizationHeaderValue) + } + authorizationHeaderValue = strings.TrimPrefix(authorizationHeaderValue, "Bearer ") + return authorizationHeaderValue, nil +} + +func (signer *requestSigner) isTimestampValid(req *http.Request) error { + signer.logInfo(req.Context(), "validate timestamp from request header") + timestampHeaderValue, err := time.Parse(time.RFC3339, req.Header.Get(timestampHeaderKey)) + if err != nil { + return err + } + + timeNow := signer.now().UTC() + + timeBeforTimestamp := timeNow.Add(-timeDiff) + timeAfterTimestamp := timeNow.Add(timeDiff) + + if !(timestampHeaderValue.After(timeBeforTimestamp) && timestampHeaderValue.Before(timeAfterTimestamp)) { + return fmt.Errorf("request is timed out: timestamp from request: %v, current time: %v", timestampHeaderValue.Format(time.RFC3339), timeNow.Format(time.RFC3339)) + } + + return nil +} + +func (signer *requestSigner) getHexHashForNormalizedHeaders(req *http.Request) (hex string, err error) { + if req.Body == nil { + return "", fmt.Errorf("payload missing") + } + + body, err := signer.getBodyFromRequest(req) + if err != nil { + return "", err + } + + signedHeaders := strings.Split(req.Header.Get(signatureHeaderKey), ",") + sort.Strings(signedHeaders) + + normalizedRequest := signer.getNormalizedRequestWithHeaderAndBody(req, signedHeaders, body) + + strNormalizedRequest := strings.Join(normalizedRequest, "\n") + signer.logInfo(req.Context(), fmt.Sprintf("normalized request: %#v", strNormalizedRequest)) + + hashNormalizedRequest := sha256.Sum256([]byte(strNormalizedRequest)) + + signer.logInfo(req.Context(), "hashing normalized request") + + return strings.ToLower(fmt.Sprintf("%x", hashNormalizedRequest)), nil +} + +func (signer *requestSigner) getNormalizedRequestWithHeaderAndBody(req *http.Request, signedHeaders []string, body []byte) []string { + var normalizedHeaders []string + + for _, name := range signedHeaders { + headerValue := req.Header.Get(name) + normalizedHeaders = append(normalizedHeaders, fmt.Sprintf("%v:%v", strings.ToLower(name), strings.TrimSpace(headerValue))) + } + + var normalizedRequest []string + normalizedRequest = append(normalizedRequest, req.Method) + normalizedRequest = append(normalizedRequest, req.URL.Path) + normalizedRequest = append(normalizedRequest, req.URL.RawQuery) + normalizedRequest = append(normalizedRequest, fmt.Sprintf("%v\n", strings.Join(normalizedHeaders, "\n"))) + + signer.logInfo(req.Context(), "hashing body") + normalizedRequest = append(normalizedRequest, signer.getHexHashedPayload(body)) + + return normalizedRequest +} + +func (signer *requestSigner) getBodyFromRequest(req *http.Request) ([]byte, error) { + if req.GetBody != nil { + signer.logInfo(req.Context(), "get a copy of request body") + + bodyReader, err := req.GetBody() + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(bodyReader) + if err != nil { + return nil, err + } + + return body, nil + } + + signer.logInfo(req.Context(), "request.GetBody is nil. Read body and create new request body with read body data") + + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + + req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + return body, nil +} + +func (signer *requestSigner) getHexHashedPayload(payload []byte) string { + hash := sha256.Sum256(payload) + return strings.ToLower(fmt.Sprintf("%x", hash)) +} + +func (signer *requestSigner) getHmacHash(normalizedRequestHash string) string { + hmacHash := hmac.New(sha256.New, signer.appSecret) + hmacHash.Write([]byte(normalizedRequestHash)) + hmacResult := hmacHash.Sum(nil) + return strings.ToLower(fmt.Sprintf("%x", hmacResult)) +} diff --git a/requestsignature/requestsignature_test.go b/requestsignature/requestsignature_test.go new file mode 100644 index 0000000..b512e2a --- /dev/null +++ b/requestsignature/requestsignature_test.go @@ -0,0 +1,485 @@ +package requestsignature_test + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/d-velop/dvelop-sdk-go/requestsignature" +) + +const timestampHeader = "x-dv-signature-timestamp" +const algorithmHeader = "x-dv-signature-algorithm" +const signedHeadersHeader = "x-dv-signature-headers" +const authorizationHeader = "authorization" + +const algorithm = "DV1-HMAC-SHA256" + +func mockLogInfo(ctx context.Context, msg string) { + log.Printf("INFO: %v", msg) +} +func mockLogError(ctx context.Context, msg string) { + log.Printf("ERROR: %v", msg) +} + +func TestRequestSigner_ValidateSignedRequest_HappyPath_Working(t *testing.T) { + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + validator := requestsignature.NewRequestSigner(appSecret, now, mockLogInfo) + err = validator.ValidateSignedRequest(req) + if err != nil { + t.Errorf("got error %v but no error expected", err) + } +} + +func TestRequestSigner_ValidateSignedRequest_AuthorationHeaderInvalid_ReturnError(t *testing.T) { + wantErrorMessage := "wrong signature in authorization header. Got 12783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104d want 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c" + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 12783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104d", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + validator := requestsignature.NewRequestSigner(appSecret, now, mockLogInfo) + err = validator.ValidateSignedRequest(req) + if err != nil { + if err.Error() != wantErrorMessage { + t.Errorf("wrong error returned: got %v want %v", err, wantErrorMessage) + } + } else { + t.Errorf("no error returned, but want error %v", wantErrorMessage) + } +} + +func TestRequestSigner_ValidateSignedRequest_WithWrongDto_ReturnError(t *testing.T) { + wantErrorMessage := "wrong signature in authorization header. Got 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c want daba3a1deb11b646540bcb42161ea0003cf6ca6c1c3282d83e8c80e91cfcd9f9" + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://xyz.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + validator := requestsignature.NewRequestSigner(appSecret, now, mockLogInfo) + err = validator.ValidateSignedRequest(req) + if err != nil { + if err.Error() != wantErrorMessage { + t.Errorf("wrong error returned: got '%v' want '%v'", err, wantErrorMessage) + } + } else { + t.Errorf("no error returned, but want error %v", wantErrorMessage) + } +} + +func TestRequestSigner_ValidateSignedRequest_RequestTimeouted_ReturnError(t *testing.T) { + wantErrorMessage := "request is timed out: timestamp from request: 2019-08-09T08:49:42Z, current time: 2019-08-09T09:49:45Z" + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + + now := func() time.Time { + return time.Date(2019, time.August, 9, 9, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + validator := requestsignature.NewRequestSigner(appSecret, now, mockLogInfo) + err = validator.ValidateSignedRequest(req) + if err != nil { + if err.Error() != wantErrorMessage { + t.Errorf("wrong error returned: got %v want %v", err, wantErrorMessage) + } + } else { + t.Errorf("no error returned, but want error %v", wantErrorMessage) + } +} + +func TestHandleSignMiddleware_HappyPath_Working(t *testing.T) { + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + handlerCalled := false + handler := func(w http.ResponseWriter, req *http.Request) { + handlerCalled = true + t.Log("handler was called") + } + + rr := httptest.NewRecorder() + requestsignature.HandleCloudSignatureMiddleware(appSecret, now, mockLogInfo, mockLogError)(http.HandlerFunc(handler)).ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Fatalf("wrong status code returned: got %v want %v", status, http.StatusOK) + } + if !handlerCalled { + t.Fatalf("test handler not called") + } +} + +func TestHandleSignMiddleware_AppSecretMissing_Return500InternalServerError(t *testing.T) { + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + handlerCalled := false + handler := func(w http.ResponseWriter, req *http.Request) { + handlerCalled = true + t.Log("handler was called") + } + + rr := httptest.NewRecorder() + requestsignature.HandleCloudSignatureMiddleware(nil, now, mockLogInfo, mockLogError)(http.HandlerFunc(handler)).ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusInternalServerError { + t.Fatalf("wrong status code returned: got %v want %v", status, http.StatusInternalServerError) + } + if handlerCalled { + t.Fatalf("Handler was called, but should not be called.") + } +} + +func TestHandleSignMiddleware_MiddlewareWasCalledByWrongMethod_Return405MethodNotAllowed(t *testing.T) { + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodGet, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + handlerCalled := false + handler := func(w http.ResponseWriter, req *http.Request) { + handlerCalled = true + t.Log("handler was called") + } + + rr := httptest.NewRecorder() + requestsignature.HandleCloudSignatureMiddleware(appSecret, now, mockLogInfo, mockLogError)(http.HandlerFunc(handler)).ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusMethodNotAllowed { + t.Fatalf("wrong status code returned: got %v want %v", status, http.StatusMethodNotAllowed) + } + if handlerCalled { + t.Fatalf("Handler was called, but should not be called.") + } +} + +func TestHandleSignMiddleware_MiddlewareWasCalledByPath_Return400BadRequest(t *testing.T) { + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event/wrongpath", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + handlerCalled := false + handler := func(w http.ResponseWriter, req *http.Request) { + handlerCalled = true + t.Log("handler was called") + } + + rr := httptest.NewRecorder() + requestsignature.HandleCloudSignatureMiddleware(appSecret, now, mockLogInfo, mockLogError)(http.HandlerFunc(handler)).ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Fatalf("wrong status code returned: got %v want %v", status, http.StatusBadRequest) + } + if handlerCalled { + t.Fatalf("Handler was called, but should not be called.") + } +} + +func TestHandleSignMiddleware_MiddlewareWasCalledWithoutContentTypeHeader_Return406NotAcceptable(t *testing.T) { + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "id", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + //"Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + handlerCalled := false + handler := func(w http.ResponseWriter, req *http.Request) { + handlerCalled = true + t.Log("handler was called") + } + + rr := httptest.NewRecorder() + requestsignature.HandleCloudSignatureMiddleware(appSecret, now, mockLogInfo, mockLogError)(http.HandlerFunc(handler)).ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusNotAcceptable { + t.Fatalf("wrong status code returned: got %v want %v", status, http.StatusBadRequest) + } + if handlerCalled { + t.Fatalf("Handler was called, but should not be called.") + } +} + +func TestHandleSignMiddleware_MiddlewareWasCalledButSignatureIsInvalid_Return403Forbidden(t *testing.T) { + appSecret, err := base64.StdEncoding.DecodeString("Rg9iJXX0Jkun9u4Rp6no8HTNEdHlfX9aZYbFJ9b6YdQ=") + if err != nil { + t.Fatalf("app secret string is not valid base64 encoded string. Error = %v", err) + } + now := func() time.Time { + return time.Date(2019, time.August, 9, 8, 49, 45, 0, time.UTC) + } + + dto := requestsignature.Dto{ + "subscribe", + "wrong id to generate wrong body hash", + "https://someone.d-velop.cloud", + } + + headers := map[string]string{ + "x-dv-signature-headers": "x-dv-signature-algorithm,x-dv-signature-headers,x-dv-signature-timestamp", + "x-dv-signature-algorithm": "DV1-HMAC-SHA256", + "x-dv-signature-timestamp": "2019-08-09T08:49:42Z", + "Authorization": "Bearer 02783453441665bf27aa465cbbac9b98507ae94c54b6be2b1882fe9a05ec104c", + "Content-Type": "application/json", + } + + payload := &bytes.Buffer{} + json.NewEncoder(payload).Encode(dto) + body := payload.Bytes() + req, _ := http.NewRequest(http.MethodPost, "/myapp/dvelop-cloud-lifecycle-event", bytes.NewReader(body)) + for key, value := range headers { + req.Header.Add(key, value) + } + + handlerCalled := false + handler := func(w http.ResponseWriter, req *http.Request) { + handlerCalled = true + t.Log("handler was called") + } + + rr := httptest.NewRecorder() + requestsignature.HandleCloudSignatureMiddleware(appSecret, now, mockLogInfo, mockLogError)(http.HandlerFunc(handler)).ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusForbidden { + t.Fatalf("wrong status code returned: got %v want %v", status, http.StatusForbidden) + } + if handlerCalled { + t.Fatalf("Handler was called, but should not be called.") + } +}