Skip to content

Commit

Permalink
added bunch of tests
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst committed Dec 13, 2023
1 parent f7f54be commit ea8d7c0
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 48 deletions.
11 changes: 1 addition & 10 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,17 +257,8 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
for key, value := range httpRequest.URL.Query() {
params[key] = value[0]
}
// todo: store session in database? Isn't session specific for a particular flow?
session := createSession(params, *ownDID)
if session.RedirectURI == "" {
// TODO: Spec says that the redirect URI is optional, but it's not clear what to do if it's not provided.
// Threat models say it's unsafe to omit redirect_uri.
// See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "redirect_uri is required",
}
}
// todo: store session in database?

switch session.ResponseType {
case responseTypeCode:
Expand Down
44 changes: 42 additions & 2 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
metadata := oauth.AuthorizationServerMetadata{
AuthorizationEndpoint: "https://example.com/holder/authorize",
}
t.Run("ok - from holder", func(t *testing.T) {
t.Run("ok - code response type - from holder", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil)
ctx.verifierRole.EXPECT().AuthorizationServerMetadata(gomock.Any(), holderDID).Return(&metadata, nil)
Expand All @@ -220,7 +220,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{
clientIDParam: holderDID.String(),
redirectURIParam: "https://example.com",
responseTypeParam: "code",
responseTypeParam: responseTypeCode,
scopeParam: "test",
}), HandleAuthorizeRequestRequestObject{
Id: "verifier",
Expand All @@ -240,6 +240,42 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
assert.Contains(t, location, "response_type=vp_token")

})
t.Run("ok - vp_token response type - from verifier", func(t *testing.T) {
ctx := newTestClient(t)
_ = ctx.client.storageEngine.GetSessionDatabase().GetStore(oAuthFlowTimeout, oauthClientStateKey...).Put("state", OAuthSession{
// this is the state from the holder that was stored at the creation of the first authorization request to the verifier
ClientID: holderDID.String(),
Scope: "test",
OwnDID: holderDID,
ClientState: "state",
RedirectURI: "https://example.com/iam/holder/cb",
ResponseType: "code",
})
ctx.vdr.EXPECT().IsOwner(gomock.Any(), holderDID).Return(true, nil)
ctx.holderRole.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&metadata, nil)
ctx.relyingParty.EXPECT().PresentationDefinition(gomock.Any(), "https://example.com/iam/verifier/presentation_definition?scope=test").Return(&pe.PresentationDefinition{}, nil)
ctx.holderRole.EXPECT().BuildPresentation(gomock.Any(), holderDID, pe.PresentationDefinition{}, metadata, "nonce").Return(&vc.VerifiablePresentation{}, &pe.PresentationSubmission{}, nil)
ctx.holderRole.EXPECT().PostAuthorizationResponse(gomock.Any(), vc.VerifiablePresentation{}, pe.PresentationSubmission{}, "https://example.com/iam/verifier/response").Return("https://example.com/iam/holder/redirect", nil)

res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{
clientIDParam: verifierDID.String(),
clientIDSchemeParam: didScheme,
clientMetadataURIParam: "https://example.com/.well-known/authorization-server/iam/verifier",
nonceParam: "nonce",
presentationDefUriParam: "https://example.com/iam/verifier/presentation_definition?scope=test",
responseURIParam: "https://example.com/iam/verifier/response",
responseTypeParam: responseTypeVPToken,
scopeParam: "test",
stateParam: "state",
}), HandleAuthorizeRequestRequestObject{
Id: "holder",
})

require.NoError(t, err)
assert.IsType(t, HandleAuthorizeRequest302Response{}, res)
location := res.(HandleAuthorizeRequest302Response).Headers.Location
assert.Equal(t, location, "https://example.com/iam/holder/redirect")
})
t.Run("missing redirect_uri", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil)
Expand Down Expand Up @@ -410,6 +446,7 @@ type testCtx struct {
vcVerifier *verifier.MockVerifier
vcr *vcr.MockVCR
verifierRole *oauthServices.MockVerifier
holderRole *oauthServices.MockHolder
}

func newTestClient(t testing.TB) *testCtx {
Expand All @@ -424,13 +461,15 @@ func newTestClient(t testing.TB) *testCtx {
relyingPary := oauthServices.NewMockRelyingParty(ctrl)
vcVerifier := verifier.NewMockVerifier(ctrl)
verifierRole := oauthServices.NewMockVerifier(ctrl)
holderRole := oauthServices.NewMockHolder(ctrl)
mockVDR := vdr.NewMockVDR(ctrl)
mockVCR := vcr.NewMockVCR(ctrl)

authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes()
authnServices.EXPECT().RelyingParty().Return(relyingPary).AnyTimes()
mockVCR.EXPECT().Verifier().Return(vcVerifier).AnyTimes()
authnServices.EXPECT().Verifier().Return(verifierRole).AnyTimes()
authnServices.EXPECT().Holder().Return(holderRole).AnyTimes()
mockVDR.EXPECT().Resolver().Return(mockResolver).AnyTimes()

return &testCtx{
Expand All @@ -441,6 +480,7 @@ func newTestClient(t testing.TB) *testCtx {
resolver: mockResolver,
vdr: mockVDR,
verifierRole: verifierRole,
holderRole: holderRole,
vcr: mockVCR,
client: &Wrapper{
auth: authnServices,
Expand Down
7 changes: 4 additions & 3 deletions auth/api/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
ssi "github.com/nuts-foundation/go-did"
Expand All @@ -39,9 +43,6 @@ import (
"github.com/nuts-foundation/nuts-node/vcr/holder"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"net/http"
"net/url"
"strings"
)

var oauthNonceKey = []string{"oauth", "nonce"}
Expand Down
52 changes: 50 additions & 2 deletions auth/api/iam/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import (
"testing"
)

var holderDID = did.MustParseDID("did:web:example.com:holder")
var issuerDID = did.MustParseDID("did:web:example.com:issuer")
var holderDID = did.MustParseDID("did:web:example.com:iam:holder")
var issuerDID = did.MustParseDID("did:web:example.com:iam:issuer")

func TestWrapper_handleAuthorizeRequestFromHolder(t *testing.T) {
defaultParams := func() map[string]string {
Expand Down Expand Up @@ -100,6 +100,54 @@ func TestWrapper_handleAuthorizeRequestFromHolder(t *testing.T) {
})
}

func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
responseURI := "https://example.com/iam/verifier/response"
defaultParams := func() map[string]string {
return map[string]string{
clientIDParam: verifierDID.String(),
clientIDSchemeParam: didScheme,
clientMetadataURIParam: "https://example.com/.well-known/authorization-server/iam/verifier",
nonceParam: "nonce",
presentationDefUriParam: "https://example.com/iam/verifier/presentation_definition?scope=test",
responseURIParam: responseURI,
responseTypeParam: responseTypeVPToken,
scopeParam: "test",
stateParam: "state",
}
}
putState := func(ctx *testCtx, state string) {
_ = ctx.client.storageEngine.GetSessionDatabase().GetStore(oAuthFlowTimeout, oauthClientStateKey...).Put(state, OAuthSession{
// this is the state from the holder that was stored at the creation of the first authorization request to the verifier
ClientID: holderDID.String(),
Scope: "test",
OwnDID: holderDID,
ClientState: "state",
RedirectURI: "https://example.com/iam/holder/cb",
ResponseType: "code",
})
}

t.Run("missing client_id", func(t *testing.T) {
ctx := newTestClient(t)
params := defaultParams()
putState(ctx, "state")
delete(params, clientIDParam)
ctx.holderRole.EXPECT().PostError(gomock.Any(), gomock.Any(), responseURI).DoAndReturn(func(ctx context.Context, err oauth.OAuth2Error, responseURI string) (string, error) {
assert.Equal(t, oauth.InvalidRequest, err.Code)
assert.Equal(t, "missing client_id parameter", err.Description)
require.NotNil(t, err.RedirectURI)
assert.Equal(t, "https://example.com/iam/holder/cb", err.RedirectURI.String())
return "redirect", nil
})

response, err := ctx.client.handleAuthorizeRequestFromVerifier(context.Background(), holderDID, params)

require.NoError(t, err)
assert.Equal(t, "redirect", response.(HandleAuthorizeRequest302Response).Headers.Location)
})
// todo other error conditions
}

func TestWrapper_sendPresentationRequest(t *testing.T) {
instance := New(nil, nil, nil, nil)

Expand Down
34 changes: 9 additions & 25 deletions auth/client/iam/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,29 +226,8 @@ func (hb HTTPClient) PostError(ctx context.Context, auth2Error oauth.OAuth2Error
data := url.Values{}
data.Set(oauth.ErrorParam, string(auth2Error.Code))
data.Set(oauth.ErrorDescriptionParam, auth2Error.Description)
request, err := http.NewRequestWithContext(ctx, http.MethodPost, verifierCallbackURL, strings.NewReader(data.Encode()))
request.Header.Add("Accept", "application/json")
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if err != nil {
return "", err
}
response, err := hb.httpClient.Do(request.WithContext(ctx))
if err != nil {
return "", err
}
if err = core.TestResponseCode(http.StatusOK, response); err != nil {
return "", err
}
// take the redirectURL from the response body and return it
var responseData []byte
if responseData, err = io.ReadAll(response.Body); err != nil {
return "", fmt.Errorf("unable to read response: %w", err)
}
var redirect oauth.Redirect
if err = json.Unmarshal(responseData, &redirect); err != nil {
return "", fmt.Errorf("unable to unmarshal response: %w, %s", err, string(responseData))
}
return redirect.RedirectURI, nil

return hb.postFormExpectRedirect(ctx, data, verifierCallbackURL)
}

func (hb HTTPClient) PostAuthorizationResponse(ctx context.Context, vp vc.VerifiablePresentation, presentationSubmission pe.PresentationSubmission, verifierResponseURI string) (string, error) {
Expand All @@ -257,7 +236,12 @@ func (hb HTTPClient) PostAuthorizationResponse(ctx context.Context, vp vc.Verifi
data := url.Values{}
data.Set(oauth.VpTokenParam, vp.Raw())
data.Set(oauth.PresentationSubmissionParam, string(psBytes))
request, err := http.NewRequestWithContext(ctx, http.MethodPost, verifierResponseURI, strings.NewReader(data.Encode()))

return hb.postFormExpectRedirect(ctx, data, verifierResponseURI)
}

func (hb HTTPClient) postFormExpectRedirect(ctx context.Context, form url.Values, redirectURL string) (string, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodPost, redirectURL, strings.NewReader(form.Encode()))
if err != nil {
return "", err
}
Expand All @@ -278,7 +262,7 @@ func (hb HTTPClient) PostAuthorizationResponse(ctx context.Context, vp vc.Verifi
}
var redirect oauth.Redirect
if err = json.Unmarshal(responseData, &redirect); err != nil {
return "", fmt.Errorf("unable to unmarshal response: %w, %s", err, string(responseData))
return "", fmt.Errorf("unable to unmarshal response: %w", err)
}
return redirect.RedirectURI, nil
}
100 changes: 95 additions & 5 deletions auth/client/iam/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ package iam

import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/auth/oauth"
Expand All @@ -29,11 +36,6 @@ import (
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
)

func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {
Expand Down Expand Up @@ -239,6 +241,94 @@ func TestHTTPClient_AccessToken(t *testing.T) {
})
}

func TestHTTPClient_PostError(t *testing.T) {
redirectReturn := oauth.Redirect{
RedirectURI: "http://test.test",
}
//bytes, _ := json.Marshal(redirectReturn)
t.Run("ok", func(t *testing.T) {
ctx := context.Background()
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: redirectReturn}
tlsServer, client := testServerAndClient(t, &handler)

redirectURI, err := client.PostError(ctx, oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: "test"}, tlsServer.URL)

require.NoError(t, err)
assert.Equal(t, redirectReturn.RedirectURI, redirectURI)
})
}

func TestHTTPClient_PostAuthorizationResponse(t *testing.T) {
presentation := vc.VerifiablePresentation{ID: &ssi.URI{URL: url.URL{Scheme: "https", Host: "test.test"}}}
submission := pe.PresentationSubmission{Id: "id"}
redirectReturn := oauth.Redirect{
RedirectURI: "http://test.test",
}
t.Run("ok", func(t *testing.T) {
ctx := context.Background()
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: redirectReturn}
tlsServer, client := testServerAndClient(t, &handler)

redirectURI, err := client.PostAuthorizationResponse(ctx, presentation, submission, tlsServer.URL)

require.NoError(t, err)
assert.Equal(t, redirectReturn.RedirectURI, redirectURI)
})
}

func TestHTTPClient_postFormExpectRedirect(t *testing.T) {
redirectReturn := oauth.Redirect{
RedirectURI: "http://test.test",
}
data := url.Values{}
data.Set("test", "test")

t.Run("error - invalid URL", func(t *testing.T) {
ctx := context.Background()
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: redirectReturn}
_, client := testServerAndClient(t, &handler)

redirectURI, err := client.postFormExpectRedirect(ctx, data, ":")

require.Error(t, err)
assert.EqualError(t, err, "parse \":\": missing protocol scheme")
assert.Empty(t, redirectURI)
})
t.Run("error - unknown host", func(t *testing.T) {
ctx := context.Background()
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: redirectReturn}
_, client := testServerAndClient(t, &handler)

redirectURI, err := client.postFormExpectRedirect(ctx, data, "http://localhost")

require.Error(t, err)
assert.ErrorContains(t, err, "connection refused")
assert.Empty(t, redirectURI)
})
t.Run("error - invalid response", func(t *testing.T) {
ctx := context.Background()
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: "}"}
tlsServer, client := testServerAndClient(t, &handler)

redirectURI, err := client.postFormExpectRedirect(ctx, data, tlsServer.URL)

require.Error(t, err)
assert.EqualError(t, err, "unable to unmarshal response: invalid character '}' looking for beginning of value")
assert.Empty(t, redirectURI)
})
t.Run("error - server error", func(t *testing.T) {
ctx := context.Background()
handler := http2.Handler{StatusCode: http.StatusBadGateway, ResponseData: "offline"}
tlsServer, client := testServerAndClient(t, &handler)

redirectURI, err := client.postFormExpectRedirect(ctx, data, tlsServer.URL)

require.Error(t, err)
assert.EqualError(t, err, "server returned HTTP 502 (expected: 200)")
assert.Empty(t, redirectURI)
})
}

func testServerAndClient(t *testing.T, handler http.Handler) (*httptest.Server, *HTTPClient) {
tlsServer := http2.TestTLSServer(t, handler)
return tlsServer, &HTTPClient{
Expand Down
2 changes: 1 addition & 1 deletion docs/_static/auth/iam.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ paths:
example: did:web:example.com
scope:
type: string
description: The scope that will be The service for which this access token can be used.
description: The scope that will be the service for which this access token can be used.
example: eOverdracht-sender
userID:
type: string
Expand Down

0 comments on commit ea8d7c0

Please sign in to comment.