diff --git a/component/gnap/as/client.go b/component/gnap/as/client.go index f625059..683323e 100644 --- a/component/gnap/as/client.go +++ b/component/gnap/as/client.go @@ -10,12 +10,16 @@ package as import ( "bytes" + "crypto" + "encoding/base64" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" "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" "github.com/trustbloc/auth/spi/gnap" @@ -120,6 +124,40 @@ func (c *Client) RequestAccess(req *gnap.AuthRequest) (*gnap.AuthResponse, error return gnapResp, nil } +// ErrInvalidInteractHash signifies that the provided interaction hash is invalid. +var ErrInvalidInteractHash = errors.New("invalid interact hash") + +// ValidateInteractHash returns whether the given interaction hash is valid for the given hash parameters. +func ValidateInteractHash(hash, myNonce, theirNonce, interactRef, reqURI string) error { + expectedHash, err := responseHash(myNonce, theirNonce, interactRef, reqURI) + if err != nil { + return err + } + + if hash != expectedHash { + return ErrInvalidInteractHash + } + + return nil +} + +func responseHash(clientNonce, serverNonce, interactRef, requestURI string) (string, error) { + hashBase := clientNonce + "\n" + serverNonce + "\n" + interactRef + "\n" + requestURI + + hasher := crypto.SHA3_512.New() + + _, err := hasher.Write([]byte(hashBase)) + if err != nil { + return "", fmt.Errorf("failed to hash: %w", err) + } + + hash := hasher.Sum(nil) + + hashB64 := base64.RawURLEncoding.EncodeToString(hash) + + return hashB64, nil +} + // Continue gnap auth request containing interact_ref. func (c *Client) Continue(req *gnap.ContinueRequest, token string) (*gnap.AuthResponse, error) { if req == nil { diff --git a/component/gnap/as/client_test.go b/component/gnap/as/client_test.go index ccb0d96..3debd40 100644 --- a/component/gnap/as/client_test.go +++ b/component/gnap/as/client_test.go @@ -198,6 +198,26 @@ func TestRequestAccess(t *testing.T) { } } +func TestValidateHash(t *testing.T) { + clientNonce := "foo" + serverNonce := "bar" + interactRef := "abc-xyz-123" + requestURI := "http://example.com/foo" + + t.Run("success", func(t *testing.T) { + hash, err := responseHash(clientNonce, serverNonce, interactRef, requestURI) + require.NoError(t, err) + + err = ValidateInteractHash(hash, clientNonce, serverNonce, interactRef, requestURI) + require.NoError(t, err) + }) + + t.Run("invalid hash", func(t *testing.T) { + err := ValidateInteractHash("blah", clientNonce, serverNonce, interactRef, requestURI) + require.ErrorIs(t, err, ErrInvalidInteractHash) + }) +} + func TestContinue(t *testing.T) { tests := []struct { name string diff --git a/pkg/gnap/api/api.go b/pkg/gnap/api/api.go index e98f08d..2c361ae 100644 --- a/pkg/gnap/api/api.go +++ b/pkg/gnap/api/api.go @@ -36,14 +36,16 @@ type InteractionHandler interface { // PrepareLoginConsentFlow takes a set of requested access tokens and subject // data, prepares a login & consent flow, and returns parameters for the user // client to initiate the login & consent flow. - PrepareInteraction(clientInteract *gnap.RequestInteract, requestedTokens []*ExpiringTokenRequest, + PrepareInteraction(clientInteract *gnap.RequestInteract, requestURI string, requestedTokens []*ExpiringTokenRequest, ) (*gnap.ResponseInteract, error) // CompleteLoginConsentFlow takes a set of access requests that the user // consented to, and the ID of the flow where this was performed, creates an // interact_ref, saves the consent set under the interact_ref, and returns the // interact_ref. - CompleteInteraction(flowID string, consentSet *ConsentResult) (string, *gnap.RequestInteract, error) + // + // Returns: interact_ref, response hash, client's RequestInteract, error + CompleteInteraction(flowID string, consentSet *ConsentResult) (string, string, *gnap.RequestInteract, error) // QueryInteraction returns the consent metadata and subject info saved under the interaction. QueryInteraction(interactRef string) (*ConsentResult, error) // DeleteInteraction deletes the interaction under interactRef if it exists. diff --git a/pkg/gnap/authhandler/auth_handler.go b/pkg/gnap/authhandler/auth_handler.go index f50dc53..c7145c5 100644 --- a/pkg/gnap/authhandler/auth_handler.go +++ b/pkg/gnap/authhandler/auth_handler.go @@ -84,9 +84,10 @@ func New(config *Config) (*AuthHandler, error) { } // HandleAccessRequest handles GNAP access requests. -func (h *AuthHandler) HandleAccessRequest( // nolint:funlen +func (h *AuthHandler) HandleAccessRequest( // nolint: funlen req *gnap.AuthRequest, reqVerifier api.Verifier, + reqURL string, ) (*gnap.AuthResponse, error) { var ( s *session.Session @@ -129,7 +130,7 @@ func (h *AuthHandler) HandleAccessRequest( // nolint:funlen s.Requested = permissions.NeedsConsent // TODO: support selecting one of multiple interaction handlers - interact, err := h.loginConsent.PrepareInteraction(req.Interact, permissions.NeedsConsent.Tokens) + interact, err := h.loginConsent.PrepareInteraction(req.Interact, reqURL, permissions.NeedsConsent.Tokens) if err != nil { return nil, fmt.Errorf("creating response interaction parameters: %w", err) } diff --git a/pkg/gnap/authhandler/auth_handler_test.go b/pkg/gnap/authhandler/auth_handler_test.go index 94261cb..78bc46f 100644 --- a/pkg/gnap/authhandler/auth_handler_test.go +++ b/pkg/gnap/authhandler/auth_handler_test.go @@ -72,7 +72,7 @@ func TestAuthHandler_HandleAccessRequest(t *testing.T) { req := &gnap.AuthRequest{} v := &mockverifier.MockVerifier{} - _, err = h.HandleAccessRequest(req, v) + _, err = h.HandleAccessRequest(req, v, "") require.Error(t, err) require.Contains(t, err.Error(), "missing client") }) @@ -89,7 +89,7 @@ func TestAuthHandler_HandleAccessRequest(t *testing.T) { } v := &mockverifier.MockVerifier{} - _, err = h.HandleAccessRequest(req, v) + _, err = h.HandleAccessRequest(req, v, "") require.Error(t, err) require.Contains(t, err.Error(), "getting client session by client ID") }) @@ -106,7 +106,7 @@ func TestAuthHandler_HandleAccessRequest(t *testing.T) { } v := &mockverifier.MockVerifier{} - _, err = h.HandleAccessRequest(req, v) + _, err = h.HandleAccessRequest(req, v, "") require.Error(t, err) require.Contains(t, err.Error(), "getting client session by key") }) @@ -127,7 +127,7 @@ func TestAuthHandler_HandleAccessRequest(t *testing.T) { ErrVerify: expectedErr, } - _, err = h.HandleAccessRequest(req, v) + _, err = h.HandleAccessRequest(req, v, "") require.Error(t, err) require.ErrorIs(t, err, expectedErr) require.Contains(t, err.Error(), "verification failure") @@ -162,7 +162,7 @@ func TestAuthHandler_HandleAccessRequest(t *testing.T) { } v := &mockverifier.MockVerifier{} - resp, err := h.HandleAccessRequest(req, v) + resp, err := h.HandleAccessRequest(req, v, "") require.Error(t, err) require.ErrorIs(t, err, expectErr) @@ -187,7 +187,7 @@ func TestAuthHandler_HandleAccessRequest(t *testing.T) { } v := &mockverifier.MockVerifier{} - resp, err := h.HandleAccessRequest(req, v) + resp, err := h.HandleAccessRequest(req, v, "") require.ErrorIs(t, err, expectErr) require.Nil(t, resp) }) @@ -211,7 +211,7 @@ func TestAuthHandler_HandleAccessRequest(t *testing.T) { } v := &mockverifier.MockVerifier{} - resp, err := h.HandleAccessRequest(req, v) + resp, err := h.HandleAccessRequest(req, v, "") require.NoError(t, err) require.Equal(t, "foo.com", resp.Interact.Redirect) diff --git a/pkg/gnap/interact/redirect/interact.go b/pkg/gnap/interact/redirect/interact.go index b460ebd..9cbce50 100644 --- a/pkg/gnap/interact/redirect/interact.go +++ b/pkg/gnap/interact/redirect/interact.go @@ -7,12 +7,14 @@ SPDX-License-Identifier: Apache-2.0 package redirect import ( + "crypto" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "github.com/hyperledger/aries-framework-go/spi/storage" + _ "golang.org/x/crypto/sha3" // nolint:gci // init sha3 hash. "github.com/trustbloc/auth/pkg/gnap/api" "github.com/trustbloc/auth/spi/gnap" @@ -40,7 +42,9 @@ const ( type txnData struct { api.ConsentResult - Interact *gnap.RequestInteract `json:"interact,omitempty"` + Interact *gnap.RequestInteract `json:"interact,omitempty"` + RequestURL string `json:"req-url,omitempty"` + ServerNonce string `json:"server-nonce,omitempty"` } // New creates a GNAP redirect-based user login&consent interaction handler. @@ -56,10 +60,13 @@ func New(config *Config) (*InteractHandler, error) { }, nil } +// TODO consider: split out the interaction hash stuff into a general handler for both redirect & push finish methods. + // PrepareInteraction initializes a redirect-based login&consent interaction, // returning the redirect parameters to be sent to the client. func (h InteractHandler) PrepareInteraction( clientInteract *gnap.RequestInteract, + requestURI string, requestedTokens []*api.ExpiringTokenRequest, ) (*gnap.ResponseInteract, error) { txnID, err := nonce() @@ -67,11 +74,17 @@ func (h InteractHandler) PrepareInteraction( return nil, err } + serverNonce, err := nonce() + if err != nil { + return nil, err + } + txn := &txnData{ ConsentResult: api.ConsentResult{ Tokens: requestedTokens, }, - Interact: clientInteract, + Interact: clientInteract, + ServerNonce: serverNonce, } txnBytes, err := json.Marshal(txn) @@ -86,6 +99,7 @@ func (h InteractHandler) PrepareInteraction( return &gnap.ResponseInteract{ Redirect: h.interactBasePath + txnIDURLQueryPrefix + txnID, + Finish: serverNonce, }, nil } @@ -94,42 +108,64 @@ func (h InteractHandler) PrepareInteraction( func (h InteractHandler) CompleteInteraction( txnID string, consentSet *api.ConsentResult, -) (string, *gnap.RequestInteract, error) { +) (string, string, *gnap.RequestInteract, error) { txnBytes, err := h.txnStore.Get(txnIDPrefix + txnID) if err != nil { - return "", nil, fmt.Errorf("loading txn data: %w", err) + return "", "", nil, fmt.Errorf("loading txn data: %w", err) } txn := &txnData{} err = json.Unmarshal(txnBytes, txn) if err != nil { - return "", nil, fmt.Errorf("parsing txn data: %w", err) + return "", "", nil, fmt.Errorf("parsing txn data: %w", err) } txn.ConsentResult.SubjectData = consentSet.SubjectData interactRef, err := nonce() if err != nil { - return "", nil, err + return "", "", nil, err + } + + hashValue, err := responseHash(txn.Interact.Finish.Nonce, txn.ServerNonce, interactRef, txn.RequestURL) + if err != nil { + return "", "", nil, fmt.Errorf("creating response hash: %w", err) } txnBytes, err = json.Marshal(txn.ConsentResult) if err != nil { - return "", nil, fmt.Errorf("marshaling txn data: %w", err) + return "", "", nil, fmt.Errorf("marshaling txn data: %w", err) } err = h.txnStore.Put(interactRefPrefix+interactRef, txnBytes) if err != nil { - return "", nil, fmt.Errorf("saving txn data: %w", err) + return "", "", nil, fmt.Errorf("saving txn data: %w", err) } err = h.txnStore.Delete(txnIDPrefix + txnID) if err != nil { - return "", nil, fmt.Errorf("deleting old txn data: %w", err) + return "", "", nil, fmt.Errorf("deleting old txn data: %w", err) } - return interactRef, txn.Interact, nil + return interactRef, hashValue, txn.Interact, nil +} + +func responseHash(clientNonce, serverNonce, interactRef, requestURI string) (string, error) { + hashBase := clientNonce + "\n" + serverNonce + "\n" + interactRef + "\n" + requestURI + + hasher := crypto.SHA3_512.New() + + _, err := hasher.Write([]byte(hashBase)) + if err != nil { + return "", fmt.Errorf("failed to hash: %w", err) + } + + hash := hasher.Sum(nil) + + hashB64 := base64.RawURLEncoding.EncodeToString(hash) + + return hashB64, nil } // QueryInteraction fetches the interaction under the given interact_ref. diff --git a/pkg/gnap/interact/redirect/interact_test.go b/pkg/gnap/interact/redirect/interact_test.go new file mode 100644 index 0000000..0153b61 --- /dev/null +++ b/pkg/gnap/interact/redirect/interact_test.go @@ -0,0 +1,237 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package redirect + +import ( + "errors" + "strings" + "testing" + + "github.com/hyperledger/aries-framework-go/component/storageutil/mem" + "github.com/stretchr/testify/require" + + "github.com/trustbloc/auth/pkg/gnap/api" + "github.com/trustbloc/auth/pkg/internal/common/mockstorage" +) + +func TestNew(t *testing.T) { + t.Run("success", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + require.NotNil(t, h) + }) + + t.Run("error", func(t *testing.T) { + expectErr := errors.New("expected error") + + h, err := New(&Config{ + StoreProvider: &mockstorage.Provider{ErrOpenStoreHandle: expectErr}, + }) + require.ErrorIs(t, err, expectErr) + require.Nil(t, h) + }) +} + +func TestInteractHandler_PrepareInteraction(t *testing.T) { + t.Run("fail to save txn data", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + expectErr := errors.New("expected error") + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{}, + ErrPut: expectErr, + } + + res, err := h.PrepareInteraction(nil, "foo", nil) + require.ErrorIs(t, err, expectErr) + require.Nil(t, res) + }) + + t.Run("success", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + res, err := h.PrepareInteraction(nil, "foo", nil) + require.NoError(t, err) + + require.True(t, strings.HasPrefix(res.Redirect, h.interactBasePath)) + require.NotEmpty(t, res.Finish) + }) +} + +func TestInteractHandler_CompleteInteraction(t *testing.T) { + t.Run("fail to load txn data", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + expectErr := errors.New("expected error") + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + txnIDPrefix: nil, + }, + ErrGet: expectErr, + } + + _, _, _, err = h.CompleteInteraction("", nil) + require.ErrorIs(t, err, expectErr) + }) + + t.Run("fail to parse txn data", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + txnIDPrefix: nil, + }, + } + + _, _, _, err = h.CompleteInteraction("", nil) + require.Error(t, err) + require.Contains(t, err.Error(), "parsing txn data") + }) + + t.Run("fail to save txn data", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + expectErr := errors.New("expected error") + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + txnIDPrefix: []byte(`{"interact":{"finish":{}},"tok":[],"sub":{}}`), + }, + ErrPut: expectErr, + } + + _, _, _, err = h.CompleteInteraction("", &api.ConsentResult{}) + require.ErrorIs(t, err, expectErr) + }) + + t.Run("fail to delete old txn data", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + expectErr := errors.New("expected error") + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + txnIDPrefix: []byte(`{"interact":{"finish":{}},"tok":[],"sub":{}}`), + }, + ErrDelete: expectErr, + } + + _, _, _, err = h.CompleteInteraction("", &api.ConsentResult{}) + require.ErrorIs(t, err, expectErr) + }) + + t.Run("success", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + txnIDPrefix: []byte(`{"interact":{"finish":{}},"tok":[],"sub":{}}`), + }, + } + + _, _, _, err = h.CompleteInteraction("", &api.ConsentResult{}) + require.NoError(t, err) + }) +} + +func TestInteractHandler_QueryInteraction(t *testing.T) { + t.Run("fail to load txn data", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + expectErr := errors.New("expected error") + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + interactRefPrefix: nil, + }, + ErrGet: expectErr, + } + + _, err = h.QueryInteraction("") + require.ErrorIs(t, err, expectErr) + }) + + t.Run("fail to parse txn data", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + interactRefPrefix: nil, + }, + } + + _, err = h.QueryInteraction("") + require.Error(t, err) + require.Contains(t, err.Error(), "parsing interaction data") + }) + + t.Run("success", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + interactRefPrefix: []byte(`{"tok":[],"sub":{}}`), + }, + } + + res, err := h.QueryInteraction("") + require.NoError(t, err) + require.NotNil(t, res) + }) +} + +func TestInteractHandler_DeleteInteraction(t *testing.T) { + t.Run("fail to delete interaction", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + expectErr := errors.New("expected error") + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + interactRefPrefix: []byte(`{"interact":{"finish":{}},"tok":[],"sub":{}}`), + }, + ErrDelete: expectErr, + } + + err = h.DeleteInteraction("") + require.ErrorIs(t, err, expectErr) + }) + + t.Run("success", func(t *testing.T) { + h, err := New(config()) + require.NoError(t, err) + + h.txnStore = &mockstorage.MockStore{ + Store: map[string][]byte{ + interactRefPrefix: []byte(`{"interact":{"finish":{}},"tok":[],"sub":{}}`), + }, + } + + err = h.DeleteInteraction("") + require.NoError(t, err) + }) +} + +func config() *Config { + return &Config{ + InteractBasePath: "https://example.com/interact-base-path", + StoreProvider: mem.NewProvider(), + } +} diff --git a/pkg/internal/common/mockinteract/interact.go b/pkg/internal/common/mockinteract/interact.go index edaa7b5..6162e5b 100644 --- a/pkg/internal/common/mockinteract/interact.go +++ b/pkg/internal/common/mockinteract/interact.go @@ -25,6 +25,7 @@ type InteractHandler struct { // PrepareInteraction mock. func (l *InteractHandler) PrepareInteraction( clientInteract *gnap.RequestInteract, + requestURI string, requestedTokens []*api.ExpiringTokenRequest, ) (*gnap.ResponseInteract, error) { return l.PrepareVal, l.PrepareErr @@ -34,8 +35,8 @@ func (l *InteractHandler) PrepareInteraction( func (l *InteractHandler) CompleteInteraction( flowID string, consentSet *api.ConsentResult, -) (string, *gnap.RequestInteract, error) { - return l.CompleteVal, nil, l.CompleteErr +) (string, string, *gnap.RequestInteract, error) { + return l.CompleteVal, "", nil, l.CompleteErr } // QueryInteraction mock. diff --git a/pkg/internal/common/mockstorage/storage.go b/pkg/internal/common/mockstorage/storage.go index 429f58e..c7d1873 100644 --- a/pkg/internal/common/mockstorage/storage.go +++ b/pkg/internal/common/mockstorage/storage.go @@ -66,11 +66,12 @@ func (p *Provider) GetOpenStores() []storage.Store { // MockStore represents a mock store. type MockStore struct { - Store map[string][]byte - lock sync.RWMutex - ErrPut error - ErrGet error - ErrQuery error + Store map[string][]byte + lock sync.RWMutex + ErrPut error + ErrGet error + ErrQuery error + ErrDelete error } // GetTags fetches all tags associated with the given key. @@ -134,7 +135,7 @@ func (s *MockStore) Get(k string) ([]byte, error) { return val, s.ErrGet } -// Delete is currently unimplemented. +// Delete returns s.ErrDelete, not actually deleting. func (s *MockStore) Delete(k string) error { - panic("implement me") + return s.ErrDelete } diff --git a/pkg/restapi/gnap/operations.go b/pkg/restapi/gnap/operations.go index 2a9d2df..ac469e8 100644 --- a/pkg/restapi/gnap/operations.go +++ b/pkg/restapi/gnap/operations.go @@ -65,7 +65,8 @@ const ( transientStoreName = "gnap_transient" // client redirect query params. - interactRefQueryParam = "interact_ref" + interactRefQueryParam = "interact_ref" + responseHashQueryParam = "hash" ) // TODO: figure out what logic should go in the access policy vs operation handlers. @@ -154,7 +155,7 @@ func (o *Operation) GetRESTHandlers() []common.Handler { } } -func (o *Operation) authRequestHandler(w http.ResponseWriter, req *http.Request) { // nolint: dupl +func (o *Operation) authRequestHandler(w http.ResponseWriter, req *http.Request) { authRequest := &gnap.AuthRequest{} bodyBytes, err := ioutil.ReadAll(req.Body) @@ -182,7 +183,7 @@ func (o *Operation) authRequestHandler(w http.ResponseWriter, req *http.Request) v := httpsig.NewVerifier(req) - resp, err := o.authHandler.HandleAccessRequest(authRequest, v) + 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) @@ -357,11 +358,14 @@ func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) return } - interactRef, clientInteract, err := o.interactionHandler.CompleteInteraction(data.TxnID, &api.ConsentResult{ - SubjectData: map[string]string{ - "sub": claims.Sub, + 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)) @@ -381,6 +385,7 @@ func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) q := clientURI.Query() q.Add(interactRefQueryParam, interactRef) + q.Add(responseHashQueryParam, responseHash) clientURI.RawQuery = q.Encode() @@ -445,7 +450,7 @@ func (o *Operation) authContinueHandler(w http.ResponseWriter, req *http.Request o.writeResponse(w, resp) } -func (o *Operation) introspectHandler(w http.ResponseWriter, req *http.Request) { // nolint: dupl +func (o *Operation) introspectHandler(w http.ResponseWriter, req *http.Request) { introspectRequest := &gnap.IntrospectRequest{} bodyBytes, err := ioutil.ReadAll(req.Body) diff --git a/pkg/restapi/gnap/operations_test.go b/pkg/restapi/gnap/operations_test.go index 99e36fa..1d7962d 100644 --- a/pkg/restapi/gnap/operations_test.go +++ b/pkg/restapi/gnap/operations_test.go @@ -471,7 +471,7 @@ func TestOIDCCallbackHandler(t *testing.T) { Method: "redirect", URI: "example.foo/client-redirect", }, - }, []*api.ExpiringTokenRequest{ + }, "", []*api.ExpiringTokenRequest{ { TokenRequest: gnap.TokenRequest{ Access: []gnap.TokenAccess{ @@ -806,7 +806,7 @@ func TestOIDCCallbackHandler(t *testing.T) { Method: "redirect", URI: "^$#^*#%$^&#$%#T^ UTTER GIBBERISH", }, - }, []*api.ExpiringTokenRequest{ + }, "", []*api.ExpiringTokenRequest{ { TokenRequest: gnap.TokenRequest{ Access: []gnap.TokenAccess{