Skip to content

Commit

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

Signed-off-by: Andrii Holovko <[email protected]>
  • Loading branch information
aholovko committed Sep 14, 2022
1 parent 94bb19a commit 16e3467
Show file tree
Hide file tree
Showing 22 changed files with 558 additions and 94 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
8 changes: 6 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 @@ -104,7 +104,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 +126,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 @@ -145,6 +146,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 +208,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 +228,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
6 changes: 4 additions & 2 deletions test/bdd/features/issuer_v1_api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
#

@all
@issuer_rest
@issuer_v1_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 Down
3 changes: 3 additions & 0 deletions test/bdd/features/verifier_profile_api.feature
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" is 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
5 changes: 5 additions & 0 deletions test/bdd/fixtures/.env
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ MONGODB_PORT=27017
# sidetree
SIDETREE_MOCK_IMAGE=ghcr.io/trustbloc-cicd/sidetree-mock
SIDETREE_MOCK_IMAGE_TAG=0.7.0-snapshot-1a17931

# OAuth authorization
HYDRA_IMAGE_TAG=v1.10.7-alpine
OATHKEEPER_IMAGE_TAG=v0.38.19-alpine
MYSQL_IMAGE_TAG=8.0.30
Loading

0 comments on commit 16e3467

Please sign in to comment.