Skip to content

Commit

Permalink
feat: OIDC client credential authorization
Browse files Browse the repository at this point in the history
Closes trustbloc#751

Signed-off-by: Andrii Holovko <[email protected]>
  • Loading branch information
aholovko committed Sep 14, 2022
1 parent 94bb19a commit b2c5dce
Show file tree
Hide file tree
Showing 22 changed files with 604 additions and 95 deletions.
9 changes: 7 additions & 2 deletions cmd/vc-rest/startcmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
verifierops "github.com/trustbloc/vcs/pkg/restapi/v0.1/verifier/operation"
"github.com/trustbloc/vcs/pkg/restapi/v1/healthcheck"
issuerv1 "github.com/trustbloc/vcs/pkg/restapi/v1/issuer"
"github.com/trustbloc/vcs/pkg/restapi/v1/mw"
verifierv1 "github.com/trustbloc/vcs/pkg/restapi/v1/verifier"
"github.com/trustbloc/vcs/pkg/storage/mongodb"
"github.com/trustbloc/vcs/pkg/storage/mongodb/issuerstore"
Expand Down Expand Up @@ -130,6 +131,10 @@ func buildEchoHandler(conf *Configuration) (*echo.Echo, error) {
e.Use(echomw.Logger())
e.Use(echomw.Recover())

if conf.StartupParameters.token != "" {
e.Use(mw.APIKeyAuth(conf.StartupParameters.token))
}

swagger, err := spec.GetSwagger()
if err != nil {
return nil, fmt.Errorf("failed to get openapi spec: %w", err)
Expand Down Expand Up @@ -161,11 +166,11 @@ func buildEchoHandler(conf *Configuration) (*echo.Echo, error) {
issuerProfileStore := issuerstore.NewProfileStore(mongodbClient)
issuerProfileSvc := issuersvc.NewProfileService(&issuersvc.ServiceConfig{
ProfileStore: issuerProfileStore,
DIDCreator: did.NewCreator(&did.CreatorConfig{
DIDCreator: did.NewCreator(&did.CreatorConfig{
VDR: conf.VDR,
DIDAnchorOrigin: conf.StartupParameters.didAnchorOrigin,
}),
KMSRegistry: kmsRegistry,
KMSRegistry: kmsRegistry,
})

issuerv1.RegisterHandlers(e, issuerv1.NewController(issuerProfileSvc, kmsRegistry))
Expand Down
2 changes: 1 addition & 1 deletion pkg/restapi/v1/issuer/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func createContext(orgID string) echo.Context {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
if orgID != "" {
req.Header.Set("Authorization", "Bearer "+orgID)
req.Header.Set("X-User", orgID)
}

rec := httptest.NewRecorder()
Expand Down
35 changes: 35 additions & 0 deletions pkg/restapi/v1/mw/api_key_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package mw

import (
"crypto/subtle"
"net/http"

"github.com/labstack/echo/v4"
)

const (
header = "X-API-Key"
)

// APIKeyAuth returns a middleware that authenticates requests using the API key from X-API-Key header.
func APIKeyAuth(apiKey string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
apiKeyHeader := c.Request().Header.Get(header)
if subtle.ConstantTimeCompare([]byte(apiKeyHeader), []byte(apiKey)) != 1 {
return &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: "Unauthorized",
}
}

return next(c)
}
}
}
63 changes: 63 additions & 0 deletions pkg/restapi/v1/mw/api_key_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package mw_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"

"github.com/trustbloc/vcs/pkg/restapi/v1/mw"
)

func TestApiKeyAuth(t *testing.T) {
t.Run("Success", func(t *testing.T) {
handlerCalled := false
handler := func(c echo.Context) error {
handlerCalled = true
return c.String(http.StatusOK, "test")
}

middlewareChain := mw.APIKeyAuth("test-api-key")(handler)

e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("X-API-Key", "test-api-key")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

err := middlewareChain(c)

require.NoError(t, err)
require.True(t, handlerCalled)
})

t.Run("401 Unauthorized", func(t *testing.T) {
handlerCalled := false
handler := func(c echo.Context) error {
handlerCalled = true
return c.String(http.StatusOK, "test")
}

middlewareChain := mw.APIKeyAuth("test-api-key")(handler)

e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("X-API-Key", "invalid-api-key")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

err := middlewareChain(c)

require.Error(t, err)
require.Contains(t, err.Error(), "Unauthorized")
require.False(t, handlerCalled)
})
}
12 changes: 6 additions & 6 deletions pkg/restapi/v1/util/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ package util

import (
"fmt"
"strings"

"github.com/labstack/echo/v4"

"github.com/trustbloc/vcs/pkg/restapi/resterr"
)

const (
userHeader = "X-User"
)

func GetOrgIDFromOIDC(ctx echo.Context) (string, error) {
// TODO: resolve orgID from auth token
authHeader := ctx.Request().Header.Get("Authorization")
if authHeader == "" || !strings.Contains(authHeader, "Bearer") {
orgID := ctx.Request().Header.Get(userHeader)
if orgID == "" {
return "", resterr.NewUnauthorizedError(fmt.Errorf("missing authorization"))
}

orgID := authHeader[len("Bearer "):] // for now assume that token is just plain orgID

return orgID, nil
}
24 changes: 16 additions & 8 deletions pkg/restapi/v1/verifier/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/labstack/echo/v4"

"github.com/trustbloc/vcs/pkg/restapi/resterr"
"github.com/trustbloc/vcs/pkg/restapi/v1/util"
"github.com/trustbloc/vcs/pkg/verifier"
)

Expand Down Expand Up @@ -48,14 +49,11 @@ func NewController(profileSvc profileService) *Controller {
// GetVerifierProfiles gets all verifier profiles for organization.
// GET /verifier/profiles.
func (c *Controller) GetVerifierProfiles(ctx echo.Context) error {
// TODO: resolve orgID from auth token
authHeader := ctx.Request().Header.Get("Authorization")
if authHeader == "" || !strings.Contains(authHeader, "Bearer") {
return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization")
orgID, err := util.GetOrgIDFromOIDC(ctx)
if err != nil {
return err
}

orgID := authHeader[len("Bearer "):] // for now assume that token is just plain orgID

profiles, err := c.profileSvc.GetAllProfiles(orgID)
if err != nil {
return fmt.Errorf("failed to get verifier profiles: %w", err)
Expand All @@ -73,12 +71,22 @@ func (c *Controller) GetVerifierProfiles(ctx echo.Context) error {
// PostVerifierProfiles creates a new verifier profile.
// POST /verifier/profiles.
func (c *Controller) PostVerifierProfiles(ctx echo.Context) error {
orgID, err := util.GetOrgIDFromOIDC(ctx)
if err != nil {
return err
}

var body CreateVerifierProfileData

if err := ctx.Bind(&body); err != nil {
if err = ctx.Bind(&body); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}

if body.OrganizationID != orgID {
return resterr.NewValidationError(resterr.InvalidValue, "organizationID",
fmt.Errorf("org id mismatch (want %q, got %q)", orgID, body.OrganizationID))
}

createdProfile, err := c.profileSvc.Create(mapCreateVerifierProfileData(&body))
if err != nil {
return fmt.Errorf("failed to create verifier profile: %w", err)
Expand Down
66 changes: 64 additions & 2 deletions pkg/restapi/v1/verifier/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestController_GetVerifierProfiles(t *testing.T) {

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("Authorization", "Bearer org1")
req.Header.Set("X-User", "org1")

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand All @@ -96,6 +96,25 @@ func TestController_GetVerifierProfiles(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code)
})

t.Run("missing authorization", func(t *testing.T) {
mockProfileSvc := NewMockProfileService(gomock.NewController(t))
mockProfileSvc.EXPECT().GetAllProfiles("org1").Times(0)

e := echo.New()

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

controller := verifier.NewController(mockProfileSvc)

err := controller.GetVerifierProfiles(c)
require.Error(t, err)
require.Contains(t, err.Error(), "missing authorization")
})

t.Run("error from profile service", func(t *testing.T) {
mockProfileSvc := NewMockProfileService(gomock.NewController(t))
mockProfileSvc.EXPECT().GetAllProfiles("org1").Times(1).Return(nil, errors.New("get all profiles error"))
Expand All @@ -104,7 +123,7 @@ func TestController_GetVerifierProfiles(t *testing.T) {

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("Authorization", "Bearer org1")
req.Header.Set("X-User", "org1")

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand All @@ -126,6 +145,7 @@ func TestController_PostVerifierProfiles(t *testing.T) {

req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(createProfileData))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("X-User", "org1")

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand All @@ -137,6 +157,45 @@ func TestController_PostVerifierProfiles(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code)
})

t.Run("missing authorization", func(t *testing.T) {
mockProfileSvc := NewMockProfileService(gomock.NewController(t))
mockProfileSvc.EXPECT().Create(gomock.Any()).Times(0)

e := echo.New()

req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(createProfileData))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

controller := verifier.NewController(mockProfileSvc)

err := controller.PostVerifierProfiles(c)
require.Error(t, err)
require.Contains(t, err.Error(), "missing authorization")
})

t.Run("invalid org id", func(t *testing.T) {
mockProfileSvc := NewMockProfileService(gomock.NewController(t))
mockProfileSvc.EXPECT().Create(gomock.Any()).Times(0)

e := echo.New()

req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(createProfileData))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("X-User", "invalid")

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

controller := verifier.NewController(mockProfileSvc)

err := controller.PostVerifierProfiles(c)
require.Error(t, err)
require.Contains(t, err.Error(), "org id mismatch")
})

t.Run("error from profile service", func(t *testing.T) {
mockProfileSvc := NewMockProfileService(gomock.NewController(t))
mockProfileSvc.EXPECT().Create(gomock.Any()).Times(1).Return(nil, errors.New("create profile error"))
Expand All @@ -145,6 +204,7 @@ func TestController_PostVerifierProfiles(t *testing.T) {

req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(createProfileData))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("X-User", "org1")

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand Down Expand Up @@ -206,6 +266,7 @@ func TestController_GetVerifierProfilesProfileID(t *testing.T) {

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("X-User", "org1")

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand All @@ -225,6 +286,7 @@ func TestController_GetVerifierProfilesProfileID(t *testing.T) {

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("X-User", "org1")

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand Down
2 changes: 1 addition & 1 deletion pkg/restapi/v1/verifier/testdata/create_profile_data.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "test profile",
"organizationID": "orgID",
"organizationID": "org1",
"url": "https://test-verifier.com",
"checks": {
"credential": {
Expand Down
1 change: 1 addition & 0 deletions scripts/generate_test_keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ subjectAltName = @alt_names
DNS.1 = localhost
DNS.2 = testnet.orb.local
DNS.4 = file-server.trustbloc.local
DNS.5 = oidc-provider.example.com
" >> "$tmp"

#create CA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
#

@all
@issuer_rest
@issuer_profile_rest
Feature: Issuer VC REST API
Background:
Given "Charlie" has been authorized with client id "National Bank" and secret "bank-secret" to use vcs

@issuerProfileRecreationV1
Scenario: Delete and recreate issuer profile
Given "Charlie" sends request to create an issuer profile with the organization "National Bank"
And "Charlie" deletes the issuer profile
Then "Charlie" can recreate the issuer profile with the organization "National Bank"


@issuerProfileUpdateV1
Scenario: Create and update issuer profile
Given "Charlie" sends request to create an issuer profile with the organization "National Bank"
Expand All @@ -23,4 +25,4 @@ Feature: Issuer VC REST API
Scenario: Create and update issuer profile
Given "Charlie" sends request to create an issuer profile with the organization "National Bank"
And "Charlie" deactivates the issuer profile
And "Charlie" activates the issuer profile
And "Charlie" activates the issuer profile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
@all
@verifier_profile_rest
Feature: Verifier Profile Management REST API
Background:
Given organization "org1" has been authorized using client id "org1" and secret "org1-secret"

Scenario: Create a new verifier profile
When organization "org1" creates a verifier profile with data from "verifier_profile_create.json"
Then verifier profile is created
Expand Down
Loading

0 comments on commit b2c5dce

Please sign in to comment.