From d4f682628ac438d583060244a0a1ac35b331f3fb Mon Sep 17 00:00:00 2001 From: Yevgen Pukhta Date: Mon, 26 Sep 2022 16:16:00 +0300 Subject: [PATCH] chore: remove OIDC server related endpoints Signed-off-by: Yevgen Pukhta --- cmd/auth-rest/startcmd/start.go | 29 +- component/gnap/as/client.go | 2 +- component/gnap/as/client_test.go | 2 +- component/gnap/rs/client.go | 2 +- component/gnap/rs/client_test.go | 2 +- pkg/restapi/controller.go | 14 +- pkg/restapi/controller_test.go | 64 +- pkg/restapi/gnap/dependencies.go | 102 - pkg/restapi/gnap/models.go | 23 - pkg/restapi/gnap/operations.go | 934 ------- pkg/restapi/gnap/operations_test.go | 1694 ------------ pkg/restapi/operation/dependencies.go | 4 - pkg/restapi/operation/models.go | 24 - pkg/restapi/operation/operations.go | 978 +++---- pkg/restapi/operation/operations_test.go | 2343 +++++++---------- test/bdd/bddtests_test.go | 4 - test/bdd/features/bootstrap_data.feature | 20 - test/bdd/features/login.feature | 18 - test/bdd/features/secrets.feature | 21 - .../auth-rest/oidc-config/providers.yaml | 12 - test/bdd/pkg/bootstrap/steps.go | 20 +- test/bdd/pkg/gnap/steps.go | 4 +- test/bdd/pkg/login/mock_wallet.go | 78 +- test/bdd/pkg/login/steps.go | 163 +- test/bdd/pkg/secrets/mock_key_server.go | 92 - test/bdd/pkg/secrets/steps.go | 102 - 26 files changed, 1325 insertions(+), 5426 deletions(-) delete mode 100644 pkg/restapi/gnap/dependencies.go delete mode 100644 pkg/restapi/gnap/models.go delete mode 100644 pkg/restapi/gnap/operations.go delete mode 100644 pkg/restapi/gnap/operations_test.go delete mode 100644 test/bdd/features/bootstrap_data.feature delete mode 100644 test/bdd/features/login.feature delete mode 100644 test/bdd/features/secrets.feature delete mode 100644 test/bdd/pkg/secrets/mock_key_server.go delete mode 100644 test/bdd/pkg/secrets/steps.go diff --git a/cmd/auth-rest/startcmd/start.go b/cmd/auth-rest/startcmd/start.go index a1f74d6..19febe5 100644 --- a/cmd/auth-rest/startcmd/start.go +++ b/cmd/auth-rest/startcmd/start.go @@ -31,9 +31,7 @@ import ( "github.com/trustbloc/auth/pkg/gnap/accesspolicy" "github.com/trustbloc/auth/pkg/gnap/interact/redirect" "github.com/trustbloc/auth/pkg/restapi" - "github.com/trustbloc/auth/pkg/restapi/common/hydra" oidcmodel "github.com/trustbloc/auth/pkg/restapi/common/oidc" - "github.com/trustbloc/auth/pkg/restapi/gnap" "github.com/trustbloc/auth/pkg/restapi/operation" ) @@ -484,36 +482,13 @@ func startAuthService(parameters *authRestParameters, srv server) error { // TODO: support creating multiple GNAP user interaction handlers interact, err := redirect.New(&redirect.Config{ StoreProvider: provider, - InteractBasePath: parameters.externalURL + gnap.InteractPath, + InteractBasePath: parameters.externalURL + operation.InteractPath, }) if err != nil { return fmt.Errorf("initializing GNAP interaction handler: %w", err) } svc, err := restapi.New(&operation.Config{ - TransientStoreProvider: provider, - StoreProvider: provider, - Hydra: hydra.NewClient(parameters.oidcParams.hydraURL, rootCAs), - OIDC: &oidcmodel.Config{ - CallbackURL: parameters.oidcParams.callbackURL, - Providers: parameters.oidcParams.providers, - }, - BootstrapConfig: &operation.BootstrapConfig{ - DocumentSDSVaultURL: parameters.bootstrapParams.documentSDSVaultURL, - KeySDSVaultURL: parameters.bootstrapParams.keySDSVaultURL, - AuthZKeyServerURL: parameters.bootstrapParams.authZKeyServerURL, - OpsKeyServerURL: parameters.bootstrapParams.opsKeyServerURL, - }, - DeviceRootCerts: parameters.devicecertParams.caCerts, - TLSConfig: &tls.Config{RootCAs: rootCAs}, //nolint:gosec - UIEndpoint: uiEndpoint, - Cookies: &operation.CookieConfig{ - AuthKey: parameters.keys.sessionCookieAuthKey, - EncKey: parameters.keys.sessionCookieEncKey, - }, - StartupTimeout: parameters.startupTimeout, - SecretsToken: parameters.secretsAPIToken, - }, &gnap.Config{ StoreProvider: provider, BaseURL: parameters.externalURL, AccessPolicyConfig: gnapAPConfig, @@ -525,7 +500,7 @@ func startAuthService(parameters *authRestParameters, srv server) error { CallbackURL: parameters.oidcParams.callbackURL, Providers: parameters.oidcParams.providers, }, - BootstrapConfig: &gnap.BootstrapConfig{ + BootstrapConfig: &operation.BootstrapConfig{ DocumentSDSVaultURL: parameters.bootstrapParams.documentSDSVaultURL, KeySDSVaultURL: parameters.bootstrapParams.keySDSVaultURL, OpsKeyServerURL: parameters.bootstrapParams.opsKeyServerURL, diff --git a/component/gnap/as/client.go b/component/gnap/as/client.go index 683323e..2d4940c 100644 --- a/component/gnap/as/client.go +++ b/component/gnap/as/client.go @@ -21,7 +21,7 @@ import ( "github.com/trustbloc/edge-core/pkg/log" _ "golang.org/x/crypto/sha3" // nolint:gci // init sha3 hash. - gnaprest "github.com/trustbloc/auth/pkg/restapi/gnap" + gnaprest "github.com/trustbloc/auth/pkg/restapi/operation" "github.com/trustbloc/auth/spi/gnap" ) diff --git a/component/gnap/as/client_test.go b/component/gnap/as/client_test.go index 3debd40..03e182a 100644 --- a/component/gnap/as/client_test.go +++ b/component/gnap/as/client_test.go @@ -29,7 +29,7 @@ import ( "github.com/square/go-jose/v3" "github.com/stretchr/testify/require" - gnaprest "github.com/trustbloc/auth/pkg/restapi/gnap" + gnaprest "github.com/trustbloc/auth/pkg/restapi/operation" "github.com/trustbloc/auth/spi/gnap" ) diff --git a/component/gnap/rs/client.go b/component/gnap/rs/client.go index 8224d62..9a19541 100644 --- a/component/gnap/rs/client.go +++ b/component/gnap/rs/client.go @@ -17,7 +17,7 @@ import ( "github.com/trustbloc/edge-core/pkg/log" - gnaprest "github.com/trustbloc/auth/pkg/restapi/gnap" + gnaprest "github.com/trustbloc/auth/pkg/restapi/operation" "github.com/trustbloc/auth/spi/gnap" ) diff --git a/component/gnap/rs/client_test.go b/component/gnap/rs/client_test.go index b97a6e2..586fa01 100644 --- a/component/gnap/rs/client_test.go +++ b/component/gnap/rs/client_test.go @@ -30,7 +30,7 @@ import ( "github.com/square/go-jose/v3" "github.com/stretchr/testify/require" - gnaprest "github.com/trustbloc/auth/pkg/restapi/gnap" + gnaprest "github.com/trustbloc/auth/pkg/restapi/operation" "github.com/trustbloc/auth/spi/gnap" ) diff --git a/pkg/restapi/controller.go b/pkg/restapi/controller.go index 2762977..05e2621 100644 --- a/pkg/restapi/controller.go +++ b/pkg/restapi/controller.go @@ -10,28 +10,18 @@ import ( "fmt" "github.com/trustbloc/auth/pkg/restapi/common" - "github.com/trustbloc/auth/pkg/restapi/gnap" "github.com/trustbloc/auth/pkg/restapi/operation" ) // New returns new controller instance. -func New(config *operation.Config, gnapConfig *gnap.Config) (*Controller, error) { +func New(gnapConfig *operation.Config) (*Controller, error) { var allHandlers []common.Handler - rpService, err := operation.New(config) - if err != nil { - return nil, fmt.Errorf("failed to initialize auth-rest operations: %w", err) - } - - allHandlers = append(allHandlers, rpService.GetRESTHandlers()...) - - gnapService, err := gnap.New(gnapConfig) + gnapService, err := operation.New(gnapConfig) if err != nil { return nil, fmt.Errorf("failed to initialize auth-rest gnap operations: %w", err) } - rpService.SetIntrospectHandler(gnapService.InternalIntrospectHandler()) - allHandlers = append(allHandlers, gnapService.GetRESTHandlers()...) return &Controller{handlers: allHandlers}, nil diff --git a/pkg/restapi/controller_test.go b/pkg/restapi/controller_test.go index a396400..f6db334 100644 --- a/pkg/restapi/controller_test.go +++ b/pkg/restapi/controller_test.go @@ -7,14 +7,11 @@ SPDX-License-Identifier: Apache-2.0 package restapi import ( - "crypto/aes" - "crypto/rand" "errors" "testing" "github.com/google/uuid" "github.com/hyperledger/aries-framework-go/component/storageutil/mem" - mockstore "github.com/hyperledger/aries-framework-go/pkg/mock/storage" "github.com/stretchr/testify/require" "github.com/trustbloc/auth/pkg/gnap/accesspolicy" @@ -22,47 +19,31 @@ import ( "github.com/trustbloc/auth/pkg/internal/common/mockoidc" "github.com/trustbloc/auth/pkg/internal/common/mockstorage" oidcmodel "github.com/trustbloc/auth/pkg/restapi/common/oidc" - "github.com/trustbloc/auth/pkg/restapi/gnap" "github.com/trustbloc/auth/pkg/restapi/operation" ) func TestController_New(t *testing.T) { t.Run("success", func(t *testing.T) { - config := config(t) - - controller, err := New(config, gnapConfig(t)) + controller, err := New(gnapConfig(t)) require.NoError(t, err) require.NotNil(t, controller) }) - t.Run("error if operations cannot start", func(t *testing.T) { - conf := config(t) - conf.TransientStoreProvider = &mockstore.MockStoreProvider{ - ErrOpenStoreHandle: errors.New("test"), - } - - _, err := New(conf, gnapConfig(t)) - require.Error(t, err) - }) - t.Run("error if gnap operations cannot start", func(t *testing.T) { - conf := config(t) gconf := gnapConfig(t) expectErr := errors.New("expected error") gconf.StoreProvider = &mockstorage.Provider{ErrOpenStoreHandle: expectErr} - _, err := New(conf, gconf) + _, err := New(gconf) require.Error(t, err) require.ErrorIs(t, err, expectErr) }) } func TestController_GetOperations(t *testing.T) { - config := config(t) - - controller, err := New(config, gnapConfig(t)) + controller, err := New(gnapConfig(t)) require.NoError(t, err) require.NotNil(t, controller) @@ -70,38 +51,12 @@ func TestController_GetOperations(t *testing.T) { require.NotEmpty(t, ops) } -func config(t *testing.T) *operation.Config { +func gnapConfig(t *testing.T) *operation.Config { t.Helper() path := mockoidc.StartProvider(t) return &operation.Config{ - OIDC: &oidcmodel.Config{ - CallbackURL: "https://example.com/callback", - Providers: map[string]*oidcmodel.ProviderConfig{ - "test": { - URL: path, - ClientID: uuid.New().String(), - ClientSecret: uuid.New().String(), - }, - }, - }, - TransientStoreProvider: mem.NewProvider(), - StoreProvider: mem.NewProvider(), - Cookies: &operation.CookieConfig{ - AuthKey: cookieKey(t), - EncKey: cookieKey(t), - }, - StartupTimeout: 1, - } -} - -func gnapConfig(t *testing.T) *gnap.Config { - t.Helper() - - path := mockoidc.StartProvider(t) - - return &gnap.Config{ StoreProvider: mem.NewProvider(), AccessPolicyConfig: &accesspolicy.Config{}, BaseURL: "example.com", @@ -120,14 +75,3 @@ func gnapConfig(t *testing.T) *gnap.Config { TransientStoreProvider: mem.NewProvider(), } } - -func cookieKey(t *testing.T) []byte { - t.Helper() - - key := make([]byte, aes.BlockSize) - - _, err := rand.Read(key) - require.NoError(t, err) - - return key -} diff --git a/pkg/restapi/gnap/dependencies.go b/pkg/restapi/gnap/dependencies.go deleted file mode 100644 index d1c1a8e..0000000 --- a/pkg/restapi/gnap/dependencies.go +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package gnap - -import ( - "context" - "net/http" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/ory/hydra-client-go/client/admin" - "golang.org/x/oauth2" -) - -type oidcProvider interface { - Name() string - OAuth2Config(scope ...string) oauth2Config - Endpoint() oauth2.Endpoint - Verify(context.Context, string) (idToken, error) -} - -type oidcProviderImpl struct { - name string - clientID string - clientSecret string - callback string - skipIssuerCheck bool - op *oidc.Provider - httpClient *http.Client -} - -func (o *oidcProviderImpl) Name() string { - return o.name -} - -func (o *oidcProviderImpl) OAuth2Config(scope ...string) oauth2Config { - return &oauth2ConfigImpl{ - client: o.httpClient, - oc: &oauth2.Config{ - ClientID: o.clientID, - ClientSecret: o.clientSecret, - Endpoint: o.op.Endpoint(), - RedirectURL: o.callback, - Scopes: scope, - }, - } -} - -func (o *oidcProviderImpl) Endpoint() oauth2.Endpoint { - return o.op.Endpoint() -} - -func (o *oidcProviderImpl) Verify(ctx context.Context, rawToken string) (idToken, error) { - return o.op.Verifier(&oidc.Config{ClientID: o.clientID, SkipIssuerCheck: o.skipIssuerCheck}).Verify(ctx, rawToken) -} - -type idToken interface { - Claims(interface{}) error -} - -type oauth2Config interface { - AuthCodeURL(string, ...oauth2.AuthCodeOption) string - Exchange(context.Context, string, ...oauth2.AuthCodeOption) (oauth2Token, error) -} - -type oauth2ConfigImpl struct { - oc *oauth2.Config - client *http.Client -} - -func (o *oauth2ConfigImpl) AuthCodeURL(state string, options ...oauth2.AuthCodeOption) string { - return o.oc.AuthCodeURL(state, options...) -} - -func (o *oauth2ConfigImpl) Exchange( - ctx context.Context, code string, options ...oauth2.AuthCodeOption) (oauth2Token, error) { - return o.oc.Exchange( - context.WithValue(ctx, oauth2.HTTPClient, o.client), - code, - options..., - ) -} - -type oauth2Token interface { - Extra(string) interface{} -} - -// Hydra is the client used to interface with the Hydra service. -type Hydra interface { - GetLoginRequest(params *admin.GetLoginRequestParams, opts ...admin.ClientOption) (*admin.GetLoginRequestOK, error) - AcceptLoginRequest(params *admin.AcceptLoginRequestParams, - opts ...admin.ClientOption) (*admin.AcceptLoginRequestOK, error) - GetConsentRequest(params *admin.GetConsentRequestParams, - opts ...admin.ClientOption) (*admin.GetConsentRequestOK, error) - AcceptConsentRequest(params *admin.AcceptConsentRequestParams, - opts ...admin.ClientOption) (*admin.AcceptConsentRequestOK, error) - IntrospectOAuth2Token(params *admin.IntrospectOAuth2TokenParams, - opts ...admin.ClientOption) (*admin.IntrospectOAuth2TokenOK, error) -} diff --git a/pkg/restapi/gnap/models.go b/pkg/restapi/gnap/models.go deleted file mode 100644 index 53d6af6..0000000 --- a/pkg/restapi/gnap/models.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package gnap - -type oidcClaims struct { - Sub string `json:"sub"` -} - -type authProviders struct { - Providers []authProvider `json:"authProviders"` -} - -type authProvider struct { - ID string `json:"id"` - Name string `json:"name"` - SignUpIconURL map[string]string `json:"signUpIconUrl"` - SignInIconURL map[string]string `json:"signInIconUrl"` - Order int `json:"order"` -} diff --git a/pkg/restapi/gnap/operations.go b/pkg/restapi/gnap/operations.go deleted file mode 100644 index 6296e40..0000000 --- a/pkg/restapi/gnap/operations.go +++ /dev/null @@ -1,934 +0,0 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package gnap - -import ( - "bytes" - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "html/template" - "io/ioutil" - "net/http" - "net/url" - "strings" - "sync" - "time" - - "github.com/cenkalti/backoff" - "github.com/coreos/go-oidc/v3/oidc" - "github.com/google/uuid" - "github.com/hyperledger/aries-framework-go/pkg/common/log" - "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" - "github.com/hyperledger/aries-framework-go/spi/storage" - "github.com/square/go-jose/v3" - "golang.org/x/oauth2" - - "github.com/trustbloc/auth/pkg/bootstrap/user" - "github.com/trustbloc/auth/pkg/gnap/accesspolicy" - "github.com/trustbloc/auth/pkg/gnap/api" - "github.com/trustbloc/auth/pkg/gnap/authhandler" - "github.com/trustbloc/auth/pkg/internal/common/support" - "github.com/trustbloc/auth/pkg/restapi/common" - oidcmodel "github.com/trustbloc/auth/pkg/restapi/common/oidc" - "github.com/trustbloc/auth/spi/gnap" - "github.com/trustbloc/auth/spi/gnap/proof/httpsig" -) - -var logger = log.New("auth-restapi") //nolint:gochecknoglobals - -const ( - gnapBasePath = "/gnap" - // AuthRequestPath endpoint for GNAP authorization request. - AuthRequestPath = gnapBasePath + "/auth" - // AuthContinuePath endpoint for GNAP authorization continuation. - AuthContinuePath = gnapBasePath + "/continue" - // AuthIntrospectPath endpoint for GNAP token introspection. - AuthIntrospectPath = gnapBasePath + "/introspect" - // InteractPath endpoint for GNAP interact. - InteractPath = gnapBasePath + "/interact" - - bootstrapPath = gnapBasePath + "/bootstrap" - - // oidc api handlers. - authProvidersPath = "/oidc/providers" - oidcLoginPath = "/oidc/login" - oidcCallbackPath = "/oidc/callback" - - // GNAP error response codes. - errInvalidRequest = "invalid_request" - errRequestDenied = "request_denied" - - // api path params. - providerQueryParam = "provider" - txnQueryParam = "txnID" - - transientStoreName = "gnap_transient" - bootstrapStoreName = "bootstrapdata" - - // client redirect query params. - interactRefQueryParam = "interact_ref" - responseHashQueryParam = "hash" - - gnapScheme = "GNAP " -) - -// TODO: figure out what logic should go in the access policy vs operation handlers. - -// BootstrapData is the user's bootstrap data. -type BootstrapData struct { - DocumentSDSVaultURL string `json:"documentSDSURL"` - KeySDSVaultURL string `json:"keySDSURL"` - OpsKeyServerURL string `json:"opsKeyServerURL"` - Data map[string]string `json:"data,omitempty"` -} - -// UpdateBootstrapDataRequest is a request to update bootstrap data. -type UpdateBootstrapDataRequest struct { - Data map[string]string `json:"data"` -} - -// Operation defines Auth Server GNAP handlers. -type Operation struct { - authHandler *authhandler.AuthHandler - interactionHandler api.InteractionHandler - introspectHandler common.Introspecter - uiEndpoint string - closePopupHTML string - authProviders []authProvider - oidcProvidersConfig map[string]*oidcmodel.ProviderConfig - cachedOIDCProviders map[string]oidcProvider - cachedOIDCProvLock sync.RWMutex - tlsConfig *tls.Config - callbackURL string - baseURL string - timeout uint64 - transientStore storage.Store - bootstrapStore storage.Store - bootstrapConfig *BootstrapConfig - gnapRSClient *gnap.RequestClient -} - -// Config defines configuration for GNAP operations. -type Config struct { - StoreProvider storage.Provider - AccessPolicyConfig *accesspolicy.Config - BaseURL string - ClosePopupHTML string - InteractionHandler api.InteractionHandler - UIEndpoint string - OIDC *oidcmodel.Config - StartupTimeout uint64 - TransientStoreProvider storage.Provider - TLSConfig *tls.Config - DisableHTTPSigVerify bool - BootstrapConfig *BootstrapConfig -} - -// BootstrapConfig holds user bootstrap-related config. -type BootstrapConfig struct { - DocumentSDSVaultURL string - KeySDSVaultURL string - OpsKeyServerURL string -} - -// New creates GNAP operation handler. -func New(config *Config) (*Operation, error) { - authProviders := make([]authProvider, 0) - - for k, v := range config.OIDC.Providers { - prov := authProvider{ - ID: k, Name: v.Name, SignUpIconURL: v.SignUpIconURL, - SignInIconURL: v.SignInIconURL, Order: v.Order, - } - - authProviders = append(authProviders, prov) - } - - auth, err := authhandler.New(&authhandler.Config{ - StoreProvider: config.StoreProvider, - AccessPolicyConfig: config.AccessPolicyConfig, - ContinuePath: config.BaseURL + AuthContinuePath, - InteractionHandler: config.InteractionHandler, - DisableHTTPSig: config.DisableHTTPSigVerify, - }) - if err != nil { - return nil, err - } - - transientStore, err := createStore(config.TransientStoreProvider, transientStoreName) - if err != nil { - return nil, err - } - - bootstrapStore, err := createStore(config.StoreProvider, bootstrapStoreName) - if err != nil { - return nil, err - } - - introspectHandler := func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return auth.HandleIntrospection(req, &skipVerify{}) - } - - gnapRSClient, err := createGNAPClient() - if err != nil { - return nil, err - } - - return &Operation{ - authHandler: auth, - uiEndpoint: config.UIEndpoint, - authProviders: authProviders, - oidcProvidersConfig: config.OIDC.Providers, - cachedOIDCProviders: make(map[string]oidcProvider), - callbackURL: config.BaseURL + oidcCallbackPath, - timeout: config.StartupTimeout, - transientStore: transientStore, - bootstrapStore: bootstrapStore, - tlsConfig: config.TLSConfig, - interactionHandler: config.InteractionHandler, - closePopupHTML: config.ClosePopupHTML, - bootstrapConfig: config.BootstrapConfig, - introspectHandler: introspectHandler, - gnapRSClient: gnapRSClient, - baseURL: config.BaseURL, - }, nil -} - -// GetRESTHandlers get all controller API handler available for this service. -func (o *Operation) GetRESTHandlers() []common.Handler { - return []common.Handler{ - support.NewHTTPHandler(AuthRequestPath, http.MethodPost, o.authRequestHandler), - // TODO add txn_id to url path - support.NewHTTPHandler(InteractPath, http.MethodGet, o.interactHandler), - support.NewHTTPHandler(AuthContinuePath, http.MethodPost, o.authContinueHandler), - support.NewHTTPHandler(AuthIntrospectPath, http.MethodPost, o.authIntrospectHandler), - - support.NewHTTPHandler(authProvidersPath, http.MethodGet, o.authProvidersHandler), - support.NewHTTPHandler(oidcLoginPath, http.MethodGet, o.oidcLoginHandler), - support.NewHTTPHandler(oidcCallbackPath, http.MethodGet, o.oidcCallbackHandler), - - support.NewHTTPHandler(bootstrapPath, http.MethodGet, o.getBootstrapDataHandler), - support.NewHTTPHandler(bootstrapPath, http.MethodPost, o.postBootstrapDataHandler), - } -} - -// SetIntrospectHandler sets the GNAP introspection handler for Operation's APIs. -func (o *Operation) SetIntrospectHandler(i common.Introspecter) { - o.introspectHandler = i -} - -func (o *Operation) authRequestHandler(w http.ResponseWriter, req *http.Request) { - logger.Debugf("handling auth request to URL: %s", req.URL.String()) - - prevURL := req.URL - - var err error - - req.URL, err = url.Parse(o.baseURL + req.URL.Path) - if err != nil { - req.URL = prevURL - } - - authRequest := &gnap.AuthRequest{} - - bodyBytes, err := ioutil.ReadAll(req.Body) - if err != nil { - logger.Errorf("error reading request body: %s", err.Error()) - w.WriteHeader(http.StatusInternalServerError) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errRequestDenied, - }) - - return - } - - req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) - - if err = json.Unmarshal(bodyBytes, authRequest); err != nil { - logger.Errorf("failed to parse gnap auth request: %s", err.Error()) - w.WriteHeader(http.StatusBadRequest) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errInvalidRequest, - }) - - return - } - - v := httpsig.NewVerifier(req) - - resp, err := o.authHandler.HandleAccessRequest(authRequest, v, "") - if err != nil { - logger.Errorf("access policy failed to handle access request: %s", err.Error()) - w.WriteHeader(http.StatusUnauthorized) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errRequestDenied, - }) - - return - } - - o.writeResponse(w, resp) -} - -func (o *Operation) interactHandler(w http.ResponseWriter, req *http.Request) { - // TODO validate txnID - txnID := req.URL.Query().Get(txnQueryParam) - - redirURL, err := url.Parse(o.uiEndpoint + "/sign-up") - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to construct redirect url") - - return - } - - q := redirURL.Query() - - q.Add(txnQueryParam, txnID) - - redirURL.RawQuery = q.Encode() - - // redirect to UI - http.Redirect(w, req, redirURL.String(), http.StatusFound) -} - -func (o *Operation) authProvidersHandler(w http.ResponseWriter, _ *http.Request) { - o.writeResponse(w, &authProviders{Providers: o.authProviders}) -} - -type oidcTransientData struct { - Provider string `json:"provider,omitempty"` - TxnID string `json:"txnID,omitempty"` -} - -func (o *Operation) oidcLoginHandler(w http.ResponseWriter, r *http.Request) { // nolint: funlen - logger.Debugf("handling request: %s", r.URL.String()) - - providerID := r.URL.Query().Get(providerQueryParam) - if providerID == "" { - o.writeErrorResponse(w, http.StatusBadRequest, "missing provider") - - return - } - - interactTxnID := r.URL.Query().Get(txnQueryParam) - if interactTxnID == "" { - o.writeErrorResponse(w, http.StatusBadRequest, "missing transaction ID") - - return - } - - provider, err := o.getProvider(providerID) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "get provider: %s", err.Error()) - - return - } - - provConfig, ok := o.oidcProvidersConfig[providerID] - if !ok { - o.writeErrorResponse(w, http.StatusInternalServerError, "provider not supported: %s", providerID) - - return - } - - scopes := []string{oidc.ScopeOpenID} - if len(provConfig.Scopes) != 0 { - scopes = append(scopes, provConfig.Scopes...) - } else { - scopes = append(scopes, "profile", "email") - } - - state := uuid.New().String() - - data := &oidcTransientData{ - Provider: providerID, - TxnID: interactTxnID, - } - - dataBytes, err := json.Marshal(data) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, - fmt.Sprintf("failed to marshal oidc txn data : %s", err)) - - return - } - - err = o.transientStore.Put(state, dataBytes) - if err != nil { - o.writeErrorResponse(w, - http.StatusInternalServerError, fmt.Sprintf("failed to write state data to transient store: %s", err)) - - return - } - - authOption := oauth2.SetAuthURLParam(providerQueryParam, providerID) - redirectURL := provider.OAuth2Config( - scopes..., - ).AuthCodeURL(state, oauth2.AccessTypeOnline, authOption) - - http.Redirect(w, r, redirectURL, http.StatusFound) - - logger.Debugf("redirected to: %s", redirectURL) -} - -func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) { // nolint:funlen,gocyclo - state := r.URL.Query().Get("state") - if state == "" { - o.writeErrorResponse(w, http.StatusBadRequest, "missing state") - - return - } - - code := r.URL.Query().Get("code") - if code == "" { - o.writeErrorResponse(w, http.StatusBadRequest, "missing code") - - return - } - - // get state and provider details from transient store - dataBytes, err := o.transientStore.Get(state) - if err != nil { - o.writeErrorResponse(w, - http.StatusBadRequest, fmt.Sprintf("failed to get state data to transient store: %s", err)) - - return - } - - data := &oidcTransientData{} - - err = json.Unmarshal(dataBytes, data) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, - fmt.Sprintf("failed to parse oidc txn data : %s", err)) - - return - } - - providerID := data.Provider - - provider, err := o.getProvider(providerID) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "get provider : %s", err.Error()) - - return - } - - oauthToken, err := provider.OAuth2Config().Exchange(r.Context(), code) - if err != nil { - o.writeErrorResponse(w, http.StatusBadGateway, - fmt.Sprintf("failed to exchange oauth2 code for token : %s", err)) - - return - } - - rawIDToken, ok := oauthToken.Extra("id_token").(string) - if !ok { - o.writeErrorResponse(w, http.StatusBadGateway, "missing id_token") - - return - } - - oidcToken, err := provider.Verify(r.Context(), rawIDToken) - if err != nil { - o.writeErrorResponse(w, http.StatusForbidden, fmt.Sprintf("failed to verify id_token : %s", err)) - - return - } - - claims := &oidcClaims{} - - err = oidcToken.Claims(claims) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, - fmt.Sprintf("failed to extract claims from id_token : %s", err)) - - return - } - - _, err = user.NewStore(o.bootstrapStore).Get(claims.Sub) - if errors.Is(err, storage.ErrDataNotFound) { - _, err = o.onboardUser(claims.Sub) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("failed to onboard new user : %s", err)) - - return - } - } - - interactRef, responseHash, clientInteract, err := o.interactionHandler.CompleteInteraction( - data.TxnID, - &api.ConsentResult{ - SubjectData: map[string]string{ - "sub": claims.Sub, - }, - }, - ) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, - fmt.Sprintf("failed to complete GNAP interaction : %s", err)) - - return - } - - clientURI, err := url.Parse(clientInteract.Finish.URI) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "client provided invalid redirect URI : %s", err.Error()) - - return - } - - // TODO: validate clientURI for security - - q := clientURI.Query() - - q.Add(interactRefQueryParam, interactRef) - q.Add(responseHashQueryParam, responseHash) - - clientURI.RawQuery = q.Encode() - - redirect := clientURI.String() - - t, err := template.ParseFiles(o.closePopupHTML) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to parse template : %s", err.Error()) - - return - } - - if err := t.Execute(w, map[string]interface{}{ - "RedirectURI": redirect, - }); err != nil { - logger.Errorf(fmt.Sprintf("failed execute html template: %s", err.Error())) - } -} - -func (o *Operation) authContinueHandler(w http.ResponseWriter, req *http.Request) { // nolint: funlen - logger.Debugf("handling continue request to URL: %s", req.URL.String()) - - prevURL := req.URL - - var err error - - req.URL, err = url.Parse(o.baseURL + req.URL.Path) - if err != nil { - req.URL = prevURL - } - - tokHeader := strings.Split(strings.Trim(req.Header.Get("Authorization"), " "), " ") - - if len(tokHeader) < 2 || tokHeader[0] != "GNAP" { - logger.Errorf("GNAP continuation endpoint requires GNAP token") - w.WriteHeader(http.StatusUnauthorized) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errRequestDenied, - }) - - return - } - - token := tokHeader[1] - - continueRequest := &gnap.ContinueRequest{} - - bodyBytes, err := ioutil.ReadAll(req.Body) - if err != nil { - logger.Errorf("error reading request body: %s", err.Error()) - w.WriteHeader(http.StatusInternalServerError) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errRequestDenied, - }) - - return - } - - req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) - - if err = json.Unmarshal(bodyBytes, continueRequest); err != nil { - logger.Errorf("failed to parse gnap continue request: %s", err.Error()) - w.WriteHeader(http.StatusBadRequest) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errInvalidRequest, - }) - - return - } - - v := httpsig.NewVerifier(req) - - resp, err := o.authHandler.HandleContinueRequest(continueRequest, token, v) - if err != nil { - logger.Errorf("access policy failed to handle continue request: %s", err.Error()) - w.WriteHeader(http.StatusUnauthorized) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errRequestDenied, - }) - - return - } - - o.writeResponse(w, resp) -} - -func (o *Operation) getBootstrapDataHandler(w http.ResponseWriter, r *http.Request) { - logger.Debugf("handling request") - - subject, proceed := o.subject(w, r) - if !proceed { - return - } - - profile, err := user.NewStore(o.bootstrapStore).Get(subject) - if errors.Is(err, storage.ErrDataNotFound) { - o.writeErrorResponse(w, http.StatusBadRequest, "invalid handle") - - return - } - - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, - fmt.Sprintf("failed to query bootstrap store for handle: %s", err)) - - return - } - - response, err := json.Marshal(&BootstrapData{ - DocumentSDSVaultURL: o.bootstrapConfig.DocumentSDSVaultURL, - KeySDSVaultURL: o.bootstrapConfig.KeySDSVaultURL, - OpsKeyServerURL: o.bootstrapConfig.OpsKeyServerURL, - Data: profile.Data, - }) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("failed to marshal bootstrap data: %s", err)) - - return - } - - // TODO We should delete the handle from the transient store after writing the response, - // but edge-core store API doesn't have a Delete() operation: https://github.com/trustbloc/edge-core/issues/45 - _, err = w.Write(response) - if err != nil { - logger.Errorf("failed to write bootstrap data to output: %s", err) - } - - logger.Debugf("finished handling request") -} - -func (o *Operation) postBootstrapDataHandler(w http.ResponseWriter, r *http.Request) { - logger.Debugf("handling request") - - subject, proceed := o.subject(w, r) - if !proceed { - return - } - - update := &UpdateBootstrapDataRequest{} - - err := json.NewDecoder(r.Body).Decode(update) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "failed to decode request: %s", err.Error()) - - return - } - - existing, err := user.NewStore(o.bootstrapStore).Get(subject) - if errors.Is(err, storage.ErrDataNotFound) { - o.writeErrorResponse(w, http.StatusConflict, "associated bootstrap data not found") - - return - } - - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to query storage: %s", err.Error()) - - return - } - - err = user.NewStore(o.bootstrapStore).Save(merge(existing, update)) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to update storage: %s", err.Error()) - - return - } - - logger.Debugf("finished handling request") -} - -type skipVerify struct{} - -// Verify skip request verification when introspecting internally through Go. -func (s skipVerify) Verify(_ *gnap.ClientKey) error { - return nil -} - -// InternalIntrospectHandler returns a handler that allows the auth server's handlers to perform GNAP introspection -// with itself as the AS and RS. -func (o *Operation) InternalIntrospectHandler() common.Introspecter { - return o.introspectHandler -} - -func (o *Operation) authIntrospectHandler(w http.ResponseWriter, req *http.Request) { - logger.Debugf("handling introspect request to URL: %s", req.URL.String()) - - prevURL := req.URL - - var err error - - req.URL, err = url.Parse(o.baseURL + req.URL.Path) - if err != nil { - req.URL = prevURL - } - - introspectRequest := &gnap.IntrospectRequest{} - - bodyBytes, err := ioutil.ReadAll(req.Body) - if err != nil { - logger.Errorf("error reading request body: %s", err.Error()) - w.WriteHeader(http.StatusInternalServerError) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errRequestDenied, - }) - - return - } - - req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) - - if err = json.Unmarshal(bodyBytes, introspectRequest); err != nil { - logger.Errorf("failed to parse gnap introspection request: %s", err.Error()) - w.WriteHeader(http.StatusBadRequest) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errInvalidRequest, - }) - - return - } - - v := httpsig.NewVerifier(req) - - resp, err := o.authHandler.HandleIntrospection(introspectRequest, v) - if err != nil { - logger.Errorf("failed to handle gnap introspection request: %s", err.Error()) - w.WriteHeader(http.StatusUnauthorized) - o.writeResponse(w, &gnap.ErrorResponse{ - Error: errRequestDenied, - }) - - return - } - - o.writeResponse(w, resp) -} - -// WriteResponse writes interface value to response. -func (o *Operation) writeResponse(rw http.ResponseWriter, v interface{}) { - rw.Header().Set("Content-Type", "application/json") - - err := json.NewEncoder(rw).Encode(v) - if err != nil { - logger.Errorf("Unable to send response: %s", err.Error()) - } -} - -// writeResponse writes interface value to response. -func (o *Operation) writeErrorResponse(rw http.ResponseWriter, status int, msg string, args ...interface{}) { - msg = fmt.Sprintf(msg, args...) - logger.Errorf(msg) - - rw.WriteHeader(status) - - if _, err := rw.Write([]byte(msg)); err != nil { - logger.Errorf("Unable to send error message, %s", err) - } -} - -func (o *Operation) getProvider(providerID string) (oidcProvider, error) { - o.cachedOIDCProvLock.RLock() - prov, ok := o.cachedOIDCProviders[providerID] - o.cachedOIDCProvLock.RUnlock() - - if ok { - return prov, nil - } - - provider, ok := o.oidcProvidersConfig[providerID] - if !ok { - return nil, fmt.Errorf("provider not supported: %s", providerID) - } - - prov, err := o.initOIDCProvider(providerID, provider) - if err != nil { - return nil, fmt.Errorf("failed to init oidc provider: %w", err) - } - - o.cachedOIDCProvLock.Lock() - o.cachedOIDCProviders[providerID] = prov - o.cachedOIDCProvLock.Unlock() - - return prov, nil -} - -func (o *Operation) initOIDCProvider(providerID string, config *oidcmodel.ProviderConfig) (oidcProvider, error) { - var idp *oidc.Provider - - err := backoff.RetryNotify( - func() error { - var idpErr error - - ctx := context.Background() - - if config.SkipIssuerCheck { - ctx = oidc.InsecureIssuerURLContext(context.Background(), config.URL) - } - - idp, idpErr = oidc.NewProvider( - oidc.ClientContext( - ctx, - &http.Client{ - Transport: &http.Transport{TLSClientConfig: o.tlsConfig}, - }, - ), - config.URL, - ) - - return idpErr - }, - backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Second), o.timeout), - func(retryErr error, t time.Duration) { - logger.Warnf( - "failed to connect to the [%s] OIDC provider, will sleep for %s before trying again : %s", - providerID, t, retryErr) - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to init oidc provider [%s] with url [%s]: %w", providerID, config.URL, err) - } - - return &oidcProviderImpl{ - name: providerID, - clientID: config.ClientID, - clientSecret: config.ClientSecret, - callback: o.callbackURL, - skipIssuerCheck: config.SkipIssuerCheck, - op: idp, - httpClient: &http.Client{Transport: &http.Transport{ - TLSClientConfig: o.tlsConfig, - }}, - }, nil -} - -func createStore(p storage.Provider, name string) (storage.Store, error) { - s, err := p.OpenStore(name) - if err != nil { - return nil, fmt.Errorf("failed to open store [%s]: %w", name, err) - } - - return s, nil -} - -func createGNAPClient() (*gnap.RequestClient, error) { - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, fmt.Errorf("creating public key for GNAP RS role: %w", err) - } - - return &gnap.RequestClient{ - IsReference: false, - Key: &gnap.ClientKey{ - Proof: "httpsig", - JWK: jwk.JWK{ - JSONWebKey: jose.JSONWebKey{ - Key: &priv.PublicKey, - KeyID: "key2", - Algorithm: "ES256", - }, - Kty: "EC", - Crv: "P-256", - }, - }, - }, nil -} - -func (o *Operation) onboardUser(sub string) (*user.Profile, error) { - userProfile := &user.Profile{ - ID: sub, - Data: make(map[string]string), - } - - err := user.NewStore(o.bootstrapStore).Save(userProfile) - if err != nil { - return nil, fmt.Errorf("failed to save user profile : %w", err) - } - - return userProfile, nil -} - -func (o *Operation) subject(w http.ResponseWriter, r *http.Request) (string, bool) { - authHeader := strings.TrimSpace(r.Header.Get("authorization")) - if authHeader == "" { - o.writeErrorResponse(w, http.StatusForbidden, "no credentials") - - return "", false - } - - switch { - case strings.HasPrefix(authHeader, gnapScheme): - return o.gnapSub(w, r, authHeader) - default: - o.writeErrorResponse(w, http.StatusBadRequest, "invalid authorization scheme") - - return "", false - } -} - -func (o *Operation) gnapSub(w http.ResponseWriter, _ *http.Request, authHeader string) (string, bool) { - token := authHeader[len(gnapScheme):] - - introspection, err := o.introspectHandler(&gnap.IntrospectRequest{ - AccessToken: token, - ResourceServer: o.gnapRSClient, - }) - if err != nil { - o.writeErrorResponse(w, http.StatusUnauthorized, "failed to introspect token: %s", err.Error()) - - return "", false - } - - if sub, ok := introspection.SubjectData["sub"]; ok { - return sub, true - } - - o.writeErrorResponse(w, http.StatusUnauthorized, "token does not grant access to subject id") - - return "", false -} - -func merge(existing *user.Profile, update *UpdateBootstrapDataRequest) *user.Profile { - merged := &user.Profile{ - ID: existing.ID, - AAGUID: existing.AAGUID, - Data: existing.Data, - } - - if merged.Data == nil { - merged.Data = make(map[string]string) - } - - for k, v := range update.Data { - if _, found := merged.Data[k]; !found { - merged.Data[k] = v - } - } - - return merged -} diff --git a/pkg/restapi/gnap/operations_test.go b/pkg/restapi/gnap/operations_test.go deleted file mode 100644 index abaccb0..0000000 --- a/pkg/restapi/gnap/operations_test.go +++ /dev/null @@ -1,1694 +0,0 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package gnap - -import ( - "bytes" - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "regexp" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/hyperledger/aries-framework-go/component/storageutil/mem" - "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" - mockstore "github.com/hyperledger/aries-framework-go/pkg/mock/storage" - "github.com/hyperledger/aries-framework-go/spi/storage" - "github.com/square/go-jose/v3" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2" - - "github.com/trustbloc/auth/pkg/bootstrap/user" - "github.com/trustbloc/auth/pkg/gnap/accesspolicy" - "github.com/trustbloc/auth/pkg/gnap/api" - "github.com/trustbloc/auth/pkg/gnap/interact/redirect" - "github.com/trustbloc/auth/pkg/internal/common/mockinteract" - "github.com/trustbloc/auth/pkg/internal/common/mockoidc" - "github.com/trustbloc/auth/pkg/internal/common/mockstorage" - oidcmodel "github.com/trustbloc/auth/pkg/restapi/common/oidc" - "github.com/trustbloc/auth/spi/gnap" - "github.com/trustbloc/auth/spi/gnap/proof/httpsig" -) - -const ( - baseURL = "http://test.auth" -) - -func TestNew(t *testing.T) { - t.Run("success", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - require.NotNil(t, o) - }) - - t.Run("failure", func(t *testing.T) { - conf := config(t) - - expectErr := errors.New("expected error") - - conf.StoreProvider = &mockstorage.Provider{ErrOpenStoreHandle: expectErr} - - o, err := New(conf) - require.Error(t, err) - require.ErrorIs(t, err, expectErr) - require.Nil(t, o) - }) - - t.Run("error if unable to open transient store", func(t *testing.T) { - config := config(t) - config.TransientStoreProvider = &mockstore.MockStoreProvider{ - ErrOpenStoreHandle: errors.New("test"), - } - _, err := New(config) - require.Error(t, err) - }) -} - -func TestOperation_GetRESTHandlers(t *testing.T) { - o := &Operation{} - - h := o.GetRESTHandlers() - require.Len(t, h, 9) -} - -func TestOperation_AuthProvidersHandler(t *testing.T) { - t.Run("success", func(t *testing.T) { - config := config(t) - o, err := New(config) - require.NoError(t, err) - - w := httptest.NewRecorder() - o.authProvidersHandler(w, nil) - - require.Equal(t, http.StatusOK, w.Code) - var resp *authProviders - err = json.Unmarshal(w.Body.Bytes(), &resp) - require.NoError(t, err) - - require.Equal(t, 2, len(resp.Providers)) - }) -} - -func TestOperation_authRequestHandler(t *testing.T) { - t.Run("fail to read body", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - expectErr := errors.New("expected error") - - req := httptest.NewRequest(http.MethodPost, AuthRequestPath, &errorReader{err: expectErr}) - - o.authRequestHandler(rw, req) - - require.Equal(t, http.StatusInternalServerError, rw.Code) - }) - - t.Run("fail to parse empty request body", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, AuthRequestPath, nil) - - o.authRequestHandler(rw, req) - - require.Equal(t, http.StatusBadRequest, rw.Code) - }) - - t.Run("auth handler error", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, AuthRequestPath, bytes.NewReader([]byte("{}"))) - - o.authRequestHandler(rw, req) - - require.Equal(t, http.StatusUnauthorized, rw.Code) - }) - - t.Run("success", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - - priv, client := clientKey(t) - - authReq := &gnap.AuthRequest{ - Client: &gnap.RequestClient{ - IsReference: false, - Key: client, - }, - } - - authReqBytes, err := json.Marshal(authReq) - require.NoError(t, err) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, baseURL+AuthRequestPath, bytes.NewReader(authReqBytes)) - - req, err = httpsig.Sign(req, authReqBytes, priv, "sha-256") - require.NoError(t, err) - - o.authRequestHandler(rw, req) - - require.Equal(t, http.StatusOK, rw.Code) - }) -} - -func TestOperation_interactHandler(t *testing.T) { - t.Run("success", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, InteractPath, nil) - - o.interactHandler(rw, req) - - require.Equal(t, http.StatusFound, rw.Code) - }) -} - -func TestOperation_authContinueHandler(t *testing.T) { - t.Run("missing Auth token", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, AuthContinuePath, nil) - - o.authContinueHandler(rw, req) - - require.Equal(t, http.StatusUnauthorized, rw.Code) - - resp := &gnap.ErrorResponse{} - require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) - require.Equal(t, errRequestDenied, resp.Error) - }) - - t.Run("Auth token not GNAP token", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, AuthContinuePath, nil) - req.Header.Add("Authorization", "Bearer mock-token") - - o.authContinueHandler(rw, req) - - require.Equal(t, http.StatusUnauthorized, rw.Code) - - resp := &gnap.ErrorResponse{} - require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) - require.Equal(t, errRequestDenied, resp.Error) - }) - - t.Run("fail to read request body", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - expectErr := errors.New("expected error") - - req := httptest.NewRequest(http.MethodPost, AuthContinuePath, &errorReader{err: expectErr}) - req.Header.Add("Authorization", "GNAP mock-token") - - o.authContinueHandler(rw, req) - - require.Equal(t, http.StatusInternalServerError, rw.Code) - - resp := &gnap.ErrorResponse{} - require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) - require.Equal(t, errRequestDenied, resp.Error) - }) - - t.Run("fail to parse empty request body", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, AuthContinuePath, nil) - req.Header.Add("Authorization", "GNAP mock-token") - - o.authContinueHandler(rw, req) - - require.Equal(t, http.StatusBadRequest, rw.Code) - - resp := &gnap.ErrorResponse{} - require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) - require.Equal(t, errInvalidRequest, resp.Error) - }) - - t.Run("auth handler error", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, AuthContinuePath, bytes.NewReader([]byte("{}"))) - req.Header.Add("Authorization", "GNAP mock-token") - - o.authContinueHandler(rw, req) - - require.Equal(t, http.StatusUnauthorized, rw.Code) - - resp := &gnap.ErrorResponse{} - require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) - require.Equal(t, errRequestDenied, resp.Error) - }) -} - -func TestOperation_authIntrospectHandler(t *testing.T) { - t.Run("fail to read request body", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - expectErr := errors.New("expected error") - - req := httptest.NewRequest(http.MethodPost, AuthRequestPath, &errorReader{err: expectErr}) - - o.authIntrospectHandler(rw, req) - - require.Equal(t, http.StatusInternalServerError, rw.Code) - }) - - t.Run("fail to parse empty request body", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, AuthRequestPath, nil) - - o.authIntrospectHandler(rw, req) - - require.Equal(t, http.StatusBadRequest, rw.Code) - }) - - t.Run("auth handler error", func(t *testing.T) { - o := &Operation{} - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, AuthRequestPath, bytes.NewReader([]byte("{}"))) - - o.authIntrospectHandler(rw, req) - - require.Equal(t, http.StatusUnauthorized, rw.Code) - }) - - t.Run("requested token does not exist", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - - priv, client := clientKey(t) - - intReq := &gnap.IntrospectRequest{ - AccessToken: "invalid token", - Proof: "httpsig", - ResourceServer: &gnap.RequestClient{ - Key: client, - }, - } - - intReqBytes, err := json.Marshal(intReq) - require.NoError(t, err) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, baseURL+AuthIntrospectPath, bytes.NewReader(intReqBytes)) - - req, err = httpsig.Sign(req, intReqBytes, priv, "sha-256") - require.NoError(t, err) - - o.authIntrospectHandler(rw, req) - - require.Equal(t, http.StatusOK, rw.Code) - - resp := &gnap.IntrospectResponse{} - - err = json.Unmarshal(rw.Body.Bytes(), resp) - require.NoError(t, err) - - require.False(t, resp.Active) - }) -} - -func TestOIDCLoginHandler(t *testing.T) { - t.Run("returns oidc request", func(t *testing.T) { - provider := uuid.New().String() - config := config(t) - svc, err := New(config) - require.NoError(t, err) - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{}, - } - svc.oidcProvidersConfig = map[string]*oidcmodel.ProviderConfig{provider: {}} - w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest(provider, "foo")) - require.Equal(t, http.StatusFound, w.Code) - require.NotEmpty(t, w.Header().Get("location")) - }) - - t.Run("provider not supported", func(t *testing.T) { - provider := uuid.New().String() - config := config(t) - svc, err := New(config) - require.NoError(t, err) - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{}, - } - w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest(provider, "foo")) - require.Equal(t, http.StatusInternalServerError, w.Code) - }) - - t.Run("bad request if provider is missing", func(t *testing.T) { - config := config(t) - svc, err := New(config) - require.NoError(t, err) - w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest("", "")) - require.Equal(t, http.StatusBadRequest, w.Code) - }) - - t.Run("bad request if txn ID is missing", func(t *testing.T) { - config := config(t) - svc, err := New(config) - require.NoError(t, err) - w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest("foo", "")) - require.Equal(t, http.StatusBadRequest, w.Code) - }) - - t.Run("bad request if provider is not supported", func(t *testing.T) { - svc, err := New(config(t)) - require.NoError(t, err) - result := httptest.NewRecorder() - svc.oidcLoginHandler(result, newOIDCLoginRequest("unsupported", "foo")) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "provider not supported") - }) - - t.Run("store error", func(t *testing.T) { - provider := uuid.New().String() - config := config(t) - svc, err := New(config) - require.NoError(t, err) - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{}, - } - svc.oidcProvidersConfig = map[string]*oidcmodel.ProviderConfig{provider: {}} - svc.transientStore = &mockstore.MockStore{ - ErrPut: errors.New("generic"), - } - - result := httptest.NewRecorder() - svc.oidcLoginHandler(result, newOIDCLoginRequest(provider, "foo")) - - require.Contains(t, result.Body.String(), "failed to write state data to transient store") - require.Equal(t, http.StatusInternalServerError, result.Code) - }) - - t.Run("error if oidc provider is invalid", func(t *testing.T) { - config := config(t) - config.OIDC.Providers = map[string]*oidcmodel.ProviderConfig{ - "test": { - URL: "INVALID", - }, - } - - svc, err := New(config) - require.NoError(t, err) - - w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest("test", "foo")) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Contains(t, w.Body.String(), "failed to init oidc provider") - }) -} - -func TestOIDCCallbackHandler(t *testing.T) { - t.Run("setup user", func(t *testing.T) { - provider := uuid.New().String() - state := uuid.New().String() - code := uuid.New().String() - config := config(t) - - templatePath, deleteTmp := tmpStaticHTML(t) - defer deleteTmp() - - config.ClosePopupHTML = templatePath - - o, err := New(config) - require.NoError(t, err) - - o.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{ - oauth2Claim: uuid.New().String(), - }, - }, - verifyVal: &mockToken{ - oidcClaimsFunc: func(v interface{}) error { - c, ok := v.(*oidcClaims) - require.True(t, ok) - c.Sub = uuid.New().String() - - return nil - }, - }, - }, - } - - respInteract, err := o.interactionHandler.PrepareInteraction(&gnap.RequestInteract{ - Start: []string{"redirect"}, - Finish: gnap.RequestFinish{ - Method: "redirect", - URI: "example.foo/client-redirect", - }, - }, "", []*api.ExpiringTokenRequest{ - { - TokenRequest: gnap.TokenRequest{ - Access: []gnap.TokenAccess{ - { - IsReference: true, - Ref: "client-id", - }, - }, - }, - }, - }) - require.NoError(t, err) - - redirURL, err := url.Parse(respInteract.Redirect) - require.NoError(t, err) - - txnID := redirURL.Query().Get("txnID") - - data := &oidcTransientData{ - Provider: provider, - TxnID: txnID, - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = o.transientStore.Put(state, dataBytes) - require.NoError(t, err) - - result := httptest.NewRecorder() - o.oidcCallbackHandler(result, newOIDCCallback(state, code)) - require.Equal(t, http.StatusOK, result.Code) - // TODO validate redirect url - }) - - t.Run("error missing state", func(t *testing.T) { - config := config(t) - svc, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback("", "code")) - require.Equal(t, http.StatusBadRequest, result.Code) - }) - - t.Run("invalid state", func(t *testing.T) { - state := uuid.New().String() - mismatch := "mismatch" - require.NotEqual(t, state, mismatch) - svc, err := New(config(t)) - require.NoError(t, err) - - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "failed to get state data to transient store") - }) - - t.Run("error missing code", func(t *testing.T) { - config := config(t) - svc, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback("state", "")) - require.Equal(t, http.StatusBadRequest, result.Code) - }) - - t.Run("internal server error if transient data is invalid", func(t *testing.T) { - svc, err := New(config(t)) - require.NoError(t, err) - - dataBytes := []byte("foo bar baz") - - err = svc.transientStore.Put("state", dataBytes) - require.NoError(t, err) - - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback("state", "code")) - - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to parse") - }) - - t.Run("bad request if oidc provider is not supported (should not happen)", func(t *testing.T) { - svc, err := New(config(t)) - require.NoError(t, err) - - data := &oidcTransientData{ - Provider: "invalid", - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = svc.transientStore.Put("state", dataBytes) - require.NoError(t, err) - - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback("state", "code")) - - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "provider not supported") - }) - - t.Run("error exchanging auth code", func(t *testing.T) { - provider := uuid.New().String() - state := uuid.New().String() - config := config(t) - config.TransientStoreProvider = &mockstore.MockStoreProvider{Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - state: {Value: []byte(state)}, - }, - }} - svc, err := New(config) - require.NoError(t, err) - - data := &oidcTransientData{ - Provider: provider, - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = svc.transientStore.Put(state, dataBytes) - require.NoError(t, err) - - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - oauth2Config: &mockOAuth2Config{ - exchangeErr: errors.New("test"), - }, - }, - } - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) - require.Equal(t, http.StatusBadGateway, result.Code) - require.Contains(t, result.Body.String(), "failed to exchange oauth2 code for token") - }) - - t.Run("error missing id_token", func(t *testing.T) { - provider := uuid.New().String() - state := uuid.New().String() - config := config(t) - config.TransientStoreProvider = &mockstore.MockStoreProvider{Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - state: {Value: []byte(state)}, - }, - }} - svc, err := New(config) - require.NoError(t, err) - - data := &oidcTransientData{ - Provider: provider, - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = svc.transientStore.Put(state, dataBytes) - require.NoError(t, err) - - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{}, - }, - verifyVal: &mockToken{}, - }, - } - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) - require.Equal(t, http.StatusBadGateway, result.Code) - }) - - t.Run("error id_token verification", func(t *testing.T) { - provider := uuid.New().String() - state := uuid.New().String() - config := config(t) - config.TransientStoreProvider = &mockstore.MockStoreProvider{Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - state: {Value: []byte(state)}, - }, - }} - svc, err := New(config) - require.NoError(t, err) - - data := &oidcTransientData{ - Provider: provider, - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = svc.transientStore.Put(state, dataBytes) - require.NoError(t, err) - - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{oauth2Claim: "id_token"}, - }, - verifyErr: errors.New("test"), - }, - } - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) - require.Equal(t, http.StatusForbidden, result.Code) - }) - - t.Run("error scanning id_token claims", func(t *testing.T) { - provider := uuid.New().String() - state := uuid.New().String() - config := config(t) - config.TransientStoreProvider = &mockstore.MockStoreProvider{Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - state: {Value: []byte(state)}, - }, - }} - svc, err := New(config) - require.NoError(t, err) - - data := &oidcTransientData{ - Provider: provider, - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = svc.transientStore.Put(state, dataBytes) - require.NoError(t, err) - - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{oauth2Claim: "id_token"}, - }, - verifyVal: &mockToken{oidcClaimsErr: errors.New("test")}, - }, - } - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) - require.Equal(t, http.StatusInternalServerError, result.Code) - }) - - t.Run("generic bootstrap store PUT error while onboarding user", func(t *testing.T) { - provider := uuid.New().String() - id := uuid.New().String() - state := uuid.New().String() - code := uuid.New().String() - config := config(t) - - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - id: {}, - }, - ErrGet: storage.ErrDataNotFound, - ErrPut: errors.New("generic"), - }, - } - - o, err := New(config) - require.NoError(t, err) - - o.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{ - oauth2Claim: uuid.New().String(), - }, - }, - verifyVal: &mockToken{ - oidcClaimsFunc: func(v interface{}) error { - c, ok := v.(*oidcClaims) - require.True(t, ok) - c.Sub = uuid.New().String() - - return nil - }, - }, - }, - } - - data := &oidcTransientData{ - Provider: provider, - TxnID: "foo", - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = o.transientStore.Put(state, dataBytes) - require.NoError(t, err) - - result := httptest.NewRecorder() - o.oidcCallbackHandler(result, newOIDCCallback(state, code)) - require.Equal(t, http.StatusInternalServerError, result.Code) - - require.Contains(t, result.Body.String(), "failed to onboard new user") - }) - - t.Run("fail to complete interaction", func(t *testing.T) { - provider := uuid.New().String() - state := uuid.New().String() - code := uuid.New().String() - config := config(t) - - expectErr := errors.New("expected error") - - config.InteractionHandler = &mockinteract.InteractHandler{ - CompleteErr: expectErr, - } - - o, err := New(config) - require.NoError(t, err) - - o.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{ - oauth2Claim: uuid.New().String(), - }, - }, - verifyVal: &mockToken{ - oidcClaimsFunc: func(v interface{}) error { - c, ok := v.(*oidcClaims) - require.True(t, ok) - c.Sub = uuid.New().String() - - return nil - }, - }, - }, - } - - data := &oidcTransientData{ - Provider: provider, - TxnID: "foo", - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = o.transientStore.Put(state, dataBytes) - require.NoError(t, err) - - result := httptest.NewRecorder() - o.oidcCallbackHandler(result, newOIDCCallback(state, code)) - require.Equal(t, http.StatusInternalServerError, result.Code) - - require.Contains(t, result.Body.String(), "failed to complete GNAP interaction") - }) - - t.Run("bad client redirect URI", func(t *testing.T) { - provider := uuid.New().String() - state := uuid.New().String() - code := uuid.New().String() - config := config(t) - - o, err := New(config) - require.NoError(t, err) - - o.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{ - oauth2Claim: uuid.New().String(), - }, - }, - verifyVal: &mockToken{ - oidcClaimsFunc: func(v interface{}) error { - c, ok := v.(*oidcClaims) - require.True(t, ok) - c.Sub = uuid.New().String() - - return nil - }, - }, - }, - } - - respInteract, err := o.interactionHandler.PrepareInteraction(&gnap.RequestInteract{ - Start: []string{"redirect"}, - Finish: gnap.RequestFinish{ - Method: "redirect", - URI: "^$#^*#%$^&#$%#T^ UTTER GIBBERISH", - }, - }, "", []*api.ExpiringTokenRequest{ - { - TokenRequest: gnap.TokenRequest{ - Access: []gnap.TokenAccess{ - { - IsReference: true, - Ref: "client-id", - }, - }, - }, - }, - }) - require.NoError(t, err) - - redirURL, err := url.Parse(respInteract.Redirect) - require.NoError(t, err) - - txnID := redirURL.Query().Get("txnID") - - data := &oidcTransientData{ - Provider: provider, - TxnID: txnID, - } - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - err = o.transientStore.Put(state, dataBytes) - require.NoError(t, err) - - result := httptest.NewRecorder() - o.oidcCallbackHandler(result, newOIDCCallback(state, code)) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "client provided invalid redirect URI") - }) - - t.Run("generic bootstrap store PUT error while onboarding user", func(t *testing.T) { - provider := uuid.New().String() - id := uuid.New().String() - state := uuid.New().String() - config := config(t) - - config.TransientStoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - state: {Value: []byte(state)}, - }, - }, - } - - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - id: {}, - }, - ErrGet: storage.ErrDataNotFound, - ErrPut: errors.New("generic"), - }, - } - - svc, err := New(config) - require.NoError(t, err) - - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{exchangeVal: &mockToken{ - oauth2Claim: uuid.New().String(), - }}, - verifyVal: &mockToken{ - oidcClaimsFunc: func(v interface{}) error { - c, ok := v.(*oidcClaims) - require.True(t, ok) - c.Sub = id - - return nil - }, - }, - }, - } - - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) - require.Equal(t, http.StatusInternalServerError, result.Code) - }) -} - -func TestGetBootstrapDataHandler(t *testing.T) { - t.Run("returns bootstrap data when using GNAP token", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - svc, err := New(config) - require.NoError(t, err) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": userSub}, - }, nil - }) - expected := &user.Profile{ - ID: uuid.New().String(), - AAGUID: uuid.New().String(), - Data: map[string]string{ - "primary vault": uuid.New().String(), - "backup vault": uuid.New().String(), - }, - } - - err = svc.bootstrapStore.Put(userSub, marshal(t, expected)) - require.NoError(t, err) - - w := httptest.NewRecorder() - - request := newGetBootstrapDataRequest() - request.Header.Set("authorization", "GNAP 123") - - svc.getBootstrapDataHandler(w, request) - require.Equal(t, http.StatusOK, w.Code) - result := &BootstrapData{} - err = json.NewDecoder(w.Body).Decode(result) - require.NoError(t, err) - require.Equal(t, config.BootstrapConfig.DocumentSDSVaultURL, result.DocumentSDSVaultURL) - require.Equal(t, config.BootstrapConfig.KeySDSVaultURL, result.KeySDSVaultURL) - require.Equal(t, config.BootstrapConfig.OpsKeyServerURL, result.OpsKeyServerURL) - require.Equal(t, expected.Data, result.Data) - }) - - t.Run("forbidden if auth header is missing", func(t *testing.T) { - svc, err := New(config(t)) - require.NoError(t, err) - w := httptest.NewRecorder() - svc.getBootstrapDataHandler(w, httptest.NewRequest(http.MethodGet, "http://examepl.com/bootstrap", nil)) - require.Equal(t, http.StatusForbidden, w.Code) - require.Contains(t, w.Body.String(), "no credentials") - }) - - t.Run("bad request if auth scheme is invalid", func(t *testing.T) { - request := newGetBootstrapDataRequest() - request.Header.Set("authorization", "invalid 123") - svc, err := New(config(t)) - require.NoError(t, err) - w := httptest.NewRecorder() - svc.getBootstrapDataHandler(w, request) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Contains(t, w.Body.String(), "invalid authorization scheme") - }) - - t.Run("unauthorized if invalid gnap token", func(t *testing.T) { - request := newGetBootstrapDataRequest() - request.Header.Set("authorization", "GNAP 123") - svc, err := New(config(t)) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return nil, fmt.Errorf("gnap introspect error") - }) - require.NoError(t, err) - w := httptest.NewRecorder() - svc.getBootstrapDataHandler(w, request) - require.Equal(t, http.StatusUnauthorized, w.Code) - require.Contains(t, w.Body.String(), "gnap introspect error") - }) - - t.Run("unauthorized if gnap token does not grant access to subject id", func(t *testing.T) { - request := newGetBootstrapDataRequest() - request.Header.Set("authorization", "GNAP 123") - svc, err := New(config(t)) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{}, - }, nil - }) - require.NoError(t, err) - w := httptest.NewRecorder() - svc.getBootstrapDataHandler(w, request) - require.Equal(t, http.StatusUnauthorized, w.Code) - require.Contains(t, w.Body.String(), "does not grant access") - }) - - t.Run("bad request if user does not have bootstrap data", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - svc, err := New(config) - require.NoError(t, err) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": userSub}, - }, nil - }) - w := httptest.NewRecorder() - svc.getBootstrapDataHandler(w, newGetBootstrapDataRequest()) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Contains(t, w.Body.String(), "invalid handle") - }) - - t.Run("internal server error if bootstrap store FETCH fails generically", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - userSub: {Value: marshal(t, &user.Profile{})}, - }, - ErrGet: errors.New("generic"), - }, - } - svc, err := New(config) - require.NoError(t, err) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": userSub}, - }, nil - }) - w := httptest.NewRecorder() - svc.getBootstrapDataHandler(w, newGetBootstrapDataRequest()) - require.Equal(t, http.StatusInternalServerError, w.Code) - require.Contains(t, w.Body.String(), "failed to query bootstrap store for handle") - }) -} - -func TestPostBootstrapDataHandler(t *testing.T) { - t.Run("updates bootstrap data when using GNAP token", func(t *testing.T) { - expected := &user.Profile{ - ID: uuid.New().String(), - AAGUID: uuid.New().String(), - Data: map[string]string{ - "docsSDS": "https://example.org/edvs/123", - "keysSDS": "https://example.org/edvs/456", - "opskeys": "https://example.org/kms/456", - }, - } - config := config(t) - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - expected.ID: {Value: marshal(t, &user.Profile{ - ID: expected.ID, - AAGUID: expected.AAGUID, - })}, - }, - }, - } - svc, err := New(config) - require.NoError(t, err) - - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": expected.ID}, - }, nil - }) - - result := httptest.NewRecorder() - - request := newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{ - Data: expected.Data, - }) - - svc.postBootstrapDataHandler(result, request) - require.Equal(t, http.StatusOK, result.Code) - raw, err := svc.bootstrapStore.Get(expected.ID) - require.NoError(t, err) - update := &user.Profile{} - err = json.NewDecoder(bytes.NewReader(raw)).Decode(update) - require.NoError(t, err) - require.Equal(t, expected, update) - }) - - t.Run("error badrequest if payload is not json", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - userSub: {Value: nil}, - }, - }, - } - svc, err := New(config) - require.NoError(t, err) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": userSub}, - }, nil - }) - request := httptest.NewRequest(http.MethodPost, "https://example.org/bootstrap", bytes.NewReader([]byte("}"))) - request.Header.Set("authorization", "GNAP 123") - result := httptest.NewRecorder() - svc.postBootstrapDataHandler(result, request) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "failed to decode request") - }) - - t.Run("error conflict if user does not exist", func(t *testing.T) { - config := config(t) - svc, err := New(config) - require.NoError(t, err) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": uuid.New().String()}, - }, nil - }) - result := httptest.NewRecorder() - svc.postBootstrapDataHandler(result, newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{})) - require.Equal(t, http.StatusConflict, result.Code) - require.Contains(t, result.Body.String(), "associated bootstrap data not found") - }) - - t.Run("internal server error on generic FETCH bootstrap store error", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - userSub: {Value: nil}, - }, - ErrGet: errors.New("generic"), - }, - } - svc, err := New(config) - require.NoError(t, err) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": userSub}, - }, nil - }) - result := httptest.NewRecorder() - svc.postBootstrapDataHandler(result, newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{})) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to query storage") - }) - - t.Run("internal server error if cannot persist update to bootstrap store", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - userSub: {Value: marshal(t, &user.Profile{})}, - }, - ErrPut: errors.New("generic"), - }, - } - svc, err := New(config) - require.NoError(t, err) - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": userSub}, - }, nil - }) - result := httptest.NewRecorder() - svc.postBootstrapDataHandler(result, newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{})) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to update storage") - }) -} - -func Test_Full_Flow(t *testing.T) { - conf := config(t) - - templatePath, deleteTmp := tmpStaticHTML(t) - defer deleteTmp() - - conf.ClosePopupHTML = templatePath - - o, err := New(conf) - require.NoError(t, err) - - authResp := &gnap.AuthResponse{} - - var ( - txnID string - interactRef string - state string - ) - - userPriv, userClient := clientKey(t) - - { - authReq := &gnap.AuthRequest{ - Client: &gnap.RequestClient{ - IsReference: false, - Key: userClient, - }, - AccessToken: []*gnap.TokenRequest{ - { - Access: []gnap.TokenAccess{ - { - IsReference: true, - Ref: "client-id", - }, - }, - }, - }, - Interact: &gnap.RequestInteract{ - Start: []string{"redirect"}, - Finish: gnap.RequestFinish{ - Method: "redirect", - URI: "example.com/client-ui", - }, - }, - } - - authReqBytes, err := json.Marshal(authReq) - require.NoError(t, err) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, baseURL+AuthRequestPath, bytes.NewReader(authReqBytes)) - - req, err = httpsig.Sign(req, authReqBytes, userPriv, "sha-256") - require.NoError(t, err) - - o.authRequestHandler(rw, req) - - require.Equal(t, http.StatusOK, rw.Code) - - require.NoError(t, json.Unmarshal(rw.Body.Bytes(), authResp)) - - redirectURL, err := url.Parse(authResp.Interact.Redirect) - require.NoError(t, err) - - txnID = redirectURL.Query().Get("txnID") - } - - provider := uuid.New().String() - - subjectID := "mock-subject-id" - - o.cachedOIDCProviders = map[string]oidcProvider{provider: &mockOIDCProvider{ - oauth2Config: &mockOAuth2Config{ - authCodeFunc: func(state string, opts ...oauth2.AuthCodeOption) string { - return "example.com/oauth2?state=" + state - }, - exchangeVal: &mockToken{ - oauth2Claim: "mock-id-token", - }, - }, - verifyVal: &mockToken{ - oidcClaimsFunc: func(v interface{}) error { - claims, ok := v.(*oidcClaims) - if !ok { - return nil - } - - claims.Sub = subjectID - - return nil - }, - }, - }} - o.oidcProvidersConfig = map[string]*oidcmodel.ProviderConfig{provider: {}} - - { - rw := httptest.NewRecorder() - - o.oidcLoginHandler(rw, newOIDCLoginRequest(provider, txnID)) - require.Equal(t, http.StatusFound, rw.Code) - redirectURL, err := url.Parse(rw.Header().Get("location")) - require.NoError(t, err) - - state = redirectURL.Query().Get("state") - } - - { - code := uuid.New().String() - - rw := httptest.NewRecorder() - - o.oidcCallbackHandler(rw, newOIDCCallback(state, code)) - - require.Equal(t, http.StatusOK, rw.Code) - - body := rw.Body.Bytes() - - rx := regexp.MustCompile("window.opener.location.href = '(.*)';") - res := rx.FindStringSubmatch(string(body)) - - u := res[1] - - u = strings.ReplaceAll(u, "\\u0026", "\u0026") - u = strings.ReplaceAll(u, "\\/", "/") - - redirectURL, err := url.Parse(u) - require.NoError(t, err) - - require.Contains(t, redirectURL.String(), "interact_ref") - - interactRef = redirectURL.Query().Get("interact_ref") - require.NotEqual(t, "", interactRef) - } - - contResp := &gnap.AuthResponse{} - - { - contReq := &gnap.ContinueRequest{ - InteractRef: interactRef, - } - - contReqBytes, err := json.Marshal(contReq) - require.NoError(t, err) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, baseURL+AuthRequestPath, bytes.NewReader(contReqBytes)) - req.Header.Add("Authorization", "GNAP "+authResp.Continue.AccessToken.Value) - - req, err = httpsig.Sign(req, contReqBytes, userPriv, "sha-256") - require.NoError(t, err) - - o.authContinueHandler(rw, req) - - require.Equal(t, http.StatusOK, rw.Code) - - require.NoError(t, json.Unmarshal(rw.Body.Bytes(), contResp)) - } - - require.Len(t, contResp.AccessToken, 1) - - rsPriv, rsClient := clientKey(t) - - { - intReq := &gnap.IntrospectRequest{ - AccessToken: contResp.AccessToken[0].Value, - Proof: "httpsig", - ResourceServer: &gnap.RequestClient{ - Key: rsClient, - }, - } - - intReqBytes, err := json.Marshal(intReq) - require.NoError(t, err) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, baseURL+AuthIntrospectPath, bytes.NewReader(intReqBytes)) - - req, err = httpsig.Sign(req, intReqBytes, rsPriv, "sha-256") - require.NoError(t, err) - - o.authIntrospectHandler(rw, req) - - require.Equal(t, http.StatusOK, rw.Code) - - resp := &gnap.IntrospectResponse{} - - err = json.Unmarshal(rw.Body.Bytes(), resp) - require.NoError(t, err) - - require.True(t, resp.Active) - - resultID := resp.SubjectData["sub"] - - // introspection returns the user's OIDC 'sub' ID value - require.Equal(t, subjectID, resultID) - } -} - -type mockOIDCProvider struct { - name string - baseURL string - oauth2Config oauth2Config - verifyVal idToken - verifyErr error -} - -func (m *mockOIDCProvider) Name() string { - return m.name -} - -func (m *mockOIDCProvider) OAuth2Config(...string) oauth2Config { - if m.oauth2Config != nil { - return m.oauth2Config - } - - return &mockOAuth2Config{} -} - -func (m *mockOIDCProvider) Endpoint() oauth2.Endpoint { - return oauth2.Endpoint{ - AuthURL: fmt.Sprintf("%s/oauth2/auth", m.baseURL), - TokenURL: fmt.Sprintf("%s/oauth2/token", m.baseURL), - } -} - -func (m *mockOIDCProvider) Verify(_ context.Context, _ string) (idToken, error) { - return m.verifyVal, m.verifyErr -} - -type mockOAuth2Config struct { - authCodeVal string - authCodeFunc func(string, ...oauth2.AuthCodeOption) string - exchangeVal oauth2Token - exchangeErr error -} - -func (m *mockOAuth2Config) AuthCodeURL(state string, options ...oauth2.AuthCodeOption) string { - if m.authCodeFunc != nil { - return m.authCodeFunc(state, options...) - } - - return m.authCodeVal -} - -func (m *mockOAuth2Config) Exchange( - ctx context.Context, code string, options ...oauth2.AuthCodeOption) (oauth2Token, error) { - return m.exchangeVal, m.exchangeErr -} - -func newOIDCLoginRequest(provider, txnID string) *http.Request { - return httptest.NewRequest(http.MethodGet, - fmt.Sprintf("http://example.com/oauth2/login?provider=%s&txnID=%s", provider, txnID), - nil) -} - -func newOIDCCallback(state, code string) *http.Request { - return httptest.NewRequest(http.MethodGet, - fmt.Sprintf("http://example.com/oauth2/callback?state=%s&code=%s", state, code), nil) -} - -func newGetBootstrapDataRequest() *http.Request { - r := httptest.NewRequest(http.MethodGet, "http://example.com/bootstrap", nil) - r.Header.Set("Authorization", "GNAP 123") - - return r -} - -func newPostBootstrapDataRequest(t *testing.T, params *UpdateBootstrapDataRequest) *http.Request { - t.Helper() - - bits, err := json.Marshal(params) - require.NoError(t, err) - - r := httptest.NewRequest(http.MethodPost, "http://example.com/bootstrap", bytes.NewReader(bits)) - r.Header.Set("Authorization", "GNAP 123") - - return r -} - -type mockToken struct { - oauth2Claim interface{} - oidcClaimsFunc func(v interface{}) error - oidcClaimsErr error -} - -func (m *mockToken) Extra(_ string) interface{} { - if m.oauth2Claim != nil { - return m.oauth2Claim - } - - return nil -} - -func (m *mockToken) Claims(v interface{}) error { - if m.oidcClaimsFunc != nil { - return m.oidcClaimsFunc(v) - } - - return m.oidcClaimsErr -} - -func config(t *testing.T) *Config { - t.Helper() - - storeProv := mem.NewProvider() - - interact, err := redirect.New(&redirect.Config{ - StoreProvider: storeProv, - InteractBasePath: InteractPath, - }) - require.NoError(t, err) - - apConfig := &accesspolicy.Config{} - - err = json.Unmarshal([]byte(accessPolicyConf), apConfig) - require.NoError(t, err) - - return &Config{ - StoreProvider: storeProv, - AccessPolicyConfig: apConfig, - BaseURL: baseURL, - InteractionHandler: interact, - OIDC: &oidcmodel.Config{ - CallbackURL: "http://test.com", - Providers: map[string]*oidcmodel.ProviderConfig{ - "mock1": { - URL: mockoidc.StartProvider(t), - ClientID: uuid.New().String(), - ClientSecret: uuid.New().String(), - }, - "mock2": { - URL: mockoidc.StartProvider(t), - ClientID: uuid.New().String(), - ClientSecret: uuid.New().String(), - }, - }, - }, - BootstrapConfig: &BootstrapConfig{ - DocumentSDSVaultURL: "http://docs.sds.example.org/sds/vaults", - KeySDSVaultURL: "http://keys.sds.example.org/sds/vaults/", - OpsKeyServerURL: "http://ops.kms.example.org/kms/keystores/", - }, - TransientStoreProvider: mem.NewProvider(), - StartupTimeout: 1, - } -} - -func marshal(t *testing.T, v interface{}) []byte { - t.Helper() - - bits, err := json.Marshal(v) - require.NoError(t, err) - - return bits -} - -type errorReader struct { - err error -} - -func (e *errorReader) Read([]byte) (int, error) { - return 0, e.err -} - -func clientKey(t *testing.T) (*jwk.JWK, *gnap.ClientKey) { - t.Helper() - - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - privJWK := jwk.JWK{ - JSONWebKey: jose.JSONWebKey{ - Key: priv, - KeyID: "key1", - Algorithm: "ES256", - }, - Kty: "EC", - Crv: "P-256", - } - - pubJWK := jwk.JWK{ - JSONWebKey: privJWK.Public(), - Kty: "EC", - Crv: "P-256", - } - - ck := gnap.ClientKey{ - Proof: "httpsig", - JWK: pubJWK, - } - - return &privJWK, &ck -} - -func tmpStaticHTML(t *testing.T) (string, func()) { - t.Helper() - - f, err := os.CreateTemp("", "tmpfile-*.html") - require.NoError(t, err) - - defer func() { - e := f.Close() - if e != nil { - fmt.Printf("failed to close tmpfile: %s", e.Error()) - } - }() - - _, err = f.Write([]byte(staticHTML)) - require.NoError(t, err) - - return f.Name(), func() { - e := os.Remove(f.Name()) - if e != nil { - fmt.Printf("failed to delete tmpfile: %s", e.Error()) - } - } -} - -const ( - accessPolicyConf = `{ - "access-types": [{ - "reference": "client-id", - "permission": "NeedsConsent", - "expires-in": 600, - "access": { - "type": "trustbloc.xyz/auth/type/client-id", - "subject-keys": ["sub"], - "userid-key": "sub" - } - }, { - "reference": "other-access", - "permission": "NeedsConsent", - "expires-in": 300, - "access": { - "type": "trustbloc.xyz/auth/type/other-access", - "actions": ["write"], - "datasets": ["foobase"] - } - } - ] -}` - staticHTML = ` - - - -Redirecting... - - - - - - - -` -) diff --git a/pkg/restapi/operation/dependencies.go b/pkg/restapi/operation/dependencies.go index fbd98bd..01d1e54 100644 --- a/pkg/restapi/operation/dependencies.go +++ b/pkg/restapi/operation/dependencies.go @@ -88,10 +88,6 @@ type oauth2Token interface { Extra(string) interface{} } -type httpClient interface { - Do(req *http.Request) (*http.Response, error) -} - // Hydra is the client used to interface with the Hydra service. type Hydra interface { GetLoginRequest(params *admin.GetLoginRequestParams, opts ...admin.ClientOption) (*admin.GetLoginRequestOK, error) diff --git a/pkg/restapi/operation/models.go b/pkg/restapi/operation/models.go index 358d567..d966135 100644 --- a/pkg/restapi/operation/models.go +++ b/pkg/restapi/operation/models.go @@ -6,34 +6,10 @@ SPDX-License-Identifier: Apache-2.0 package operation -// BootstrapData is the user's bootstrap data. -type BootstrapData struct { - DocumentSDSVaultURL string `json:"documentSDSURL"` - KeySDSVaultURL string `json:"keySDSURL"` - AuthZKeyServerURL string `json:"authzKeyServerURL"` - OpsKeyServerURL string `json:"opsKeyServerURL"` - Data map[string]string `json:"data,omitempty"` -} - type oidcClaims struct { Sub string `json:"sub"` } -// UpdateBootstrapDataRequest is a request to update bootstrap data. -type UpdateBootstrapDataRequest struct { - Data map[string]string `json:"data"` -} - -// SetSecretRequest is the payload of a request to set a secret. -type SetSecretRequest struct { - Secret []byte `json:"secret"` -} - -// GetSecretResponse is the response's payload to a request to get a secret. -type GetSecretResponse struct { - Secret string `json:"secret"` -} - type authProviders struct { Providers []authProvider `json:"authProviders"` } diff --git a/pkg/restapi/operation/operations.go b/pkg/restapi/operation/operations.go index d1cd6be..3b4f768 100644 --- a/pkg/restapi/operation/operations.go +++ b/pkg/restapi/operation/operations.go @@ -7,17 +7,17 @@ SPDX-License-Identifier: Apache-2.0 package operation import ( + "bytes" "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" - "crypto/x509" - "encoding/base64" "encoding/json" - "encoding/pem" "errors" "fmt" + "html/template" + "io/ioutil" "net/http" "net/url" "strings" @@ -27,104 +27,121 @@ import ( "github.com/cenkalti/backoff" "github.com/coreos/go-oidc/v3/oidc" "github.com/google/uuid" + "github.com/hyperledger/aries-framework-go/pkg/common/log" "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" "github.com/hyperledger/aries-framework-go/spi/storage" - "github.com/ory/hydra-client-go/client/admin" - "github.com/ory/hydra-client-go/models" "github.com/square/go-jose/v3" - "github.com/trustbloc/edge-core/pkg/log" - tlsutils "github.com/trustbloc/edge-core/pkg/utils/tls" "golang.org/x/oauth2" "github.com/trustbloc/auth/pkg/bootstrap/user" + "github.com/trustbloc/auth/pkg/gnap/accesspolicy" + "github.com/trustbloc/auth/pkg/gnap/api" + "github.com/trustbloc/auth/pkg/gnap/authhandler" "github.com/trustbloc/auth/pkg/internal/common/support" "github.com/trustbloc/auth/pkg/restapi/common" oidcmodel "github.com/trustbloc/auth/pkg/restapi/common/oidc" - "github.com/trustbloc/auth/pkg/restapi/common/store/cookie" "github.com/trustbloc/auth/spi/gnap" + "github.com/trustbloc/auth/spi/gnap/proof/httpsig" ) +var logger = log.New("auth-restapi") //nolint:gochecknoglobals + const ( - hydraLoginPath = "/hydra/login" - hydraConsentPath = "/hydra/consent" - oidcLoginPath = "/oauth2/login" - oidcCallbackPath = "/oauth2/callback" - bootstrapPath = "/bootstrap" - secretsPath = "/secret" - deviceCertPath = "/device" - authProvidersPath = "/oauth2/providers" + gnapBasePath = "/gnap" + // AuthRequestPath endpoint for GNAP authorization request. + AuthRequestPath = gnapBasePath + "/auth" + // AuthContinuePath endpoint for GNAP authorization continuation. + AuthContinuePath = gnapBasePath + "/continue" + // AuthIntrospectPath endpoint for GNAP token introspection. + AuthIntrospectPath = gnapBasePath + "/introspect" + // InteractPath endpoint for GNAP interact. + InteractPath = gnapBasePath + "/interact" + + bootstrapPath = gnapBasePath + "/bootstrap" + + // oidc api handlers. + authProvidersPath = "/oidc/providers" + oidcLoginPath = "/oidc/login" + oidcCallbackPath = "/oidc/callback" + + // GNAP error response codes. + errInvalidRequest = "invalid_request" + errRequestDenied = "request_denied" + // api path params. - providerQueryParam = "provider" - stateCookie = "oauth2_state" - providerCookie = "oauth2_provider" - hydraLoginChallengeCookie = "hydra_login_challenge" + providerQueryParam = "provider" + txnQueryParam = "txnID" - transientStoreName = "transient" + transientStoreName = "gnap_transient" bootstrapStoreName = "bootstrapdata" - secretsStoreName = "secrets" - // redirect url parameter. - userProfileQueryParam = "up" + // client redirect query params. + interactRefQueryParam = "interact_ref" + responseHashQueryParam = "hash" + + gnapScheme = "GNAP " ) -var logger = log.New("auth-restapi") //nolint:gochecknoglobals +// TODO: figure out what logic should go in the access policy vs operation handlers. + +// BootstrapData is the user's bootstrap data. +type BootstrapData struct { + DocumentSDSVaultURL string `json:"documentSDSURL"` + KeySDSVaultURL string `json:"keySDSURL"` + OpsKeyServerURL string `json:"opsKeyServerURL"` + Data map[string]string `json:"data,omitempty"` +} -// Operation defines handlers. +// UpdateBootstrapDataRequest is a request to update bootstrap data. +type UpdateBootstrapDataRequest struct { + Data map[string]string `json:"data"` +} + +// Operation defines Auth Server GNAP handlers. type Operation struct { - client httpClient - requestTokens map[string]string - transientStore storage.Store + authHandler *authhandler.AuthHandler + interactionHandler api.InteractionHandler + introspectHandler common.Introspecter + uiEndpoint string + closePopupHTML string + authProviders []authProvider oidcProvidersConfig map[string]*oidcmodel.ProviderConfig cachedOIDCProviders map[string]oidcProvider - uiEndpoint string - bootstrapStore storage.Store - secretsStore storage.Store - bootstrapConfig *BootstrapConfig - hydra Hydra - deviceRootCerts *x509.CertPool - cookies cookie.Store - secretsToken string + cachedOIDCProvLock sync.RWMutex tlsConfig *tls.Config callbackURL string + baseURL string timeout uint64 - cachedOIDCProvLock sync.RWMutex - authProviders []authProvider - introspectHandler common.Introspecter + transientStore storage.Store + bootstrapStore storage.Store + bootstrapConfig *BootstrapConfig gnapRSClient *gnap.RequestClient } -// Config defines configuration for rp operations. +// Config defines configuration for GNAP operations. type Config struct { - TLSConfig *tls.Config - RequestTokens map[string]string - OIDC *oidcmodel.Config + StoreProvider storage.Provider + AccessPolicyConfig *accesspolicy.Config + BaseURL string + ClosePopupHTML string + InteractionHandler api.InteractionHandler UIEndpoint string + OIDC *oidcmodel.Config + StartupTimeout uint64 TransientStoreProvider storage.Provider - StoreProvider storage.Provider + TLSConfig *tls.Config + DisableHTTPSigVerify bool BootstrapConfig *BootstrapConfig - Hydra Hydra - DeviceRootCerts []string - DeviceCertSystemPool bool - Cookies *CookieConfig - StartupTimeout uint64 - SecretsToken string -} - -// CookieConfig holds cookie configuration. -type CookieConfig struct { - AuthKey []byte - EncKey []byte } // BootstrapConfig holds user bootstrap-related config. type BootstrapConfig struct { DocumentSDSVaultURL string KeySDSVaultURL string - AuthZKeyServerURL string OpsKeyServerURL string } -// New returns rp operation instance. +// New creates GNAP operation handler. func New(config *Config) (*Operation, error) { authProviders := make([]authProvider, 0) @@ -137,65 +154,71 @@ func New(config *Config) (*Operation, error) { authProviders = append(authProviders, prov) } - svc := &Operation{ - client: &http.Client{Transport: &http.Transport{TLSClientConfig: config.TLSConfig}}, - requestTokens: config.RequestTokens, - bootstrapConfig: config.BootstrapConfig, - hydra: config.Hydra, - uiEndpoint: config.UIEndpoint, - cookies: cookie.NewStore(config.Cookies.AuthKey, config.Cookies.EncKey), - secretsToken: config.SecretsToken, - oidcProvidersConfig: config.OIDC.Providers, - tlsConfig: config.TLSConfig, - callbackURL: config.OIDC.CallbackURL, - cachedOIDCProviders: make(map[string]oidcProvider), - timeout: config.StartupTimeout, - authProviders: authProviders, - } - - var err error - - svc.transientStore, err = createStore(config.TransientStoreProvider) + auth, err := authhandler.New(&authhandler.Config{ + StoreProvider: config.StoreProvider, + AccessPolicyConfig: config.AccessPolicyConfig, + ContinuePath: config.BaseURL + AuthContinuePath, + InteractionHandler: config.InteractionHandler, + DisableHTTPSig: config.DisableHTTPSigVerify, + }) if err != nil { - return nil, fmt.Errorf("failed to create store: %w", err) + return nil, err } - svc.bootstrapStore, err = openStore(config.StoreProvider, bootstrapStoreName) + transientStore, err := createStore(config.TransientStoreProvider, transientStoreName) if err != nil { return nil, err } - svc.deviceRootCerts, err = tlsutils.GetCertPool(config.DeviceCertSystemPool, config.DeviceRootCerts) + bootstrapStore, err := createStore(config.StoreProvider, bootstrapStoreName) if err != nil { return nil, err } - svc.secretsStore, err = openStore(config.StoreProvider, secretsStoreName) - if err != nil { - return nil, err + introspectHandler := func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { + return auth.HandleIntrospection(req, &skipVerify{}) } - svc.gnapRSClient, err = createGNAPClient() + gnapRSClient, err := createGNAPClient() if err != nil { return nil, err } - return svc, nil + return &Operation{ + authHandler: auth, + uiEndpoint: config.UIEndpoint, + authProviders: authProviders, + oidcProvidersConfig: config.OIDC.Providers, + cachedOIDCProviders: make(map[string]oidcProvider), + callbackURL: config.BaseURL + oidcCallbackPath, + timeout: config.StartupTimeout, + transientStore: transientStore, + bootstrapStore: bootstrapStore, + tlsConfig: config.TLSConfig, + interactionHandler: config.InteractionHandler, + closePopupHTML: config.ClosePopupHTML, + bootstrapConfig: config.BootstrapConfig, + introspectHandler: introspectHandler, + gnapRSClient: gnapRSClient, + baseURL: config.BaseURL, + }, nil } // GetRESTHandlers get all controller API handler available for this service. func (o *Operation) GetRESTHandlers() []common.Handler { return []common.Handler{ + support.NewHTTPHandler(AuthRequestPath, http.MethodPost, o.authRequestHandler), + // TODO add txn_id to url path + support.NewHTTPHandler(InteractPath, http.MethodGet, o.interactHandler), + support.NewHTTPHandler(AuthContinuePath, http.MethodPost, o.authContinueHandler), + support.NewHTTPHandler(AuthIntrospectPath, http.MethodPost, o.authIntrospectHandler), + support.NewHTTPHandler(authProvidersPath, http.MethodGet, o.authProvidersHandler), - support.NewHTTPHandler(hydraLoginPath, http.MethodGet, o.hydraLoginHandler), support.NewHTTPHandler(oidcLoginPath, http.MethodGet, o.oidcLoginHandler), support.NewHTTPHandler(oidcCallbackPath, http.MethodGet, o.oidcCallbackHandler), - support.NewHTTPHandler(hydraConsentPath, http.MethodGet, o.hydraConsentHandler), + support.NewHTTPHandler(bootstrapPath, http.MethodGet, o.getBootstrapDataHandler), support.NewHTTPHandler(bootstrapPath, http.MethodPost, o.postBootstrapDataHandler), - support.NewHTTPHandler(secretsPath, http.MethodPost, o.postSecretHandler), - support.NewHTTPHandler(secretsPath, http.MethodGet, o.getSecretHandler), - support.NewHTTPHandler(deviceCertPath, http.MethodPost, o.deviceCertHandler), } } @@ -204,95 +227,90 @@ func (o *Operation) SetIntrospectHandler(i common.Introspecter) { o.introspectHandler = i } -func (o *Operation) authProvidersHandler(w http.ResponseWriter, _ *http.Request) { - o.writeResponse(w, &authProviders{Providers: o.authProviders}) -} - -func (o *Operation) hydraLoginHandler(w http.ResponseWriter, r *http.Request) { //nolint:funlen - logger.Debugf("handling login request: %s", r.URL.String()) +func (o *Operation) authRequestHandler(w http.ResponseWriter, req *http.Request) { + logger.Debugf("handling auth request to URL: %s", req.URL.String()) - challenge := r.URL.Query().Get("login_challenge") - if challenge == "" { - o.writeErrorResponse(w, http.StatusBadRequest, "missing challenge on login request") + prevURL := req.URL - return - } + var err error - jar, err := o.cookies.Open(r) + req.URL, err = url.Parse(o.baseURL + req.URL.Path) if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to open cookie store: %s", err.Error()) - - return + req.URL = prevURL } - jar.Set(hydraLoginChallengeCookie, challenge) + authRequest := &gnap.AuthRequest{} - err = jar.Save(r, w) + bodyBytes, err := ioutil.ReadAll(req.Body) if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to save hydra login cookie: %s", err.Error()) + logger.Errorf("error reading request body: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) return } - req := admin.NewGetLoginRequestParams() + req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) - req.SetLoginChallenge(challenge) - - // ensure login request is valid - resp, err := o.hydra.GetLoginRequest(req) - if err != nil { - o.writeErrorResponse(w, - http.StatusBadGateway, "failed to fetch login request from hydra: %s", err.Error()) + if err = json.Unmarshal(bodyBytes, authRequest); err != nil { + logger.Errorf("failed to parse gnap auth request: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errInvalidRequest, + }) return } - // TODO need to check if the relying party (login.Payload.Client.ClientID) is registered: - // https://github.com/trustbloc/auth/issues/53. + v := httpsig.NewVerifier(req) - // fetching the request url from the valid login request to fetch provider (custom parameter) - providerID, err := o.fetchProviderFromURL(resp.Payload.RequestURL) + resp, err := o.authHandler.HandleAccessRequest(authRequest, v, "") if err != nil { - o.writeErrorResponse(w, - http.StatusBadRequest, "failed to fetch the provider name: %s", err.Error()) + logger.Errorf("access policy failed to handle access request: %s", err.Error()) + w.WriteHeader(http.StatusUnauthorized) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) return } - if providerID != "" { - redirectURL := oidcLoginPath + "?" + providerQueryParam + "=" + providerID + o.writeResponse(w, resp) +} - http.Redirect(w, r, redirectURL, http.StatusFound) - logger.Debugf("redirected to oidc login: %s", redirectURL) +func (o *Operation) interactHandler(w http.ResponseWriter, req *http.Request) { + // TODO validate txnID + txnID := req.URL.Query().Get(txnQueryParam) + + redirURL, err := url.Parse(o.uiEndpoint + "/sign-up") + if err != nil { + o.writeErrorResponse(w, http.StatusInternalServerError, "failed to construct redirect url") return } - redirectURL := o.uiEndpoint + q := redirURL.Query() - http.Redirect(w, r, redirectURL, http.StatusFound) - logger.Debugf("redirected to: %s", redirectURL) -} + q.Add(txnQueryParam, txnID) -func (o *Operation) fetchProviderFromURL(requestURL *string) (string, error) { - parsedURL, err := url.Parse(*requestURL) - if err != nil { - return "", fmt.Errorf("failed to parse url: %s", parsedURL) - } + redirURL.RawQuery = q.Encode() - params, err := url.ParseQuery(parsedURL.RawQuery) - if err != nil { - return "", fmt.Errorf("failed to parse raw query for provider name: %s", parsedURL.RawQuery) - } + // redirect to UI + http.Redirect(w, req, redirURL.String(), http.StatusFound) +} - if providerName, ok := params[providerQueryParam]; ok { - return providerName[0], nil - } +func (o *Operation) authProvidersHandler(w http.ResponseWriter, _ *http.Request) { + o.writeResponse(w, &authProviders{Providers: o.authProviders}) +} - return "", nil +type oidcTransientData struct { + Provider string `json:"provider,omitempty"` + TxnID string `json:"txnID,omitempty"` } -func (o *Operation) oidcLoginHandler(w http.ResponseWriter, r *http.Request) { +func (o *Operation) oidcLoginHandler(w http.ResponseWriter, r *http.Request) { // nolint: funlen logger.Debugf("handling request: %s", r.URL.String()) providerID := r.URL.Query().Get(providerQueryParam) @@ -302,28 +320,16 @@ func (o *Operation) oidcLoginHandler(w http.ResponseWriter, r *http.Request) { return } - provider, err := o.getProvider(providerID) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "get provider: %s", err.Error()) - - return - } - - state := uuid.New().String() - - jar, err := o.cookies.Open(r) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to open session cookies: %s", err.Error()) + interactTxnID := r.URL.Query().Get(txnQueryParam) + if interactTxnID == "" { + o.writeErrorResponse(w, http.StatusBadRequest, "missing transaction ID") return } - jar.Set(stateCookie, state) - jar.Set(providerCookie, provider.Name()) - - err = jar.Save(r, w) + provider, err := o.getProvider(providerID) if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to persist session cookies: %w", err.Error()) + o.writeErrorResponse(w, http.StatusBadRequest, "get provider: %s", err.Error()) return } @@ -342,6 +348,29 @@ func (o *Operation) oidcLoginHandler(w http.ResponseWriter, r *http.Request) { scopes = append(scopes, "profile", "email") } + state := uuid.New().String() + + data := &oidcTransientData{ + Provider: providerID, + TxnID: interactTxnID, + } + + dataBytes, err := json.Marshal(data) + if err != nil { + o.writeErrorResponse(w, http.StatusInternalServerError, + fmt.Sprintf("failed to marshal oidc txn data : %s", err)) + + return + } + + err = o.transientStore.Put(state, dataBytes) + if err != nil { + o.writeErrorResponse(w, + http.StatusInternalServerError, fmt.Sprintf("failed to write state data to transient store: %s", err)) + + return + } + authOption := oauth2.SetAuthURLParam(providerQueryParam, providerID) redirectURL := provider.OAuth2Config( scopes..., @@ -352,7 +381,7 @@ func (o *Operation) oidcLoginHandler(w http.ResponseWriter, r *http.Request) { logger.Debugf("redirected to: %s", redirectURL) } -func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) { //nolint:funlen,gocyclo +func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) { // nolint:funlen,gocyclo state := r.URL.Query().Get("state") if state == "" { o.writeErrorResponse(w, http.StatusBadRequest, "missing state") @@ -367,41 +396,28 @@ func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) return } - jar, err := o.cookies.Open(r) + // get state and provider details from transient store + dataBytes, err := o.transientStore.Get(state) if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to get cookies: %s", err.Error()) - - return - } - - cookieState, found := jar.Get(stateCookie) - if !found { - o.writeErrorResponse(w, http.StatusBadRequest, "missing state cookie") + o.writeErrorResponse(w, + http.StatusBadRequest, fmt.Sprintf("failed to get state data to transient store: %s", err)) return } - if state != cookieState { - o.writeErrorResponse(w, http.StatusBadRequest, "invalid state parameter") + data := &oidcTransientData{} - return - } - - cookieProvider, found := jar.Get(providerCookie) - if !found { - o.writeErrorResponse(w, http.StatusBadRequest, "missing provider cookie") + err = json.Unmarshal(dataBytes, data) + if err != nil { + o.writeErrorResponse(w, http.StatusInternalServerError, + fmt.Sprintf("failed to parse oidc txn data : %s", err)) return } - hydraLoginChallenge, found := jar.Get(hydraLoginChallengeCookie) - if !found { - o.writeErrorResponse(w, http.StatusBadRequest, "missing hydra login challenge cookie") - - return - } + providerID := data.Provider - provider, err := o.getProvider(fmt.Sprintf("%s", cookieProvider)) + provider, err := o.getProvider(providerID) if err != nil { o.writeErrorResponse(w, http.StatusBadRequest, "get provider : %s", err.Error()) @@ -450,112 +466,118 @@ func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) } } + interactRef, responseHash, clientInteract, err := o.interactionHandler.CompleteInteraction( + data.TxnID, + &api.ConsentResult{ + SubjectData: map[string]string{ + "sub": claims.Sub, + }, + }, + ) if err != nil { o.writeErrorResponse(w, http.StatusInternalServerError, - fmt.Sprintf("failed to fetch user profile from store : %s", err)) + fmt.Sprintf("failed to complete GNAP interaction : %s", err)) return } - jar.Delete(hydraLoginChallengeCookie) - jar.Delete(stateCookie) - jar.Delete(providerCookie) - - err = jar.Save(r, w) + clientURI, err := url.Parse(clientInteract.Finish.URI) if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to delete cookies: %s", err.Error()) + o.writeErrorResponse(w, http.StatusBadRequest, "client provided invalid redirect URI : %s", err.Error()) return } - accept := admin.NewAcceptLoginRequestParams() + // TODO: validate clientURI for security - accept.SetLoginChallenge(fmt.Sprintf("%s", hydraLoginChallenge)) - accept.SetBody(&models.AcceptLoginRequest{ - Subject: &claims.Sub, - }) + q := clientURI.Query() + + q.Add(interactRefQueryParam, interactRef) + q.Add(responseHashQueryParam, responseHash) - loginResponse, err := o.hydra.AcceptLoginRequest(accept) + clientURI.RawQuery = q.Encode() + + redirect := clientURI.String() + + t, err := template.ParseFiles(o.closePopupHTML) if err != nil { - o.writeErrorResponse(w, http.StatusBadGateway, - "hydra failed to accept login request : %s", err.Error()) + o.writeErrorResponse(w, http.StatusInternalServerError, "failed to parse template : %s", err.Error()) return } - redirectURL := *loginResponse.Payload.RedirectTo - - http.Redirect(w, r, redirectURL, http.StatusFound) - logger.Debugf("redirected to: %s", redirectURL) + if err := t.Execute(w, map[string]interface{}{ + "RedirectURI": redirect, + }); err != nil { + logger.Errorf(fmt.Sprintf("failed execute html template: %s", err.Error())) + } } -func (o *Operation) hydraConsentHandler(w http.ResponseWriter, r *http.Request) { - logger.Debugf("handling request: %s", r.URL.String()) +func (o *Operation) authContinueHandler(w http.ResponseWriter, req *http.Request) { // nolint: funlen + logger.Debugf("handling continue request to URL: %s", req.URL.String()) - challenge := r.URL.Query().Get("consent_challenge") - if challenge == "" { - o.writeErrorResponse(w, http.StatusBadRequest, "missing consent_challenge") + prevURL := req.URL - return - } - - req := admin.NewGetConsentRequestParamsWithContext(r.Context()) - req.SetConsentChallenge(challenge) + var err error - consent, err := o.hydra.GetConsentRequest(req) + req.URL, err = url.Parse(o.baseURL + req.URL.Path) if err != nil { - o.writeErrorResponse(w, http.StatusBadGateway, - "failed to fetch consent request from hydra : %s", err) - - return + req.URL = prevURL } - // ensure user exists - _, err = user.NewStore(o.bootstrapStore).Get(consent.Payload.Subject) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to query for user profile: %s", err.Error()) + tokHeader := strings.Split(strings.Trim(req.Header.Get("Authorization"), " "), " ") + + if len(tokHeader) < 2 || tokHeader[0] != "GNAP" { + logger.Errorf("GNAP continuation endpoint requires GNAP token") + w.WriteHeader(http.StatusUnauthorized) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) return } - params := admin.NewAcceptConsentRequestParams() + token := tokHeader[1] - params.SetContext(r.Context()) - params.SetConsentChallenge(challenge) - params.SetBody(&models.AcceptConsentRequest{ - GrantAccessTokenAudience: consent.Payload.RequestedAccessTokenAudience, - GrantScope: consent.Payload.RequestedScope, - HandledAt: models.NullTime(time.Now()), - Remember: true, - Session: nil, - }) + continueRequest := &gnap.ContinueRequest{} - accepted, err := o.hydra.AcceptConsentRequest(params) + bodyBytes, err := ioutil.ReadAll(req.Body) if err != nil { - o.writeErrorResponse(w, http.StatusBadGateway, "hydra failed to accept consent request: %s", err.Error()) + logger.Errorf("error reading request body: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) return } - redirectURL := *accepted.Payload.RedirectTo + req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) - http.Redirect(w, r, redirectURL, http.StatusFound) - logger.Debugf("redirected to: %s", redirectURL) -} + if err = json.Unmarshal(bodyBytes, continueRequest); err != nil { + logger.Errorf("failed to parse gnap continue request: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errInvalidRequest, + }) -// TODO onboard user at key server and SDS: https://github.com/trustbloc/auth/issues/38 -func (o *Operation) onboardUser(sub string) (*user.Profile, error) { - userProfile := &user.Profile{ - ID: sub, - Data: make(map[string]string), + return } - err := user.NewStore(o.bootstrapStore).Save(userProfile) + v := httpsig.NewVerifier(req) + + resp, err := o.authHandler.HandleContinueRequest(continueRequest, token, v) if err != nil { - return nil, fmt.Errorf("failed to save user profile : %w", err) + logger.Errorf("access policy failed to handle continue request: %s", err.Error()) + w.WriteHeader(http.StatusUnauthorized) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) + + return } - return userProfile, nil + o.writeResponse(w, resp) } func (o *Operation) getBootstrapDataHandler(w http.ResponseWriter, r *http.Request) { @@ -583,7 +605,6 @@ func (o *Operation) getBootstrapDataHandler(w http.ResponseWriter, r *http.Reque response, err := json.Marshal(&BootstrapData{ DocumentSDSVaultURL: o.bootstrapConfig.DocumentSDSVaultURL, KeySDSVaultURL: o.bootstrapConfig.KeySDSVaultURL, - AuthZKeyServerURL: o.bootstrapConfig.AuthZKeyServerURL, OpsKeyServerURL: o.bootstrapConfig.OpsKeyServerURL, Data: profile.Data, }) @@ -643,216 +664,80 @@ func (o *Operation) postBootstrapDataHandler(w http.ResponseWriter, r *http.Requ logger.Debugf("finished handling request") } -func (o *Operation) postSecretHandler(w http.ResponseWriter, r *http.Request) { - logger.Debugf("handling request") - - subject, proceed := o.subject(w, r) - if !proceed { - return - } - - // ensure user exists - _, err := user.NewStore(o.bootstrapStore).Get(subject) - if errors.Is(err, storage.ErrDataNotFound) { - o.writeErrorResponse(w, http.StatusConflict, "no such user") - - return - } - - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to query bootstrap store: %s", err.Error()) - - return - } - - // ensure secret is not set already - _, err = o.secretsStore.Get(subject) - if err == nil { - o.writeErrorResponse(w, http.StatusConflict, "secret already set") - - return - } - - if !errors.Is(err, storage.ErrDataNotFound) { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to query secrets store: %s", err.Error()) - - return - } - - payload := &SetSecretRequest{} - - err = json.NewDecoder(r.Body).Decode(payload) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "failed to decode payload: %s", err.Error()) - - return - } - - err = o.secretsStore.Put(subject, payload.Secret) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to save to secrets store: %s", err.Error()) +type skipVerify struct{} - return - } - - logger.Debugf("finished handling request") +// Verify skip request verification when introspecting internally through Go. +func (s skipVerify) Verify(_ *gnap.ClientKey) error { + return nil } -func (o *Operation) getSecretHandler(w http.ResponseWriter, r *http.Request) { - logger.Debugf("handling request") - - token, proceed := o.bearerToken(w, r) - if !proceed { - return - } - - if token != o.secretsToken { - o.writeErrorResponse(w, http.StatusForbidden, "unauthorized") - - return - } - - sub := r.URL.Query().Get("sub") - if sub == "" { - o.writeErrorResponse(w, http.StatusBadRequest, "missing parameter") - - return - } - - secret, err := o.secretsStore.Get(sub) - if errors.Is(err, storage.ErrDataNotFound) { - o.writeErrorResponse(w, http.StatusBadRequest, "non-existent user") - - return - } - - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to query secrets store: %s", err.Error()) +// InternalIntrospectHandler returns a handler that allows the auth server's handlers to perform GNAP introspection +// with itself as the AS and RS. +func (o *Operation) InternalIntrospectHandler() common.Introspecter { + return o.introspectHandler +} - return - } +func (o *Operation) authIntrospectHandler(w http.ResponseWriter, req *http.Request) { + logger.Debugf("handling introspect request to URL: %s", req.URL.String()) - response, err := json.Marshal(&GetSecretResponse{ - Secret: base64.StdEncoding.EncodeToString(secret), - }) - if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to encode response: %s", err.Error()) + prevURL := req.URL - return - } + var err error - _, err = w.Write(response) + req.URL, err = url.Parse(o.baseURL + req.URL.Path) if err != nil { - logger.Errorf("failed to write response: %s", err.Error()) - - return + req.URL = prevURL } - logger.Debugf("finished handling request") -} - -type certHolder struct { - X5C []string `json:"x5c"` - Sub string `json:"sub"` - AAGUID string `json:"aaguid,omitempty"` -} - -func (o *Operation) deviceCertHandler(w http.ResponseWriter, r *http.Request) { - dec := json.NewDecoder(r.Body) + introspectRequest := &gnap.IntrospectRequest{} - var ch certHolder - - err := dec.Decode(&ch) + bodyBytes, err := ioutil.ReadAll(req.Body) if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "cert request invalid json") + logger.Errorf("error reading request body: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) return } - userProfile, err := user.NewStore(o.bootstrapStore).Get(ch.Sub) - if errors.Is(err, storage.ErrDataNotFound) { - o.writeErrorResponse(w, http.StatusBadRequest, "invalid user profile id") + req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) - return - } else if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, "failed to load user profile") + if err = json.Unmarshal(bodyBytes, introspectRequest); err != nil { + logger.Errorf("failed to parse gnap introspection request: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errInvalidRequest, + }) return } - err = o.verifyDeviceCert(&ch) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, err.Error()) - } - - userProfile.AAGUID = ch.AAGUID + v := httpsig.NewVerifier(req) - profileBytes, err := json.Marshal(userProfile) + resp, err := o.authHandler.HandleIntrospection(introspectRequest, v) if err != nil { - o.writeErrorResponse(w, http.StatusInternalServerError, - fmt.Sprintf("failed to marshal user profile data : %s", err)) + logger.Errorf("failed to handle gnap introspection request: %s", err.Error()) + w.WriteHeader(http.StatusUnauthorized) + o.writeResponse(w, &gnap.ErrorResponse{ + Error: errRequestDenied, + }) return } - o.handleAuthResult(w, r, profileBytes) -} - -func (o *Operation) verifyDeviceCert(ch *certHolder) error { - if len(ch.X5C) == 0 { - return errors.New("missing device certificate") - } - - var certs []*x509.Certificate - - for _, x5c := range ch.X5C { - block, _ := pem.Decode([]byte(x5c)) - if block == nil || block.Bytes == nil { - return errors.New("can't parse certificate PEM") - } - - cert, e := x509.ParseCertificate(block.Bytes) - if e != nil { - return errors.New("can't parse certificate") - } - - certs = append(certs, cert) - } - - // first element is cert to verify - deviceCert := certs[0] - // any additional certs are intermediate certs - intermediateCerts := certs[1:] - - intermediatePool := x509.NewCertPool() - - for _, iCert := range intermediateCerts { - intermediatePool.AddCert(iCert) - } - - _, err := deviceCert.Verify(x509.VerifyOptions{Intermediates: intermediatePool, Roots: o.deviceRootCerts}) - if err != nil { - return errors.New("cert chain fails to authenticate") - } - - return nil + o.writeResponse(w, resp) } -func (o *Operation) handleAuthResult(w http.ResponseWriter, r *http.Request, profileBytes []byte) { - handle := url.QueryEscape(uuid.New().String()) +// WriteResponse writes interface value to response. +func (o *Operation) writeResponse(rw http.ResponseWriter, v interface{}) { + rw.Header().Set("Content-Type", "application/json") - err := o.transientStore.Put(handle, profileBytes) + err := json.NewEncoder(rw).Encode(v) if err != nil { - o.writeErrorResponse(w, - http.StatusInternalServerError, fmt.Sprintf("failed to write handle to transient store: %s", err)) - - return + logger.Errorf("Unable to send response: %s", err.Error()) } - - redirectURL := fmt.Sprintf("%s?%s=%s", o.uiEndpoint, userProfileQueryParam, handle) - - http.Redirect(w, r, redirectURL, http.StatusFound) - logger.Debugf("redirected to: %s", redirectURL) } // writeResponse writes interface value to response. @@ -867,116 +752,6 @@ func (o *Operation) writeErrorResponse(rw http.ResponseWriter, status int, msg s } } -// WriteResponse writes interface value to response. -func (o *Operation) writeResponse(rw http.ResponseWriter, v interface{}) { - rw.Header().Set("Content-Type", "application/json") - - err := json.NewEncoder(rw).Encode(v) - if err != nil { - logger.Errorf("Unable to send response, %s", err.Error()) - } -} - -const ( - bearerScheme = "Bearer " - gnapScheme = "GNAP " -) - -func (o *Operation) subject(w http.ResponseWriter, r *http.Request) (string, bool) { - authHeader := strings.TrimSpace(r.Header.Get("authorization")) - if authHeader == "" { - o.writeErrorResponse(w, http.StatusForbidden, "no credentials") - - return "", false - } - - switch { - case strings.HasPrefix(authHeader, bearerScheme): - return o.oidcSub(w, r, authHeader) - case strings.HasPrefix(authHeader, gnapScheme): - return o.gnapSub(w, r, authHeader) - default: - o.writeErrorResponse(w, http.StatusBadRequest, "invalid authorization scheme") - - return "", false - } -} - -func (o *Operation) oidcSub(w http.ResponseWriter, r *http.Request, authHeader string) (string, bool) { - encoded := authHeader[len(bearerScheme):] - - token, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "failed to decode token: %s", err.Error()) - - return "", false - } - - request := admin.NewIntrospectOAuth2TokenParams() - request.SetContext(r.Context()) - request.SetToken(string(token)) - - introspection, err := o.hydra.IntrospectOAuth2Token(request) - if err != nil { - o.writeErrorResponse(w, http.StatusBadGateway, "failed to introspect token: %s", err.Error()) - - return "", false - } - - return introspection.Payload.Sub, true -} - -func (o *Operation) gnapSub(w http.ResponseWriter, _ *http.Request, authHeader string) (string, bool) { - token := authHeader[len(gnapScheme):] - - introspection, err := o.introspectHandler(&gnap.IntrospectRequest{ - AccessToken: token, - ResourceServer: o.gnapRSClient, - }) - if err != nil { - o.writeErrorResponse(w, http.StatusUnauthorized, "failed to introspect token: %s", err.Error()) - - return "", false - } - - if sub, ok := introspection.SubjectData["sub"]; ok { - return sub, true - } - - o.writeErrorResponse(w, http.StatusUnauthorized, "token does not grant access to subject id") - - return "", false -} - -func (o *Operation) bearerToken(w http.ResponseWriter, r *http.Request) (string, bool) { - const scheme = "Bearer " - - // https://tools.ietf.org/html/rfc6750#section-2.1 - authHeader := strings.TrimSpace(r.Header.Get("authorization")) - if authHeader == "" { - o.writeErrorResponse(w, http.StatusForbidden, "no credentials") - - return "", false - } - - if !strings.HasPrefix(authHeader, scheme) { - o.writeErrorResponse(w, http.StatusBadRequest, "invalid authorization scheme") - - return "", false - } - - encoded := authHeader[len(scheme):] - - token, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - o.writeErrorResponse(w, http.StatusBadRequest, "failed to decode token: %s", err.Error()) - - return "", false - } - - return string(token), true -} - func (o *Operation) getProvider(providerID string) (oidcProvider, error) { o.cachedOIDCProvLock.RLock() prov, ok := o.cachedOIDCProviders[providerID] @@ -1003,47 +778,6 @@ func (o *Operation) getProvider(providerID string) (oidcProvider, error) { return prov, nil } -func createStore(p storage.Provider) (storage.Store, error) { - s, err := p.OpenStore(transientStoreName) - if err != nil { - return nil, fmt.Errorf("failed to open store [%s] : %w", transientStoreName, err) - } - - return s, nil -} - -func openStore(provider storage.Provider, name string) (storage.Store, error) { - s, err := provider.OpenStore(name) - if err != nil { - return nil, fmt.Errorf("failed to open store [%s] : %w", transientStoreName, err) - } - - return s, nil -} - -func createGNAPClient() (*gnap.RequestClient, error) { - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, fmt.Errorf("creating public key for GNAP RS role: %w", err) - } - - return &gnap.RequestClient{ - IsReference: false, - Key: &gnap.ClientKey{ - Proof: "httpsig", - JWK: jwk.JWK{ - JSONWebKey: jose.JSONWebKey{ - Key: &priv.PublicKey, - KeyID: "key1", - Algorithm: "ES256", - }, - Kty: "EC", - Crv: "P-256", - }, - }, - }, nil -} - func (o *Operation) initOIDCProvider(providerID string, config *oidcmodel.ProviderConfig) (oidcProvider, error) { var idp *oidc.Provider @@ -1093,6 +827,92 @@ func (o *Operation) initOIDCProvider(providerID string, config *oidcmodel.Provid }, nil } +func createStore(p storage.Provider, name string) (storage.Store, error) { + s, err := p.OpenStore(name) + if err != nil { + return nil, fmt.Errorf("failed to open store [%s]: %w", name, err) + } + + return s, nil +} + +func createGNAPClient() (*gnap.RequestClient, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("creating public key for GNAP RS role: %w", err) + } + + return &gnap.RequestClient{ + IsReference: false, + Key: &gnap.ClientKey{ + Proof: "httpsig", + JWK: jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: &priv.PublicKey, + KeyID: "key2", + Algorithm: "ES256", + }, + Kty: "EC", + Crv: "P-256", + }, + }, + }, nil +} + +func (o *Operation) onboardUser(sub string) (*user.Profile, error) { + userProfile := &user.Profile{ + ID: sub, + Data: make(map[string]string), + } + + err := user.NewStore(o.bootstrapStore).Save(userProfile) + if err != nil { + return nil, fmt.Errorf("failed to save user profile : %w", err) + } + + return userProfile, nil +} + +func (o *Operation) subject(w http.ResponseWriter, r *http.Request) (string, bool) { + authHeader := strings.TrimSpace(r.Header.Get("authorization")) + if authHeader == "" { + o.writeErrorResponse(w, http.StatusForbidden, "no credentials") + + return "", false + } + + switch { + case strings.HasPrefix(authHeader, gnapScheme): + return o.gnapSub(w, r, authHeader) + default: + o.writeErrorResponse(w, http.StatusBadRequest, "invalid authorization scheme") + + return "", false + } +} + +func (o *Operation) gnapSub(w http.ResponseWriter, _ *http.Request, authHeader string) (string, bool) { + token := authHeader[len(gnapScheme):] + + introspection, err := o.introspectHandler(&gnap.IntrospectRequest{ + AccessToken: token, + ResourceServer: o.gnapRSClient, + }) + if err != nil { + o.writeErrorResponse(w, http.StatusUnauthorized, "failed to introspect token: %s", err.Error()) + + return "", false + } + + if sub, ok := introspection.SubjectData["sub"]; ok { + return sub, true + } + + o.writeErrorResponse(w, http.StatusUnauthorized, "token does not grant access to subject id") + + return "", false +} + func merge(existing *user.Profile, update *UpdateBootstrapDataRequest) *user.Profile { merged := &user.Profile{ ID: existing.ID, diff --git a/pkg/restapi/operation/operations_test.go b/pkg/restapi/operation/operations_test.go index 095891b..149a311 100644 --- a/pkg/restapi/operation/operations_test.go +++ b/pkg/restapi/operation/operations_test.go @@ -9,62 +9,63 @@ package operation import ( "bytes" "context" - "crypto/aes" - "crypto/ed25519" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/x509" - "crypto/x509/pkix" - "encoding/base64" "encoding/json" - "encoding/pem" "errors" "fmt" - "math" - "math/big" "net/http" "net/http/httptest" + "net/url" + "os" + "regexp" "strings" "testing" - "time" "github.com/google/uuid" "github.com/hyperledger/aries-framework-go/component/storageutil/mem" + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" mockstore "github.com/hyperledger/aries-framework-go/pkg/mock/storage" "github.com/hyperledger/aries-framework-go/spi/storage" - "github.com/ory/hydra-client-go/client/admin" - "github.com/ory/hydra-client-go/models" + "github.com/square/go-jose/v3" "github.com/stretchr/testify/require" "golang.org/x/oauth2" "github.com/trustbloc/auth/pkg/bootstrap/user" + "github.com/trustbloc/auth/pkg/gnap/accesspolicy" + "github.com/trustbloc/auth/pkg/gnap/api" + "github.com/trustbloc/auth/pkg/gnap/interact/redirect" + "github.com/trustbloc/auth/pkg/internal/common/mockinteract" "github.com/trustbloc/auth/pkg/internal/common/mockoidc" "github.com/trustbloc/auth/pkg/internal/common/mockstorage" oidcmodel "github.com/trustbloc/auth/pkg/restapi/common/oidc" - "github.com/trustbloc/auth/pkg/restapi/common/store/cookie" "github.com/trustbloc/auth/spi/gnap" + "github.com/trustbloc/auth/spi/gnap/proof/httpsig" +) + +const ( + baseURL = "http://test.auth" ) func TestNew(t *testing.T) { t.Run("success", func(t *testing.T) { - config := config(t) - svc, err := New(config) + o, err := New(config(t)) require.NoError(t, err) - require.NotNil(t, svc) - require.NotEmpty(t, svc.GetRESTHandlers()) + require.NotNil(t, o) }) - t.Run("success, bootstrap store already exists", func(t *testing.T) { - config := config(t) + t.Run("failure", func(t *testing.T) { + conf := config(t) - config.TransientStoreProvider = mem.NewProvider() + expectErr := errors.New("expected error") - _, err := config.TransientStoreProvider.OpenStore(bootstrapStoreName) - require.NoError(t, err) + conf.StoreProvider = &mockstorage.Provider{ErrOpenStoreHandle: expectErr} - svc, err := New(config) - require.NoError(t, err) - require.NotNil(t, svc) - require.NotEmpty(t, svc.GetRESTHandlers()) + o, err := New(conf) + require.Error(t, err) + require.ErrorIs(t, err, expectErr) + require.Nil(t, o) }) t.Run("error if unable to open transient store", func(t *testing.T) { @@ -75,24 +76,277 @@ func TestNew(t *testing.T) { _, err := New(config) require.Error(t, err) }) +} + +func TestOperation_GetRESTHandlers(t *testing.T) { + o := &Operation{} - t.Run("error if device certificate root CAs are invalid", func(t *testing.T) { + h := o.GetRESTHandlers() + require.Len(t, h, 9) +} + +func TestOperation_AuthProvidersHandler(t *testing.T) { + t.Run("success", func(t *testing.T) { config := config(t) - config.DeviceRootCerts = []string{"invalid"} + o, err := New(config) + require.NoError(t, err) - _, err := New(config) - require.Error(t, err) + w := httptest.NewRecorder() + o.authProvidersHandler(w, nil) + + require.Equal(t, http.StatusOK, w.Code) + var resp *authProviders + err = json.Unmarshal(w.Body.Bytes(), &resp) + require.NoError(t, err) + + require.Equal(t, 2, len(resp.Providers)) }) +} - t.Run("error if cannot open secrets store", func(t *testing.T) { - config := config(t) - config.StoreProvider = &mockstore.MockStoreProvider{ - FailNamespace: secretsStoreName, - Store: &mockstore.MockStore{}, +func TestOperation_authRequestHandler(t *testing.T) { + t.Run("fail to read body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + expectErr := errors.New("expected error") + + req := httptest.NewRequest(http.MethodPost, AuthRequestPath, &errorReader{err: expectErr}) + + o.authRequestHandler(rw, req) + + require.Equal(t, http.StatusInternalServerError, rw.Code) + }) + + t.Run("fail to parse empty request body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, AuthRequestPath, nil) + + o.authRequestHandler(rw, req) + + require.Equal(t, http.StatusBadRequest, rw.Code) + }) + + t.Run("auth handler error", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, AuthRequestPath, bytes.NewReader([]byte("{}"))) + + o.authRequestHandler(rw, req) + + require.Equal(t, http.StatusUnauthorized, rw.Code) + }) + + t.Run("success", func(t *testing.T) { + o, err := New(config(t)) + require.NoError(t, err) + + priv, client := clientKey(t) + + authReq := &gnap.AuthRequest{ + Client: &gnap.RequestClient{ + IsReference: false, + Key: client, + }, } - _, err := New(config) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to open store for name space secrets") + + authReqBytes, err := json.Marshal(authReq) + require.NoError(t, err) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, baseURL+AuthRequestPath, bytes.NewReader(authReqBytes)) + + req, err = httpsig.Sign(req, authReqBytes, priv, "sha-256") + require.NoError(t, err) + + o.authRequestHandler(rw, req) + + require.Equal(t, http.StatusOK, rw.Code) + }) +} + +func TestOperation_interactHandler(t *testing.T) { + t.Run("success", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, InteractPath, nil) + + o.interactHandler(rw, req) + + require.Equal(t, http.StatusFound, rw.Code) + }) +} + +func TestOperation_authContinueHandler(t *testing.T) { + t.Run("missing Auth token", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, AuthContinuePath, nil) + + o.authContinueHandler(rw, req) + + require.Equal(t, http.StatusUnauthorized, rw.Code) + + resp := &gnap.ErrorResponse{} + require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) + require.Equal(t, errRequestDenied, resp.Error) + }) + + t.Run("Auth token not GNAP token", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, AuthContinuePath, nil) + req.Header.Add("Authorization", "Bearer mock-token") + + o.authContinueHandler(rw, req) + + require.Equal(t, http.StatusUnauthorized, rw.Code) + + resp := &gnap.ErrorResponse{} + require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) + require.Equal(t, errRequestDenied, resp.Error) + }) + + t.Run("fail to read request body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + expectErr := errors.New("expected error") + + req := httptest.NewRequest(http.MethodPost, AuthContinuePath, &errorReader{err: expectErr}) + req.Header.Add("Authorization", "GNAP mock-token") + + o.authContinueHandler(rw, req) + + require.Equal(t, http.StatusInternalServerError, rw.Code) + + resp := &gnap.ErrorResponse{} + require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) + require.Equal(t, errRequestDenied, resp.Error) + }) + + t.Run("fail to parse empty request body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, AuthContinuePath, nil) + req.Header.Add("Authorization", "GNAP mock-token") + + o.authContinueHandler(rw, req) + + require.Equal(t, http.StatusBadRequest, rw.Code) + + resp := &gnap.ErrorResponse{} + require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) + require.Equal(t, errInvalidRequest, resp.Error) + }) + + t.Run("auth handler error", func(t *testing.T) { + o, err := New(config(t)) + require.NoError(t, err) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, AuthContinuePath, bytes.NewReader([]byte("{}"))) + req.Header.Add("Authorization", "GNAP mock-token") + + o.authContinueHandler(rw, req) + + require.Equal(t, http.StatusUnauthorized, rw.Code) + + resp := &gnap.ErrorResponse{} + require.NoError(t, json.Unmarshal(rw.Body.Bytes(), resp)) + require.Equal(t, errRequestDenied, resp.Error) + }) +} + +func TestOperation_authIntrospectHandler(t *testing.T) { + t.Run("fail to read request body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + expectErr := errors.New("expected error") + + req := httptest.NewRequest(http.MethodPost, AuthRequestPath, &errorReader{err: expectErr}) + + o.authIntrospectHandler(rw, req) + + require.Equal(t, http.StatusInternalServerError, rw.Code) + }) + + t.Run("fail to parse empty request body", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, AuthRequestPath, nil) + + o.authIntrospectHandler(rw, req) + + require.Equal(t, http.StatusBadRequest, rw.Code) + }) + + t.Run("auth handler error", func(t *testing.T) { + o := &Operation{} + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, AuthRequestPath, bytes.NewReader([]byte("{}"))) + + o.authIntrospectHandler(rw, req) + + require.Equal(t, http.StatusUnauthorized, rw.Code) + }) + + t.Run("requested token does not exist", func(t *testing.T) { + o, err := New(config(t)) + require.NoError(t, err) + + priv, client := clientKey(t) + + intReq := &gnap.IntrospectRequest{ + AccessToken: "invalid token", + Proof: "httpsig", + ResourceServer: &gnap.RequestClient{ + Key: client, + }, + } + + intReqBytes, err := json.Marshal(intReq) + require.NoError(t, err) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, baseURL+AuthIntrospectPath, bytes.NewReader(intReqBytes)) + + req, err = httpsig.Sign(req, intReqBytes, priv, "sha-256") + require.NoError(t, err) + + o.authIntrospectHandler(rw, req) + + require.Equal(t, http.StatusOK, rw.Code) + + resp := &gnap.IntrospectResponse{} + + err = json.Unmarshal(rw.Body.Bytes(), resp) + require.NoError(t, err) + + require.False(t, resp.Active) }) } @@ -102,13 +356,12 @@ func TestOIDCLoginHandler(t *testing.T) { config := config(t) svc, err := New(config) require.NoError(t, err) - svc.cookies = mockCookies() svc.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{}, } svc.oidcProvidersConfig = map[string]*oidcmodel.ProviderConfig{provider: {}} w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest(provider)) + svc.oidcLoginHandler(w, newOIDCLoginRequest(provider, "foo")) require.Equal(t, http.StatusFound, w.Code) require.NotEmpty(t, w.Header().Get("location")) }) @@ -118,32 +371,29 @@ func TestOIDCLoginHandler(t *testing.T) { config := config(t) svc, err := New(config) require.NoError(t, err) - svc.cookies = mockCookies() svc.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{}, } w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest(provider)) + svc.oidcLoginHandler(w, newOIDCLoginRequest(provider, "foo")) require.Equal(t, http.StatusInternalServerError, w.Code) }) - t.Run("internal server error if cannot open cookie store", func(t *testing.T) { - svc, err := New(config(t)) + t.Run("bad request if provider is missing", func(t *testing.T) { + config := config(t) + svc, err := New(config) require.NoError(t, err) - svc.cookies = &cookie.MockStore{ - OpenErr: errors.New("test"), - } w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest("mock1")) - require.Equal(t, http.StatusInternalServerError, w.Code) + svc.oidcLoginHandler(w, newOIDCLoginRequest("", "")) + require.Equal(t, http.StatusBadRequest, w.Code) }) - t.Run("bad request if provider is missing", func(t *testing.T) { + t.Run("bad request if txn ID is missing", func(t *testing.T) { config := config(t) svc, err := New(config) require.NoError(t, err) w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest("")) + svc.oidcLoginHandler(w, newOIDCLoginRequest("foo", "")) require.Equal(t, http.StatusBadRequest, w.Code) }) @@ -151,28 +401,29 @@ func TestOIDCLoginHandler(t *testing.T) { svc, err := New(config(t)) require.NoError(t, err) result := httptest.NewRecorder() - svc.oidcLoginHandler(result, newOIDCLoginRequest("unsupported")) + svc.oidcLoginHandler(result, newOIDCLoginRequest("unsupported", "foo")) require.Equal(t, http.StatusBadRequest, result.Code) require.Contains(t, result.Body.String(), "provider not supported") }) - t.Run("internal server error if cannot save cookies", func(t *testing.T) { + t.Run("store error", func(t *testing.T) { provider := uuid.New().String() config := config(t) svc, err := New(config) require.NoError(t, err) - svc.cookies = &cookie.MockStore{ - Jar: &cookie.MockJar{ - SaveErr: errors.New("test"), - }, - } svc.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{}, } - w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest(provider)) - require.Equal(t, http.StatusInternalServerError, w.Code) - require.Contains(t, w.Body.String(), "failed to persist session cookies") + svc.oidcProvidersConfig = map[string]*oidcmodel.ProviderConfig{provider: {}} + svc.transientStore = &mockstore.MockStore{ + ErrPut: errors.New("generic"), + } + + result := httptest.NewRecorder() + svc.oidcLoginHandler(result, newOIDCLoginRequest(provider, "foo")) + + require.Contains(t, result.Body.String(), "failed to write state data to transient store") + require.Equal(t, http.StatusInternalServerError, result.Code) }) t.Run("error if oidc provider is invalid", func(t *testing.T) { @@ -185,10 +436,9 @@ func TestOIDCLoginHandler(t *testing.T) { svc, err := New(config) require.NoError(t, err) - svc.cookies = mockCookies() w := httptest.NewRecorder() - svc.oidcLoginHandler(w, newOIDCLoginRequest("test")) + svc.oidcLoginHandler(w, newOIDCLoginRequest("test", "foo")) require.Equal(t, http.StatusBadRequest, w.Code) require.Contains(t, w.Body.String(), "failed to init oidc provider") }) @@ -199,21 +449,16 @@ func TestOIDCCallbackHandler(t *testing.T) { provider := uuid.New().String() state := uuid.New().String() code := uuid.New().String() - hydraChallenge := uuid.New().String() - hydraRedirectURL := fmt.Sprintf("http://example.org/foo/%s", uuid.New().String()) - config := config(t) - config.Hydra = &mockHydra{ - acceptLoginRequestValue: &admin.AcceptLoginRequestOK{Payload: &models.CompletedRequest{ - RedirectTo: &hydraRedirectURL, - }}, - } + templatePath, deleteTmp := tmpStaticHTML(t) + defer deleteTmp() + + config.ClosePopupHTML = templatePath o, err := New(config) require.NoError(t, err) - o.cookies = mockCookies(withState(state), withHydraLoginChallenge(hydraChallenge), withProvider(provider)) o.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{ name: provider, @@ -234,10 +479,46 @@ func TestOIDCCallbackHandler(t *testing.T) { }, } + respInteract, err := o.interactionHandler.PrepareInteraction(&gnap.RequestInteract{ + Start: []string{"redirect"}, + Finish: gnap.RequestFinish{ + Method: "redirect", + URI: "example.foo/client-redirect", + }, + }, "", []*api.ExpiringTokenRequest{ + { + TokenRequest: gnap.TokenRequest{ + Access: []gnap.TokenAccess{ + { + IsReference: true, + Ref: "client-id", + }, + }, + }, + }, + }) + require.NoError(t, err) + + redirURL, err := url.Parse(respInteract.Redirect) + require.NoError(t, err) + + txnID := redirURL.Query().Get("txnID") + + data := &oidcTransientData{ + Provider: provider, + TxnID: txnID, + } + + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = o.transientStore.Put(state, dataBytes) + require.NoError(t, err) + result := httptest.NewRecorder() o.oidcCallbackHandler(result, newOIDCCallback(state, code)) - require.Equal(t, http.StatusFound, result.Code) - require.Equal(t, hydraRedirectURL, result.Header().Get("location")) + require.Equal(t, http.StatusOK, result.Code) + // TODO validate redirect url }) t.Run("error missing state", func(t *testing.T) { @@ -249,17 +530,17 @@ func TestOIDCCallbackHandler(t *testing.T) { require.Equal(t, http.StatusBadRequest, result.Code) }) - t.Run("error mismatching state", func(t *testing.T) { + t.Run("invalid state", func(t *testing.T) { state := uuid.New().String() mismatch := "mismatch" require.NotEqual(t, state, mismatch) svc, err := New(config(t)) require.NoError(t, err) - svc.cookies = mockCookies(withState("MISMATCH"), withHydraLoginChallenge("challenge")) + result := httptest.NewRecorder() svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "invalid state parameter") + require.Contains(t, result.Body.String(), "failed to get state data to transient store") }) t.Run("error missing code", func(t *testing.T) { @@ -271,176 +552,69 @@ func TestOIDCCallbackHandler(t *testing.T) { require.Equal(t, http.StatusBadRequest, result.Code) }) - t.Run("bad request if missing state cookie", func(t *testing.T) { + t.Run("internal server error if transient data is invalid", func(t *testing.T) { svc, err := New(config(t)) require.NoError(t, err) - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback("state", "code")) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "missing state cookie") - }) - t.Run("bad request if missing hydra login challenge cookie", func(t *testing.T) { - svc, err := New(config(t)) - require.NoError(t, err) - svc.cookies = mockCookies(withState("state"), withProvider("provider")) - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback("state", "code")) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "missing hydra login challenge cookie") - }) + dataBytes := []byte("foo bar baz") - t.Run("bad request if missing provider cookie", func(t *testing.T) { - svc, err := New(config(t)) + err = svc.transientStore.Put("state", dataBytes) require.NoError(t, err) - svc.cookies = mockCookies(withState("state")) + result := httptest.NewRecorder() svc.oidcCallbackHandler(result, newOIDCCallback("state", "code")) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "missing provider cookie") + + require.Equal(t, http.StatusInternalServerError, result.Code) + require.Contains(t, result.Body.String(), "failed to parse") }) t.Run("bad request if oidc provider is not supported (should not happen)", func(t *testing.T) { svc, err := New(config(t)) require.NoError(t, err) - svc.cookies = mockCookies(withState("state"), withHydraLoginChallenge("challenge"), withProvider("INVALID")) - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback("state", "code")) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "provider not supported") - }) - t.Run("internal server error if cannot open cookies", func(t *testing.T) { - svc, err := New(config(t)) - require.NoError(t, err) - svc.cookies = &cookie.MockStore{ - OpenErr: errors.New("test"), + data := &oidcTransientData{ + Provider: "invalid", } + + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = svc.transientStore.Put("state", dataBytes) + require.NoError(t, err) + result := httptest.NewRecorder() svc.oidcCallbackHandler(result, newOIDCCallback("state", "code")) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to get cookies") + + require.Equal(t, http.StatusBadRequest, result.Code) + require.Contains(t, result.Body.String(), "provider not supported") }) - t.Run("generic bootstrap store FETCH error", func(t *testing.T) { + t.Run("error exchanging auth code", func(t *testing.T) { provider := uuid.New().String() - id := uuid.New().String() state := uuid.New().String() config := config(t) - - config.TransientStoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - state: {Value: []byte(state)}, - }, + config.TransientStoreProvider = &mockstore.MockStoreProvider{Store: &mockstore.MockStore{ + Store: map[string]mockstore.DBEntry{ + state: {Value: []byte(state)}, }, - } + }} + svc, err := New(config) + require.NoError(t, err) - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - id: {}, - }, - ErrGet: errors.New("generic"), - }, + data := &oidcTransientData{ + Provider: provider, } - svc, err := New(config) + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = svc.transientStore.Put(state, dataBytes) require.NoError(t, err) - svc.cookies = mockCookies(withState(state), withHydraLoginChallenge("challenge"), withProvider(provider)) svc.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{ - name: provider, oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{ - oauth2Claim: uuid.New().String(), - }, - }, - verifyVal: &mockToken{ - oidcClaimsFunc: func(v interface{}) error { - c, ok := v.(*oidcClaims) - require.True(t, ok) - c.Sub = id - - return nil - }, - }, - }, - } - - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) - require.Equal(t, http.StatusInternalServerError, result.Code) - }) - - t.Run("generic bootstrap store PUT error while onboarding user", func(t *testing.T) { - provider := uuid.New().String() - id := uuid.New().String() - state := uuid.New().String() - config := config(t) - - config.TransientStoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - state: {Value: []byte(state)}, - }, - }, - } - - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - id: {}, - }, - ErrGet: storage.ErrDataNotFound, - ErrPut: errors.New("generic"), - }, - } - - svc, err := New(config) - require.NoError(t, err) - - svc.cookies = mockCookies(withState(state), withHydraLoginChallenge("challenge"), withProvider(provider)) - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - name: provider, - oauth2Config: &mockOAuth2Config{exchangeVal: &mockToken{ - oauth2Claim: uuid.New().String(), - }}, - verifyVal: &mockToken{ - oidcClaimsFunc: func(v interface{}) error { - c, ok := v.(*oidcClaims) - require.True(t, ok) - c.Sub = id - - return nil - }, - }, - }, - } - - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) - require.Equal(t, http.StatusInternalServerError, result.Code) - }) - - t.Run("error exchanging auth code", func(t *testing.T) { - provider := uuid.New().String() - state := uuid.New().String() - config := config(t) - config.TransientStoreProvider = &mockstore.MockStoreProvider{Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - state: {Value: []byte(state)}, - }, - }} - svc, err := New(config) - require.NoError(t, err) - svc.cookies = mockCookies(withState(state), withHydraLoginChallenge("challenge"), withProvider(provider)) - svc.cachedOIDCProviders = map[string]oidcProvider{ - provider: &mockOIDCProvider{ - oauth2Config: &mockOAuth2Config{ - exchangeErr: errors.New("test"), + exchangeErr: errors.New("test"), }, }, } @@ -461,7 +635,17 @@ func TestOIDCCallbackHandler(t *testing.T) { }} svc, err := New(config) require.NoError(t, err) - svc.cookies = mockCookies(withState(state), withHydraLoginChallenge("challenge"), withProvider(provider)) + + data := &oidcTransientData{ + Provider: provider, + } + + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = svc.transientStore.Put(state, dataBytes) + require.NoError(t, err) + svc.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{ name: provider, @@ -487,7 +671,17 @@ func TestOIDCCallbackHandler(t *testing.T) { }} svc, err := New(config) require.NoError(t, err) - svc.cookies = mockCookies(withState(state), withHydraLoginChallenge("challenge"), withProvider(provider)) + + data := &oidcTransientData{ + Provider: provider, + } + + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = svc.transientStore.Put(state, dataBytes) + require.NoError(t, err) + svc.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{ name: provider, @@ -513,7 +707,17 @@ func TestOIDCCallbackHandler(t *testing.T) { }} svc, err := New(config) require.NoError(t, err) - svc.cookies = mockCookies(withState(state), withHydraLoginChallenge("challenge"), withProvider(provider)) + + data := &oidcTransientData{ + Provider: provider, + } + + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = svc.transientStore.Put(state, dataBytes) + require.NoError(t, err) + svc.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{ name: provider, @@ -527,45 +731,80 @@ func TestOIDCCallbackHandler(t *testing.T) { svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) require.Equal(t, http.StatusInternalServerError, result.Code) }) - t.Run("PUT error while storing user info while handling callback user", func(t *testing.T) { + + t.Run("generic bootstrap store PUT error while onboarding user", func(t *testing.T) { + provider := uuid.New().String() id := uuid.New().String() state := uuid.New().String() + code := uuid.New().String() config := config(t) - config.TransientStoreProvider = &mockstore.MockStoreProvider{ + config.StoreProvider = &mockstore.MockStoreProvider{ Store: &mockstore.MockStore{ Store: map[string]mockstore.DBEntry{ - id: {Value: []byte("{}")}, + id: {}, }, ErrGet: storage.ErrDataNotFound, ErrPut: errors.New("generic"), }, } - svc, err := New(config) + o, err := New(config) + require.NoError(t, err) + + o.cachedOIDCProviders = map[string]oidcProvider{ + provider: &mockOIDCProvider{ + name: provider, + oauth2Config: &mockOAuth2Config{ + exchangeVal: &mockToken{ + oauth2Claim: uuid.New().String(), + }, + }, + verifyVal: &mockToken{ + oidcClaimsFunc: func(v interface{}) error { + c, ok := v.(*oidcClaims) + require.True(t, ok) + c.Sub = uuid.New().String() + + return nil + }, + }, + }, + } + + data := &oidcTransientData{ + Provider: provider, + TxnID: "foo", + } + + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = o.transientStore.Put(state, dataBytes) require.NoError(t, err) result := httptest.NewRecorder() - svc.handleAuthResult(result, newOIDCCallback(state, "code"), nil) + o.oidcCallbackHandler(result, newOIDCCallback(state, code)) require.Equal(t, http.StatusInternalServerError, result.Code) + + require.Contains(t, result.Body.String(), "failed to onboard new user") }) - t.Run("error bad gateway if hydra fails to accept login request", func(t *testing.T) { + t.Run("fail to complete interaction", func(t *testing.T) { provider := uuid.New().String() state := uuid.New().String() code := uuid.New().String() - hydraChallenge := uuid.New().String() - config := config(t) - config.Hydra = &mockHydra{ - acceptLoginRequestErr: errors.New("test"), + expectErr := errors.New("expected error") + + config.InteractionHandler = &mockinteract.InteractHandler{ + CompleteErr: expectErr, } o, err := New(config) require.NoError(t, err) - o.cookies = mockCookies(withState(state), withHydraLoginChallenge(hydraChallenge), withProvider(provider)) o.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{ name: provider, @@ -586,30 +825,40 @@ func TestOIDCCallbackHandler(t *testing.T) { }, } + data := &oidcTransientData{ + Provider: provider, + TxnID: "foo", + } + + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = o.transientStore.Put(state, dataBytes) + require.NoError(t, err) + result := httptest.NewRecorder() o.oidcCallbackHandler(result, newOIDCCallback(state, code)) - require.Equal(t, http.StatusBadGateway, result.Code) - require.Contains(t, result.Body.String(), "hydra failed to accept login request") + require.Equal(t, http.StatusInternalServerError, result.Code) + + require.Contains(t, result.Body.String(), "failed to complete GNAP interaction") }) - t.Run("internal server error if cannot delete cookies", func(t *testing.T) { + t.Run("bad client redirect URI", func(t *testing.T) { provider := uuid.New().String() - svc, err := New(config(t)) + state := uuid.New().String() + code := uuid.New().String() + config := config(t) + + o, err := New(config) require.NoError(t, err) - svc.cookies = &cookie.MockStore{ - Jar: &cookie.MockJar{ - Cookies: map[interface{}]interface{}{ - stateCookie: "state", - hydraLoginChallengeCookie: "challenge", - providerCookie: provider, - }, - SaveErr: errors.New("test"), - }, - } - svc.cachedOIDCProviders = map[string]oidcProvider{ + + o.cachedOIDCProviders = map[string]oidcProvider{ provider: &mockOIDCProvider{ + name: provider, oauth2Config: &mockOAuth2Config{ - exchangeVal: &mockToken{oauth2Claim: uuid.New().String()}, + exchangeVal: &mockToken{ + oauth2Claim: uuid.New().String(), + }, }, verifyVal: &mockToken{ oidcClaimsFunc: func(v interface{}) error { @@ -622,172 +871,111 @@ func TestOIDCCallbackHandler(t *testing.T) { }, }, } - result := httptest.NewRecorder() - svc.oidcCallbackHandler(result, newOIDCCallback("state", "code")) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to delete cookies") - }) -} - -func TestOperations_HydraConsentHandler(t *testing.T) { - t.Run("redirects back to hydra", func(t *testing.T) { - redirectURL := fmt.Sprintf("https://example.org/foo/%s", uuid.New().String()) - sub := uuid.New().String() - challenge := uuid.New().String() - config := config(t) - config.Hydra = &mockHydra{ - getConsentRequestValue: &admin.GetConsentRequestOK{Payload: &models.ConsentRequest{ - Subject: sub, - Challenge: &challenge, - }}, - acceptConsentRequestValue: &admin.AcceptConsentRequestOK{Payload: &models.CompletedRequest{ - RedirectTo: &redirectURL, - }}, - } - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - sub: {Value: marshal(t, &user.Profile{})}, + respInteract, err := o.interactionHandler.PrepareInteraction(&gnap.RequestInteract{ + Start: []string{"redirect"}, + Finish: gnap.RequestFinish{ + Method: "redirect", + URI: "^$#^*#%$^&#$%#T^ UTTER GIBBERISH", + }, + }, "", []*api.ExpiringTokenRequest{ + { + TokenRequest: gnap.TokenRequest{ + Access: []gnap.TokenAccess{ + { + IsReference: true, + Ref: "client-id", + }, + }, }, }, - } - - o, err := New(config) + }) require.NoError(t, err) - result := httptest.NewRecorder() - o.hydraConsentHandler(result, newHydraConsentHTTPRequest(uuid.New().String())) - require.Equal(t, http.StatusFound, result.Code) - require.Equal(t, redirectURL, result.Header().Get("location")) - }) - - t.Run("err bad request if consent challenge is missing", func(t *testing.T) { - o, err := New(config(t)) + redirURL, err := url.Parse(respInteract.Redirect) require.NoError(t, err) - result := httptest.NewRecorder() - o.hydraConsentHandler(result, newHydraConsentHTTPRequest("")) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "missing consent_challenge") - }) + txnID := redirURL.Query().Get("txnID") - t.Run("err badgateway if cannot fetch hydra consent request", func(t *testing.T) { - config := config(t) - config.Hydra = &mockHydra{ - getConsentRequestErr: errors.New("test"), + data := &oidcTransientData{ + Provider: provider, + TxnID: txnID, } - o, err := New(config) + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + + err = o.transientStore.Put(state, dataBytes) require.NoError(t, err) result := httptest.NewRecorder() - o.hydraConsentHandler(result, newHydraConsentHTTPRequest("challenge")) - require.Equal(t, http.StatusBadGateway, result.Code) - require.Contains(t, result.Body.String(), "failed to fetch consent request from hydra") + o.oidcCallbackHandler(result, newOIDCCallback(state, code)) + require.Equal(t, http.StatusBadRequest, result.Code) + require.Contains(t, result.Body.String(), "client provided invalid redirect URI") }) - t.Run("err internalservererror if cannot fetch user from store", func(t *testing.T) { + t.Run("generic bootstrap store PUT error while onboarding user", func(t *testing.T) { + provider := uuid.New().String() + id := uuid.New().String() + state := uuid.New().String() config := config(t) - config.StoreProvider = &mockstore.MockStoreProvider{ + + config.TransientStoreProvider = &mockstore.MockStoreProvider{ Store: &mockstore.MockStore{ - ErrGet: errors.New("test"), + Store: map[string]mockstore.DBEntry{ + state: {Value: []byte(state)}, + }, }, } - config.Hydra = &mockHydra{ - getConsentRequestValue: &admin.GetConsentRequestOK{Payload: &models.ConsentRequest{ - Subject: uuid.New().String(), - }}, - } - o, err := New(config) - require.NoError(t, err) - - result := httptest.NewRecorder() - o.hydraConsentHandler(result, newHydraConsentHTTPRequest("challenge")) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to query for user profile") - }) - - t.Run("error badgateway if hydra fails to accept consent request", func(t *testing.T) { - sub := uuid.New().String() - challenge := uuid.New().String() - - config := config(t) config.StoreProvider = &mockstore.MockStoreProvider{ Store: &mockstore.MockStore{ Store: map[string]mockstore.DBEntry{ - sub: {Value: marshal(t, &user.Profile{})}, + id: {}, }, + ErrGet: storage.ErrDataNotFound, + ErrPut: errors.New("generic"), }, } - config.Hydra = &mockHydra{ - getConsentRequestValue: &admin.GetConsentRequestOK{Payload: &models.ConsentRequest{ - Challenge: &challenge, - Subject: sub, - }}, - acceptConsentRequestErr: errors.New("test"), - } - o, err := New(config) + svc, err := New(config) require.NoError(t, err) + svc.cachedOIDCProviders = map[string]oidcProvider{ + provider: &mockOIDCProvider{ + name: provider, + oauth2Config: &mockOAuth2Config{exchangeVal: &mockToken{ + oauth2Claim: uuid.New().String(), + }}, + verifyVal: &mockToken{ + oidcClaimsFunc: func(v interface{}) error { + c, ok := v.(*oidcClaims) + require.True(t, ok) + c.Sub = id + + return nil + }, + }, + }, + } + result := httptest.NewRecorder() - o.hydraConsentHandler(result, newHydraConsentHTTPRequest(challenge)) - require.Equal(t, http.StatusBadGateway, result.Code) - require.Contains(t, result.Body.String(), "hydra failed to accept consent request") + svc.oidcCallbackHandler(result, newOIDCCallback(state, "code")) + require.Equal(t, http.StatusInternalServerError, result.Code) }) } func TestGetBootstrapDataHandler(t *testing.T) { - t.Run("returns bootstrap data when using OIDC token", func(t *testing.T) { + t.Run("returns bootstrap data when using GNAP token", func(t *testing.T) { userSub := uuid.New().String() config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } svc, err := New(config) require.NoError(t, err) - expected := &user.Profile{ - ID: uuid.New().String(), - AAGUID: uuid.New().String(), - Data: map[string]string{ - "primary vault": uuid.New().String(), - "backup vault": uuid.New().String(), - }, - } - - err = svc.bootstrapStore.Put(userSub, marshal(t, expected)) - require.NoError(t, err) - - w := httptest.NewRecorder() - svc.getBootstrapDataHandler(w, newGetBootstrapDataRequest()) - require.Equal(t, http.StatusOK, w.Code) - result := &BootstrapData{} - err = json.NewDecoder(w.Body).Decode(result) - require.NoError(t, err) - require.Equal(t, config.BootstrapConfig.DocumentSDSVaultURL, result.DocumentSDSVaultURL) - require.Equal(t, config.BootstrapConfig.KeySDSVaultURL, result.KeySDSVaultURL) - require.Equal(t, config.BootstrapConfig.AuthZKeyServerURL, result.AuthZKeyServerURL) - require.Equal(t, config.BootstrapConfig.OpsKeyServerURL, result.OpsKeyServerURL) - require.Equal(t, expected.Data, result.Data) - }) - - t.Run("returns bootstrap data when using GNAP token", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - - svc, err := New(config) - require.NoError(t, err) - - svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { - return &gnap.IntrospectResponse{ - SubjectData: map[string]string{"sub": userSub}, - }, nil - }) - + svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { + return &gnap.IntrospectResponse{ + SubjectData: map[string]string{"sub": userSub}, + }, nil + }) expected := &user.Profile{ ID: uuid.New().String(), AAGUID: uuid.New().String(), @@ -812,7 +1000,6 @@ func TestGetBootstrapDataHandler(t *testing.T) { require.NoError(t, err) require.Equal(t, config.BootstrapConfig.DocumentSDSVaultURL, result.DocumentSDSVaultURL) require.Equal(t, config.BootstrapConfig.KeySDSVaultURL, result.KeySDSVaultURL) - require.Equal(t, config.BootstrapConfig.AuthZKeyServerURL, result.AuthZKeyServerURL) require.Equal(t, config.BootstrapConfig.OpsKeyServerURL, result.OpsKeyServerURL) require.Equal(t, expected.Data, result.Data) }) @@ -867,27 +1054,16 @@ func TestGetBootstrapDataHandler(t *testing.T) { require.Contains(t, w.Body.String(), "does not grant access") }) - t.Run("badrequest if token is not base64 encoded", func(t *testing.T) { - request := newGetBootstrapDataRequest() - request.Header.Set("authorization", "Bearer 123") - svc, err := New(config(t)) - require.NoError(t, err) - w := httptest.NewRecorder() - svc.getBootstrapDataHandler(w, request) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Contains(t, w.Body.String(), "failed to decode token") - }) - t.Run("bad request if user does not have bootstrap data", func(t *testing.T) { userSub := uuid.New().String() config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } svc, err := New(config) require.NoError(t, err) + svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { + return &gnap.IntrospectResponse{ + SubjectData: map[string]string{"sub": userSub}, + }, nil + }) w := httptest.NewRecorder() svc.getBootstrapDataHandler(w, newGetBootstrapDataRequest()) require.Equal(t, http.StatusBadRequest, w.Code) @@ -897,11 +1073,6 @@ func TestGetBootstrapDataHandler(t *testing.T) { t.Run("internal server error if bootstrap store FETCH fails generically", func(t *testing.T) { userSub := uuid.New().String() config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } config.StoreProvider = &mockstore.MockStoreProvider{ Store: &mockstore.MockStore{ Store: map[string]mockstore.DBEntry{ @@ -912,6 +1083,11 @@ func TestGetBootstrapDataHandler(t *testing.T) { } svc, err := New(config) require.NoError(t, err) + svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { + return &gnap.IntrospectResponse{ + SubjectData: map[string]string{"sub": userSub}, + }, nil + }) w := httptest.NewRecorder() svc.getBootstrapDataHandler(w, newGetBootstrapDataRequest()) require.Equal(t, http.StatusInternalServerError, w.Code) @@ -920,65 +1096,17 @@ func TestGetBootstrapDataHandler(t *testing.T) { } func TestPostBootstrapDataHandler(t *testing.T) { - t.Run("updates bootstrap data", func(t *testing.T) { - expected := &user.Profile{ - ID: uuid.New().String(), - AAGUID: uuid.New().String(), - Data: map[string]string{ - "docsSDS": "https://example.org/edvs/123", - "keysSDS": "https://example.org/edvs/456", - "authkeys": "https://example.org/kms/123", - "opskeys": "https://example.org/kms/456", - }, - } - config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: expected.ID, - }}, - } - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - expected.ID: {Value: marshal(t, &user.Profile{ - ID: expected.ID, - AAGUID: expected.AAGUID, - })}, - }, - }, - } - svc, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - svc.postBootstrapDataHandler(result, newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{ - Data: expected.Data, - })) - require.Equal(t, http.StatusOK, result.Code) - raw, err := svc.bootstrapStore.Get(expected.ID) - require.NoError(t, err) - update := &user.Profile{} - err = json.NewDecoder(bytes.NewReader(raw)).Decode(update) - require.NoError(t, err) - require.Equal(t, expected, update) - }) - t.Run("updates bootstrap data when using GNAP token", func(t *testing.T) { expected := &user.Profile{ ID: uuid.New().String(), AAGUID: uuid.New().String(), Data: map[string]string{ - "docsSDS": "https://example.org/edvs/123", - "keysSDS": "https://example.org/edvs/456", - "authkeys": "https://example.org/kms/123", - "opskeys": "https://example.org/kms/456", + "docsSDS": "https://example.org/edvs/123", + "keysSDS": "https://example.org/edvs/456", + "opskeys": "https://example.org/kms/456", }, } config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: expected.ID, - }}, - } config.StoreProvider = &mockstore.MockStoreProvider{ Store: &mockstore.MockStore{ Store: map[string]mockstore.DBEntry{ @@ -1004,8 +1132,6 @@ func TestPostBootstrapDataHandler(t *testing.T) { Data: expected.Data, }) - request.Header.Set("authorization", "GNAP 123") - svc.postBootstrapDataHandler(result, request) require.Equal(t, http.StatusOK, result.Code) raw, err := svc.bootstrapStore.Get(expected.ID) @@ -1017,16 +1143,24 @@ func TestPostBootstrapDataHandler(t *testing.T) { }) t.Run("error badrequest if payload is not json", func(t *testing.T) { + userSub := uuid.New().String() config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: uuid.New().String(), - }}, + config.StoreProvider = &mockstore.MockStoreProvider{ + Store: &mockstore.MockStore{ + Store: map[string]mockstore.DBEntry{ + userSub: {Value: nil}, + }, + }, } svc, err := New(config) require.NoError(t, err) + svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { + return &gnap.IntrospectResponse{ + SubjectData: map[string]string{"sub": userSub}, + }, nil + }) request := httptest.NewRequest(http.MethodPost, "https://example.org/bootstrap", bytes.NewReader([]byte("}"))) - request.Header.Set("authorization", "Bearer "+base64.StdEncoding.EncodeToString([]byte("test"))) + request.Header.Set("authorization", "GNAP 123") result := httptest.NewRecorder() svc.postBootstrapDataHandler(result, request) require.Equal(t, http.StatusBadRequest, result.Code) @@ -1035,13 +1169,13 @@ func TestPostBootstrapDataHandler(t *testing.T) { t.Run("error conflict if user does not exist", func(t *testing.T) { config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: uuid.New().String(), - }}, - } svc, err := New(config) require.NoError(t, err) + svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { + return &gnap.IntrospectResponse{ + SubjectData: map[string]string{"sub": uuid.New().String()}, + }, nil + }) result := httptest.NewRecorder() svc.postBootstrapDataHandler(result, newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{})) require.Equal(t, http.StatusConflict, result.Code) @@ -1059,13 +1193,13 @@ func TestPostBootstrapDataHandler(t *testing.T) { ErrGet: errors.New("generic"), }, } - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } svc, err := New(config) require.NoError(t, err) + svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { + return &gnap.IntrospectResponse{ + SubjectData: map[string]string{"sub": userSub}, + }, nil + }) result := httptest.NewRecorder() svc.postBootstrapDataHandler(result, newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{})) require.Equal(t, http.StatusInternalServerError, result.Code) @@ -1083,804 +1217,276 @@ func TestPostBootstrapDataHandler(t *testing.T) { ErrPut: errors.New("generic"), }, } - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } svc, err := New(config) require.NoError(t, err) + svc.SetIntrospectHandler(func(req *gnap.IntrospectRequest) (*gnap.IntrospectResponse, error) { + return &gnap.IntrospectResponse{ + SubjectData: map[string]string{"sub": userSub}, + }, nil + }) result := httptest.NewRecorder() svc.postBootstrapDataHandler(result, newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{})) require.Equal(t, http.StatusInternalServerError, result.Code) require.Contains(t, result.Body.String(), "failed to update storage") }) - - t.Run("err badgateway if cannot introspect token at hydra", func(t *testing.T) { - config := config(t) - config.Hydra = &mockHydra{ - introspectErr: errors.New("test"), - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.postBootstrapDataHandler(result, newPostBootstrapDataRequest(t, &UpdateBootstrapDataRequest{})) - require.Equal(t, http.StatusBadGateway, result.Code) - require.Contains(t, result.Body.String(), "failed to introspect token") - }) } -func TestOperation_DeviceCertHandler(t *testing.T) { - t.Run("invalid request json", func(t *testing.T) { - config := config(t) +func Test_Full_Flow(t *testing.T) { + conf := config(t) - svc, err := New(config) - require.NoError(t, err) + templatePath, deleteTmp := tmpStaticHTML(t) + defer deleteTmp() - w := httptest.NewRecorder() + conf.ClosePopupHTML = templatePath - svc.deviceCertHandler(w, httptest.NewRequest(http.MethodPost, "http://example.com/device", - bytes.NewReader([]byte("not json")))) + o, err := New(conf) + require.NoError(t, err) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Contains(t, w.Body.String(), "invalid json") - }) + authResp := &gnap.AuthResponse{} - t.Run("missing device certificate", func(t *testing.T) { - config := config(t) + var ( + txnID string + interactRef string + state string + ) - handle := uuid.New().String() - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - handle: {Value: marshal(t, &user.Profile{})}, + userPriv, userClient := clientKey(t) + + { + authReq := &gnap.AuthRequest{ + Client: &gnap.RequestClient{ + IsReference: false, + Key: userClient, + }, + AccessToken: []*gnap.TokenRequest{ + { + Access: []gnap.TokenAccess{ + { + IsReference: true, + Ref: "client-id", + }, + }, + }, + }, + Interact: &gnap.RequestInteract{ + Start: []string{"redirect"}, + Finish: gnap.RequestFinish{ + Method: "redirect", + URI: "example.com/client-ui", }, }, } - svc, err := New(config) + authReqBytes, err := json.Marshal(authReq) require.NoError(t, err) - w := httptest.NewRecorder() + rw := httptest.NewRecorder() - data := certHolder{ - X5C: nil, - Sub: handle, - AAGUID: "AAGUID", - } + req := httptest.NewRequest(http.MethodPost, baseURL+AuthRequestPath, bytes.NewReader(authReqBytes)) - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) + req, err = httpsig.Sign(req, authReqBytes, userPriv, "sha-256") + require.NoError(t, err) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Equal(t, "missing device certificate", w.Body.String()) - }) + o.authRequestHandler(rw, req) - t.Run("invalid user profile id", func(t *testing.T) { - config := config(t) + require.Equal(t, http.StatusOK, rw.Code) - svc, err := New(config) + require.NoError(t, json.Unmarshal(rw.Body.Bytes(), authResp)) + + redirectURL, err := url.Parse(authResp.Interact.Redirect) require.NoError(t, err) - w := httptest.NewRecorder() + txnID = redirectURL.Query().Get("txnID") + } - data := certHolder{ - X5C: []string{"abc", "abcd"}, - Sub: "bad handle", - AAGUID: "AAGUID", - } + provider := uuid.New().String() - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) + subjectID := "mock-subject-id" - require.Equal(t, http.StatusBadRequest, w.Code) - require.Equal(t, "invalid user profile id", w.Body.String()) - }) + o.cachedOIDCProviders = map[string]oidcProvider{provider: &mockOIDCProvider{ + oauth2Config: &mockOAuth2Config{ + authCodeFunc: func(state string, opts ...oauth2.AuthCodeOption) string { + return "example.com/oauth2?state=" + state + }, + exchangeVal: &mockToken{ + oauth2Claim: "mock-id-token", + }, + }, + verifyVal: &mockToken{ + oidcClaimsFunc: func(v interface{}) error { + claims, ok := v.(*oidcClaims) + if !ok { + return nil + } - t.Run("can't load profile from store", func(t *testing.T) { - config := config(t) - handle := uuid.New().String() - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - handle: {Value: marshal(t, &user.Profile{})}, - }, - ErrGet: errors.New("get error"), + claims.Sub = subjectID + + return nil }, - } + }, + }} + o.oidcProvidersConfig = map[string]*oidcmodel.ProviderConfig{provider: {}} - svc, err := New(config) + { + rw := httptest.NewRecorder() + + o.oidcLoginHandler(rw, newOIDCLoginRequest(provider, txnID)) + require.Equal(t, http.StatusFound, rw.Code) + redirectURL, err := url.Parse(rw.Header().Get("location")) require.NoError(t, err) - w := httptest.NewRecorder() + state = redirectURL.Query().Get("state") + } - data := certHolder{ - X5C: []string{"abc", "abcd"}, - Sub: handle, - AAGUID: "AAGUID", - } + { + code := uuid.New().String() - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) + rw := httptest.NewRecorder() - require.Equal(t, http.StatusInternalServerError, w.Code) - require.Equal(t, "failed to load user profile", w.Body.String()) - }) + o.oidcCallbackHandler(rw, newOIDCCallback(state, code)) - t.Run("invalid cert PEM", func(t *testing.T) { - config := config(t) - handle := uuid.New().String() - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - handle: {Value: marshal(t, &user.Profile{})}, - }, - }, - } + require.Equal(t, http.StatusOK, rw.Code) - svc, err := New(config) - require.NoError(t, err) + body := rw.Body.Bytes() - w := httptest.NewRecorder() + rx := regexp.MustCompile("window.opener.location.href = '(.*)';") + res := rx.FindStringSubmatch(string(body)) - data := certHolder{ - X5C: []string{"abc", "abcd"}, - Sub: handle, - AAGUID: "AAGUID", - } + u := res[1] - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) + u = strings.ReplaceAll(u, "\\u0026", "\u0026") + u = strings.ReplaceAll(u, "\\/", "/") - require.Equal(t, http.StatusBadRequest, w.Code) - require.Equal(t, "can't parse certificate PEM", w.Body.String()) - }) + redirectURL, err := url.Parse(u) + require.NoError(t, err) - t.Run("PEM does not encode a certificate", func(t *testing.T) { - config := config(t) - handle := uuid.New().String() - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - handle: {Value: marshal(t, &user.Profile{})}, - }, - }, - } + require.Contains(t, redirectURL.String(), "interact_ref") - svc, err := New(config) - require.NoError(t, err) + interactRef = redirectURL.Query().Get("interact_ref") + require.NotEqual(t, "", interactRef) + } - w := httptest.NewRecorder() + contResp := &gnap.AuthResponse{} - data := certHolder{ - X5C: []string{ - string(pem.EncodeToMemory(&pem.Block{ - Type: "NOT A CERT", - Bytes: []byte("definitely not a cert"), - })), - }, - Sub: handle, - AAGUID: "AAGUID", + { + contReq := &gnap.ContinueRequest{ + InteractRef: interactRef, } - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) + contReqBytes, err := json.Marshal(contReq) + require.NoError(t, err) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Equal(t, "can't parse certificate", w.Body.String()) - }) + rw := httptest.NewRecorder() - t.Run("cert is not signed by chain from root CAs", func(t *testing.T) { - config := config(t) - handle := uuid.New().String() - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - handle: {Value: marshal(t, &user.Profile{})}, - }, - }, - } + req := httptest.NewRequest(http.MethodPost, baseURL+AuthRequestPath, bytes.NewReader(contReqBytes)) + req.Header.Add("Authorization", "GNAP "+authResp.Continue.AccessToken.Value) - svc, err := New(config) + req, err = httpsig.Sign(req, contReqBytes, userPriv, "sha-256") require.NoError(t, err) - w := httptest.NewRecorder() + o.authContinueHandler(rw, req) - data := certHolder{ - X5C: []string{ - makeSelfSignedCert(t), - }, - Sub: handle, - AAGUID: "AAGUID", - } + require.Equal(t, http.StatusOK, rw.Code) - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) + require.NoError(t, json.Unmarshal(rw.Body.Bytes(), contResp)) + } - require.Equal(t, http.StatusBadRequest, w.Code) - require.Equal(t, "cert chain fails to authenticate", w.Body.String()) - }) + require.Len(t, contResp.AccessToken, 1) - t.Run("success - device cert is a root cert", func(t *testing.T) { - config := config(t) - handle := uuid.New().String() - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - handle: {Value: marshal(t, &user.Profile{})}, - }, + rsPriv, rsClient := clientKey(t) + + { + intReq := &gnap.IntrospectRequest{ + AccessToken: contResp.AccessToken[0].Value, + Proof: "httpsig", + ResourceServer: &gnap.RequestClient{ + Key: rsClient, }, } - svc, err := New(config) + intReqBytes, err := json.Marshal(intReq) require.NoError(t, err) - w := httptest.NewRecorder() - - _, devicePEM, _ := makeCACert(t) + rw := httptest.NewRecorder() - // device PEM is added to roots - ok := svc.deviceRootCerts.AppendCertsFromPEM([]byte(devicePEM)) - require.True(t, ok) + req := httptest.NewRequest(http.MethodPost, baseURL+AuthIntrospectPath, bytes.NewReader(intReqBytes)) - data := certHolder{ - X5C: []string{ - devicePEM, - }, - Sub: handle, - AAGUID: "AAGUID", - } + req, err = httpsig.Sign(req, intReqBytes, rsPriv, "sha-256") + require.NoError(t, err) - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) + o.authIntrospectHandler(rw, req) - require.Equal(t, http.StatusFound, w.Code) - }) + require.Equal(t, http.StatusOK, rw.Code) - t.Run("success - device cert is signed by root cert", func(t *testing.T) { - config := config(t) - handle := uuid.New().String() - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - handle: {Value: marshal(t, &user.Profile{})}, - }, - }, - } + resp := &gnap.IntrospectResponse{} - svc, err := New(config) + err = json.Unmarshal(rw.Body.Bytes(), resp) require.NoError(t, err) - w := httptest.NewRecorder() - - rootCert, rootPEM, rootKey := makeCACert(t) + require.True(t, resp.Active) - _, devicePEM, _ := makeChildCert(t, rootCert, rootKey, false) + resultID := resp.SubjectData["sub"] - // CA cert PEM is added to roots - ok := svc.deviceRootCerts.AppendCertsFromPEM([]byte(rootPEM)) - require.True(t, ok) + // introspection returns the user's OIDC 'sub' ID value + require.Equal(t, subjectID, resultID) + } +} - data := certHolder{ - X5C: []string{ - devicePEM, - }, - Sub: handle, - AAGUID: "AAGUID", - } +type mockOIDCProvider struct { + name string + baseURL string + oauth2Config oauth2Config + verifyVal idToken + verifyErr error +} - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) +func (m *mockOIDCProvider) Name() string { + return m.name +} - require.Equal(t, http.StatusFound, w.Code) - }) - - t.Run("success - device cert is signed by a rooted certificate chain", func(t *testing.T) { - config := config(t) - handle := uuid.New().String() - config.StoreProvider = &mockstore.MockStoreProvider{ - Store: &mockstore.MockStore{ - Store: map[string]mockstore.DBEntry{ - handle: {Value: marshal(t, &user.Profile{})}, - }, - }, - } - - svc, err := New(config) - require.NoError(t, err) - - w := httptest.NewRecorder() - - rootCert, rootPEM, rootKey := makeCACert(t) - - i1Cert, i1PEM, i1Key := makeChildCert(t, rootCert, rootKey, true) - i2Cert, i2PEM, i2Key := makeChildCert(t, i1Cert, i1Key, true) - - _, devicePEM, _ := makeChildCert(t, i2Cert, i2Key, false) - - // CA cert PEM is added to roots - ok := svc.deviceRootCerts.AppendCertsFromPEM([]byte(rootPEM)) - require.True(t, ok) - - data := certHolder{ - X5C: []string{ - devicePEM, - i2PEM, - i1PEM, - }, - Sub: handle, - AAGUID: "AAGUID", - } - - svc.deviceCertHandler(w, newDeviceCertRequest(t, &data)) - - require.Equal(t, "", w.Body.String()) - require.Equal(t, http.StatusFound, w.Code) - }) -} - -func TestOperation_AuthProvidersHandler(t *testing.T) { - t.Run("success", func(t *testing.T) { - config := config(t) - o, err := New(config) - require.NoError(t, err) - - w := httptest.NewRecorder() - o.authProvidersHandler(w, nil) - - require.Equal(t, http.StatusOK, w.Code) - var resp *authProviders - err = json.Unmarshal(w.Body.Bytes(), &resp) - require.NoError(t, err) - - require.Equal(t, 2, len(resp.Providers)) - }) -} - -func TestOperation_HydraLoginHandler(t *testing.T) { - t.Run("redirects to login UI", func(t *testing.T) { - uiEndpoint := "/ui" - rURL := "https://test-com/oauth2?provider=" - hydraLoginRequest := &admin.GetLoginRequestOK{Payload: &models.LoginRequest{ - Client: &models.OAuth2Client{ - ClientID: uuid.New().String(), - Scope: "registered scopes", - }, - RequestedScope: []string{"requested", "scope"}, - RequestURL: &rURL, - }} - - config := config(t) - config.UIEndpoint = uiEndpoint - config.Hydra = &mockHydra{ - getLoginRequestValue: hydraLoginRequest, - } - - o, err := New(config) - require.NoError(t, err) - - o.cookies = mockCookies() - - w := httptest.NewRecorder() - o.hydraLoginHandler(w, newHydraLoginHTTPRequest(uuid.New().String())) - - require.Equal(t, http.StatusFound, w.Code) - require.True(t, strings.HasPrefix(w.Header().Get("Location"), uiEndpoint)) - require.Equal(t, uiEndpoint, w.Header().Get("location")) - }) - - t.Run("redirects to hydra login", func(t *testing.T) { - hydraLoginURL := "/oauth2/login?provider=mockbank" - rURL := "https://test-com/oauth2?provider=mockbank" - hydraLoginRequest := &admin.GetLoginRequestOK{Payload: &models.LoginRequest{ - Client: &models.OAuth2Client{ - ClientID: uuid.New().String(), - Scope: "registered scopes", - }, - RequestedScope: []string{"requested", "scope"}, - RequestURL: &rURL, - }} - - config := config(t) - config.Hydra = &mockHydra{ - getLoginRequestValue: hydraLoginRequest, - } - - o, err := New(config) - require.NoError(t, err) - - o.cookies = mockCookies() - - w := httptest.NewRecorder() - o.hydraLoginHandler(w, newHydraLoginHTTPRequest(uuid.New().String())) - - require.Equal(t, http.StatusFound, w.Code) - require.True(t, strings.HasPrefix(w.Header().Get("Location"), hydraLoginURL)) - require.Equal(t, hydraLoginURL, w.Header().Get("location")) - }) - t.Run("redirects to hydra login error", func(t *testing.T) { - rURL := "https://user:abc{DEf1=ghi@example.com:5432/db?" - hydraLoginRequest := &admin.GetLoginRequestOK{Payload: &models.LoginRequest{ - Client: &models.OAuth2Client{ - ClientID: uuid.New().String(), - Scope: "registered scopes", - }, - RequestedScope: []string{"requested", "scope"}, - RequestURL: &rURL, - }} - - config := config(t) - config.Hydra = &mockHydra{ - getLoginRequestValue: hydraLoginRequest, - } - - o, err := New(config) - require.NoError(t, err) - - o.cookies = mockCookies() - - w := httptest.NewRecorder() - o.hydraLoginHandler(w, newHydraLoginHTTPRequest(uuid.New().String())) - - require.Equal(t, http.StatusBadRequest, w.Code) - }) - - t.Run("error bad request if login_challenge is missing", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - - w := httptest.NewRecorder() - o.hydraLoginHandler(w, httptest.NewRequest(http.MethodGet, "/login", nil)) - - require.Equal(t, http.StatusBadRequest, w.Code) - }) - - t.Run("error bad gateway if cannot fetch login request from hydra", func(t *testing.T) { - config := config(t) - config.Hydra = &mockHydra{ - getLoginRequestErr: errors.New("test"), - } - - o, err := New(config) - require.NoError(t, err) - - w := httptest.NewRecorder() - o.hydraLoginHandler(w, newHydraLoginHTTPRequest(uuid.New().String())) - - require.Equal(t, http.StatusBadGateway, w.Code) - }) - - t.Run("error internal server error if cannot open cookie store", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - - o.cookies = &cookie.MockStore{ - OpenErr: errors.New("test"), - } - - w := httptest.NewRecorder() - o.hydraLoginHandler(w, newHydraLoginHTTPRequest(uuid.New().String())) - require.Equal(t, http.StatusInternalServerError, w.Code) - }) -} - -func TestOperation_FetchProviderNameError(t *testing.T) { - tests := []struct { - in string - out string - }{ - {"https://user:abc{DEf1=ghi@example.com:5432/db?", "failed to parse url"}, - {"hi/there?: err=%+v url=%+v\\n", "failed to parse raw query for provider name"}, +func (m *mockOIDCProvider) OAuth2Config(...string) oauth2Config { + if m.oauth2Config != nil { + return m.oauth2Config } - config := config(t) - o, err := New(config) - require.NoError(t, err) + return &mockOAuth2Config{} +} - for _, tt := range tests { - t.Run(tt.in, func(t *testing.T) { - provider, err := o.fetchProviderFromURL(&tt.in) - require.Error(t, err) - require.Empty(t, provider) - require.Contains(t, err.Error(), tt.out) - }) +func (m *mockOIDCProvider) Endpoint() oauth2.Endpoint { + return oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s/oauth2/auth", m.baseURL), + TokenURL: fmt.Sprintf("%s/oauth2/token", m.baseURL), } } -func TestOperation_FetchProviderSuccess(t *testing.T) { - t.Run("fetch provider name success", func(t *testing.T) { - rURL := "https://test-com/oauth2?provider=test" - config := config(t) - - o, err := New(config) - require.NoError(t, err) - - provider, err := o.fetchProviderFromURL(&rURL) - require.NoError(t, err) - require.Equal(t, "test", provider) - }) - t.Run("no provider query optional parameter", func(t *testing.T) { - rURL := "https://test-com/oauth2?clientID=" - config := config(t) - - o, err := New(config) - require.NoError(t, err) - - provider, err := o.fetchProviderFromURL(&rURL) - require.NoError(t, err) - require.Equal(t, "", provider) - }) +func (m *mockOIDCProvider) Verify(_ context.Context, _ string) (idToken, error) { + return m.verifyVal, m.verifyErr } -func TestPostSecretHandler(t *testing.T) { - t.Run("saves secret", func(t *testing.T) { - secret := secret(t) - secrets := make(map[string][]byte) - userSub := uuid.New().String() - config := config(t) - config.StoreProvider = &mockstorage.Provider{ - Stores: map[string]storage.Store{ - bootstrapStoreName: &mockstorage.MockStore{ - Store: map[string][]byte{ - userSub: marshal(t, &user.Profile{}), - }, - }, - secretsStoreName: &mockstorage.MockStore{Store: secrets}, - }, - } - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.postSecretHandler(result, newPostSecretRequest(t, secret)) - require.Equal(t, http.StatusOK, result.Code) - require.Contains(t, secrets, userSub) - require.Equal(t, secret, secrets[userSub]) - }) - - t.Run("error forbidden if request is not authenticated", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - result := httptest.NewRecorder() - o.postSecretHandler(result, httptest.NewRequest(http.MethodPost, "http://example.org/", nil)) - require.Equal(t, http.StatusForbidden, result.Code) - require.Contains(t, result.Body.String(), "no credentials") - }) - - t.Run("error statusconflict if user does not exist", func(t *testing.T) { - config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: uuid.New().String(), - }}, - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.postSecretHandler(result, newPostSecretRequest(t, nil)) - require.Equal(t, http.StatusConflict, result.Code) - require.Contains(t, result.Body.String(), "no such user") - }) - - t.Run("internal server error on generic bootstrap store FETCH error", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } - config.StoreProvider = &mockstorage.Provider{ - Store: &mockstorage.MockStore{ - Store: map[string][]byte{userSub: marshal(t, &user.Profile{})}, - ErrGet: errors.New("test"), - }, - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.postSecretHandler(result, newPostSecretRequest(t, nil)) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to query bootstrap store") - }) - - t.Run("error statusconflict if secret is already set for the user", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } - config.StoreProvider = &mockstorage.Provider{ - Stores: map[string]storage.Store{ - bootstrapStoreName: &mockstorage.MockStore{Store: map[string][]byte{ - userSub: marshal(t, &user.Profile{}), - }}, - secretsStoreName: &mockstorage.MockStore{Store: map[string][]byte{ - userSub: []byte("existing"), - }}, - }, - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.postSecretHandler(result, newPostSecretRequest(t, nil)) - require.Equal(t, http.StatusConflict, result.Code) - require.Contains(t, result.Body.String(), "secret already set") - }) - - t.Run("error internalservererror if cannot query secrets store", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } - config.StoreProvider = &mockstorage.Provider{ - Stores: map[string]storage.Store{ - bootstrapStoreName: &mockstorage.MockStore{Store: map[string][]byte{ - userSub: marshal(t, &user.Profile{}), - }}, - secretsStoreName: &mockstorage.MockStore{ - Store: map[string][]byte{userSub: marshal(t, &user.Profile{})}, - ErrGet: errors.New("test"), - }, - }, - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.postSecretHandler(result, newPostSecretRequest(t, nil)) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to query secrets store") - }) - - t.Run("error internalservererror if cannot save to secrets store", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.Hydra = &mockHydra{ - introspectValue: &admin.IntrospectOAuth2TokenOK{Payload: &models.OAuth2TokenIntrospection{ - Sub: userSub, - }}, - } - config.StoreProvider = &mockstorage.Provider{ - Stores: map[string]storage.Store{ - bootstrapStoreName: &mockstorage.MockStore{Store: map[string][]byte{ - userSub: marshal(t, &user.Profile{}), - }}, - secretsStoreName: &mockstorage.MockStore{ - Store: make(map[string][]byte), - ErrPut: errors.New("test"), - }, - }, - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.postSecretHandler(result, newPostSecretRequest(t, nil)) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to save to secrets store") - }) +type mockOAuth2Config struct { + authCodeVal string + authCodeFunc func(string, ...oauth2.AuthCodeOption) string + exchangeVal oauth2Token + exchangeErr error } -func TestGetSecretHandler(t *testing.T) { - t.Run("returns secret", func(t *testing.T) { - expected := uuid.New().String() - userSub := uuid.New().String() - config := config(t) - config.StoreProvider = &mockstorage.Provider{ - Store: &mockstorage.MockStore{ - Store: map[string][]byte{ - userSub: []byte(expected), - }, - }, - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.getSecretHandler(result, newGetSecretRequest(t, userSub, config.SecretsToken)) - require.Equal(t, http.StatusOK, result.Code) - payload := &GetSecretResponse{} - err = json.NewDecoder(result.Body).Decode(payload) - require.NoError(t, err) - decoded, err := base64.StdEncoding.DecodeString(payload.Secret) - require.NoError(t, err) - require.Equal(t, expected, string(decoded)) - }) - - t.Run("status forbidden if missing bearer token", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - result := httptest.NewRecorder() - o.getSecretHandler(result, httptest.NewRequest(http.MethodGet, "http://example.org/secrets/123", nil)) - require.Equal(t, http.StatusForbidden, result.Code) - require.Contains(t, result.Body.String(), "no credentials") - }) - - t.Run("status bad request if authorization header is not bearer", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - request := httptest.NewRequest(http.MethodGet, "http://example.org/secrets/123", nil) - request.Header.Set("authorization", "blahblah "+base64.StdEncoding.EncodeToString([]byte("INVALID"))) - result := httptest.NewRecorder() - o.getSecretHandler(result, request) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "invalid authorization scheme") - }) - - t.Run("status bad request if token is not base64", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - request := httptest.NewRequest(http.MethodGet, "http://example.org/secrets/123", nil) - request.Header.Set("authorization", "Bearer @&#$@^&*^") - result := httptest.NewRecorder() - o.getSecretHandler(result, request) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "failed to decode token") - }) - - t.Run("status unauthorized if token is invalid", func(t *testing.T) { - o, err := New(config(t)) - require.NoError(t, err) - request := httptest.NewRequest(http.MethodGet, "http://example.org/secrets/123", nil) - request.Header.Set("authorization", "Bearer "+base64.StdEncoding.EncodeToString([]byte("INVALID"))) - result := httptest.NewRecorder() - o.getSecretHandler(result, request) - require.Equal(t, http.StatusForbidden, result.Code) - require.Contains(t, result.Body.String(), "unauthorized") - }) - - t.Run("status badrequest if query parameter is missing", func(t *testing.T) { - config := config(t) - o, err := New(config) - require.NoError(t, err) - request := httptest.NewRequest(http.MethodGet, "http://example.org/secrets?sub=", nil) - request.Header.Set("authorization", "Bearer "+base64.StdEncoding.EncodeToString([]byte(config.SecretsToken))) - result := httptest.NewRecorder() - o.getSecretHandler(result, request) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "missing parameter") - }) - - t.Run("error badrequest if user does not exist", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.getSecretHandler(result, newGetSecretRequest(t, userSub, config.SecretsToken)) - require.Equal(t, http.StatusBadRequest, result.Code) - require.Contains(t, result.Body.String(), "non-existent user") - }) +func (m *mockOAuth2Config) AuthCodeURL(state string, options ...oauth2.AuthCodeOption) string { + if m.authCodeFunc != nil { + return m.authCodeFunc(state, options...) + } - t.Run("internal server error on generic secrets store FETCH error", func(t *testing.T) { - userSub := uuid.New().String() - config := config(t) - config.StoreProvider = &mockstorage.Provider{ - Store: &mockstorage.MockStore{ - Store: map[string][]byte{userSub: marshal(t, &user.Profile{})}, - ErrGet: errors.New("generic"), - }, - } - o, err := New(config) - require.NoError(t, err) - result := httptest.NewRecorder() - o.getSecretHandler(result, newGetSecretRequest(t, userSub, config.SecretsToken)) - require.Equal(t, http.StatusInternalServerError, result.Code) - require.Contains(t, result.Body.String(), "failed to query secrets store") - }) + return m.authCodeVal } -func newHydraLoginHTTPRequest(challenge string) *http.Request { - return httptest.NewRequest(http.MethodGet, - fmt.Sprintf("http://auth.com/hydra/login?login_challenge=%s", challenge), nil) +func (m *mockOAuth2Config) Exchange( + ctx context.Context, code string, options ...oauth2.AuthCodeOption) (oauth2Token, error) { + return m.exchangeVal, m.exchangeErr } -func newHydraConsentHTTPRequest(challenge string) *http.Request { +func newOIDCLoginRequest(provider, txnID string) *http.Request { return httptest.NewRequest(http.MethodGet, - fmt.Sprintf("http://auth.com/hydra/consent?consent_challenge=%s", challenge), nil) -} - -func newOIDCLoginRequest(provider string) *http.Request { - return httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com/oauth2/login?provider=%s", provider), nil) + fmt.Sprintf("http://example.com/oauth2/login?provider=%s&txnID=%s", provider, txnID), + nil) } func newOIDCCallback(state, code string) *http.Request { @@ -1890,7 +1496,7 @@ func newOIDCCallback(state, code string) *http.Request { func newGetBootstrapDataRequest() *http.Request { r := httptest.NewRequest(http.MethodGet, "http://example.com/bootstrap", nil) - r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte("1234567random")))) + r.Header.Set("Authorization", "GNAP 123") return r } @@ -1902,85 +1508,54 @@ func newPostBootstrapDataRequest(t *testing.T, params *UpdateBootstrapDataReques require.NoError(t, err) r := httptest.NewRequest(http.MethodPost, "http://example.com/bootstrap", bytes.NewReader(bits)) - r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte("1234567random")))) + r.Header.Set("Authorization", "GNAP 123") return r } -func newPostSecretRequest(t *testing.T, secret []byte) *http.Request { - t.Helper() - - payload, err := json.Marshal(&SetSecretRequest{Secret: secret}) - require.NoError(t, err) - - request := httptest.NewRequest(http.MethodPost, "http://example.com/secret", bytes.NewReader(payload)) - request.Header.Set( - "Authorization", - fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte("1234567random"))), - ) - - return request -} - -func newGetSecretRequest(t *testing.T, sub, token string) *http.Request { - t.Helper() - - r := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://www.example.org/secrets?sub=%s", sub), nil) - - r.Header.Set( - "authorization", - fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(token))), - ) - - return r -} - -func newDeviceCertRequest(t *testing.T, data *certHolder) *http.Request { - t.Helper() - - dataBytes, err := json.Marshal(data) - require.NoError(t, err) - - dataReader := bytes.NewReader(dataBytes) - - return httptest.NewRequest(http.MethodPost, "http://example.com/device", dataReader) -} - -type mockOIDCProvider struct { - name string - baseURL string - oauth2Config oauth2Config - verifyVal idToken - verifyErr error -} - -func (m *mockOIDCProvider) Name() string { - return m.name +type mockToken struct { + oauth2Claim interface{} + oidcClaimsFunc func(v interface{}) error + oidcClaimsErr error } -func (m *mockOIDCProvider) OAuth2Config(...string) oauth2Config { - if m.oauth2Config != nil { - return m.oauth2Config +func (m *mockToken) Extra(_ string) interface{} { + if m.oauth2Claim != nil { + return m.oauth2Claim } - return &mockOAuth2Config{} + return nil } -func (m *mockOIDCProvider) Endpoint() oauth2.Endpoint { - return oauth2.Endpoint{ - AuthURL: fmt.Sprintf("%s/oauth2/auth", m.baseURL), - TokenURL: fmt.Sprintf("%s/oauth2/token", m.baseURL), +func (m *mockToken) Claims(v interface{}) error { + if m.oidcClaimsFunc != nil { + return m.oidcClaimsFunc(v) } -} -func (m *mockOIDCProvider) Verify(_ context.Context, _ string) (idToken, error) { - return m.verifyVal, m.verifyErr + return m.oidcClaimsErr } func config(t *testing.T) *Config { t.Helper() + storeProv := mem.NewProvider() + + interact, err := redirect.New(&redirect.Config{ + StoreProvider: storeProv, + InteractBasePath: InteractPath, + }) + require.NoError(t, err) + + apConfig := &accesspolicy.Config{} + + err = json.Unmarshal([]byte(accessPolicyConf), apConfig) + require.NoError(t, err) + return &Config{ + StoreProvider: storeProv, + AccessPolicyConfig: apConfig, + BaseURL: baseURL, + InteractionHandler: interact, OIDC: &oidcmodel.Config{ CallbackURL: "http://test.com", Providers: map[string]*oidcmodel.ProviderConfig{ @@ -1996,35 +1571,16 @@ func config(t *testing.T) *Config { }, }, }, - TransientStoreProvider: mem.NewProvider(), - StoreProvider: mem.NewProvider(), BootstrapConfig: &BootstrapConfig{ DocumentSDSVaultURL: "http://docs.sds.example.org/sds/vaults", KeySDSVaultURL: "http://keys.sds.example.org/sds/vaults/", - AuthZKeyServerURL: "http://auth.kms.example.org/kms/keystores/", OpsKeyServerURL: "http://ops.kms.example.org/kms/keystores/", }, - Hydra: &mockHydra{}, - Cookies: &CookieConfig{ - AuthKey: cookieKey(t), - EncKey: cookieKey(t), - }, - StartupTimeout: 1, - SecretsToken: uuid.New().String(), + TransientStoreProvider: mem.NewProvider(), + StartupTimeout: 1, } } -func cookieKey(t *testing.T) []byte { - t.Helper() - - key := make([]byte, aes.BlockSize) - - _, err := rand.Read(key) - require.NoError(t, err) - - return key -} - func marshal(t *testing.T, v interface{}) []byte { t.Helper() @@ -2034,234 +1590,105 @@ func marshal(t *testing.T, v interface{}) []byte { return bits } -type mockOAuth2Config struct { - authCodeVal string - authCodeFunc func(string, ...oauth2.AuthCodeOption) string - exchangeVal oauth2Token - exchangeErr error -} - -func (m *mockOAuth2Config) AuthCodeURL(state string, options ...oauth2.AuthCodeOption) string { - if m.authCodeFunc != nil { - return m.authCodeFunc(state, options...) - } - - return m.authCodeVal -} - -func (m *mockOAuth2Config) Exchange( - ctx context.Context, code string, options ...oauth2.AuthCodeOption) (oauth2Token, error) { - return m.exchangeVal, m.exchangeErr -} - -type mockToken struct { - oauth2Claim interface{} - oidcClaimsFunc func(v interface{}) error - oidcClaimsErr error -} - -func (m *mockToken) Extra(_ string) interface{} { - if m.oauth2Claim != nil { - return m.oauth2Claim - } - - return nil -} - -func (m *mockToken) Claims(v interface{}) error { - if m.oidcClaimsFunc != nil { - return m.oidcClaimsFunc(v) - } - - return m.oidcClaimsErr +type errorReader struct { + err error } -type mockHydra struct { - getLoginRequestValue *admin.GetLoginRequestOK - getLoginRequestErr error - acceptLoginRequestValue *admin.AcceptLoginRequestOK - acceptLoginRequestErr error - getConsentRequestValue *admin.GetConsentRequestOK - getConsentRequestErr error - acceptConsentRequestValue *admin.AcceptConsentRequestOK - acceptConsentRequestErr error - introspectValue *admin.IntrospectOAuth2TokenOK - introspectErr error +func (e *errorReader) Read([]byte) (int, error) { + return 0, e.err } -func (m *mockHydra) GetLoginRequest(_ *admin.GetLoginRequestParams, - _ ...admin.ClientOption) (*admin.GetLoginRequestOK, error) { - return m.getLoginRequestValue, m.getLoginRequestErr -} - -func (m *mockHydra) AcceptLoginRequest(_ *admin.AcceptLoginRequestParams, - _ ...admin.ClientOption) (*admin.AcceptLoginRequestOK, error) { - return m.acceptLoginRequestValue, m.acceptLoginRequestErr -} - -func (m *mockHydra) GetConsentRequest(_ *admin.GetConsentRequestParams, - _ ...admin.ClientOption) (*admin.GetConsentRequestOK, error) { - return m.getConsentRequestValue, m.getConsentRequestErr -} - -func (m *mockHydra) AcceptConsentRequest(_ *admin.AcceptConsentRequestParams, - _ ...admin.ClientOption) (*admin.AcceptConsentRequestOK, error) { - return m.acceptConsentRequestValue, m.acceptConsentRequestErr -} - -func (m *mockHydra) IntrospectOAuth2Token(params *admin.IntrospectOAuth2TokenParams, - _ ...admin.ClientOption) (*admin.IntrospectOAuth2TokenOK, error) { - return m.introspectValue, m.introspectErr -} - -// makeSelfSignedCert returns a PEM-encoded self-signed certificate. -func makeSelfSignedCert(t *testing.T) string { - t.Helper() - - template := x509.Certificate{ - SerialNumber: big.NewInt(1234), - NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(time.Hour * 24), - KeyUsage: x509.KeyUsageDigitalSignature, - BasicConstraintsValid: true, - } - - pub, priv, err := ed25519.GenerateKey(rand.Reader) - require.NoError(t, err) - - certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv) - require.NoError(t, err) - - pemBytes := &bytes.Buffer{} - err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) - require.NoError(t, err) - - return pemBytes.String() -} - -// makeCACert returns a CA certificate, self-signed, with its PEM encoding and private key. -func makeCACert(t *testing.T) (*x509.Certificate, string, interface{}) { +func clientKey(t *testing.T) (*jwk.JWK, *gnap.ClientKey) { t.Helper() - certSerialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt32)) + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - template := x509.Certificate{ - SerialNumber: certSerialNumber, - Subject: pkix.Name{ - CommonName: "Testing CA", - SerialNumber: fmt.Sprint(*certSerialNumber), + privJWK := jwk.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: priv, + KeyID: "key1", + Algorithm: "ES256", }, - NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(time.Hour * 24), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - IsCA: true, + Kty: "EC", + Crv: "P-256", } - pub, priv, err := ed25519.GenerateKey(rand.Reader) - require.NoError(t, err) - - certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv) - require.NoError(t, err) + pubJWK := jwk.JWK{ + JSONWebKey: privJWK.Public(), + Kty: "EC", + Crv: "P-256", + } - pemBytes := &bytes.Buffer{} - err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) - require.NoError(t, err) + ck := gnap.ClientKey{ + Proof: "httpsig", + JWK: pubJWK, + } - return &template, pemBytes.String(), priv + return &privJWK, &ck } -// makeChildCert returns a certificate signed by parent, with its PEM encoding and private key. -func makeChildCert(t *testing.T, parent *x509.Certificate, parentPriv interface{}, - isIntermediate bool) (*x509.Certificate, string, interface{}) { +func tmpStaticHTML(t *testing.T) (string, func()) { t.Helper() - certSerialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt32)) - require.NoError(t, err) - - keyID := make([]byte, 16) - _, err = rand.Read(keyID) - require.NoError(t, err) - - template := x509.Certificate{ - SerialNumber: certSerialNumber, - Subject: pkix.Name{ - CommonName: "Testing child cert", - SerialNumber: fmt.Sprint(*certSerialNumber), - }, - NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(time.Hour * 24), - KeyUsage: x509.KeyUsageDigitalSignature, - BasicConstraintsValid: true, - SubjectKeyId: keyID, - IsCA: isIntermediate, - } - - if isIntermediate { - template.KeyUsage |= x509.KeyUsageCertSign - } - - pub, priv, err := ed25519.GenerateKey(rand.Reader) + f, err := os.CreateTemp("", "tmpfile-*.html") require.NoError(t, err) - certBytes, err := x509.CreateCertificate(rand.Reader, &template, parent, pub, parentPriv) - require.NoError(t, err) + defer func() { + e := f.Close() + if e != nil { + fmt.Printf("failed to close tmpfile: %s", e.Error()) + } + }() - pemBytes := &bytes.Buffer{} - err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) + _, err = f.Write([]byte(staticHTML)) require.NoError(t, err) - return &template, pemBytes.String(), priv -} - -type cookieOpt func(map[string]string) - -func withState(state string) cookieOpt { - return func(c map[string]string) { - c[stateCookie] = state - } -} - -func withHydraLoginChallenge(challenge string) cookieOpt { - return func(c map[string]string) { - c[hydraLoginChallengeCookie] = challenge - } -} - -func withProvider(provider string) cookieOpt { - return func(c map[string]string) { - c[providerCookie] = provider - } -} - -func mockCookies(c ...cookieOpt) *cookie.MockStore { - t := make(map[string]string) - - for i := range c { - c[i](t) - } - - cookies := make(map[interface{}]interface{}, len(c)) - - for k, v := range t { - cookies[k] = v - } - - return &cookie.MockStore{ - Jar: &cookie.MockJar{ - Cookies: cookies, - }, + return f.Name(), func() { + e := os.Remove(f.Name()) + if e != nil { + fmt.Printf("failed to delete tmpfile: %s", e.Error()) + } } } -func secret(t *testing.T) []byte { - t.Helper() - - s := make([]byte, 256) - - _, err := rand.Reader.Read(s) - require.NoError(t, err) - - return s -} +const ( + accessPolicyConf = `{ + "access-types": [{ + "reference": "client-id", + "permission": "NeedsConsent", + "expires-in": 600, + "access": { + "type": "trustbloc.xyz/auth/type/client-id", + "subject-keys": ["sub"], + "userid-key": "sub" + } + }, { + "reference": "other-access", + "permission": "NeedsConsent", + "expires-in": 300, + "access": { + "type": "trustbloc.xyz/auth/type/other-access", + "actions": ["write"], + "datasets": ["foobase"] + } + } + ] +}` + staticHTML = ` + + + +Redirecting... + + + + + + + +` +) diff --git a/test/bdd/bddtests_test.go b/test/bdd/bddtests_test.go index 901d5b0..f02315e 100644 --- a/test/bdd/bddtests_test.go +++ b/test/bdd/bddtests_test.go @@ -21,8 +21,6 @@ import ( "github.com/trustbloc/auth/test/bdd/pkg/common" bddctx "github.com/trustbloc/auth/test/bdd/pkg/context" "github.com/trustbloc/auth/test/bdd/pkg/gnap" - "github.com/trustbloc/auth/test/bdd/pkg/login" - "github.com/trustbloc/auth/test/bdd/pkg/secrets" ) func TestMain(m *testing.M) { @@ -133,8 +131,6 @@ func FeatureContext(s *godog.ScenarioContext) { } common.NewSteps(bddContext).RegisterSteps(s) - login.NewSteps(bddContext).RegisterSteps(s) bootstrap.NewSteps(bddContext).RegisterSteps(s) - secrets.NewSteps(bddContext).RegisterSteps(s) gnap.NewSteps(bddContext).RegisterSteps(s) } diff --git a/test/bdd/features/bootstrap_data.feature b/test/bdd/features/bootstrap_data.feature deleted file mode 100644 index 7b4155a..0000000 --- a/test/bdd/features/bootstrap_data.feature +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright SecureKey Technologies Inc. All Rights Reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# - -@all -@bootstrap -Feature: Bootstrap data - Background: Wallet login - Given a wallet that has logged in - - Scenario: Fetch bootstrap data - When the wallet executes an HTTP GET on the bootstrap endpoint - Then auth returns the SDS and KeyServer URLs - - Scenario: Update bootstrap data - When the wallet executes an HTTP POST on the bootstrap endpoint - And the wallet executes an HTTP GET on the bootstrap endpoint - Then auth returns the updated bootstrap data diff --git a/test/bdd/features/login.feature b/test/bdd/features/login.feature deleted file mode 100644 index a32c5ff..0000000 --- a/test/bdd/features/login.feature +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright SecureKey Technologies Inc. All Rights Reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# - -@all -@login -Feature: Wallet login - Background: Register wallet as OIDC client - Given the wallet is registered as an OIDC client - - Scenario: User authentication - When the wallet redirects the user to authenticate at auth - And the user picks their third party OIDC provider - And the user authenticates with the third party OIDC provider - Then the user is redirected back to the wallet - And the user has authenticated to the wallet diff --git a/test/bdd/features/secrets.feature b/test/bdd/features/secrets.feature deleted file mode 100644 index 3962cc0..0000000 --- a/test/bdd/features/secrets.feature +++ /dev/null @@ -1,21 +0,0 @@ -# -# Copyright SecureKey Technologies Inc. All Rights Reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# - -@all -@secrets -Feature: Secrets - Background: Wallet login - Given a user logged in with their wallet - - Scenario: Put secret - When the wallet stores the secret in auth - And the key server queries auth for the secret - Then the key server receives the secret - - Scenario: User attempts to store secret twice - When the wallet stores the secret in auth - And the wallet attempts to store the secret again - Then auth returns an error diff --git a/test/bdd/fixtures/auth-rest/oidc-config/providers.yaml b/test/bdd/fixtures/auth-rest/oidc-config/providers.yaml index 3a3e59a..5804376 100644 --- a/test/bdd/fixtures/auth-rest/oidc-config/providers.yaml +++ b/test/bdd/fixtures/auth-rest/oidc-config/providers.yaml @@ -6,18 +6,6 @@ providers: mockbank: - url: https://third.party.oidc.provider.example.com:5555/ - clientID: auth - clientSecret: auth-secret - name: Demo OIDC - signUpIconURL: - en: https://localhost:8070/static/images/sign-up-icon-en.svg - fr: https://localhost:8070/static/images/sign-up-icon-fr.svg - signInIconURL: - en: https://localhost:8070/static/images/sign-in-icon-en.svg - fr: https://localhost:8070/static/images/sign-in-icon-fr.svg - order: 1 - mockbank1: url: https://third.party.oidc.provider.example.com:5555/ clientID: auth1 clientSecret: auth-secret diff --git a/test/bdd/pkg/bootstrap/steps.go b/test/bdd/pkg/bootstrap/steps.go index 74af1bd..688ae02 100644 --- a/test/bdd/pkg/bootstrap/steps.go +++ b/test/bdd/pkg/bootstrap/steps.go @@ -20,7 +20,7 @@ import ( ) const ( - bootstrapDataPath = login.AUTH_HOST + "/bootstrap" + bootstrapDataPath = login.AUTH_HOST + "/gnap/bootstrap" docsSDSURL = "https://TODO.docs.sds.org" keysSDSURL = "https://TODO.keys.sds.org" authKeyServerURL = "https://TODO.auth.keyserver.org" @@ -40,7 +40,6 @@ func NewSteps(ctx *bddctx.BDDContext) *Steps { } func (s *Steps) RegisterSteps(gs *godog.ScenarioContext) { - gs.Step("a wallet that has logged in", s.userLoggedIn) gs.Step("a wallet that has logged in with GNAP", s.userLoggedInGNAP) gs.Step("the wallet executes an HTTP GET on the bootstrap endpoint", s.walletFetchesBootstrapData) gs.Step("auth returns the SDS and KeyServer URLs", s.hubAuthReturnsSDSAndKeyServerURLs) @@ -48,17 +47,6 @@ func (s *Steps) RegisterSteps(gs *godog.ScenarioContext) { gs.Step("auth returns the updated bootstrap data", s.hubAuthReturnsUpdatedBootstrapData) } -func (s *Steps) userLoggedIn() error { - var err error - - s.wallet, err = login.NewSteps(s.ctx).NewWalletLogin() - if err != nil { - return fmt.Errorf("failed to login user: %w", err) - } - - return nil -} - func (s *Steps) userLoggedInGNAP() error { var err error @@ -94,12 +82,6 @@ func (s *Steps) hubAuthReturnsSDSAndKeyServerURLs() error { ) } - if s.bootstrapDataResult.AuthZKeyServerURL != authKeyServerURL { - return fmt.Errorf( - "invalid auth keyserver URL: expected %s got %s", authKeyServerURL, s.bootstrapDataResult.AuthZKeyServerURL, - ) - } - if s.bootstrapDataResult.OpsKeyServerURL != opsKeyServerURL { return fmt.Errorf( "invalid keyServer URL: expected %s got %s", opsKeyServerURL, s.bootstrapDataResult.OpsKeyServerURL, diff --git a/test/bdd/pkg/gnap/steps.go b/test/bdd/pkg/gnap/steps.go index 55d6d20..41bccde 100644 --- a/test/bdd/pkg/gnap/steps.go +++ b/test/bdd/pkg/gnap/steps.go @@ -21,6 +21,7 @@ import ( "github.com/cucumber/godog" "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" "github.com/square/go-jose/v3" + "github.com/trustbloc/auth/component/gnap/as" "github.com/trustbloc/auth/component/gnap/rs" "github.com/trustbloc/auth/spi/gnap" @@ -34,10 +35,9 @@ const ( expectedInteractURL = authServerURL + "/gnap/interact" oidcProviderSelectorURL = authServerURL + "/oidc/login" - oidcCallbackURLURL = authServerURL + "/oidc/callback" authServerSignUpURL = authServerURL + "/ui/sign-up" - mockOIDCProviderName = "mockbank1" // providers.yaml + mockOIDCProviderName = "mockbank" // providers.yaml ) type Steps struct { diff --git a/test/bdd/pkg/login/mock_wallet.go b/test/bdd/pkg/login/mock_wallet.go index b2eb166..2b51754 100644 --- a/test/bdd/pkg/login/mock_wallet.go +++ b/test/bdd/pkg/login/mock_wallet.go @@ -20,6 +20,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "regexp" "strings" "github.com/coreos/go-oidc/v3/oidc" @@ -29,12 +30,14 @@ import ( "github.com/ory/hydra-client-go/client/admin" "github.com/ory/hydra-client-go/models" "github.com/square/go-jose/v3" + "golang.org/x/oauth2" + "github.com/trustbloc/auth/component/gnap/as" "github.com/trustbloc/auth/spi/gnap" "github.com/trustbloc/auth/spi/gnap/proof/httpsig" - "golang.org/x/oauth2" - "github.com/trustbloc/auth/pkg/restapi/operation" + restapi_gnap "github.com/trustbloc/auth/pkg/restapi/operation" + ) type MockWallet struct { @@ -53,7 +56,6 @@ type MockWallet struct { ReceivedCallback bool UserData *UserClaims CallbackErr error - Secret string gnap *gnapParams } @@ -193,13 +195,9 @@ func (m *MockWallet) gnapReqAccess() error { const ( authServerURL = "https://auth.trustbloc.local:8070" - expectedInteractURL = authServerURL + "/gnap/interact" - oidcProviderSelectorURL = authServerURL + "/oidc/login" - oidcCallbackURLURL = authServerURL + "/oidc/callback" authServerSignUpURL = authServerURL + "/ui/sign-up" - - gnapOIDCProviderName = "mockbank1" // providers.yaml + gnapOIDCProviderName = "mockbank" // providers.yaml ) func (m *MockWallet) gnapInteract() error { @@ -297,6 +295,18 @@ func (m *MockWallet) gnapInteract() error { clientRedirect := result.Header.Get("Location") + body, err := ioutil.ReadAll(result.Body) + if err != nil { + return fmt.Errorf("failed to read result body: %w", err) + } + + rx := regexp.MustCompile("window.opener.location.href = '(.*)';") + res := rx.FindStringSubmatch(string(body)) + + clientRedirect = res[1] + clientRedirect = strings.Replace(clientRedirect, "\\u0026", "\u0026", -1) + clientRedirect = strings.Replace(clientRedirect, "\\/", "/", -1) + // TODO validate the client finishURL if !strings.HasPrefix(clientRedirect, mockClientFinishURI) { return fmt.Errorf( @@ -347,7 +357,7 @@ func (m *MockWallet) gnapContinueRequest() error { return nil } -func (m *MockWallet) FetchBootstrapData(endpoint string) (*operation.BootstrapData, error) { +func (m *MockWallet) FetchBootstrapData(endpoint string) (*restapi_gnap.BootstrapData, error) { request, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to construct http request: %w", err) @@ -381,12 +391,12 @@ func (m *MockWallet) FetchBootstrapData(endpoint string) (*operation.BootstrapDa ) } - data := &operation.BootstrapData{} + data := &restapi_gnap.BootstrapData{} return data, json.NewDecoder(response.Body).Decode(data) } -func (m *MockWallet) UpdateBootstrapData(endpoint string, update *operation.UpdateBootstrapDataRequest) error { +func (m *MockWallet) UpdateBootstrapData(endpoint string, update *restapi_gnap.UpdateBootstrapDataRequest) error { payload, err := json.Marshal(update) if err != nil { return fmt.Errorf("failed to marshal payload: %w", err) @@ -428,52 +438,6 @@ func (m *MockWallet) UpdateBootstrapData(endpoint string, update *operation.Upda return nil } -func (m *MockWallet) CreateAndPushSecretToHubAuth(endpoint string) error { - m.Secret = uuid.New().String() - - payload, err := json.Marshal(&operation.SetSecretRequest{ - Secret: []byte(m.Secret), - }) - if err != nil { - return fmt.Errorf("failed to marshal request: %w", err) - } - - request, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload)) - if err != nil { - return fmt.Errorf("failed to create http request: %w", err) - } - - err = m.addAuthHeaders(request, payload) - if err != nil { - return err - } - - response, err := m.httpClient.Do(request) - if err != nil { - return fmt.Errorf("failed to push secret to auth: %w", err) - } - - defer func() { - closeErr := response.Body.Close() - if closeErr != nil { - fmt.Printf("WARNING - failed to close response body: %s\n", closeErr.Error()) - } - }() - - if response.StatusCode != http.StatusOK { - msg, err := ioutil.ReadAll(response.Body) - if err != nil { - fmt.Printf("WARNING - failed to read response body: %s\n", err.Error()) - } - - return fmt.Errorf( - "unexpected response: code=%d msg=%s", response.StatusCode, msg, - ) - } - - return nil -} - func (m *MockWallet) ServeHTTP(w http.ResponseWriter, r *http.Request) { m.ReceivedCallback = true diff --git a/test/bdd/pkg/login/steps.go b/test/bdd/pkg/login/steps.go index edf129b..9bc37e8 100644 --- a/test/bdd/pkg/login/steps.go +++ b/test/bdd/pkg/login/steps.go @@ -7,16 +7,9 @@ SPDX-License-Identifier: Apache-2.0 package login import ( - "bytes" - "encoding/json" "fmt" - "io/ioutil" "net/http" "net/http/cookiejar" - "strings" - - "github.com/cucumber/godog" - "github.com/google/uuid" bddctx "github.com/trustbloc/auth/test/bdd/pkg/context" ) @@ -25,26 +18,8 @@ const ( AUTH_HOST = "https://auth.trustbloc.local:8070" hubAuthHydraAdminURL = "https://localhost:4445" hubAuthOIDCProviderURL = "https://localhost:4444/" - hubAuthOIDCProviderSelectionURL = AUTH_HOST + "/ui" - hubAuthSelectOIDCProviderURL = AUTH_HOST + "/oauth2/login" - mockLoginURL = "https://localhost:8099/mock/login" - mockAuthenticationURL = "https://localhost:8099/mock/authn" - mockConsentURL = "https://localhost:8099/mock/consent" - mockAuthorizationURL = "https://localhost:8099/mock/authz" - mockOIDCProviderName = "mockbank" // providers.yaml ) -// defines the payload expected by the mock login consent server's /authn endpoint -type userAuthenticationConfig struct { - Sub string `json:"sub"` - Fail bool `json:"fail,omitempty"` -} - -type userAuthorizationConfig struct { - UserClaims *UserClaims `json:"user_claims,omitempty"` - Fail bool `json:"fail,omitempty"` -} - // BDD tests can configure type UserClaims struct { Sub string `json:"sub"` @@ -65,54 +40,23 @@ type Steps struct { expectedUserData *UserClaims } -func (s *Steps) RegisterSteps(gs *godog.ScenarioContext) { - gs.Step("the wallet is registered as an OIDC client", s.registerWallet) - gs.Step("the wallet redirects the user to authenticate at auth", s.walletRedirectsUserToAuthenticate) - gs.Step("the user picks their third party OIDC provider", s.userSelectsThirdPartyOIDCProvider) - gs.Step("the user authenticates with the third party OIDC provider", s.userAuthenticatesAtThirdPartyProvider) - gs.Step("the user is redirected back to the wallet", s.userRedirectedBackToWallet) - gs.Step("the user has authenticated to the wallet", s.userHasAuthenticatedToTheWallet) -} - // NewWalletLogin returns a new common.MockWallet that is logged in. -func (s *Steps) NewWalletLogin() (*MockWallet, error) { +func (s *Steps) NewWalletLoginGNAP() (*MockWallet, error) { err := s.registerWallet() if err != nil { return nil, err } - err = s.walletRedirectsUserToAuthenticate() - if err != nil { - return nil, err - } - - err = s.userSelectsThirdPartyOIDCProvider() - if err != nil { - return nil, err - } - - err = s.userAuthenticatesAtThirdPartyProvider() - if err != nil { - return nil, err - } - - err = s.userRedirectedBackToWallet() + err = s.wallet.GNAPLogin(authServerURL) if err != nil { return nil, err } - return s.wallet, s.userHasAuthenticatedToTheWallet() -} - -// NewWalletLogin returns a new common.MockWallet that is logged in. -func (s *Steps) NewWalletLoginGNAP() (*MockWallet, error) { - err := s.registerWallet() - if err != nil { - return nil, err + // TODO TEMP FIX - find the way how to validate Sub + s.expectedUserData = &UserClaims{ + Sub: s.wallet.UserData.Sub, } - err = s.wallet.GNAPLogin(authServerURL) - return s.wallet, s.userHasAuthenticatedToTheWallet() } @@ -130,103 +74,6 @@ func (s *Steps) registerWallet() error { return nil } -func (s *Steps) walletRedirectsUserToAuthenticate() error { - result, err := s.wallet.RequestUserAuthentication() - if err != nil { - return fmt.Errorf("mock wallet failed to redirect user for authentication: %w", err) - } - - if result.Request.URL.String() != hubAuthOIDCProviderSelectionURL { - return fmt.Errorf( - "the user ended up at the wrong login URL; expected %s got %s", - mockLoginURL, result.Request.URL.String(), - ) - } - - return nil -} - -func (s *Steps) userSelectsThirdPartyOIDCProvider() error { - request := fmt.Sprintf("%s?provider=%s", hubAuthSelectOIDCProviderURL, mockOIDCProviderName) - - result, err := s.browser.Get(request) - if err != nil { - return fmt.Errorf("user failed to select OIDC provider using request %s: %w", request, err) - } - - if !strings.HasPrefix(result.Request.URL.String(), mockLoginURL) { - return fmt.Errorf( - "user at wrong third party OIDC provider; expected %s got %s", - mockLoginURL, result.Request.URL.String(), - ) - } - - return nil -} - -func (s *Steps) userAuthenticatesAtThirdPartyProvider() error { - s.expectedUserData = &UserClaims{ - Sub: uuid.New().String(), - Name: "John Smith", - GivenName: "John", - FamilyName: "Smith", - Email: "john.smith@example.org", - } - - authn, err := json.Marshal(&userAuthenticationConfig{ - Sub: s.expectedUserData.Sub, - }) - if err != nil { - return fmt.Errorf("failed to marshal user authn config: %w", err) - } - - response, err := s.browser.Post(mockAuthenticationURL, "application/json", bytes.NewReader(authn)) - if err != nil { - return fmt.Errorf("user failed to send authentication data: %w", err) - } - - if !strings.HasPrefix(response.Request.URL.String(), mockConsentURL) { - return fmt.Errorf( - "user is at the wrong third party consent url; expected %s got %s", - mockConsentURL, response.Request.URL.String(), - ) - } - - authz, err := json.Marshal(&userAuthorizationConfig{ - UserClaims: s.expectedUserData, - }) - if err != nil { - return fmt.Errorf("failed to marshal user authz config: %w", err) - } - - response, err = s.browser.Post(mockAuthorizationURL, "application/json", bytes.NewReader(authz)) - if err != nil { - return fmt.Errorf("user failed to send authorization data: %w", err) - } - - if response.StatusCode != http.StatusOK { - msg, err := ioutil.ReadAll(response.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %w", err) - } - - return fmt.Errorf( - "unexpected status code; expected %d got %d msg=%s", - http.StatusOK, response.StatusCode, msg, - ) - } - - return nil -} - -func (s *Steps) userRedirectedBackToWallet() error { - if !s.wallet.ReceivedCallback { - return fmt.Errorf("the wallet has not received a callback") - } - - return nil -} - func (s *Steps) userHasAuthenticatedToTheWallet() error { if s.wallet.CallbackErr != nil { return fmt.Errorf("wallet failed to execute callback successfully: %w", s.wallet.CallbackErr) diff --git a/test/bdd/pkg/secrets/mock_key_server.go b/test/bdd/pkg/secrets/mock_key_server.go deleted file mode 100644 index 9e1fd3b..0000000 --- a/test/bdd/pkg/secrets/mock_key_server.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package secrets - -import ( - "crypto/tls" - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - - "github.com/trustbloc/auth/pkg/restapi/operation" -) - -func NewMockKeyServer(token string, tlsConfig *tls.Config) *MockKeyServer { - return &MockKeyServer{ - ApiToken: token, - TLSConfig: tlsConfig, - } -} - -type MockKeyServer struct { - ApiToken string - TLSConfig *tls.Config - UserSecret string -} - -func (m *MockKeyServer) FetchSecretShare(endpoint string) error { - request, err := http.NewRequest(http.MethodGet, endpoint, nil) - if err != nil { - return fmt.Errorf("failed to create http request: %w", err) - } - - m.addToken(request) - - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: m.TLSConfig, - }, - } - - response, err := client.Do(request) - if err != nil { - return fmt.Errorf("failed to invoke %s: %w", endpoint, err) - } - - defer func() { - closeErr := response.Body.Close() - if closeErr != nil { - fmt.Printf("WARNING - failed to close response body: %s\n", closeErr.Error()) - } - }() - - if response.StatusCode != http.StatusOK { - msg, err := ioutil.ReadAll(response.Body) - if err != nil { - fmt.Printf("WARNING - failed to read response body: %s\n", err.Error()) - } - - return fmt.Errorf( - "unexpected response: code=%d msg=%s", response.StatusCode, msg, - ) - } - - result := &operation.GetSecretResponse{} - - err = json.NewDecoder(response.Body).Decode(result) - if err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - decoded, err := base64.StdEncoding.DecodeString(result.Secret) - if err != nil { - return fmt.Errorf("failed to decode secret: %w", err) - } - - m.UserSecret = string(decoded) - - return nil -} - -func (m *MockKeyServer) addToken(r *http.Request) { - r.Header.Set( - "authorization", - fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(m.ApiToken))), - ) -} diff --git a/test/bdd/pkg/secrets/steps.go b/test/bdd/pkg/secrets/steps.go deleted file mode 100644 index 76b6009..0000000 --- a/test/bdd/pkg/secrets/steps.go +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright SecureKey Technologies Inc. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package secrets - -import ( - "fmt" - "strings" - - "github.com/cucumber/godog" - "github.com/trustbloc/auth/test/bdd/pkg/login" - - bddctx "github.com/trustbloc/auth/test/bdd/pkg/context" -) - -const ( - secretsEndpoint = login.AUTH_HOST + "/secret" - apiToken = "test_token" -) - -func NewSteps(ctx *bddctx.BDDContext) *Steps { - return &Steps{ - ctx: ctx, - keyServer: NewMockKeyServer(apiToken, ctx.TLSConfig()), - } -} - -type Steps struct { - ctx *bddctx.BDDContext - wallet *login.MockWallet - keyServer *MockKeyServer - updateSecretErr error -} - -func (s *Steps) RegisterSteps(gs *godog.ScenarioContext) { - gs.Step("a user logged in with their wallet", s.userLogin) - gs.Step("the wallet stores the secret in auth", s.walletStoresSecretInHubAuth) - gs.Step("the key server queries auth for the secret", s.keyServerFetchesSecret) - gs.Step("the key server receives the secret", s.keyServerReceivesTheSameSecret) - gs.Step("the wallet attempts to store the secret again", s.walletAttemptsStoringSecretAgain) - gs.Step("auth returns an error", s.updateSecretResultsInError) -} - -func (s *Steps) userLogin() error { - var err error - - s.wallet, err = login.NewSteps(s.ctx).NewWalletLogin() - if err != nil { - return fmt.Errorf("wallet failed to login: %w", err) - } - - return nil -} - -func (s *Steps) walletStoresSecretInHubAuth() error { - err := s.wallet.CreateAndPushSecretToHubAuth(secretsEndpoint) - if err != nil { - return fmt.Errorf("wallet failed to store secret in auth: %w", err) - } - - return nil -} - -func (s *Steps) keyServerFetchesSecret() error { - err := s.keyServer.FetchSecretShare(fmt.Sprintf("%s?sub=%s", secretsEndpoint, s.wallet.UserData.Sub)) - if err != nil { - return fmt.Errorf("key server failed to fetch the user's secret: %w", err) - } - - return nil -} - -func (s *Steps) keyServerReceivesTheSameSecret() error { - if s.keyServer.UserSecret != s.wallet.Secret { - return fmt.Errorf( - "keyServer received an unexpected secret: expected %s got %s", - s.wallet.Secret, s.keyServer.UserSecret, - ) - } - - return nil -} - -func (s *Steps) walletAttemptsStoringSecretAgain() error { - s.updateSecretErr = s.wallet.CreateAndPushSecretToHubAuth(secretsEndpoint) - if s.updateSecretErr == nil { - return fmt.Errorf("expected an error while pushing the secrets again but got nil") - } - - return nil -} - -func (s *Steps) updateSecretResultsInError() error { - if !strings.Contains(s.updateSecretErr.Error(), "secret already set") { - return fmt.Errorf("unexpected error message from auth: %s", s.updateSecretErr.Error()) - } - - return nil -}