Skip to content
This repository has been archived by the owner on Apr 11, 2023. It is now read-only.

Commit

Permalink
Merge pull request #228 from Moopli/gnap-server
Browse files Browse the repository at this point in the history
feat: gnap response hash & validation
  • Loading branch information
fqutishat authored Jun 3, 2022
2 parents 5c72fe1 + ecdcea0 commit 0b87579
Show file tree
Hide file tree
Showing 11 changed files with 381 additions and 40 deletions.
38 changes: 38 additions & 0 deletions component/gnap/as/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions component/gnap/as/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions pkg/gnap/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions pkg/gnap/authhandler/auth_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
14 changes: 7 additions & 7 deletions pkg/gnap/authhandler/auth_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
Expand All @@ -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")
})
Expand All @@ -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")
})
Expand All @@ -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")
Expand Down Expand Up @@ -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)

Expand All @@ -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)
})
Expand All @@ -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)
Expand Down
56 changes: 46 additions & 10 deletions pkg/gnap/interact/redirect/interact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -56,22 +60,31 @@ 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()
if err != nil {
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)
Expand All @@ -86,6 +99,7 @@ func (h InteractHandler) PrepareInteraction(

return &gnap.ResponseInteract{
Redirect: h.interactBasePath + txnIDURLQueryPrefix + txnID,
Finish: serverNonce,
}, nil
}

Expand All @@ -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.
Expand Down
Loading

0 comments on commit 0b87579

Please sign in to comment.