Skip to content

Commit

Permalink
added e2e test for OpenID4VP s2s flow (#2617)
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst authored Nov 27, 2023
1 parent ab0aefa commit 30a3bd6
Show file tree
Hide file tree
Showing 27 changed files with 365 additions and 40 deletions.
13 changes: 9 additions & 4 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"embed"
"encoding/json"
"errors"
"fmt"
"github.com/labstack/echo/v4"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/audit"
Expand Down Expand Up @@ -81,6 +82,9 @@ func (r Wrapper) Routes(router core.EchoRouter) {
return r.middleware(ctx, request, operationID, f)
}
},
func(f StrictHandlerFunc, operationID string) StrictHandlerFunc {
return audit.StrictMiddleware(f, apiModuleName, operationID)
},
}))
auditMiddleware := audit.Middleware(apiModuleName)
// The following handler is of the OpenID4VCI wallet which is called by the holder (wallet owner)
Expand Down Expand Up @@ -110,12 +114,12 @@ func (r Wrapper) middleware(ctx echo.Context, request interface{}, operationID s
if strings.HasPrefix(ctx.Request().URL.Path, "/iam/") {
ctx.Set(core.ErrorWriterContextKey, &oauth.Oauth2ErrorWriter{})
}
audit.StrictMiddleware(f, apiModuleName, operationID)

return f(ctx, request)
}

// HandleTokenRequest handles calls to the token endpoint for exchanging a grant (e.g authorization code or pre-authorized code) for an access token.
func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequestRequestObject) (HandleTokenRequestResponseObject, error) {
func (r Wrapper) HandleTokenRequest(_ context.Context, request HandleTokenRequestRequestObject) (HandleTokenRequestResponseObject, error) {
switch request.Body.GrantType {
case "authorization_code":
// Options:
Expand All @@ -125,7 +129,7 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
Code: oauth.UnsupportedGrantType,
Description: "not implemented yet",
}
case "vp_token":
case "vp_token-bearer":
// Options:
// - service-to-service vp_token flow
return nil, oauth.OAuth2Error{
Expand All @@ -141,7 +145,8 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
}
default:
return nil, oauth.OAuth2Error{
Code: oauth.UnsupportedGrantType,
Code: oauth.UnsupportedGrantType,
Description: fmt.Sprintf("grant_type '%s' is not supported", request.Body.GrantType),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ func TestWrapper_HandleTokenRequest(t *testing.T) {
},
})

requireOAuthError(t, err, oauth.UnsupportedGrantType, "")
requireOAuthError(t, err, oauth.UnsupportedGrantType, "grant_type 'unsupported' is not supported")
assert.Nil(t, res)
})
}
Expand Down
9 changes: 8 additions & 1 deletion auth/client/iam/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (hb HTTPClient) AccessToken(ctx context.Context, tokenEndpoint string, vp v
data.Set(oauth.PresentationSubmissionParam, string(presentationSubmission))
data.Set(oauth.ScopeParam, scopes)
request, err := http.NewRequestWithContext(ctx, http.MethodPost, presentationDefinitionURL.String(), strings.NewReader(data.Encode()))
request.Header.Add("Accept", "application/json")
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if err != nil {
return token, err
Expand All @@ -158,9 +159,15 @@ func (hb HTTPClient) AccessToken(ctx context.Context, tokenEndpoint string, vp v
if innerErr := core.TestResponseCode(http.StatusBadRequest, response); innerErr != nil {
// a non oauth error, the response body could contain a lot of stuff. We'll log and return the entire error
log.Logger().Debugf("authorization server token endpoint returned non oauth error (statusCode=%d)", response.StatusCode)
return token, err
}
httpErr := err.(core.HttpError)
oauthError := oauth.OAuth2Error{}
if err := json.Unmarshal(httpErr.ResponseBody, &oauthError); err != nil {
return token, fmt.Errorf("unable to unmarshal OAuth error response: %w", err)
}

return token, err
return token, oauthError
}

var responseData []byte
Expand Down
6 changes: 3 additions & 3 deletions auth/client/iam/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,10 @@ func TestHTTPClient_AccessToken(t *testing.T) {
_, err := client.AccessToken(ctx, tlsServer.URL, vc.VerifiablePresentation{}, pe.PresentationSubmission{}, "test")

require.Error(t, err)
// check if the error is a http error
httpError, ok := err.(core.HttpError)
// check if the error is an OAuth error
oauthError, ok := err.(oauth.OAuth2Error)
require.True(t, ok)
assert.Equal(t, "{\"error\":\"invalid_scope\"}", string(httpError.ResponseBody))
assert.Equal(t, oauth.InvalidScope, oauthError.Code)
})
t.Run("error - generic server error", func(t *testing.T) {
ctx := context.Background()
Expand Down
20 changes: 12 additions & 8 deletions auth/oauth/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,23 @@ func (p Oauth2ErrorWriter) Write(echoContext echo.Context, _ int, _ string, err
}
redirectURI, _ := url.Parse(oauthErr.RedirectURI)
if oauthErr.RedirectURI == "" || redirectURI == nil {
// Can't redirect the user-agent back, render error as JSON or plain text (depending on content-type)
// Can't redirect the user-agent back, render error as JSON or plain text (depending on accept/content-type)
accept := echoContext.Request().Header.Get("Accept")
if strings.Contains(accept, "application/json") {
// Return JSON response
return echoContext.JSON(oauthErr.StatusCode(), oauthErr)
}
contentType := echoContext.Request().Header.Get("Content-Type")
if strings.Contains(contentType, "application/json") {
// Return JSON response
return echoContext.JSON(oauthErr.StatusCode(), oauthErr)
} else {
// Return plain text response
parts := []string{string(oauthErr.Code)}
if oauthErr.Description != "" {
parts = append(parts, oauthErr.Description)
}
return echoContext.String(oauthErr.StatusCode(), strings.Join(parts, " - "))
}
// Return plain text response
parts := []string{string(oauthErr.Code)}
if oauthErr.Description != "" {
parts = append(parts, oauthErr.Description)
}
return echoContext.String(oauthErr.StatusCode(), strings.Join(parts, " - "))
}
// Redirect the user-agent back to the client
query := redirectURI.Query()
Expand Down
4 changes: 2 additions & 2 deletions auth/services/oauth/relying_party.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,11 @@ func determineFormat(formats map[string]map[string][]string) (string, error) {
for format := range formats {
switch format {
case openid4vc.VerifiablePresentationJWTFormat:
fallthrough
return format, nil
case openid4vc.VerifiablePresentationJSONLDFormat:
fallthrough
case "jwt_vp_json":
return format, nil
return openid4vc.VerifiablePresentationJSONLDFormat, nil
default:
continue
}
Expand Down
6 changes: 2 additions & 4 deletions auth/services/oauth/relying_party_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"fmt"
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
http2 "github.com/nuts-foundation/nuts-node/test/http"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -154,10 +153,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) {
_, err := ctx.relyingParty.RequestRFC021AccessToken(context.Background(), walletDID, ctx.verifierDID, scopes)

require.Error(t, err)
httpError, ok := err.(core.HttpError)
oauthError, ok := err.(oauth.OAuth2Error)
require.True(t, ok)
assert.Equal(t, http.StatusBadRequest, httpError.StatusCode)
assert.Equal(t, oauthErrorBytes, httpError.ResponseBody)
assert.Equal(t, oauth.InvalidScope, oauthError.Code)
})
t.Run("error - no matching credentials", func(t *testing.T) {
ctx := createOAuthRPContext(t)
Expand Down
40 changes: 40 additions & 0 deletions e2e-tests/oauth-flow/openid4vp/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
version: "3.7"
services:
nodeA-backend:
image: "${IMAGE_NODE_A:-nutsfoundation/nuts-node:master}"
ports:
- "11323:1323"
environment:
NUTS_CONFIGFILE: /opt/nuts/nuts.yaml
volumes:
- "./node-A/nuts.yaml:/opt/nuts/nuts.yaml:ro"
- "./node-A/data:/opt/nuts/data:rw"
- "../../tls-certs/nodeA-backend-certificate.pem:/opt/nuts/certificate-and-key.pem:ro"
- "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro"
- "./node-A/presentationexchangemapping.json:/opt/nuts/presentationexchangemapping.json:ro"
healthcheck:
interval: 1s # Make test run quicker by checking health status more often
nodeA:
image: nginx:1.25.1
ports:
- "10443:443"
volumes:
- "./node-A/nginx.conf:/etc/nginx/nginx.conf:ro"
- "../../tls-certs/nodeA-certificate.pem:/etc/nginx/ssl/server.pem:ro"
- "../../tls-certs/nodeA-certificate.pem:/etc/nginx/ssl/key.pem:ro"
- "../../tls-certs/truststore.pem:/etc/nginx/ssl/truststore.pem:ro"
- "./node-A/html:/etc/nginx/html:ro"
nodeB:
image: "${IMAGE_NODE_B:-nutsfoundation/nuts-node:master}"
ports:
- "21323:1323"
environment:
NUTS_CONFIGFILE: /opt/nuts/nuts.yaml
volumes:
- "./node-B/data:/opt/nuts/data:rw"
- "./node-B/nuts.yaml:/opt/nuts/nuts.yaml:ro"
- "../../tls-certs/nodeB-certificate.pem:/opt/nuts/certificate-and-key.pem:ro"
- "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro"
- "../../tls-certs/truststore.pem:/etc/ssl/certs/truststore.pem:ro"
healthcheck:
interval: 1s # Make test run quicker by checking health status more often
File renamed without changes.
62 changes: 62 additions & 0 deletions e2e-tests/oauth-flow/openid4vp/node-A/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

keepalive_timeout 65;

include /etc/nginx/conf.d/*.conf;

upstream nodeA-backend {
server nodeA-backend:1323;
}

server {
server_name nodeA;
listen 443 ssl;
http2 on;
ssl_certificate /etc/nginx/ssl/server.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_client_certificate /etc/nginx/ssl/truststore.pem;
ssl_verify_client optional;
ssl_verify_depth 1;
ssl_protocols TLSv1.3;

location / {
proxy_set_header X-Ssl-Client-Cert $ssl_client_escaped_cert;
proxy_pass http://nodeA-backend;
}

location /ping {
auth_request /delegated;
auth_request_set $auth_status $upstream_status;
}

location = /delegated {
internal;
proxy_pass http://nodeA-backend/internal/auth/v1/accesstoken/verify;
proxy_method HEAD;
proxy_pass_request_body off;
proxy_set_header X-Ssl-Client-Cert $ssl_client_escaped_cert;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
}
}
19 changes: 19 additions & 0 deletions e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
verbosity: debug
strictmode: false
internalratelimiter: false
datadir: /opt/nuts/data
http:
default:
address: :1323
auth:
publicurl: https://nodeA
v2apienabled: true
presentationexchangemappingfile: /opt/nuts/presentationexchangemapping.json
contractvalidators:
- dummy
irma:
autoupdateschemas: false
tls:
truststorefile: /opt/nuts/truststore.pem
certfile: /opt/nuts/certificate-and-key.pem
certkeyfile: /opt/nuts/certificate-and-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"test": {
"format": {
"ldp_vp": {
"proof_type": ["JsonWebSignature2020"]
},
"ldp_vc": {
"proof_type": ["JsonWebSignature2020"]
}
},
"id": "pd_any_care_organization",
"name": "Care organization",
"purpose": "Finding a care organization for authorizing access to medical metadata",
"input_descriptors": [
{
"id": "id_nuts_care_organization_cred",
"constraints": {
"fields": [
{
"path": ["$.type"],
"filter": {
"type": "string",
"const": "NutsOrganizationCredential"
}
},
{
"path": ["$.credentialSubject.organization.name"],
"filter": {
"type": "string"
}
},
{
"path": ["$.credentialSubject.organization.city"],
"filter": {
"type": "string"
}
}
]
}
}
]
}
}
20 changes: 20 additions & 0 deletions e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
verbosity: debug
strictmode: false
internalratelimiter: false
datadir: /opt/nuts/data
http:
default:
address: :1323
auth:
tlsenabled: true
v2apienabled: true
publicurl: https://nodeB
contractvalidators:
- dummy
irma:
autoupdateschemas: false
tls:
truststorefile: /opt/nuts/truststore.pem
certfile: /opt/nuts/certificate-and-key.pem
certkeyfile: /opt/nuts/certificate-and-key.pem

Loading

0 comments on commit 30a3bd6

Please sign in to comment.