diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 57998c3bec..e00a07ed73 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -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" @@ -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) @@ -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: @@ -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{ @@ -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), } } } diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index ffe9bcb30f..237ad5b7d3 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -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) }) } diff --git a/auth/client/iam/client.go b/auth/client/iam/client.go index 0bb167fc3c..d7cf10a4cb 100644 --- a/auth/client/iam/client.go +++ b/auth/client/iam/client.go @@ -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 @@ -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 diff --git a/auth/client/iam/client_test.go b/auth/client/iam/client_test.go index 5df91fbfcd..6228c7b2dd 100644 --- a/auth/client/iam/client_test.go +++ b/auth/client/iam/client_test.go @@ -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() diff --git a/auth/oauth/error.go b/auth/oauth/error.go index 23e98d8f48..173992f764 100644 --- a/auth/oauth/error.go +++ b/auth/oauth/error.go @@ -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() diff --git a/auth/services/oauth/relying_party.go b/auth/services/oauth/relying_party.go index 6960bf6d21..e270923e7d 100644 --- a/auth/services/oauth/relying_party.go +++ b/auth/services/oauth/relying_party.go @@ -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 } diff --git a/auth/services/oauth/relying_party_test.go b/auth/services/oauth/relying_party_test.go index 2f0e828400..85a186e38e 100644 --- a/auth/services/oauth/relying_party_test.go +++ b/auth/services/oauth/relying_party_test.go @@ -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" @@ -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) diff --git a/e2e-tests/oauth-flow/openid4vp/docker-compose.yml b/e2e-tests/oauth-flow/openid4vp/docker-compose.yml new file mode 100644 index 0000000000..4f62fcffad --- /dev/null +++ b/e2e-tests/oauth-flow/openid4vp/docker-compose.yml @@ -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 diff --git a/e2e-tests/oauth-flow/node-A/html/ping b/e2e-tests/oauth-flow/openid4vp/node-A/html/ping similarity index 100% rename from e2e-tests/oauth-flow/node-A/html/ping rename to e2e-tests/oauth-flow/openid4vp/node-A/html/ping diff --git a/e2e-tests/oauth-flow/openid4vp/node-A/nginx.conf b/e2e-tests/oauth-flow/openid4vp/node-A/nginx.conf new file mode 100644 index 0000000000..c4c00b58aa --- /dev/null +++ b/e2e-tests/oauth-flow/openid4vp/node-A/nginx.conf @@ -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; + } + } +} diff --git a/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml b/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml new file mode 100644 index 0000000000..bf51193545 --- /dev/null +++ b/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml @@ -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 diff --git a/e2e-tests/oauth-flow/openid4vp/node-A/presentationexchangemapping.json b/e2e-tests/oauth-flow/openid4vp/node-A/presentationexchangemapping.json new file mode 100644 index 0000000000..57a0759051 --- /dev/null +++ b/e2e-tests/oauth-flow/openid4vp/node-A/presentationexchangemapping.json @@ -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" + } + } + ] + } + } + ] + } +} diff --git a/e2e-tests/oauth-flow/node-B/createsigningsessionrequesttemplate.json b/e2e-tests/oauth-flow/openid4vp/node-B/createsigningsessionrequesttemplate.json similarity index 100% rename from e2e-tests/oauth-flow/node-B/createsigningsessionrequesttemplate.json rename to e2e-tests/oauth-flow/openid4vp/node-B/createsigningsessionrequesttemplate.json diff --git a/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml b/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml new file mode 100644 index 0000000000..3691731054 --- /dev/null +++ b/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml @@ -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 + diff --git a/e2e-tests/oauth-flow/openid4vp/run-test.sh b/e2e-tests/oauth-flow/openid4vp/run-test.sh new file mode 100755 index 0000000000..7bb9ae1c4a --- /dev/null +++ b/e2e-tests/oauth-flow/openid4vp/run-test.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +source ../../util.sh + +echo "------------------------------------" +echo "Cleaning up running Docker containers and volumes, and key material..." +echo "------------------------------------" +docker compose down +docker compose rm -f -v +rm -rf ./node-*/data +mkdir ./node-A/data ./node-B/data # 'data' dirs will be created with root owner by docker if they do not exit. This creates permission issues on CI. + +echo "------------------------------------" +echo "Starting Docker containers..." +echo "------------------------------------" +docker compose up -d +docker compose up --wait nodeA-backend nodeB + +echo "------------------------------------" +echo "Registering vendors..." +echo "------------------------------------" +# Register Vendor A +VENDOR_A_DIDDOC=$(docker compose exec nodeA-backend nuts vdr create-did) +VENDOR_A_DID=$(echo $VENDOR_A_DIDDOC | jq -r .id) +echo Vendor A DID: $VENDOR_A_DID +# Add assertionMethod +VENDOR_A_KEYID=$(echo $VENDOR_A_DIDDOC | jq -r '.verificationMethod[0].id') +VENDOR_A_DIDDOC=$(echo $VENDOR_A_DIDDOC | jq ". |= . + {assertionMethod: [\"${VENDOR_A_KEYID}\"]}") +# Perform update +echo $VENDOR_A_DIDDOC > ./node-A/data/updated-did.json +DIDDOC_HASH=$(docker compose exec nodeA-backend nuts vdr resolve $VENDOR_A_DID --metadata | jq -r .hash) +docker compose exec nodeA-backend nuts vdr update "${VENDOR_A_DID}" "${DIDDOC_HASH}" /opt/nuts/data/updated-did.json + +# Register Vendor B +VENDOR_B_DIDDOC=$(docker compose exec nodeB nuts vdr create-did) +VENDOR_B_DID=$(echo $VENDOR_B_DIDDOC | jq -r .id) +echo Vendor B DID: $VENDOR_B_DID +# Add assertionMethod +VENDOR_B_KEYID=$(echo $VENDOR_B_DIDDOC | jq -r '.verificationMethod[0].id') +VENDOR_B_DIDDOC=$(echo $VENDOR_B_DIDDOC | jq ". |= . + {assertionMethod: [\"${VENDOR_B_KEYID}\"]}") +# Perform update +echo $VENDOR_B_DIDDOC > ./node-B/data/updated-did.json +DIDDOC_HASH=$(docker compose exec nodeB nuts vdr resolve $VENDOR_B_DID --metadata | jq -r .hash) +docker compose exec nodeB nuts vdr update "${VENDOR_B_DID}" "${DIDDOC_HASH}" /opt/nuts/data/updated-did.json + +# Issue NutsOrganizationCredential for Vendor B +REQUEST="{\"type\":\"NutsOrganizationCredential\",\"issuer\":\"${VENDOR_B_DID}\", \"credentialSubject\": {\"id\":\"${VENDOR_B_DID}\", \"organization\":{\"name\":\"Caresoft B.V.\", \"city\":\"Caretown\"}},\"visibility\": \"public\"}" +RESPONSE=$(echo $REQUEST | curl -X POST --data-binary @- http://localhost:21323/internal/vcr/v2/issuer/vc -H "Content-Type:application/json") +if echo $RESPONSE | grep -q "VerifiableCredential"; then + echo "VC issued" +else + echo "FAILED: Could not issue NutsOrganizationCredential to node-B" 1>&2 + echo $RESPONSE + exitWithDockerLogs 1 +fi + +echo "---------------------------------------" +echo "Perform OAuth 2.0 OpenID4VP-s2s flow..." +echo "---------------------------------------" +# Request access token +# Create DID for A with :nuts: replaced with :web: +VENDOR_A_DID_WEB=$(echo $VENDOR_A_DID | sed 's/:nuts/:web:nodeA:iam/') +VENDOR_B_DID_WEB=$(echo $VENDOR_B_DID | sed 's/:nuts/:web:nodeB:iam/') +REQUEST="{\"verifier\":\"${VENDOR_A_DID_WEB}\",\"scope\":\"test\"}" +RESPONSE=$(echo $REQUEST | curl -X POST -s --data-binary @- http://localhost:21323/internal/auth/v2/$VENDOR_B_DID/request-access-token -H "Content-Type:application/json" -v) +#if echo $RESPONSE | grep -q "access_token"; then +# echo $RESPONSE | sed -E 's/.*"access_token":"([^"]*).*/\1/' > ./node-B/data/accesstoken.txt +# echo "access token stored in ./node-B/data/accesstoken.txt" +#else +# echo "FAILED: Could not get access token from node-A" 1>&2 +# echo $RESPONSE +# exitWithDockerLogs 1 +#fi +if echo $RESPONSE | grep -q "unsupported_grant_type - not implemented yet"; then + echo "Good so far!" +else + echo "FAILED: Could not get access token from node-A" 1>&2 + echo $RESPONSE + exitWithDockerLogs 1 +fi + +#echo "------------------------------------" +#echo "Retrieving data..." +#echo "------------------------------------" +# +#RESPONSE=$(docker compose exec nodeB curl --insecure --cert /opt/nuts/certificate-and-key.pem --key /opt/nuts/certificate-and-key.pem https://nodeA:443/ping -H "Authorization: bearer $(cat ./node-B/data/accesstoken.txt)" -v) +#if echo $RESPONSE | grep -q "pong"; then +# echo "success!" +#else +# echo "FAILED: Could not ping node-A" 1>&2 +# echo $RESPONSE +# exitWithDockerLogs 1 +#fi + +echo "------------------------------------" +echo "Stopping Docker containers..." +echo "------------------------------------" +docker compose stop diff --git a/e2e-tests/oauth-flow/docker-compose.yml b/e2e-tests/oauth-flow/rfc002/docker-compose.yml similarity index 63% rename from e2e-tests/oauth-flow/docker-compose.yml rename to e2e-tests/oauth-flow/rfc002/docker-compose.yml index 196a8fbfef..3652616e34 100644 --- a/e2e-tests/oauth-flow/docker-compose.yml +++ b/e2e-tests/oauth-flow/rfc002/docker-compose.yml @@ -9,8 +9,8 @@ services: 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" + - "../../tls-certs/nodeA-backend-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" + - "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" healthcheck: interval: 1s # Make test run quicker by checking health status more often nodeA: @@ -19,9 +19,9 @@ services: - "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" + - "../../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}" @@ -32,7 +32,7 @@ services: 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/nodeB-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" + - "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" healthcheck: interval: 1s # Make test run quicker by checking health status more often diff --git a/e2e-tests/oauth-flow/rfc002/node-A/html/ping b/e2e-tests/oauth-flow/rfc002/node-A/html/ping new file mode 100644 index 0000000000..ed53c21358 --- /dev/null +++ b/e2e-tests/oauth-flow/rfc002/node-A/html/ping @@ -0,0 +1 @@ +pong \ No newline at end of file diff --git a/e2e-tests/oauth-flow/node-A/nginx.conf b/e2e-tests/oauth-flow/rfc002/node-A/nginx.conf similarity index 100% rename from e2e-tests/oauth-flow/node-A/nginx.conf rename to e2e-tests/oauth-flow/rfc002/node-A/nginx.conf diff --git a/e2e-tests/oauth-flow/node-A/nuts.yaml b/e2e-tests/oauth-flow/rfc002/node-A/nuts.yaml similarity index 100% rename from e2e-tests/oauth-flow/node-A/nuts.yaml rename to e2e-tests/oauth-flow/rfc002/node-A/nuts.yaml diff --git a/e2e-tests/oauth-flow/rfc002/node-B/createsigningsessionrequesttemplate.json b/e2e-tests/oauth-flow/rfc002/node-B/createsigningsessionrequesttemplate.json new file mode 100644 index 0000000000..aa14f51674 --- /dev/null +++ b/e2e-tests/oauth-flow/rfc002/node-B/createsigningsessionrequesttemplate.json @@ -0,0 +1,4 @@ +{ + "means": "dummy", + "payload": "BASE64_CONTRACT" +} \ No newline at end of file diff --git a/e2e-tests/oauth-flow/node-B/nuts.yaml b/e2e-tests/oauth-flow/rfc002/node-B/nuts.yaml similarity index 100% rename from e2e-tests/oauth-flow/node-B/nuts.yaml rename to e2e-tests/oauth-flow/rfc002/node-B/nuts.yaml diff --git a/e2e-tests/oauth-flow/run-test.sh b/e2e-tests/oauth-flow/rfc002/run-test.sh similarity index 99% rename from e2e-tests/oauth-flow/run-test.sh rename to e2e-tests/oauth-flow/rfc002/run-test.sh index 51e70b2b7b..da18d9249a 100755 --- a/e2e-tests/oauth-flow/run-test.sh +++ b/e2e-tests/oauth-flow/rfc002/run-test.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -source ../util.sh +source ../../util.sh echo "------------------------------------" echo "Cleaning up running Docker containers and volumes, and key material..." diff --git a/e2e-tests/oauth-flow/run-tests.sh b/e2e-tests/oauth-flow/run-tests.sh index 5c6919b383..48f6cf33c2 100755 --- a/e2e-tests/oauth-flow/run-tests.sh +++ b/e2e-tests/oauth-flow/run-tests.sh @@ -2,7 +2,16 @@ set -e # make script fail if any of the tests returns a non-zero exit code -echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" -echo "!! Running test: OAuth flow !!" -echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +echo "!! Running test: OAuth flow (rfc002) !!" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +pushd rfc002 ./run-test.sh +popd + +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +echo "!! Running test: OAuth flow (OpenID4VP-s2s) !!" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +pushd openid4vp +./run-test.sh +popd diff --git a/vcr/holder/wallet.go b/vcr/holder/wallet.go index 5d96dd3300..ca5d6275dc 100644 --- a/vcr/holder/wallet.go +++ b/vcr/holder/wallet.go @@ -103,7 +103,7 @@ func (h wallet) BuildPresentation(ctx context.Context, credentials []vc.Verifiab case JSONLDPresentationFormat: return h.buildJSONLDPresentation(ctx, *signerDID, credentials, options, key) default: - return nil, errors.New("unsupported presentation proof format") + return nil, fmt.Errorf("unsupported presentation proof format: %s", options.Format) } } diff --git a/vcr/holder/wallet_test.go b/vcr/holder/wallet_test.go index fa5a79a1e8..6d6d0b0df8 100644 --- a/vcr/holder/wallet_test.go +++ b/vcr/holder/wallet_test.go @@ -245,7 +245,7 @@ func TestWallet_BuildPresentation(t *testing.T) { result, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, PresentationOptions{Format: "paper"}, &testDID, true) - assert.EqualError(t, err, "unsupported presentation proof format") + assert.EqualError(t, err, "unsupported presentation proof format: paper") assert.Nil(t, result) }) }) diff --git a/vdr/didweb/util.go b/vdr/didweb/util.go index 5518843dc6..f263734be4 100644 --- a/vdr/didweb/util.go +++ b/vdr/didweb/util.go @@ -34,6 +34,7 @@ var errInvalidWebDIDURL = errors.New("URL does not represent a Web DID") // - https://localhost/.well-known/did.json -> did:web:localhost // - https://localhost/alice+and+bob/path/did.json -> did:web:localhost:alice%2Band%2Bbob:path // - https://localhost:3000/alice -> did:web:localhost%3A3000:alice +// - https://nodeA/iam/5/ -> did:web:nodeA:iam:5 func URLToDID(u url.URL) (*did.DID, error) { path := u.Path if u.RawPath != "" { @@ -43,11 +44,19 @@ func URLToDID(u url.URL) (*did.DID, error) { path, _ = strings.CutSuffix(path, "/.well-known/did.json") path, _ = strings.CutSuffix(path, "/did.json") parts := strings.Split(path, "/") - for i, part := range parts { - part = percentEncodeString(part) - parts[i] = part + j := 0 + for _, part := range parts { + if len(part) > 0 { + part = percentEncodeString(part) + parts[j] = part + j++ + } + } + parts = parts[:j] + str := "did:web:" + percentEncodeString(u.Host) + if len(parts) > 0 { + str += ":" + strings.Join(parts, ":") } - str := "did:web:" + percentEncodeString(u.Host) + strings.Join(parts, ":") result, err := did.ParseDID(str) if err != nil { return nil, errors.Join(errInvalidWebDIDURL, err) diff --git a/vdr/didweb/util_test.go b/vdr/didweb/util_test.go index cb4299a9d8..e47c82866a 100644 --- a/vdr/didweb/util_test.go +++ b/vdr/didweb/util_test.go @@ -55,6 +55,13 @@ func TestUrlToDid(t *testing.T) { require.NoError(t, err) assert.Equal(t, "did:web:localhost:something-else", result.String()) }) + t.Run("empty part", func(t *testing.T) { + requestUrl, _ := url.Parse("https://localhost/iam/5/") + result, err := URLToDID(*requestUrl) + + require.NoError(t, err) + assert.Equal(t, "did:web:localhost:iam:5", result.String()) + }) t.Run("encoded path", func(t *testing.T) { requestUrl, _ := url.Parse("https://localhost/x/y%2Fz/did.json") result, err := URLToDID(*requestUrl)