From 387bece7487c9c02631cfdd63445874a1fd912e5 Mon Sep 17 00:00:00 2001 From: shaniah868 Date: Mon, 14 Feb 2022 21:11:13 -0400 Subject: [PATCH 1/6] feature: WEOS-1343 Create middleware for handling JWT in the incoming Authorization header -Regenerate mocks -Registered Authorization Middleware -Added test for authorization middleware -Added some functionality to Authorization -Made a new yaml file for security testing --- context/context.go | 2 + controllers/rest/api.go | 1 + controllers/rest/controller_standard_test.go | 34 +- controllers/rest/fixtures/blog-security.yaml | 717 ++++++++++++++++++ controllers/rest/middleware_context.go | 2 +- controllers/rest/middleware_initialize.go | 54 ++ .../rest/middleware_initialize_test.go | 60 ++ .../rest/operation_initializers_test.go | 2 +- controllers/rest/weos_mocks_test.go | 143 ++-- go.mod | 1 + go.sum | 7 + model/interfaces.go | 4 +- model/mocks_test.go | 143 ++-- 13 files changed, 1041 insertions(+), 129 deletions(-) create mode 100644 controllers/rest/fixtures/blog-security.yaml diff --git a/context/context.go b/context/context.go index 5d9a5630..23176bef 100644 --- a/context/context.go +++ b/context/context.go @@ -30,6 +30,8 @@ const FILTERS ContextKey = "_filters" const SORTS ContextKey = "_sorts" const PAYLOAD ContextKey = "_payload" const SEQUENCE_NO string = "sequence_no" +const AUTHORIZATION string = "Authorization" +const OPENIDCONNECTURL string = "openIdConnectUrl" //Path initializers are run per path and can be used to configure routes that are not defined in the open api spec const METHODS_FOUND ContextKey = "_methods_found" diff --git a/controllers/rest/api.go b/controllers/rest/api.go index ccbfa5f6..073ca348 100644 --- a/controllers/rest/api.go +++ b/controllers/rest/api.go @@ -284,6 +284,7 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error { p.RegisterController("CreateBatchController", CreateBatchController) //register standard middleware p.RegisterMiddleware("Context", Context) + p.RegisterMiddleware("AuthorizationMiddleware", AuthorizationMiddleware) p.RegisterMiddleware("CreateMiddleware", CreateMiddleware) p.RegisterMiddleware("CreateBatchMiddleware", CreateBatchMiddleware) p.RegisterMiddleware("UpdateMiddleware", UpdateMiddleware) diff --git a/controllers/rest/controller_standard_test.go b/controllers/rest/controller_standard_test.go index 997df94a..c2a4d9ad 100644 --- a/controllers/rest/controller_standard_test.go +++ b/controllers/rest/controller_standard_test.go @@ -115,7 +115,7 @@ func TestStandardControllers_Create(t *testing.T) { Property: mockPayload, } - projections := &ProjectionMock{ + projections := &GormProjectionMock{ GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { if ctx == nil { t.Errorf("expected to find context but got nil") @@ -281,7 +281,7 @@ func TestStandardControllers_CreateBatch(t *testing.T) { }, } - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return nil, nil }, @@ -457,7 +457,7 @@ func TestStandardControllers_Update(t *testing.T) { mockEntity.SequenceNo = int64(1) mockEntity.Property = mockBlog - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return nil, nil }, @@ -575,7 +575,7 @@ func TestStandardControllers_View(t *testing.T) { }} t.Run("Testing the generic view endpoint", func(t *testing.T) { - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return map[string]interface{}{ "id": "1", @@ -628,7 +628,7 @@ func TestStandardControllers_View(t *testing.T) { } }) t.Run("Testing view with entity id", func(t *testing.T) { - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { if entityFactory == nil { t.Errorf("expected to find entity factory got nil") @@ -696,7 +696,7 @@ func TestStandardControllers_View(t *testing.T) { } }) t.Run("invalid entity id should return 404", func(t *testing.T) { - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return map[string]interface{}{ "id": "1", @@ -771,7 +771,7 @@ func TestStandardControllers_View(t *testing.T) { } }) t.Run("invalid numeric entity id should return 404", func(t *testing.T) { - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return map[string]interface{}{ "id": "1", @@ -846,7 +846,7 @@ func TestStandardControllers_View(t *testing.T) { } }) t.Run("view with sequence no", func(t *testing.T) { - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return map[string]interface{}{ "id": "1", @@ -926,7 +926,7 @@ func TestStandardControllers_View(t *testing.T) { } }) t.Run("view with invalid sequence no", func(t *testing.T) { - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return map[string]interface{}{ "id": "1", @@ -1019,7 +1019,7 @@ func TestStandardControllers_List(t *testing.T) { array := []map[string]interface{}{} array = append(array, mockBlog, mockBlog1) - mockProjection := &ProjectionMock{ + mockProjection := &GormProjectionMock{ GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { return array, 2, nil }, @@ -1133,7 +1133,7 @@ func TestStandardControllers_ListFilters(t *testing.T) { array := []map[string]interface{}{} array = append(array, mockBlog, mockBlog1) - mockProjection := &ProjectionMock{ + mockProjection := &GormProjectionMock{ GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { if entityFactory == nil { t.Errorf("no entity factory found") @@ -1382,7 +1382,7 @@ func TestStandardControllers_FormUrlEncoded_Create(t *testing.T) { Property: mockPayload, } - projections := &ProjectionMock{ + projections := &GormProjectionMock{ GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { return mockContentEntity, nil }, @@ -1540,7 +1540,7 @@ func TestStandardControllers_FormData_Create(t *testing.T) { Property: mockPayload, } - projections := &ProjectionMock{ + projections := &GormProjectionMock{ GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { return mockContentEntity, nil }, @@ -1690,7 +1690,7 @@ func TestStandardControllers_DeleteEtag(t *testing.T) { mockEntity.SequenceNo = int64(1) mockEntity.Property = mockBlog - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return nil, nil }, @@ -1799,7 +1799,7 @@ func TestStandardControllers_DeleteID(t *testing.T) { }, } - projection := &ProjectionMock{ + projection := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return mockInterface, nil }, @@ -1852,7 +1852,7 @@ func TestStandardControllers_DeleteID(t *testing.T) { }, } - projection1 := &ProjectionMock{ + projection1 := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return mockInterface1, nil }, @@ -1907,7 +1907,7 @@ func TestStandardControllers_DeleteID(t *testing.T) { err1 := fmt.Errorf("this is an error") - projection1 := &ProjectionMock{ + projection1 := &GormProjectionMock{ GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { return nil, err1 }, diff --git a/controllers/rest/fixtures/blog-security.yaml b/controllers/rest/fixtures/blog-security.yaml new file mode 100644 index 00000000..b4355da2 --- /dev/null +++ b/controllers/rest/fixtures/blog-security.yaml @@ -0,0 +1,717 @@ +openapi: 3.0.3 +info: + title: Blog + description: Blog example + version: 1.0.0 +servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 +x-weos-config: + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + database: + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +components: + securitySchemes: + Auth0: + type: openIdConnect + openIdConnectUrl: https://samples.auth0.com/.well-known/openid-configuration + schemas: + Category: + type: object + properties: + title: + type: string + description: + type: string + required: + - title + x-identifier: + - title + Author: + type: object + properties: + id: + type: string + format: ksuid + firstName: + type: string + lastName: + type: string + email: + type: string + format: email + required: + - firstName + - lastName + x-identifier: + - id + - email + Blog: + type: object + properties: + url: + type: string + format: uri + title: + type: string + description: + type: string + status: + type: string + nullable: true + enum: + - null + - unpublished + - published + image: + type: string + format: byte + categories: + type: array + items: + $ref: "#/components/schemas/Category" + posts: + type: array + items: + $ref: "#/components/schemas/Post" + lastUpdated: + type: string + format: date-time + created: + type: string + format: date-time + required: + - title + - url + Post: + type: object + properties: + title: + type: string + description: + type: string + author: + $ref: "#/components/schemas/Author" + created: + type: string + format: date-time +security: + - Auth0: ["email","name"] + +paths: + /health: + summary: Health Check + get: + security: [] + x-controller: HealthCheck + x-middleware: + - Recover + - AuthorizationMiddleware + responses: + 200: + description: Health Response + 500: + description: API Internal Error + /blogs: + parameters: + - in: header + name: someHeader + schema: + type: string + - in: header + name: someOtherHeader + schema: + type: string + x-context-name: soh + - in: header + name: X-Account-Id + schema: + type: string + x-context-name: AccountID + - in: query + name: q + schema: + type: string + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + post: + operationId: Add Blog + summary: Create Blog + x-projection: Default + x-event-dispatcher: Default + x-command-disptacher: Default + parameters: + - in: header + name: Authorization + schema: + type: string + x-middleware: + - AuthorizationMiddleware + requestBody: + description: Blog info that is submitted + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Blog" + multipart/form-data: + schema: + $ref: "#/components/schemas/Blog" + responses: + 201: + description: Add Blog to Aggregator + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + get: + operationId: Get Blogs + summary: Get List of Blogs + parameters: + - in: query + name: page + schema: + type: integer + - in: query + name: l + x-alias: limit + schema: + type: integer + - in: query + name: _filters + schema: + type: array + items: + type: object + properties: + field: + type: string + operator: + type: string + values: + type: array + items: + type: string + + required: false + description: query string + x-context: + filters: + - field: status + operator: eq + values: + - Active + - field: lastUpdated + operator: between + values: + - 2021-12-17 15:46:00 + - 2021-12-18 15:46:00 + - field: categories + operator: in + values: + - Technology + - Javascript + sorts: + - field: title + order: asc + page: 1 + limit: 10 + responses: + 200: + description: List of blogs + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + blogs: + type: array + x-alias: items + items: + $ref: "#/components/schemas/Blog" + put: + operationId: Import blogs + requestBody: + content: + text/csv: + schema: + type: string + format: binary + responses: + 201: + description: items created + + /blogs/{id}: + parameters: + - in: query + name: sequence_no + schema: + type: integer + - in: query + name: use_entity_id + schema: + type: boolean + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + - in: header + name: If-Match + schema: + type: string + required: false + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + summary: Get Blog by id + operationId: Get Blog + responses: + 200: + description: Blog details without any supporting collections + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + - in: header + name: If-Match + schema: + type: string + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + summary: Update blog details + operationId: Update Blog + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + responses: + 200: + description: Update Blog + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + - in: header + name: If-Match + schema: + type: string + x-schema: "Blog" + summary: Delete blog + operationId: Delete Blog + responses: + 200: + description: Blog Deleted + + /posts/: + post: + operationId: Create Blog Post + summary: Create Blog Post + requestBody: + description: Post details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Post" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + put: + operationId: Import Blog Posts + summary: Import Blog Posts + requestBody: + description: List of posts to import + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + application/x-www-form-urlencoded: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + responses: + 201: + description: Post + get: + operationId: Get Posts + summary: Get a blog's list of posts + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog posts + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + type: array + items: + $ref: "#/components/schemas/Post" + + /posts/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get blog post by id + responses: + 200: + description: Get blog post information + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + responses: + 200: + description: Get blog post information + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + summary: Delete post + responses: + 200: + description: Delete post + + + /categories/: + post: + operationId: Create Blog Category + summary: Create Blog Category + requestBody: + description: Post details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Category" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + get: + operationId: Get Categories + summary: Get a blog's list of categories + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog categories + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + type: array + items: + $ref: "#/components/schemas/Category" + + /categories/{title}: + get: + parameters: + - in: path + name: title + schema: + type: string + required: true + summary: Get blog category by title + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + put: + parameters: + - in: path + name: title + schema: + type: string + required: true + summary: Update category + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + delete: + parameters: + - in: path + name: title + schema: + type: string + required: true + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + summary: Delete category + responses: + 200: + description: Delete category + + /authors/: + post: + operationId: Create Blog Author + summary: Create Blog Author + requestBody: + description: Author details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Author" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + get: + operationId: Get Authors + summary: Get a blog's list of authors + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog authors + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + type: array + items: + $ref: "#/components/schemas/Author" + + /authors/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: header + name: email + schema: + type: string + format: email + required: true + summary: Get Author by email and id + responses: + 200: + description: Get author information + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: header + name: email + schema: + type: string + format: email + required: true + summary: Update Author details + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + responses: + 200: + description: Author details + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: header + name: email + schema: + type: string + format: email + required: true + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + summary: Delete author + responses: + 200: + description: Delete author diff --git a/controllers/rest/middleware_context.go b/controllers/rest/middleware_context.go index 1b53ae74..ff34b1e5 100644 --- a/controllers/rest/middleware_context.go +++ b/controllers/rest/middleware_context.go @@ -125,7 +125,7 @@ func parseParams(c echo.Context, cc context.Context, parameter *openapi3.Paramet if err == nil { val = v } - case "If-Match", "If-None-Match": //default type is string + case "If-Match", "If-None-Match", "Authorization": //default type is string default: var filters map[string]interface{} filters = map[string]interface{}{} diff --git a/controllers/rest/middleware_initialize.go b/controllers/rest/middleware_initialize.go index a336c95a..efcabc2d 100644 --- a/controllers/rest/middleware_initialize.go +++ b/controllers/rest/middleware_initialize.go @@ -2,6 +2,10 @@ package rest import ( "encoding/json" + "github.com/coreos/go-oidc/v3/oidc" + weosContext "github.com/wepala/weos/context" + "github.com/wepala/weos/model" + "net/http" "strings" "time" @@ -238,3 +242,53 @@ func addRelations(struc ds.Builder, relations map[string]string, structs map[str } return struc, nil } + +//AuthorizationMiddleware handling JWT in incoming Authentication header +func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctxt echo.Context) error { + if operation.Security != nil && len(*operation.Security) == 0 { + return nil + } + newContext := ctxt.Request().Context() + token, ok := newContext.Value(weosContext.AUTHORIZATION).(string) + if !ok || token == "" { + api.e.Logger.Debugf("no JWT token was found") + return NewControllerError("no JWT token was found", nil, http.StatusUnauthorized) + } + var openIdConnectUrl string + if openIdUrl, ok := api.Swagger.Components.SecuritySchemes["Auth0"].Value.ExtensionProps.Extensions[weosContext.OPENIDCONNECTURL]; ok { + + err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl) + if err != nil { + api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err) + return NewControllerError("unable to unmarshal open id connect url", err, http.StatusBadRequest) + } + } else { + api.EchoInstance().Logger.Errorf("no open id connect url found") + return NewControllerError("no open id connect url found", nil, http.StatusBadRequest) + } + algs := []string{"RS256", "RS384", "RS512", "HS256"} + keySet := oidc.NewRemoteKeySet(newContext, openIdConnectUrl) + tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ + ClientID: "", + SupportedSigningAlgs: algs, + SkipClientIDCheck: true, + SkipExpiryCheck: false, + SkipIssuerCheck: true, + Now: time.Now, + }) + jwtToken := strings.Replace(token, "Bearer ", "", -1) + idToken, err := tokenVerifier.Verify(newContext, jwtToken) + if err != nil { + api.e.Logger.Debugf(err.Error()) + return NewControllerError("unexpected error verifying token", err, http.StatusUnauthorized) + } + if idToken != nil { + + } + return nil + + } + } +} diff --git a/controllers/rest/middleware_initialize_test.go b/controllers/rest/middleware_initialize_test.go index 2b7e2b9a..e5730192 100644 --- a/controllers/rest/middleware_initialize_test.go +++ b/controllers/rest/middleware_initialize_test.go @@ -1,7 +1,11 @@ package rest_test import ( + "bytes" + "encoding/json" "io/ioutil" + "net/http" + "net/http/httptest" "os" "regexp" "strings" @@ -102,3 +106,59 @@ func TestXRemove(t *testing.T) { } }) } + +func TestAuthenticateMiddleware(t *testing.T) { + //instantiate api + api, err := rest.New("./fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading api '%s'", err) + } + err = api.Initialize(context.TODO()) + if err != nil { + t.Fatalf("un expected error initializing api '%s'", err) + } + e := api.EchoInstance() + + t.Run("no jwt token added when required", func(t *testing.T) { + description := "testing 1st blog description" + mockBlog := &TestBlog{Description: &description} + reqBytes, err := json.Marshal(mockBlog) + if err != nil { + t.Fatalf("error setting up request %s", err) + } + body := bytes.NewReader(reqBytes) + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/blogs", body) + req.Header.Set("Content-Type", "application/json") + e.ServeHTTP(resp, req) + if resp.Result().StatusCode != http.StatusUnauthorized { + t.Errorf("expected the response code to be %d, got %d", http.StatusUnauthorized, resp.Result().StatusCode) + } + }) + t.Run("security parameter array is empty", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/health", nil) + e.ServeHTTP(resp, req) + if resp.Result().StatusCode != http.StatusOK { + t.Errorf("expected the response code to be %d, got %d", http.StatusOK, resp.Result().StatusCode) + } + }) + t.Run("jwt token added", func(t *testing.T) { + description := "testing 1st blog description" + mockBlog := &TestBlog{Description: &description} + reqBytes, err := json.Marshal(mockBlog) + if err != nil { + t.Fatalf("error setting up request %s", err) + } + token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKb2huIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqRG9lQGdtYWlsLmNvbSIsImlhdCI6MTY0NDg4Nzc2OSwiZXhwIjoxNjQ0ODg4NzY5fQ.sBL03kXCIbjzD5MdzCRb71g8LLQhgr9R7a0-3cJxySw" + body := bytes.NewReader(reqBytes) + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/blogs", body) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + e.ServeHTTP(resp, req) + if resp.Result().StatusCode != http.StatusCreated { + t.Errorf("expected the response code to be %d, got %d", http.StatusCreated, resp.Result().StatusCode) + } + }) +} diff --git a/controllers/rest/operation_initializers_test.go b/controllers/rest/operation_initializers_test.go index d6253c83..a8eef3ba 100644 --- a/controllers/rest/operation_initializers_test.go +++ b/controllers/rest/operation_initializers_test.go @@ -153,7 +153,7 @@ paths: return nil }}) api.RegisterEventStore("HealthCheck", &EventRepositoryMock{}) - api.RegisterProjection("HealthCheck", &ProjectionMock{}) + api.RegisterProjection("HealthCheck", &GormProjectionMock{}) t.Run("attach user defined controller", func(t *testing.T) { ctxt, err := rest.UserDefinedInitializer(baseCtxt, api, "/health", http.MethodGet, api.Swagger, api.Swagger.Paths["/health"], api.Swagger.Paths["/health"].Get) diff --git a/controllers/rest/weos_mocks_test.go b/controllers/rest/weos_mocks_test.go index 38685b4e..612ce8b1 100644 --- a/controllers/rest/weos_mocks_test.go +++ b/controllers/rest/weos_mocks_test.go @@ -550,41 +550,47 @@ func (mock *EventRepositoryMock) ReplayEventsCalls() []struct { return calls } -// Ensure, that ProjectionMock does implement model.Projection. +// Ensure, that GormProjectionMock does implement model.GormProjection. // If this is not the case, regenerate this file with moq. -var _ model.Projection = &ProjectionMock{} +var _ model.GormProjection = &GormProjectionMock{} -// ProjectionMock is a mock implementation of model.Projection. +// GormProjectionMock is a mock implementation of model.GormProjection. // -// func TestSomethingThatUsesProjection(t *testing.T) { +// func TestSomethingThatUsesGormProjection(t *testing.T) { // -// // make and configure a mocked model.Projection -// mockedProjection := &ProjectionMock{ -// GetByEntityIDFunc: func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { -// panic("mock out the GetByEntityID method") -// }, -// GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { -// panic("mock out the GetByKey method") -// }, -// GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { -// panic("mock out the GetContentEntities method") -// }, -// GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { -// panic("mock out the GetContentEntity method") -// }, -// GetEventHandlerFunc: func() model.EventHandler { -// panic("mock out the GetEventHandler method") -// }, -// MigrateFunc: func(ctx context.Context, builders map[string]dynamicstruct.Builder, deletedFields map[string][]string) error { -// panic("mock out the Migrate method") -// }, -// } +// // make and configure a mocked model.GormProjection +// mockedGormProjection := &GormProjectionMock{ +// DBFunc: func() *gorm.DB { +// panic("mock out the DB method") +// }, +// GetByEntityIDFunc: func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { +// panic("mock out the GetByEntityID method") +// }, +// GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { +// panic("mock out the GetByKey method") +// }, +// GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { +// panic("mock out the GetContentEntities method") +// }, +// GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { +// panic("mock out the GetContentEntity method") +// }, +// GetEventHandlerFunc: func() model.EventHandler { +// panic("mock out the GetEventHandler method") +// }, +// MigrateFunc: func(ctx context.Context, builders map[string]ds.Builder, deletedFields map[string][]string) error { +// panic("mock out the Migrate method") +// }, +// } // -// // use mockedProjection in code that requires model.Projection -// // and then make assertions. +// // use mockedGormProjection in code that requires model.GormProjection +// // and then make assertions. // -// } -type ProjectionMock struct { +// } +type GormProjectionMock struct { + // DBFunc mocks the DB method. + DBFunc func() *gorm.DB + // GetByEntityIDFunc mocks the GetByEntityID method. GetByEntityIDFunc func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) @@ -605,6 +611,9 @@ type ProjectionMock struct { // calls tracks calls to the methods. calls struct { + // DB holds details about calls to the DB method. + DB []struct { + } // GetByEntityID holds details about calls to the GetByEntityID method. GetByEntityID []struct { // Ctxt is the ctxt argument value. @@ -662,6 +671,7 @@ type ProjectionMock struct { DeletedFields map[string][]string } } + lockDB sync.RWMutex lockGetByEntityID sync.RWMutex lockGetByKey sync.RWMutex lockGetContentEntities sync.RWMutex @@ -670,10 +680,36 @@ type ProjectionMock struct { lockMigrate sync.RWMutex } +// DB calls DBFunc. +func (mock *GormProjectionMock) DB() *gorm.DB { + if mock.DBFunc == nil { + panic("GormProjectionMock.DBFunc: method is nil but GormProjection.DB was just called") + } + callInfo := struct { + }{} + mock.lockDB.Lock() + mock.calls.DB = append(mock.calls.DB, callInfo) + mock.lockDB.Unlock() + return mock.DBFunc() +} + +// DBCalls gets all the calls that were made to DB. +// Check the length with: +// len(mockedGormProjection.DBCalls()) +func (mock *GormProjectionMock) DBCalls() []struct { +} { + var calls []struct { + } + mock.lockDB.RLock() + calls = mock.calls.DB + mock.lockDB.RUnlock() + return calls +} + // GetByEntityID calls GetByEntityIDFunc. -func (mock *ProjectionMock) GetByEntityID(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { +func (mock *GormProjectionMock) GetByEntityID(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { if mock.GetByEntityIDFunc == nil { - panic("ProjectionMock.GetByEntityIDFunc: method is nil but Projection.GetByEntityID was just called") + panic("GormProjectionMock.GetByEntityIDFunc: method is nil but GormProjection.GetByEntityID was just called") } callInfo := struct { Ctxt context.Context @@ -692,8 +728,8 @@ func (mock *ProjectionMock) GetByEntityID(ctxt context.Context, entityFactory mo // GetByEntityIDCalls gets all the calls that were made to GetByEntityID. // Check the length with: -// len(mockedProjection.GetByEntityIDCalls()) -func (mock *ProjectionMock) GetByEntityIDCalls() []struct { +// len(mockedGormProjection.GetByEntityIDCalls()) +func (mock *GormProjectionMock) GetByEntityIDCalls() []struct { Ctxt context.Context EntityFactory model.EntityFactory ID string @@ -710,9 +746,9 @@ func (mock *ProjectionMock) GetByEntityIDCalls() []struct { } // GetByKey calls GetByKeyFunc. -func (mock *ProjectionMock) GetByKey(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { +func (mock *GormProjectionMock) GetByKey(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { if mock.GetByKeyFunc == nil { - panic("ProjectionMock.GetByKeyFunc: method is nil but Projection.GetByKey was just called") + panic("GormProjectionMock.GetByKeyFunc: method is nil but GormProjection.GetByKey was just called") } callInfo := struct { Ctxt context.Context @@ -731,8 +767,8 @@ func (mock *ProjectionMock) GetByKey(ctxt context.Context, entityFactory model.E // GetByKeyCalls gets all the calls that were made to GetByKey. // Check the length with: -// len(mockedProjection.GetByKeyCalls()) -func (mock *ProjectionMock) GetByKeyCalls() []struct { +// len(mockedGormProjection.GetByKeyCalls()) +func (mock *GormProjectionMock) GetByKeyCalls() []struct { Ctxt context.Context EntityFactory model.EntityFactory Identifiers map[string]interface{} @@ -749,9 +785,9 @@ func (mock *ProjectionMock) GetByKeyCalls() []struct { } // GetContentEntities calls GetContentEntitiesFunc. -func (mock *ProjectionMock) GetContentEntities(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { +func (mock *GormProjectionMock) GetContentEntities(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { if mock.GetContentEntitiesFunc == nil { - panic("ProjectionMock.GetContentEntitiesFunc: method is nil but Projection.GetContentEntities was just called") + panic("GormProjectionMock.GetContentEntitiesFunc: method is nil but GormProjection.GetContentEntities was just called") } callInfo := struct { Ctx context.Context @@ -778,8 +814,8 @@ func (mock *ProjectionMock) GetContentEntities(ctx context.Context, entityFactor // GetContentEntitiesCalls gets all the calls that were made to GetContentEntities. // Check the length with: -// len(mockedProjection.GetContentEntitiesCalls()) -func (mock *ProjectionMock) GetContentEntitiesCalls() []struct { +// len(mockedGormProjection.GetContentEntitiesCalls()) +func (mock *GormProjectionMock) GetContentEntitiesCalls() []struct { Ctx context.Context EntityFactory model.EntityFactory Page int @@ -804,9 +840,9 @@ func (mock *ProjectionMock) GetContentEntitiesCalls() []struct { } // GetContentEntity calls GetContentEntityFunc. -func (mock *ProjectionMock) GetContentEntity(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { +func (mock *GormProjectionMock) GetContentEntity(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { if mock.GetContentEntityFunc == nil { - panic("ProjectionMock.GetContentEntityFunc: method is nil but Projection.GetContentEntity was just called") + panic("GormProjectionMock.GetContentEntityFunc: method is nil but GormProjection.GetContentEntity was just called") } callInfo := struct { Ctx context.Context @@ -825,8 +861,8 @@ func (mock *ProjectionMock) GetContentEntity(ctx context.Context, entityFactory // GetContentEntityCalls gets all the calls that were made to GetContentEntity. // Check the length with: -// len(mockedProjection.GetContentEntityCalls()) -func (mock *ProjectionMock) GetContentEntityCalls() []struct { +// len(mockedGormProjection.GetContentEntityCalls()) +func (mock *GormProjectionMock) GetContentEntityCalls() []struct { Ctx context.Context EntityFactory model.EntityFactory WeosID string @@ -843,9 +879,9 @@ func (mock *ProjectionMock) GetContentEntityCalls() []struct { } // GetEventHandler calls GetEventHandlerFunc. -func (mock *ProjectionMock) GetEventHandler() model.EventHandler { +func (mock *GormProjectionMock) GetEventHandler() model.EventHandler { if mock.GetEventHandlerFunc == nil { - panic("ProjectionMock.GetEventHandlerFunc: method is nil but Projection.GetEventHandler was just called") + panic("GormProjectionMock.GetEventHandlerFunc: method is nil but GormProjection.GetEventHandler was just called") } callInfo := struct { }{} @@ -857,8 +893,8 @@ func (mock *ProjectionMock) GetEventHandler() model.EventHandler { // GetEventHandlerCalls gets all the calls that were made to GetEventHandler. // Check the length with: -// len(mockedProjection.GetEventHandlerCalls()) -func (mock *ProjectionMock) GetEventHandlerCalls() []struct { +// len(mockedGormProjection.GetEventHandlerCalls()) +func (mock *GormProjectionMock) GetEventHandlerCalls() []struct { } { var calls []struct { } @@ -869,9 +905,9 @@ func (mock *ProjectionMock) GetEventHandlerCalls() []struct { } // Migrate calls MigrateFunc. -func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]ds.Builder, deletedFields map[string][]string) error { +func (mock *GormProjectionMock) Migrate(ctx context.Context, builders map[string]ds.Builder, deletedFields map[string][]string) error { if mock.MigrateFunc == nil { - panic("ProjectionMock.MigrateFunc: method is nil but Projection.Migrate was just called") + panic("GormProjectionMock.MigrateFunc: method is nil but GormProjection.Migrate was just called") } callInfo := struct { Ctx context.Context @@ -890,8 +926,8 @@ func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]ds. // MigrateCalls gets all the calls that were made to Migrate. // Check the length with: -// len(mockedProjection.MigrateCalls()) -func (mock *ProjectionMock) MigrateCalls() []struct { +// len(mockedGormProjection.MigrateCalls()) +func (mock *GormProjectionMock) MigrateCalls() []struct { Ctx context.Context Builders map[string]ds.Builder DeletedFields map[string][]string @@ -907,7 +943,6 @@ func (mock *ProjectionMock) MigrateCalls() []struct { return calls } - // Ensure, that LogMock does implement model.Log. // If this is not the case, regenerate this file with moq. var _ model.Log = &LogMock{} diff --git a/go.mod b/go.mod index d686961b..24abb359 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/wepala/weos go 1.16 require ( + github.com/coreos/go-oidc/v3 v3.1.0 github.com/cucumber/godog v0.12.2 github.com/getkin/kin-openapi v0.15.0 github.com/jinzhu/inflection v1.0.0 diff --git a/go.sum b/go.sum index 1df06ecc..00f76fad 100644 --- a/go.sum +++ b/go.sum @@ -191,7 +191,10 @@ github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgU github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.1.0 h1:6avEvcdvTa1qYsOZ6I5PRkSYHzpTNWgKYmaJfaYbrRw= +github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -878,6 +881,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -890,6 +894,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1056,6 +1061,7 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -1129,6 +1135,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/model/interfaces.go b/model/interfaces.go index 9fd5fb41..18ea5479 100644 --- a/model/interfaces.go +++ b/model/interfaces.go @@ -1,11 +1,11 @@ package model -//go:generate moq -out temp_mocks_test.go -pkg model_test . Projection +//go:generate moq -out temp_mocks_test.go -pkg model_test . GormProjection import ( ds "github.com/ompluscator/dynamic-struct" "golang.org/x/net/context" - "time" "gorm.io/gorm" + "time" ) type CommandDispatcher interface { diff --git a/model/mocks_test.go b/model/mocks_test.go index 4d6df5c5..f0046990 100644 --- a/model/mocks_test.go +++ b/model/mocks_test.go @@ -550,41 +550,47 @@ func (mock *EventRepositoryMock) ReplayEventsCalls() []struct { return calls } -// Ensure, that ProjectionMock does implement model.Projection. +// Ensure, that GormProjectionMock does implement model.GormProjection. // If this is not the case, regenerate this file with moq. -var _ model.Projection = &ProjectionMock{} +var _ model.GormProjection = &GormProjectionMock{} -// ProjectionMock is a mock implementation of model.Projection. +// GormProjectionMock is a mock implementation of model.GormProjection. // -// func TestSomethingThatUsesProjection(t *testing.T) { +// func TestSomethingThatUsesGormProjection(t *testing.T) { // -// // make and configure a mocked model.Projection -// mockedProjection := &ProjectionMock{ -// GetByEntityIDFunc: func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { -// panic("mock out the GetByEntityID method") -// }, -// GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { -// panic("mock out the GetByKey method") -// }, -// GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { -// panic("mock out the GetContentEntities method") -// }, -// GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { -// panic("mock out the GetContentEntity method") -// }, -// GetEventHandlerFunc: func() model.EventHandler { -// panic("mock out the GetEventHandler method") -// }, -// MigrateFunc: func(ctx context.Context, builders map[string]dynamicstruct.Builder, deletedFields map[string][]string) error { -// panic("mock out the Migrate method") -// }, -// } +// // make and configure a mocked model.GormProjection +// mockedGormProjection := &GormProjectionMock{ +// DBFunc: func() *gorm.DB { +// panic("mock out the DB method") +// }, +// GetByEntityIDFunc: func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { +// panic("mock out the GetByEntityID method") +// }, +// GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { +// panic("mock out the GetByKey method") +// }, +// GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { +// panic("mock out the GetContentEntities method") +// }, +// GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { +// panic("mock out the GetContentEntity method") +// }, +// GetEventHandlerFunc: func() model.EventHandler { +// panic("mock out the GetEventHandler method") +// }, +// MigrateFunc: func(ctx context.Context, builders map[string]ds.Builder, deletedFields map[string][]string) error { +// panic("mock out the Migrate method") +// }, +// } // -// // use mockedProjection in code that requires model.Projection -// // and then make assertions. +// // use mockedGormProjection in code that requires model.GormProjection +// // and then make assertions. // -// } -type ProjectionMock struct { +// } +type GormProjectionMock struct { + // DBFunc mocks the DB method. + DBFunc func() *gorm.DB + // GetByEntityIDFunc mocks the GetByEntityID method. GetByEntityIDFunc func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) @@ -605,6 +611,9 @@ type ProjectionMock struct { // calls tracks calls to the methods. calls struct { + // DB holds details about calls to the DB method. + DB []struct { + } // GetByEntityID holds details about calls to the GetByEntityID method. GetByEntityID []struct { // Ctxt is the ctxt argument value. @@ -662,6 +671,7 @@ type ProjectionMock struct { DeletedFields map[string][]string } } + lockDB sync.RWMutex lockGetByEntityID sync.RWMutex lockGetByKey sync.RWMutex lockGetContentEntities sync.RWMutex @@ -670,10 +680,36 @@ type ProjectionMock struct { lockMigrate sync.RWMutex } +// DB calls DBFunc. +func (mock *GormProjectionMock) DB() *gorm.DB { + if mock.DBFunc == nil { + panic("GormProjectionMock.DBFunc: method is nil but GormProjection.DB was just called") + } + callInfo := struct { + }{} + mock.lockDB.Lock() + mock.calls.DB = append(mock.calls.DB, callInfo) + mock.lockDB.Unlock() + return mock.DBFunc() +} + +// DBCalls gets all the calls that were made to DB. +// Check the length with: +// len(mockedGormProjection.DBCalls()) +func (mock *GormProjectionMock) DBCalls() []struct { +} { + var calls []struct { + } + mock.lockDB.RLock() + calls = mock.calls.DB + mock.lockDB.RUnlock() + return calls +} + // GetByEntityID calls GetByEntityIDFunc. -func (mock *ProjectionMock) GetByEntityID(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { +func (mock *GormProjectionMock) GetByEntityID(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { if mock.GetByEntityIDFunc == nil { - panic("ProjectionMock.GetByEntityIDFunc: method is nil but Projection.GetByEntityID was just called") + panic("GormProjectionMock.GetByEntityIDFunc: method is nil but GormProjection.GetByEntityID was just called") } callInfo := struct { Ctxt context.Context @@ -692,8 +728,8 @@ func (mock *ProjectionMock) GetByEntityID(ctxt context.Context, entityFactory mo // GetByEntityIDCalls gets all the calls that were made to GetByEntityID. // Check the length with: -// len(mockedProjection.GetByEntityIDCalls()) -func (mock *ProjectionMock) GetByEntityIDCalls() []struct { +// len(mockedGormProjection.GetByEntityIDCalls()) +func (mock *GormProjectionMock) GetByEntityIDCalls() []struct { Ctxt context.Context EntityFactory model.EntityFactory ID string @@ -710,9 +746,9 @@ func (mock *ProjectionMock) GetByEntityIDCalls() []struct { } // GetByKey calls GetByKeyFunc. -func (mock *ProjectionMock) GetByKey(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { +func (mock *GormProjectionMock) GetByKey(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { if mock.GetByKeyFunc == nil { - panic("ProjectionMock.GetByKeyFunc: method is nil but Projection.GetByKey was just called") + panic("GormProjectionMock.GetByKeyFunc: method is nil but GormProjection.GetByKey was just called") } callInfo := struct { Ctxt context.Context @@ -731,8 +767,8 @@ func (mock *ProjectionMock) GetByKey(ctxt context.Context, entityFactory model.E // GetByKeyCalls gets all the calls that were made to GetByKey. // Check the length with: -// len(mockedProjection.GetByKeyCalls()) -func (mock *ProjectionMock) GetByKeyCalls() []struct { +// len(mockedGormProjection.GetByKeyCalls()) +func (mock *GormProjectionMock) GetByKeyCalls() []struct { Ctxt context.Context EntityFactory model.EntityFactory Identifiers map[string]interface{} @@ -749,9 +785,9 @@ func (mock *ProjectionMock) GetByKeyCalls() []struct { } // GetContentEntities calls GetContentEntitiesFunc. -func (mock *ProjectionMock) GetContentEntities(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { +func (mock *GormProjectionMock) GetContentEntities(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { if mock.GetContentEntitiesFunc == nil { - panic("ProjectionMock.GetContentEntitiesFunc: method is nil but Projection.GetContentEntities was just called") + panic("GormProjectionMock.GetContentEntitiesFunc: method is nil but GormProjection.GetContentEntities was just called") } callInfo := struct { Ctx context.Context @@ -778,8 +814,8 @@ func (mock *ProjectionMock) GetContentEntities(ctx context.Context, entityFactor // GetContentEntitiesCalls gets all the calls that were made to GetContentEntities. // Check the length with: -// len(mockedProjection.GetContentEntitiesCalls()) -func (mock *ProjectionMock) GetContentEntitiesCalls() []struct { +// len(mockedGormProjection.GetContentEntitiesCalls()) +func (mock *GormProjectionMock) GetContentEntitiesCalls() []struct { Ctx context.Context EntityFactory model.EntityFactory Page int @@ -804,9 +840,9 @@ func (mock *ProjectionMock) GetContentEntitiesCalls() []struct { } // GetContentEntity calls GetContentEntityFunc. -func (mock *ProjectionMock) GetContentEntity(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { +func (mock *GormProjectionMock) GetContentEntity(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { if mock.GetContentEntityFunc == nil { - panic("ProjectionMock.GetContentEntityFunc: method is nil but Projection.GetContentEntity was just called") + panic("GormProjectionMock.GetContentEntityFunc: method is nil but GormProjection.GetContentEntity was just called") } callInfo := struct { Ctx context.Context @@ -825,8 +861,8 @@ func (mock *ProjectionMock) GetContentEntity(ctx context.Context, entityFactory // GetContentEntityCalls gets all the calls that were made to GetContentEntity. // Check the length with: -// len(mockedProjection.GetContentEntityCalls()) -func (mock *ProjectionMock) GetContentEntityCalls() []struct { +// len(mockedGormProjection.GetContentEntityCalls()) +func (mock *GormProjectionMock) GetContentEntityCalls() []struct { Ctx context.Context EntityFactory model.EntityFactory WeosID string @@ -843,9 +879,9 @@ func (mock *ProjectionMock) GetContentEntityCalls() []struct { } // GetEventHandler calls GetEventHandlerFunc. -func (mock *ProjectionMock) GetEventHandler() model.EventHandler { +func (mock *GormProjectionMock) GetEventHandler() model.EventHandler { if mock.GetEventHandlerFunc == nil { - panic("ProjectionMock.GetEventHandlerFunc: method is nil but Projection.GetEventHandler was just called") + panic("GormProjectionMock.GetEventHandlerFunc: method is nil but GormProjection.GetEventHandler was just called") } callInfo := struct { }{} @@ -857,8 +893,8 @@ func (mock *ProjectionMock) GetEventHandler() model.EventHandler { // GetEventHandlerCalls gets all the calls that were made to GetEventHandler. // Check the length with: -// len(mockedProjection.GetEventHandlerCalls()) -func (mock *ProjectionMock) GetEventHandlerCalls() []struct { +// len(mockedGormProjection.GetEventHandlerCalls()) +func (mock *GormProjectionMock) GetEventHandlerCalls() []struct { } { var calls []struct { } @@ -869,9 +905,9 @@ func (mock *ProjectionMock) GetEventHandlerCalls() []struct { } // Migrate calls MigrateFunc. -func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]dynamicstruct.Builder, deletedFields map[string][]string) error { +func (mock *GormProjectionMock) Migrate(ctx context.Context, builders map[string]dynamicstruct.Builder, deletedFields map[string][]string) error { if mock.MigrateFunc == nil { - panic("ProjectionMock.MigrateFunc: method is nil but Projection.Migrate was just called") + panic("GormProjectionMock.MigrateFunc: method is nil but GormProjection.Migrate was just called") } callInfo := struct { Ctx context.Context @@ -890,8 +926,8 @@ func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]dyn // MigrateCalls gets all the calls that were made to Migrate. // Check the length with: -// len(mockedProjection.MigrateCalls()) -func (mock *ProjectionMock) MigrateCalls() []struct { +// len(mockedGormProjection.MigrateCalls()) +func (mock *GormProjectionMock) MigrateCalls() []struct { Ctx context.Context Builders map[string]dynamicstruct.Builder DeletedFields map[string][]string @@ -907,7 +943,6 @@ func (mock *ProjectionMock) MigrateCalls() []struct { return calls } - // Ensure, that LogMock does implement model.Log. // If this is not the case, regenerate this file with moq. var _ model.Log = &LogMock{} From d8a0af9199da2fc352f56d44c740c94854e95f95 Mon Sep 17 00:00:00 2001 From: shaniah868 Date: Tue, 15 Feb 2022 14:13:22 -0400 Subject: [PATCH 2/6] feature: WEOS-1343 Create middleware for handling JWT in the incoming Authorization header -Moved authorization middleware to standard controller file as well as test to that test file --- controllers/rest/controller_standard.go | 51 ++++++++++++++++ controllers/rest/controller_standard_test.go | 56 +++++++++++++++++ controllers/rest/middleware_initialize.go | 54 ----------------- .../rest/middleware_initialize_test.go | 60 ------------------- go.mod | 1 + go.sum | 18 ++++++ 6 files changed, 126 insertions(+), 114 deletions(-) diff --git a/controllers/rest/controller_standard.go b/controllers/rest/controller_standard.go index b4b4a4ff..828d0bd8 100644 --- a/controllers/rest/controller_standard.go +++ b/controllers/rest/controller_standard.go @@ -4,9 +4,11 @@ import ( "encoding/json" "errors" "fmt" + "github.com/coreos/go-oidc/v3/oidc" "net/http" "strconv" "strings" + "time" "github.com/wepala/weos/projections" @@ -664,3 +666,52 @@ func HealthCheck(api *RESTAPI, projection projections.Projection, commandDispatc } } + +//AuthorizationMiddleware handling JWT in incoming Authorization header +func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctxt echo.Context) error { + if operation.Security != nil && len(*operation.Security) == 0 { + return nil + } + newContext := ctxt.Request().Context() + token, ok := newContext.Value(weoscontext.AUTHORIZATION).(string) + if !ok || token == "" { + api.e.Logger.Debugf("no JWT token was found") + return NewControllerError("no JWT token was found", nil, http.StatusUnauthorized) + } + var openIdConnectUrl string + if openIdUrl, ok := api.Swagger.Components.SecuritySchemes["Auth0"].Value.ExtensionProps.Extensions[weoscontext.OPENIDCONNECTURL]; ok { + err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl) + if err != nil { + api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err) + return NewControllerError("unable to unmarshal open id connect url", err, http.StatusBadRequest) + } + } else { + api.EchoInstance().Logger.Errorf("no open id connect url found") + return NewControllerError("no open id connect url found", nil, http.StatusBadRequest) + } + jwtToken := strings.Replace(token, "Bearer ", "", -1) + algs := []string{"RS256", "RS384", "RS512", "HS256"} + keySet := oidc.NewRemoteKeySet(newContext, openIdConnectUrl) + tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ + ClientID: "", + SupportedSigningAlgs: algs, + SkipClientIDCheck: true, + SkipExpiryCheck: false, + SkipIssuerCheck: true, + Now: time.Now, + }) + idToken, err := tokenVerifier.Verify(newContext, jwtToken) + if err != nil { + api.e.Logger.Debugf(err.Error()) + return NewControllerError("unexpected error verifying token", err, http.StatusUnauthorized) + } + if idToken != nil { + + } + return nil + + } + } +} diff --git a/controllers/rest/controller_standard_test.go b/controllers/rest/controller_standard_test.go index c2a4d9ad..4197b241 100644 --- a/controllers/rest/controller_standard_test.go +++ b/controllers/rest/controller_standard_test.go @@ -1948,3 +1948,59 @@ func TestStandardControllers_DeleteID(t *testing.T) { } }) } + +func TestStandardControllers_AuthenticateMiddleware(t *testing.T) { + //instantiate api + api, err := rest.New("./fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading api '%s'", err) + } + err = api.Initialize(context.TODO()) + if err != nil { + t.Fatalf("un expected error initializing api '%s'", err) + } + e := api.EchoInstance() + + t.Run("no jwt token added when required", func(t *testing.T) { + description := "testing 1st blog description" + mockBlog := &TestBlog{Description: &description} + reqBytes, err := json.Marshal(mockBlog) + if err != nil { + t.Fatalf("error setting up request %s", err) + } + body := bytes.NewReader(reqBytes) + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/blogs", body) + req.Header.Set("Content-Type", "application/json") + e.ServeHTTP(resp, req) + if resp.Result().StatusCode != http.StatusUnauthorized { + t.Errorf("expected the response code to be %d, got %d", http.StatusUnauthorized, resp.Result().StatusCode) + } + }) + t.Run("security parameter array is empty", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/health", nil) + e.ServeHTTP(resp, req) + if resp.Result().StatusCode != http.StatusOK { + t.Errorf("expected the response code to be %d, got %d", http.StatusOK, resp.Result().StatusCode) + } + }) + t.Run("jwt token added", func(t *testing.T) { + description := "testing 1st blog description" + mockBlog := &TestBlog{Description: &description} + reqBytes, err := json.Marshal(mockBlog) + if err != nil { + t.Fatalf("error setting up request %s", err) + } + token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJRCI6ImtieXVGRGlkTExtMjgwTEl3VkZpYXpPcWpPM3R5OEtIIiwiY3JlYXRlZF9hdCI6IjIwMjItMDItMTVUMDE6MTk6MDguMTE2WiIsImVtYWlsIjoic2hhbmlhaC5zaW1vbkB3ZXBhbGEuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZhbWlseV9uYW1lIjoiU2ltb24iLCJnaXZlbl9uYW1lIjoiU2hhbmlhaCIsImlkZW50aXRpZXMiOlt7InByb3ZpZGVyIjoiZ29vZ2xlLW9hdXRoMiIsInVzZXJfaWQiOiIxMDEyNTMxMjc4NTMyMDMwMTQ4NDciLCJjb25uZWN0aW9uIjoiZ29vZ2xlLW9hdXRoMiIsImlzU29jaWFsIjp0cnVlfV0sImxvY2FsZSI6ImVuIiwibmFtZSI6IlNoYW5pYWggU2ltb24iLCJuaWNrbmFtZSI6InNoYW5pYWguc2ltb24iLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2pyOTcyTFgxWFMzSURCdUNVVnlVWW9tVXFOUGptZmVvV0Y1MUdRPXM5Ni1jIiwidXBkYXRlZF9hdCI6IjIwMjItMDItMTVUMTQ6Mjk6NTEuMTQwWiIsInVzZXJfaWQiOiJnb29nbGUtb2F1dGgyfDEwMTI1MzEyNzg1MzIwMzAxNDg0NyIsInVzZXJfbWV0YWRhdGEiOnt9LCJhcHBfbWV0YWRhdGEiOnt9LCJpc3MiOiJodHRwczovL3NhbXBsZXMuYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTAxMjUzMTI3ODUzMjAzMDE0ODQ3IiwiYXVkIjoia2J5dUZEaWRMTG0yODBMSXdWRmlhek9xak8zdHk4S0giLCJpYXQiOjE2NDQ5MzUzOTgsImV4cCI6MTY0NDk3MTM5OH0.3F913gEHid9kj9jJn9_6CVI0jkOiR_G9aKY43bPQ4j4" + body := bytes.NewReader(reqBytes) + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/blogs", body) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + e.ServeHTTP(resp, req) + if resp.Result().StatusCode != http.StatusCreated { + t.Errorf("expected the response code to be %d, got %d", http.StatusCreated, resp.Result().StatusCode) + } + }) +} diff --git a/controllers/rest/middleware_initialize.go b/controllers/rest/middleware_initialize.go index efcabc2d..a336c95a 100644 --- a/controllers/rest/middleware_initialize.go +++ b/controllers/rest/middleware_initialize.go @@ -2,10 +2,6 @@ package rest import ( "encoding/json" - "github.com/coreos/go-oidc/v3/oidc" - weosContext "github.com/wepala/weos/context" - "github.com/wepala/weos/model" - "net/http" "strings" "time" @@ -242,53 +238,3 @@ func addRelations(struc ds.Builder, relations map[string]string, structs map[str } return struc, nil } - -//AuthorizationMiddleware handling JWT in incoming Authentication header -func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(ctxt echo.Context) error { - if operation.Security != nil && len(*operation.Security) == 0 { - return nil - } - newContext := ctxt.Request().Context() - token, ok := newContext.Value(weosContext.AUTHORIZATION).(string) - if !ok || token == "" { - api.e.Logger.Debugf("no JWT token was found") - return NewControllerError("no JWT token was found", nil, http.StatusUnauthorized) - } - var openIdConnectUrl string - if openIdUrl, ok := api.Swagger.Components.SecuritySchemes["Auth0"].Value.ExtensionProps.Extensions[weosContext.OPENIDCONNECTURL]; ok { - - err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl) - if err != nil { - api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err) - return NewControllerError("unable to unmarshal open id connect url", err, http.StatusBadRequest) - } - } else { - api.EchoInstance().Logger.Errorf("no open id connect url found") - return NewControllerError("no open id connect url found", nil, http.StatusBadRequest) - } - algs := []string{"RS256", "RS384", "RS512", "HS256"} - keySet := oidc.NewRemoteKeySet(newContext, openIdConnectUrl) - tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ - ClientID: "", - SupportedSigningAlgs: algs, - SkipClientIDCheck: true, - SkipExpiryCheck: false, - SkipIssuerCheck: true, - Now: time.Now, - }) - jwtToken := strings.Replace(token, "Bearer ", "", -1) - idToken, err := tokenVerifier.Verify(newContext, jwtToken) - if err != nil { - api.e.Logger.Debugf(err.Error()) - return NewControllerError("unexpected error verifying token", err, http.StatusUnauthorized) - } - if idToken != nil { - - } - return nil - - } - } -} diff --git a/controllers/rest/middleware_initialize_test.go b/controllers/rest/middleware_initialize_test.go index e5730192..2b7e2b9a 100644 --- a/controllers/rest/middleware_initialize_test.go +++ b/controllers/rest/middleware_initialize_test.go @@ -1,11 +1,7 @@ package rest_test import ( - "bytes" - "encoding/json" "io/ioutil" - "net/http" - "net/http/httptest" "os" "regexp" "strings" @@ -106,59 +102,3 @@ func TestXRemove(t *testing.T) { } }) } - -func TestAuthenticateMiddleware(t *testing.T) { - //instantiate api - api, err := rest.New("./fixtures/blog-security.yaml") - if err != nil { - t.Fatalf("unexpected error loading api '%s'", err) - } - err = api.Initialize(context.TODO()) - if err != nil { - t.Fatalf("un expected error initializing api '%s'", err) - } - e := api.EchoInstance() - - t.Run("no jwt token added when required", func(t *testing.T) { - description := "testing 1st blog description" - mockBlog := &TestBlog{Description: &description} - reqBytes, err := json.Marshal(mockBlog) - if err != nil { - t.Fatalf("error setting up request %s", err) - } - body := bytes.NewReader(reqBytes) - resp := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/blogs", body) - req.Header.Set("Content-Type", "application/json") - e.ServeHTTP(resp, req) - if resp.Result().StatusCode != http.StatusUnauthorized { - t.Errorf("expected the response code to be %d, got %d", http.StatusUnauthorized, resp.Result().StatusCode) - } - }) - t.Run("security parameter array is empty", func(t *testing.T) { - resp := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/health", nil) - e.ServeHTTP(resp, req) - if resp.Result().StatusCode != http.StatusOK { - t.Errorf("expected the response code to be %d, got %d", http.StatusOK, resp.Result().StatusCode) - } - }) - t.Run("jwt token added", func(t *testing.T) { - description := "testing 1st blog description" - mockBlog := &TestBlog{Description: &description} - reqBytes, err := json.Marshal(mockBlog) - if err != nil { - t.Fatalf("error setting up request %s", err) - } - token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKb2huIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqRG9lQGdtYWlsLmNvbSIsImlhdCI6MTY0NDg4Nzc2OSwiZXhwIjoxNjQ0ODg4NzY5fQ.sBL03kXCIbjzD5MdzCRb71g8LLQhgr9R7a0-3cJxySw" - body := bytes.NewReader(reqBytes) - resp := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/blogs", body) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - e.ServeHTTP(resp, req) - if resp.Result().StatusCode != http.StatusCreated { - t.Errorf("expected the response code to be %d, got %d", http.StatusCreated, resp.Result().StatusCode) - } - }) -} diff --git a/go.mod b/go.mod index 24abb359..953c1b9f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/labstack/echo/v4 v4.5.0 github.com/labstack/gommon v0.3.0 + github.com/lestrrat-go/jwx v1.2.18 github.com/lib/pq v1.10.2 github.com/mattn/go-sqlite3 v1.14.9 github.com/ompluscator/dynamic-struct v1.3.0 diff --git a/go.sum b/go.sum index 00f76fad..d8311da5 100644 --- a/go.sum +++ b/go.sum @@ -227,6 +227,9 @@ github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjI github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= @@ -300,6 +303,8 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.9.4 h1:L8MLKG2mvVXiQu07qB6hmfqeSYQdOnqPot2GhsIwIaI= +github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -521,6 +526,18 @@ github.com/labstack/echo/v4 v4.5.0 h1:JXk6H5PAw9I3GwizqUHhYyS4f45iyGebR/c1xNCeOC github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc= +github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.18 h1:RV4hcTRUlPVYUnGqATKXEojoOsLexoU8Na4KheVzxQ8= +github.com/lestrrat-go/jwx v1.2.18/go.mod h1:bWTBO7IHHVMtNunM8so9MT8wD+euEY1PzGEyCnuI2qM= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -813,6 +830,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= From f042c7288eeb4085aa12f684eae0cd0ce4019acf Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Tue, 15 Feb 2022 16:05:36 -0400 Subject: [PATCH 3/6] feature: WEOS-1349 Rough Implementation of oidc lib * Moved the client lib setup to outside the middleware func * Updated test to use test keys for an unused account (develoeprs needs to have an environment variable OAUTH_TEST_KEY. This is set in the pipeline) --- .github/workflows/dev.yaml | 1 + .github/workflows/release.yaml | 1 + controllers/rest/controller_standard.go | 45 +++++++++----------- controllers/rest/controller_standard_test.go | 4 +- go.mod | 1 - go.sum | 18 -------- 6 files changed, 25 insertions(+), 45 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index a3653433..20afbd0c 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -13,6 +13,7 @@ env: SLACK_ICON: https://github.com/wepala.png?size=48 SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_FOOTER: copyright 2022 Wepala + OAUTH_TEST_KEY: ${{ secrets.OAUTH_TEST_KEY }} jobs: build-api: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 795f86b9..5c6fae0d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -11,6 +11,7 @@ env: SLACK_ICON: https://github.com/wepala.png?size=48 SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_FOOTER: copyright 2022 Wepala + OAUTH_TEST_KEY: ${{ secrets.OAUTH_TEST_KEY }} jobs: build-api: diff --git a/controllers/rest/controller_standard.go b/controllers/rest/controller_standard.go index 828d0bd8..5cdcd7d3 100644 --- a/controllers/rest/controller_standard.go +++ b/controllers/rest/controller_standard.go @@ -669,39 +669,36 @@ func HealthCheck(api *RESTAPI, projection projections.Projection, commandDispatc //AuthorizationMiddleware handling JWT in incoming Authorization header func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc { + var openIdConnectUrl string + if openIdUrl, ok := api.Swagger.Components.SecuritySchemes["Auth0"].Value.ExtensionProps.Extensions[weoscontext.OPENIDCONNECTURL]; ok { + err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl) + if err != nil { + api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err) + } + } else { + api.EchoInstance().Logger.Errorf("no open id connect url found") + } + + algs := []string{"RS256", "RS384", "RS512", "HS256"} + keySet := oidc.NewRemoteKeySet(context.Background(), "https://dev-bhjqt6zc.us.auth0.com/.well-known/jwks.json") + tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ + ClientID: "Y9IvGucEhViFd58GL0bBoNrgEk3ohW88", + SupportedSigningAlgs: algs, + SkipClientIDCheck: true, + SkipExpiryCheck: false, + SkipIssuerCheck: true, + Now: time.Now, + }) + return func(next echo.HandlerFunc) echo.HandlerFunc { return func(ctxt echo.Context) error { - if operation.Security != nil && len(*operation.Security) == 0 { - return nil - } newContext := ctxt.Request().Context() token, ok := newContext.Value(weoscontext.AUTHORIZATION).(string) if !ok || token == "" { api.e.Logger.Debugf("no JWT token was found") return NewControllerError("no JWT token was found", nil, http.StatusUnauthorized) } - var openIdConnectUrl string - if openIdUrl, ok := api.Swagger.Components.SecuritySchemes["Auth0"].Value.ExtensionProps.Extensions[weoscontext.OPENIDCONNECTURL]; ok { - err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl) - if err != nil { - api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err) - return NewControllerError("unable to unmarshal open id connect url", err, http.StatusBadRequest) - } - } else { - api.EchoInstance().Logger.Errorf("no open id connect url found") - return NewControllerError("no open id connect url found", nil, http.StatusBadRequest) - } jwtToken := strings.Replace(token, "Bearer ", "", -1) - algs := []string{"RS256", "RS384", "RS512", "HS256"} - keySet := oidc.NewRemoteKeySet(newContext, openIdConnectUrl) - tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ - ClientID: "", - SupportedSigningAlgs: algs, - SkipClientIDCheck: true, - SkipExpiryCheck: false, - SkipIssuerCheck: true, - Now: time.Now, - }) idToken, err := tokenVerifier.Verify(newContext, jwtToken) if err != nil { api.e.Logger.Debugf(err.Error()) diff --git a/controllers/rest/controller_standard_test.go b/controllers/rest/controller_standard_test.go index 4197b241..44b90838 100644 --- a/controllers/rest/controller_standard_test.go +++ b/controllers/rest/controller_standard_test.go @@ -1992,14 +1992,14 @@ func TestStandardControllers_AuthenticateMiddleware(t *testing.T) { if err != nil { t.Fatalf("error setting up request %s", err) } - token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJRCI6ImtieXVGRGlkTExtMjgwTEl3VkZpYXpPcWpPM3R5OEtIIiwiY3JlYXRlZF9hdCI6IjIwMjItMDItMTVUMDE6MTk6MDguMTE2WiIsImVtYWlsIjoic2hhbmlhaC5zaW1vbkB3ZXBhbGEuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZhbWlseV9uYW1lIjoiU2ltb24iLCJnaXZlbl9uYW1lIjoiU2hhbmlhaCIsImlkZW50aXRpZXMiOlt7InByb3ZpZGVyIjoiZ29vZ2xlLW9hdXRoMiIsInVzZXJfaWQiOiIxMDEyNTMxMjc4NTMyMDMwMTQ4NDciLCJjb25uZWN0aW9uIjoiZ29vZ2xlLW9hdXRoMiIsImlzU29jaWFsIjp0cnVlfV0sImxvY2FsZSI6ImVuIiwibmFtZSI6IlNoYW5pYWggU2ltb24iLCJuaWNrbmFtZSI6InNoYW5pYWguc2ltb24iLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2pyOTcyTFgxWFMzSURCdUNVVnlVWW9tVXFOUGptZmVvV0Y1MUdRPXM5Ni1jIiwidXBkYXRlZF9hdCI6IjIwMjItMDItMTVUMTQ6Mjk6NTEuMTQwWiIsInVzZXJfaWQiOiJnb29nbGUtb2F1dGgyfDEwMTI1MzEyNzg1MzIwMzAxNDg0NyIsInVzZXJfbWV0YWRhdGEiOnt9LCJhcHBfbWV0YWRhdGEiOnt9LCJpc3MiOiJodHRwczovL3NhbXBsZXMuYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTAxMjUzMTI3ODUzMjAzMDE0ODQ3IiwiYXVkIjoia2J5dUZEaWRMTG0yODBMSXdWRmlhek9xak8zdHk4S0giLCJpYXQiOjE2NDQ5MzUzOTgsImV4cCI6MTY0NDk3MTM5OH0.3F913gEHid9kj9jJn9_6CVI0jkOiR_G9aKY43bPQ4j4" + token := os.Getenv("OAUTH_TEST_KEY") body := bytes.NewReader(reqBytes) resp := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/blogs", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) e.ServeHTTP(resp, req) - if resp.Result().StatusCode != http.StatusCreated { + if resp.Result().StatusCode != http.StatusOK { t.Errorf("expected the response code to be %d, got %d", http.StatusCreated, resp.Result().StatusCode) } }) diff --git a/go.mod b/go.mod index 953c1b9f..24abb359 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/labstack/echo/v4 v4.5.0 github.com/labstack/gommon v0.3.0 - github.com/lestrrat-go/jwx v1.2.18 github.com/lib/pq v1.10.2 github.com/mattn/go-sqlite3 v1.14.9 github.com/ompluscator/dynamic-struct v1.3.0 diff --git a/go.sum b/go.sum index d8311da5..00f76fad 100644 --- a/go.sum +++ b/go.sum @@ -227,9 +227,6 @@ github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjI github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= @@ -303,8 +300,6 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/goccy/go-json v0.9.4 h1:L8MLKG2mvVXiQu07qB6hmfqeSYQdOnqPot2GhsIwIaI= -github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -526,18 +521,6 @@ github.com/labstack/echo/v4 v4.5.0 h1:JXk6H5PAw9I3GwizqUHhYyS4f45iyGebR/c1xNCeOC github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= -github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= -github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= -github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= -github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc= -github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= -github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= -github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= -github.com/lestrrat-go/jwx v1.2.18 h1:RV4hcTRUlPVYUnGqATKXEojoOsLexoU8Na4KheVzxQ8= -github.com/lestrrat-go/jwx v1.2.18/go.mod h1:bWTBO7IHHVMtNunM8so9MT8wD+euEY1PzGEyCnuI2qM= -github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -830,7 +813,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= From 82063c9c52712586dbab0c49702377758bc4af85 Mon Sep 17 00:00:00 2001 From: shaniah868 Date: Wed, 16 Feb 2022 12:05:48 -0400 Subject: [PATCH 4/6] feature: WEOS-1343 Create middleware for handling JWT in the incoming Authorization header -Added the correct openidconnecturl in the test yaml file -Added X-USER-ID contextkey -Added a securitycheck flag -Added a for loop for getting the security schemes --- context/context.go | 3 +- controllers/rest/controller_standard.go | 105 ++++++++++++++----- controllers/rest/controller_standard_test.go | 6 +- controllers/rest/fixtures/blog-security.yaml | 2 +- 4 files changed, 88 insertions(+), 28 deletions(-) diff --git a/context/context.go b/context/context.go index 23176bef..0286d59f 100644 --- a/context/context.go +++ b/context/context.go @@ -31,7 +31,8 @@ const SORTS ContextKey = "_sorts" const PAYLOAD ContextKey = "_payload" const SEQUENCE_NO string = "sequence_no" const AUTHORIZATION string = "Authorization" -const OPENIDCONNECTURL string = "openIdConnectUrl" +const OPEN_ID_CONNECT_URL string = "openIdConnectUrl" +const USER_ID_EXTENSION ContextKey = "X-USER-ID" //Path initializers are run per path and can be used to configure routes that are not defined in the open api spec const METHODS_FOUND ContextKey = "_methods_found" diff --git a/controllers/rest/controller_standard.go b/controllers/rest/controller_standard.go index 5cdcd7d3..2a67b025 100644 --- a/controllers/rest/controller_standard.go +++ b/controllers/rest/controller_standard.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "github.com/coreos/go-oidc/v3/oidc" + "io/ioutil" "net/http" "strconv" "strings" @@ -670,28 +671,51 @@ func HealthCheck(api *RESTAPI, projection projections.Projection, commandDispatc //AuthorizationMiddleware handling JWT in incoming Authorization header func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc { var openIdConnectUrl string - if openIdUrl, ok := api.Swagger.Components.SecuritySchemes["Auth0"].Value.ExtensionProps.Extensions[weoscontext.OPENIDCONNECTURL]; ok { - err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl) - if err != nil { - api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err) - } - } else { - api.EchoInstance().Logger.Errorf("no open id connect url found") + securityCheck := true + var verifiers []*oidc.IDTokenVerifier + algs := []string{"RS256", "RS384", "RS512", "HS256"} + if operation.Security != nil && len(*operation.Security) == 0 { + securityCheck = false } + for _, schemes := range api.Swagger.Components.SecuritySchemes { + //get the open id connect url + if openIdUrl, ok := schemes.Value.ExtensionProps.Extensions[weoscontext.OPEN_ID_CONNECT_URL]; ok { + err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl) + if err != nil { + api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err) + } else { + //get the Jwk url + jwksUrl, err := GetJwkUrl(openIdConnectUrl) + if err != nil { + api.EchoInstance().Logger.Errorf("unexpected error getting the jwks url: %s", err) + } else { + //create key set and verifier + keySet := oidc.NewRemoteKeySet(context.Background(), jwksUrl) + tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ + ClientID: "", + SupportedSigningAlgs: algs, + SkipClientIDCheck: true, + SkipExpiryCheck: false, + SkipIssuerCheck: true, + Now: time.Now, + }) + verifiers = append(verifiers, tokenVerifier) + } + } + } - algs := []string{"RS256", "RS384", "RS512", "HS256"} - keySet := oidc.NewRemoteKeySet(context.Background(), "https://dev-bhjqt6zc.us.auth0.com/.well-known/jwks.json") - tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ - ClientID: "Y9IvGucEhViFd58GL0bBoNrgEk3ohW88", - SupportedSigningAlgs: algs, - SkipClientIDCheck: true, - SkipExpiryCheck: false, - SkipIssuerCheck: true, - Now: time.Now, - }) + } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(ctxt echo.Context) error { + var err error + if !securityCheck { + return next(ctxt) + } + if len(verifiers) == 0 { + api.e.Logger.Debugf("unexpected error no verifiers were set") + return NewControllerError("unexpected error no verifiers were set", nil, http.StatusBadRequest) + } newContext := ctxt.Request().Context() token, ok := newContext.Value(weoscontext.AUTHORIZATION).(string) if !ok || token == "" { @@ -699,16 +723,49 @@ func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, co return NewControllerError("no JWT token was found", nil, http.StatusUnauthorized) } jwtToken := strings.Replace(token, "Bearer ", "", -1) - idToken, err := tokenVerifier.Verify(newContext, jwtToken) - if err != nil { - api.e.Logger.Debugf(err.Error()) - return NewControllerError("unexpected error verifying token", err, http.StatusUnauthorized) + var idToken *oidc.IDToken + for _, tokenVerifier := range verifiers { + idToken, err = tokenVerifier.Verify(newContext, jwtToken) + if err != nil || idToken == nil { + api.e.Logger.Debugf(err.Error()) + return NewControllerError("unexpected error verifying token", err, http.StatusUnauthorized) + } } - if idToken != nil { - } - return nil + newContext = context.WithValue(newContext, weoscontext.USER_ID_EXTENSION, idToken.Subject) + request := ctxt.Request().WithContext(newContext) + ctxt.SetRequest(request) + return next(ctxt) } } } + +//GetJwkUrl fetches the jwk url from the open id connect url +func GetJwkUrl(openIdUrl string) (string, error) { + //fetches the response from the connect id url + resp, err := http.Get(openIdUrl) + if err != nil || resp == nil { + return "", fmt.Errorf("unexpected error fetching open id connect url: %s", err) + } + defer resp.Body.Close() + // reads the body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("unable to read response body: %v", err) + } + //check the response status + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("expected open id connect url response code to be %d got %d ", http.StatusOK, resp.StatusCode) + } + // unmarshall the body to a struct we can use to find the jwk uri + var info map[string]interface{} + err = json.Unmarshal(body, &info) + if err != nil { + return "", fmt.Errorf("unexpected error unmarshalling open id connect url response %s", err) + } + if info["jwks_uri"] == nil || info["jwks_uri"].(string) == "" { + return "", fmt.Errorf("no jwks uri found") + } + return info["jwks_uri"].(string), nil +} diff --git a/controllers/rest/controller_standard_test.go b/controllers/rest/controller_standard_test.go index 44b90838..f551a1b4 100644 --- a/controllers/rest/controller_standard_test.go +++ b/controllers/rest/controller_standard_test.go @@ -1987,7 +1987,9 @@ func TestStandardControllers_AuthenticateMiddleware(t *testing.T) { }) t.Run("jwt token added", func(t *testing.T) { description := "testing 1st blog description" - mockBlog := &TestBlog{Description: &description} + url := "www.example.com" + title := "example" + mockBlog := &TestBlog{Title: &title, Url: &url, Description: &description} reqBytes, err := json.Marshal(mockBlog) if err != nil { t.Fatalf("error setting up request %s", err) @@ -1999,7 +2001,7 @@ func TestStandardControllers_AuthenticateMiddleware(t *testing.T) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) e.ServeHTTP(resp, req) - if resp.Result().StatusCode != http.StatusOK { + if resp.Result().StatusCode != http.StatusCreated { t.Errorf("expected the response code to be %d, got %d", http.StatusCreated, resp.Result().StatusCode) } }) diff --git a/controllers/rest/fixtures/blog-security.yaml b/controllers/rest/fixtures/blog-security.yaml index b4355da2..4fa5fb0a 100644 --- a/controllers/rest/fixtures/blog-security.yaml +++ b/controllers/rest/fixtures/blog-security.yaml @@ -31,7 +31,7 @@ components: securitySchemes: Auth0: type: openIdConnect - openIdConnectUrl: https://samples.auth0.com/.well-known/openid-configuration + openIdConnectUrl: https://dev-bhjqt6zc.us.auth0.com/.well-known/openid-configuration schemas: Category: type: object From 0b2c18f618a721cf1782ceba84393b09f220b9f4 Mon Sep 17 00:00:00 2001 From: shaniah868 Date: Wed, 16 Feb 2022 16:03:21 -0400 Subject: [PATCH 5/6] feature: WEOS-1343 Create an operation initializer for setting middleware on operation -Fix Module test --- model/domain_service_test.go | 10 +++++----- model/module_test.go | 2 +- model/receiver_test.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/model/domain_service_test.go b/model/domain_service_test.go index d03e31a7..b5f2397f 100644 --- a/model/domain_service_test.go +++ b/model/domain_service_test.go @@ -193,7 +193,7 @@ func TestDomainService_Update(t *testing.T) { } existingBlog.SequenceNo = int64(1) - projectionMock := &ProjectionMock{ + projectionMock := &GormProjectionMock{ GetContentEntityFunc: func(ctx context3.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { if entityFactory == nil { return nil, fmt.Errorf("expected entity factory got nil") @@ -342,7 +342,7 @@ func TestDomainService_UpdateCompoundPrimaryKeyID(t *testing.T) { dService := model.NewDomainService(newContext, mockEventRepository, nil, nil) existingBlog, err := dService.Create(newContext, reqBytes, entityType) - projectionMock := &ProjectionMock{ + projectionMock := &GormProjectionMock{ GetContentEntityFunc: func(ctx context3.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { return existingBlog, nil }, @@ -444,7 +444,7 @@ func TestDomainService_UpdateCompoundPrimaryKeyGuidTitle(t *testing.T) { dService := model.NewDomainService(newContext, mockEventRepository, nil, nil) existingBlog, err := dService.Create(newContext, reqBytes, entityType) - projectionMock := &ProjectionMock{ + projectionMock := &GormProjectionMock{ GetContentEntityFunc: func(ctx context3.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { return existingBlog, nil }, @@ -553,7 +553,7 @@ func TestDomainService_UpdateWithoutIdentifier(t *testing.T) { dService := model.NewDomainService(newContext, mockEventRepository, nil, nil) existingBlog, err := dService.Create(newContext, reqBytes, entityType) - projectionMock := &ProjectionMock{ + projectionMock := &GormProjectionMock{ GetContentEntityFunc: func(ctx context3.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { return existingBlog, nil }, @@ -631,7 +631,7 @@ func TestDomainService_Delete(t *testing.T) { dService := model.NewDomainService(newContext, mockEventRepository, nil, nil) existingBlog, _ := dService.Create(newContext, reqBytes, entityType) - projectionMock := &ProjectionMock{ + projectionMock := &GormProjectionMock{ GetContentEntityFunc: func(ctx context3.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { return existingBlog, nil }, diff --git a/model/module_test.go b/model/module_test.go index 229c4a69..d535337c 100644 --- a/model/module_test.go +++ b/model/module_test.go @@ -217,7 +217,7 @@ func TestWeOSApp_AddProjection(t *testing.T) { Formatter: "text", }, } - mockProjection := &ProjectionMock{ + mockProjection := &GormProjectionMock{ GetEventHandlerFunc: func() weos.EventHandler { return func(ctx context.Context, event weos.Event) error { return nil diff --git a/model/receiver_test.go b/model/receiver_test.go index 7b091d7e..978aeaea 100644 --- a/model/receiver_test.go +++ b/model/receiver_test.go @@ -173,7 +173,7 @@ func TestUpdateContentType(t *testing.T) { event := model.NewEntityEvent("update", existingBlog, existingBlog.ID, existingPayload) existingBlog.NewChange(event) - projectionMock := &ProjectionMock{ + projectionMock := &GormProjectionMock{ GetContentEntityFunc: func(ctx context3.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { return existingBlog, nil }, @@ -283,7 +283,7 @@ func TestDeleteContentType(t *testing.T) { event := model.NewEntityEvent("delete", existingBlog, existingBlog.ID, existingPayload) existingBlog.NewChange(event) - projectionMock := &ProjectionMock{ + projectionMock := &GormProjectionMock{ GetContentEntityFunc: func(ctx context3.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { return existingBlog, nil }, From 6202269ce28a4a0907f914034323d8729adef314 Mon Sep 17 00:00:00 2001 From: shaniah868 Date: Thu, 17 Feb 2022 10:25:06 -0400 Subject: [PATCH 6/6] feature: WEOS-1343 Create an operation initializer for setting middleware on operation -Added an extension for skipexpirycheck -Added code to set the skipexpirycheck -Added validation check for the openidconnecturl --- context/context.go | 1 - controllers/rest/controller_standard.go | 53 +++++++++++++------- controllers/rest/fixtures/blog-security.yaml | 1 + controllers/rest/openapi_extensions.go | 6 +++ 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/context/context.go b/context/context.go index 0286d59f..d665718b 100644 --- a/context/context.go +++ b/context/context.go @@ -31,7 +31,6 @@ const SORTS ContextKey = "_sorts" const PAYLOAD ContextKey = "_payload" const SEQUENCE_NO string = "sequence_no" const AUTHORIZATION string = "Authorization" -const OPEN_ID_CONNECT_URL string = "openIdConnectUrl" const USER_ID_EXTENSION ContextKey = "X-USER-ID" //Path initializers are run per path and can be used to configure routes that are not defined in the open api spec diff --git a/controllers/rest/controller_standard.go b/controllers/rest/controller_standard.go index 2a67b025..8cae27d3 100644 --- a/controllers/rest/controller_standard.go +++ b/controllers/rest/controller_standard.go @@ -679,28 +679,43 @@ func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, co } for _, schemes := range api.Swagger.Components.SecuritySchemes { //get the open id connect url - if openIdUrl, ok := schemes.Value.ExtensionProps.Extensions[weoscontext.OPEN_ID_CONNECT_URL]; ok { + if openIdUrl, ok := schemes.Value.ExtensionProps.Extensions[OPENIDCONNECTURLEXTENSION]; ok { err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl) if err != nil { api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err) } else { - //get the Jwk url - jwksUrl, err := GetJwkUrl(openIdConnectUrl) - if err != nil { - api.EchoInstance().Logger.Errorf("unexpected error getting the jwks url: %s", err) + //check if it is a valid open id connect url + if !strings.Contains(openIdConnectUrl, ".well-known/openid-configuration") { + api.EchoInstance().Logger.Warnf("invalid open id connect url: %s", openIdConnectUrl) } else { - //create key set and verifier - keySet := oidc.NewRemoteKeySet(context.Background(), jwksUrl) - tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ - ClientID: "", - SupportedSigningAlgs: algs, - SkipClientIDCheck: true, - SkipExpiryCheck: false, - SkipIssuerCheck: true, - Now: time.Now, - }) - verifiers = append(verifiers, tokenVerifier) + //get the Jwk url from open id connect url + jwksUrl, err := GetJwkUrl(openIdConnectUrl) + if err != nil { + api.EchoInstance().Logger.Errorf("unexpected error getting the jwks url: %s", err) + } else { + //by default skipExpiryCheck is false meaning it will not run an expiry check + skipExpiryCheck := false + //get skipexpirycheck that is an extension in the openapi spec + if expireCheck, ok := schemes.Value.ExtensionProps.Extensions[SKIPEXPIRYCHECKEXTENSION]; ok { + err := json.Unmarshal(expireCheck.(json.RawMessage), &skipExpiryCheck) + if err != nil { + api.EchoInstance().Logger.Errorf("unable to unmarshal skip expiry '%s'", err) + } + } + //create key set and verifier + keySet := oidc.NewRemoteKeySet(context.Background(), jwksUrl) + tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{ + ClientID: "", + SupportedSigningAlgs: algs, + SkipClientIDCheck: true, + SkipExpiryCheck: skipExpiryCheck, + SkipIssuerCheck: true, + Now: time.Now, + }) + verifiers = append(verifiers, tokenVerifier) + } } + } } @@ -713,13 +728,13 @@ func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, co return next(ctxt) } if len(verifiers) == 0 { - api.e.Logger.Debugf("unexpected error no verifiers were set") + api.e.Logger.Errorf("unexpected error no verifiers were set") return NewControllerError("unexpected error no verifiers were set", nil, http.StatusBadRequest) } newContext := ctxt.Request().Context() token, ok := newContext.Value(weoscontext.AUTHORIZATION).(string) if !ok || token == "" { - api.e.Logger.Debugf("no JWT token was found") + api.e.Logger.Errorf("no JWT token was found") return NewControllerError("no JWT token was found", nil, http.StatusUnauthorized) } jwtToken := strings.Replace(token, "Bearer ", "", -1) @@ -727,7 +742,7 @@ func AuthorizationMiddleware(api *RESTAPI, projection projections.Projection, co for _, tokenVerifier := range verifiers { idToken, err = tokenVerifier.Verify(newContext, jwtToken) if err != nil || idToken == nil { - api.e.Logger.Debugf(err.Error()) + api.e.Logger.Errorf(err.Error()) return NewControllerError("unexpected error verifying token", err, http.StatusUnauthorized) } } diff --git a/controllers/rest/fixtures/blog-security.yaml b/controllers/rest/fixtures/blog-security.yaml index 4fa5fb0a..2b8be9fb 100644 --- a/controllers/rest/fixtures/blog-security.yaml +++ b/controllers/rest/fixtures/blog-security.yaml @@ -32,6 +32,7 @@ components: Auth0: type: openIdConnect openIdConnectUrl: https://dev-bhjqt6zc.us.auth0.com/.well-known/openid-configuration + skipExpiryCheck: true schemas: Category: type: object diff --git a/controllers/rest/openapi_extensions.go b/controllers/rest/openapi_extensions.go index 38e8981a..a7a40bda 100644 --- a/controllers/rest/openapi_extensions.go +++ b/controllers/rest/openapi_extensions.go @@ -35,3 +35,9 @@ const CommandDispatcherExtension = "x-command-dispatcher" //EventStoreExtension set custom event store const EventStoreExtension = "x-event-store" + +//OPENIDCONNECTURLEXTENSION set the open id connect url +const OPENIDCONNECTURLEXTENSION = "openIdConnectUrl" + +//SKIPEXPIRYCHECKEXTENSION set the expiry check +const SKIPEXPIRYCHECKEXTENSION = "skipExpiryCheck"