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 15, 2022
1 parent 94bb19a commit ef1700d
Show file tree
Hide file tree
Showing 22 changed files with 720 additions and 132 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)
})
}
16 changes: 8 additions & 8 deletions pkg/restapi/v1/util/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@ SPDX-License-Identifier: Apache-2.0
package util

import (
"fmt"
"strings"
"errors"

"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") {
return "", resterr.NewUnauthorizedError(fmt.Errorf("missing authorization"))
orgID := ctx.Request().Header.Get(userHeader)
if orgID == "" {
return "", resterr.NewUnauthorizedError(errors.New("missing authorization"))
}

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

return orgID, nil
}
113 changes: 83 additions & 30 deletions pkg/restapi/v1/verifier/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ 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"
)

const (
verifierProfileSvcComponent = "verifier.ProfileService"
)

var _ ServerInterface = (*Controller)(nil) // make sure Controller implements ServerInterface

type profileService interface {
Expand Down Expand Up @@ -48,17 +53,14 @@ 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)
return fmt.Errorf("get all profiles: %w", err)
}

var verifierProfiles []*VerifierProfile
Expand All @@ -73,25 +75,40 @@ 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 {
return echo.NewHTTPError(http.StatusBadRequest, err)
if err = ctx.Bind(&body); err != nil {
return resterr.NewValidationError(resterr.InvalidValue, "requestBody", 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)
return fmt.Errorf("create profile: %w", err)
}

return ctx.JSON(http.StatusOK, mapProfile(createdProfile))
}

// DeleteVerifierProfilesProfileID deletes profile from VCS storage.
// DELETE /verifier/profiles/{profileID}.
func (c *Controller) DeleteVerifierProfilesProfileID(_ echo.Context, profileID string) error {
if err := c.profileSvc.Delete(profileID); err != nil {
return fmt.Errorf("failed to delete verifier profile: %w", err)
func (c *Controller) DeleteVerifierProfilesProfileID(ctx echo.Context, profileID string) error {
profile, err := c.accessProfile(ctx, profileID)
if err != nil {
return err
}

if err = c.profileSvc.Delete(profile.ID); err != nil {
return fmt.Errorf("delete profile: %w", err)
}

return nil
Expand All @@ -100,13 +117,9 @@ func (c *Controller) DeleteVerifierProfilesProfileID(_ echo.Context, profileID s
// GetVerifierProfilesProfileID gets profile by ID.
// GET /verifier/profiles/{profileID}.
func (c *Controller) GetVerifierProfilesProfileID(ctx echo.Context, profileID string) error {
profile, err := c.profileSvc.GetProfile(profileID)
profile, err := c.accessProfile(ctx, profileID)
if err != nil {
if errors.Is(err, verifier.ErrProfileNotFound) {
return echo.NewHTTPError(http.StatusNotFound, err)
}

return fmt.Errorf("failed to get verifier profile: %w", err)
return err
}

return ctx.JSON(http.StatusOK, mapProfile(profile))
Expand All @@ -115,43 +128,83 @@ func (c *Controller) GetVerifierProfilesProfileID(ctx echo.Context, profileID st
// PutVerifierProfilesProfileID updates profile.
// PUT /verifier/profiles/{profileID}.
func (c *Controller) PutVerifierProfilesProfileID(ctx echo.Context, profileID string) error {
profile, err := c.accessProfile(ctx, profileID)
if err != nil {
return err
}

var body UpdateVerifierProfileData

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

profileUpdate := mapUpdateVerifierProfileData(&body)
profileUpdate.ID = profileID
profileUpdate.ID = profile.ID

updatedProfile, err := c.profileSvc.Update(profileUpdate)
if err != nil {
return fmt.Errorf("failed to update verifier profile: %w", err)
return fmt.Errorf("update profile: %w", err)
}

return ctx.JSON(http.StatusOK, mapProfile(updatedProfile))
}

// PostVerifierProfilesProfileIDActivate activates profile.
// POST /verifier/profiles/{profileID}/activate.
func (c *Controller) PostVerifierProfilesProfileIDActivate(_ echo.Context, profileID string) error {
if err := c.profileSvc.ActivateProfile(profileID); err != nil {
return fmt.Errorf("failed to activate verifier profile: %w", err)
func (c *Controller) PostVerifierProfilesProfileIDActivate(ctx echo.Context, profileID string) error {
profile, err := c.accessProfile(ctx, profileID)
if err != nil {
return err
}

if err = c.profileSvc.ActivateProfile(profile.ID); err != nil {
return fmt.Errorf("activate profile: %w", err)
}

return nil
}

// PostVerifierProfilesProfileIDDeactivate deactivates profile.
// POST /verifier/profiles/{profileID}/deactivate.
func (c *Controller) PostVerifierProfilesProfileIDDeactivate(_ echo.Context, profileID string) error {
if err := c.profileSvc.DeactivateProfile(profileID); err != nil {
return fmt.Errorf("failed to deactivate verifier profile: %w", err)
func (c *Controller) PostVerifierProfilesProfileIDDeactivate(ctx echo.Context, profileID string) error {
profile, err := c.accessProfile(ctx, profileID)
if err != nil {
return err
}

if err = c.profileSvc.DeactivateProfile(profile.ID); err != nil {
return fmt.Errorf("deactivate profile: %w", err)
}

return nil
}

func (c *Controller) accessProfile(ctx echo.Context, profileID string) (*verifier.Profile, error) {
orgID, err := util.GetOrgIDFromOIDC(ctx)
if err != nil {
return nil, err
}

profile, err := c.profileSvc.GetProfile(profileID)
if err != nil {
if errors.Is(err, verifier.ErrProfileNotFound) {
return nil, resterr.NewValidationError(resterr.DoesntExist, "profile",
fmt.Errorf("no profile with id %s", profileID))
}

return nil, resterr.NewSystemError(verifierProfileSvcComponent, "GetProfile", err)
}

// block access to profiles of other organizations
if profile.OrganizationID != orgID {
return nil, resterr.NewValidationError(resterr.DoesntExist, "profile",
fmt.Errorf("no profile with id %s", profileID))
}

return profile, nil
}

func mapCreateVerifierProfileData(data *CreateVerifierProfileData) *verifier.Profile {
profile := &verifier.Profile{
Name: data.Name,
Expand Down
Loading

0 comments on commit ef1700d

Please sign in to comment.