Skip to content

Commit

Permalink
feat: add support 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 dbf7d07
Show file tree
Hide file tree
Showing 19 changed files with 503 additions and 61 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
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
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
65 changes: 65 additions & 0 deletions test/bdd/fixtures/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,71 @@ services:
networks:
- bdd_net

oathkeeper.trustbloc.local:
container_name: oathkeeper.trustbloc.local
image: oryd/oathkeeper:${OATHKEEPER_IMAGE_TAG}
ports:
- "4455:4455"
command: /bin/sh -c "cp /etc/tls/ec-cacert.pem /usr/local/share/ca-certificates/;update-ca-certificates;oathkeeper serve proxy --config /oathkeeper/config.yaml"
user: root
entrypoint: ""
environment:
- LOG_LEVEL=debug
- PORT=4455
- ISSUER_URL=https://oathkeeper-proxy.trustbloc.local
- SERVE_PROXY_TLS_KEY_PATH=/etc/tls/ec-key.pem
- SERVE_PROXY_TLS_CERT_PATH=/etc/tls/ec-pubCert.pem
- LOG_LEAK_SENSITIVE_VALUES=true
volumes:
- ./oathkeeper-config:/oathkeeper
- ./keys/tls:/etc/tls
networks:
- bdd_net

oidc-provider.example.com:
container_name: oidc-provider.example.com
image: oryd/hydra:${HYDRA_IMAGE_TAG}
ports:
- "4444:4444"
- "4445:4445"
command: /bin/sh -c "sleep 20 && hydra migrate sql --read-from-env --yes; (sleep 10; tmp/hydra_configure.sh)& hydra serve all"
entrypoint: ""
environment:
- DSN=mysql://thirdpartyoidc:thirdpartyoidc-secret-pw@tcp(mysql:3306)/thirdpartyoidc?max_conns=20&max_idle_conns=4
- URLS_SELF_ISSUER=https://oidc-provider.example.com:4444/
- SECRETS_SYSTEM=testSecretsSystem
- OIDC_SUBJECT_TYPES_SUPPORTED=public
- OIDC_SUBJECT_TYPE_PAIRWISE_SALT=testSecretsSystem
- SERVE_TLS_KEY_PATH=/etc/tls/ec-key.pem
- SERVE_TLS_CERT_PATH=/etc/tls/ec-pubCert.pem
- SERVE_PUBLIC_PORT=4444
- SERVE_ADMIN_PORT=4445
- LOG_LEAK_SENSITIVE_VALUES=true
restart: unless-stopped
volumes:
- ./keys/tls:/etc/tls
- ./hydra-config/hydra_configure.sh:/tmp/hydra_configure.sh
depends_on:
- mysql
networks:
- bdd_net

mysql:
container_name: mysql
image: mysql:${MYSQL_IMAGE_TAG}
ports:
- "3306:3306"
restart: always
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: secret
logging:
driver: "none"
volumes:
- ./mysql-config:/docker-entrypoint-initdb.d
networks:
- bdd_net

networks:
bdd_net:
driver: bridge
20 changes: 20 additions & 0 deletions test/bdd/fixtures/hydra-config/hydra_configure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/sh
#
# Copyright SecureKey Technologies Inc. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#

echo "Creating client for org1..."
# will use --skip-tls-verify because hydra doesn't trust self-signed certificate
# remove it when using real certificate
hydra clients create \
--endpoint https://oidc-provider.example.com:4445 \
--id org1 \
--secret org1-secret \
--grant-types client_credentials \
--response-types token,code \
--scope org_admin \
--skip-tls-verify

echo "Finished creating client for org1"
Loading

0 comments on commit dbf7d07

Please sign in to comment.