diff --git a/lws10-test-suite/.gitignore b/lws10-test-suite/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/lws10-test-suite/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/lws10-test-suite/README.md b/lws10-test-suite/README.md new file mode 100644 index 0000000..45eb0dd --- /dev/null +++ b/lws10-test-suite/README.md @@ -0,0 +1,98 @@ +# LWS Protocol 1.0 Test Suite + +Test suite for the [Linked Web Storage Protocol 1.0](https://www.w3.org/TR/lws10-core/) +and its authentication suite specifications: + +- [LWS 1.0 Authentication Suite: Self-signed Identity using did:key](https://www.w3.org/TR/lws10-authn-ssi-did-key/) +- [LWS 1.0 Authentication Suite: OpenID Connect](https://www.w3.org/TR/lws10-authn-openid/) +- [LWS 1.0 Authentication Suite: SAML 2.0](https://www.w3.org/TR/lws10-authn-saml/) + +## Directory Structure + +``` +tests/ +├── context.jsonld # Shared JSON-LD context for all manifests +├── manifest.jsonld # Root manifest (core protocol tests) +├── auth/ +│ ├── did-key/manifest.jsonld # did:key authentication suite tests +│ ├── oidc/manifest.jsonld # OpenID Connect authentication suite tests +│ └── saml/manifest.jsonld # SAML 2.0 authentication suite tests +├── containers/ +│ └── alice-notes.json # Example container representation body +├── linksets/ +│ ├── alice-notes.meta.json # Linkset for /alice/notes/ +│ └── shoppinglist.meta.json # Linkset for /alice/notes/shoppinglist.txt +└── resources/ + ├── storage-description.json # Storage description resource body + ├── authorization-server-metadata.json # AS metadata body + ├── shoppinglist.txt # Data resource body + ├── token-response-200.json # Successful token exchange response + └── token-response-400.json # Failed token exchange response +``` + +## Conventions + +### Inline vs. by-reference properties + +Properties that carry their value directly use a plain JSON name: + +```json +"body": "some content" +``` + +Properties that reference an external resource by URL append `URL` to the name: + +```json +"bodyURL": "../containers/alice-notes.json" +``` + +The same convention applies to `href` / `hrefTemplate` for Link header values, and +`url` / `urlTemplate` for request targets. + +### Test types + +| Type | Meaning | +|------|---------| +| `lwst:ValidationTest` | The server MUST respond as described for conformance | +| `lwst:NegativeTest` | The server MUST reject the request as described | + +### Traits (applied to tests for filtering) + +`Get`, `Post`, `Put`, `Delete`, `Patch` — HTTP method used +`Public` — no authentication required +`Private` — resource requires authentication/authorization +`Authn` — exercises authentication behaviour +`Authz` — exercises authorization behaviour +`Container` — targets a Container resource +`DataResource` — targets a DataResource +`Discovery` — exercises service/storage description discovery + +### Authorization roles (ODRL-based) + +| Compact name | Meaning | +|-------------|---------| +| `Role-Public` | Any agent, authenticated or not | +| `Role-Authenticated` | Any agent that presents a valid access token | +| `Role-Owner` | The resource manager (owner) of the storage | + +### Prerequisite hierarchy + +Each test's `prereqs.hierarchy` array describes the resources (and their access policies) +that a test harness MUST establish before issuing the test request. An empty array means +no prior state is required. + +### Authentication + +`prereqs.authentication` and `request.authentication` hold the WebID/subject URI of the +agent to authenticate as, or `null` for anonymous requests. + +## Test certificate authority + +Tests that require authentication may use a test CA whose certificate is distributed +alongside this test suite. Implementations SHOULD accept this CA only in test/non-production +environments. + +## Status values + +All tests start with `mf:Proposed`. They advance to `mf:Approved` once an independent +implementation demonstrates conformance. diff --git a/lws10-test-suite/auth/did-key b/lws10-test-suite/auth/did-key new file mode 120000 index 0000000..40cfeee --- /dev/null +++ b/lws10-test-suite/auth/did-key @@ -0,0 +1 @@ +../mnt/user-data/outputs/lws-tests/auth/did-key \ No newline at end of file diff --git a/lws10-test-suite/auth/oidc b/lws10-test-suite/auth/oidc new file mode 120000 index 0000000..a9c4dbd --- /dev/null +++ b/lws10-test-suite/auth/oidc @@ -0,0 +1 @@ +../mnt/user-data/outputs/lws-tests/auth/oidc \ No newline at end of file diff --git a/lws10-test-suite/auth/saml b/lws10-test-suite/auth/saml new file mode 120000 index 0000000..f830e38 --- /dev/null +++ b/lws10-test-suite/auth/saml @@ -0,0 +1 @@ +../mnt/user-data/outputs/lws-tests/auth/saml \ No newline at end of file diff --git a/lws10-test-suite/containers/alice-notes.json b/lws10-test-suite/containers/alice-notes.json new file mode 100644 index 0000000..d32ad7e --- /dev/null +++ b/lws10-test-suite/containers/alice-notes.json @@ -0,0 +1,17 @@ +{ + "@context": "https://www.w3.org/ns/lws/v1", + "id": "https://storage.example/alice/notes/", + "type": "Container", + "items": [ + { + "id": "https://storage.example/alice/notes/shoppinglist.txt", + "type": "DataResource", + "contentType": "text/plain", + "size": 47 + }, + { + "id": "https://storage.example/alice/notes/ideas/", + "type": "Container" + } + ] +} diff --git a/lws10-test-suite/context.jsonld b/lws10-test-suite/context.jsonld new file mode 100644 index 0000000..17931a0 --- /dev/null +++ b/lws10-test-suite/context.jsonld @@ -0,0 +1,94 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/lws-tests/v1#", + + "lws": "https://www.w3.org/ns/lws/v1#", + "lwst": "https://www.w3.org/ns/lws-tests/v1#", + "mf": "https://www.w3.org/ns/test-manifest#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "odrl": "http://www.w3.org/ns/odrl/2/", + + "id": "@id", + "type": "@type", + "comment": "rdfs:comment", + "label": "rdfs:label", + + "name": {"@id": "mf:name", "@type": "xsd:string"}, + "entries": {"@id": "mf:entries","@container": "@list", "@type": "@id"}, + "status": {"@id": "mf:status", "@type": "@vocab"}, + "include": {"@id": "mf:include","@type": "@id", "@container": "@list"}, + + "traits": {"@id": "lwst:traits", "@type": "@vocab", "@container": "@set"}, + + "prereqs": {"@id": "lwst:prereqs"}, + "hierarchy": {"@id": "lwst:hierarchy", "@container": "@list"}, + "authentication": {"@id": "lwst:authentication"}, + "authorization": {"@id": "lwst:authorization"}, + + "url": {"@id": "lwst:url", "@type": "@id"}, + "urlTemplate": {"@id": "lwst:url", "@type": "xsd:string"}, + + "read": {"@id": "odrl:read", "@type": "@vocab"}, + "write": {"@id": "odrl:write", "@type": "@vocab"}, + "append": {"@id": "odrl:use", "@type": "@vocab"}, + "control": {"@id": "odrl:modify","@type": "@vocab"}, + + "request": {"@id": "lwst:request"}, + "method": {"@id": "lwst:method", "@type": "xsd:string"}, + "slug": {"@id": "lwst:slug", "@type": "xsd:string"}, + "contentType": {"@id": "lwst:contentType", "@type": "xsd:string"}, + + "linkHeaders": {"@id": "lwst:linkHeaders", "@container": "@list"}, + "otherHeaders": {"@id": "lwst:otherHeaders", "@container": "@list"}, + "rel": {"@id": "lwst:rel", "@type": "xsd:string"}, + "href": {"@id": "lwst:href", "@type": "@id"}, + "hrefTemplate": {"@id": "lwst:href", "@type": "xsd:string"}, + "mediaType": {"@id": "lwst:mediaType", "@type": "xsd:string"}, + "headerName": {"@id": "lwst:headerName", "@type": "xsd:string"}, + "headerValue": {"@id": "lwst:headerValue", "@type": "xsd:string"}, + + "body": {"@id": "lwst:body", "@type": "xsd:string"}, + "bodyURL": {"@id": "lwst:body", "@type": "@id"}, + + "response": {"@id": "lwst:response"}, + "statusCode": {"@id": "lwst:statusCode", "@type": "xsd:integer"}, + "authenticationChallenge": {"@id": "lwst:authenticationChallenge"}, + "authorizationHint": {"@id": "lwst:authorizationHint"}, + + "wwwAuthenticate": {"@id": "lwst:wwwAuthenticate", "@type": "xsd:string"}, + "asUri": {"@id": "lwst:asUri", "@type": "@id"}, + "realm": {"@id": "lwst:realm", "@type": "@id"}, + + "locationURL": {"@id": "lwst:location", "@type": "@id"}, + + "authSuites": {"@id": "lwst:authSuites", "@type": "@id", "@container": "@set"}, + "facets": {"@id": "lwst:facets", "@type": "@id", "@container": "@set"}, + + "Role-Public": "lwst:Role-Public", + "Role-Owner": "lwst:Role-Owner", + "Role-Authenticated": "lwst:Role-Authenticated", + + "Proposed": "mf:Proposed", + "Approved": "mf:Approved", + "Pending": "mf:Pending", + + "ValidationTest": "lwst:ValidationTest", + "NegativeTest": "lwst:NegativeTest", + "Manifest": "mf:Manifest", + + "Get": "lwst:Get", + "Post": "lwst:Post", + "Put": "lwst:Put", + "Delete": "lwst:Delete", + "Patch": "lwst:Patch", + "Public": "lwst:Public", + "Private": "lwst:Private", + "Authn": "lwst:Authn", + "Authz": "lwst:Authz", + "Container": "lwst:Container", + "DataResource": "lwst:DataResource", + "Discovery": "lwst:Discovery" + } +} diff --git a/lws10-test-suite/linksets/alice-notes.meta.json b/lws10-test-suite/linksets/alice-notes.meta.json new file mode 100644 index 0000000..871e2f8 --- /dev/null +++ b/lws10-test-suite/linksets/alice-notes.meta.json @@ -0,0 +1,22 @@ +{ + "linkset": [ + { + "anchor": "https://storage.example/alice/notes/", + "https://www.w3.org/ns/lws#storageDescription": [ + { "href": "https://storage.example/alice/description" } + ], + "up": [ + { "href": "https://storage.example/alice/" } + ], + "type": [ + { "href": "https://www.w3.org/ns/lws#Container" } + ], + "linkset": [ + { + "href": "https://storage.example/alice/notes/.meta", + "type": "application/linkset+json" + } + ] + } + ] +} diff --git a/lws10-test-suite/linksets/shoppinglist.meta.json b/lws10-test-suite/linksets/shoppinglist.meta.json new file mode 100644 index 0000000..36e9447 --- /dev/null +++ b/lws10-test-suite/linksets/shoppinglist.meta.json @@ -0,0 +1,22 @@ +{ + "linkset": [ + { + "anchor": "https://storage.example/alice/notes/shoppinglist.txt", + "https://www.w3.org/ns/lws#storageDescription": [ + { "href": "https://storage.example/alice/description" } + ], + "up": [ + { "href": "https://storage.example/alice/notes/" } + ], + "type": [ + { "href": "https://www.w3.org/ns/lws#DataResource" } + ], + "linkset": [ + { + "href": "https://storage.example/alice/notes/shoppinglist.txt.meta", + "type": "application/linkset+json" + } + ] + } + ] +} diff --git a/lws10-test-suite/manifest.html b/lws10-test-suite/manifest.html new file mode 100644 index 0000000..f7320d1 --- /dev/null +++ b/lws10-test-suite/manifest.html @@ -0,0 +1,779 @@ + + + + + + LWS Protocol Test Suite + + + + + + +
+ +
+ +
+ +
+
+
Loading manifest…
+
+
+ + + + diff --git a/lws10-test-suite/manifest.jsonld b/lws10-test-suite/manifest.jsonld new file mode 100644 index 0000000..e8d75ce --- /dev/null +++ b/lws10-test-suite/manifest.jsonld @@ -0,0 +1,810 @@ +{ + "@context": [ + { "@base": "https://raw.githubusercontent.com/w3c/lws-protocol/main/lws10-test-suite/" }, + "./context.jsonld" + ], + "@graph": [ + { + "@type": "Manifest", + "@id": "", + "label": "LWS Protocol 1.0 Test Suite", + "comment": "Tests for Linked Web Storage Protocol 1.0 (https://www.w3.org/TR/lws10-core/). Tests cover container operations, data resource operations, discovery, authentication, and authorization.", + "authSuites": [ + "https://www.w3.org/TR/lws10-authn-ssi-did-key/", + "https://www.w3.org/TR/lws10-authn-openid/", + "https://www.w3.org/TR/lws10-authn-saml/" + ], + "include": [ + "auth/did-key/manifest.jsonld", + "auth/oidc/manifest.jsonld", + "auth/saml/manifest.jsonld" + ], + "entries": [ + + { + "@type": "ValidationTest", + "@id": "#discovery-unauthorized-response-headers", + "name": "discovery-unauthorized-response-headers", + "traits": ["Get", "Discovery", "Private"], + "status": "Proposed", + "comment": "unauthorized GET -> 401 { WWW-Authenticate: [Bearer, as_uri, realm], Link }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization-server-discovery", + "prereqs": { + "hierarchy": [ + { + "url": "/storage_1/metadata/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/storage_1/metadata/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/storage_1" + }, + "linkHeaders": [ + { + "rel": "storageDescription", + "hrefTemplate": "https://storage.example/storage_1/metadata" + } + ], + "otherHeaders": [] + } + }, + + { + "@type": "ValidationTest", + "@id": "#discovery-storage-description", + "name": "discovery-storage-description", + "traits": ["Get", "Discovery", "Public"], + "status": "Proposed", + "comment": "public GET [Accept: lws+json] -> 200 lws+json { Link: [storageDescription], body: {@type: Storage, service: storageDescription} }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#discovery", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/description", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/lws+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/lws+json", + "linkHeaders": [ + { + "rel": "storageDescription", + "hrefTemplate": "/alice/description" + } + ], + "bodyURL": "resources/storage-description.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#discovery-get-links-storageDescription", + "name": "discovery-get-links-storageDescription", + "traits": ["Get", "Discovery", "Public"], + "status": "Proposed", + "comment": "public GET container -> 200 { Link: [storageDescription] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#discovery", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "linkHeaders": [ + { + "rel": "storageDescription", + "hrefTemplate": "/alice/description" + } + ] + } + }, + + { + "@type": "ValidationTest", + "@id": "#getContainer", + "name": "getContainer", + "traits": ["Get", "Public", "Container"], + "status": "Proposed", + "comment": "public GET container [Accept: lws+json] -> 200 lws+json { Link: [linkset, up, type=Container, storageDescription] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#read-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/lws+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/lws+json", + "authenticationChallenge": null, + "authorizationHint": null, + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/.meta", + "mediaType": "application/linkset+json" + }, + { + "rel": "up", + "hrefTemplate": "/alice/" + }, + { + "rel": "type", + "href": "https://www.w3.org/ns/lws#Container" + }, + { + "rel": "storageDescription", + "hrefTemplate": "/alice/description" + } + ], + "otherHeaders": [], + "bodyURL": "containers/alice-notes.json" + } + }, + + { + "@type": "NegativeTest", + "@id": "#getContainer-private-unauthorized", + "name": "getContainer-private-unauthorized", + "traits": ["Get", "Private", "Container", "Authz"], + "status": "Proposed", + "comment": "unauthn GET private-container -> 401 { WWW-Authenticate: [Bearer, as_uri, realm] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/" + } + } + }, + + { + "@type": "ValidationTest", + "@id": "#getContainer-authenticated-owner", + "name": "getContainer-authenticated-owner", + "traits": ["Get", "Private", "Container", "Authn", "Authz"], + "status": "Proposed", + "comment": "owner GET private-container [Accept: lws+json] -> 200 lws+json { Link: [linkset, up, type=Container] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#read-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/lws+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/lws+json", + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/.meta", + "mediaType": "application/linkset+json" + }, + { "rel": "up", "hrefTemplate": "/alice/" }, + { "rel": "type", "href": "https://www.w3.org/ns/lws#Container" } + ], + "bodyURL": "containers/alice-notes.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#createDataResource", + "name": "createDataResource", + "traits": ["Post", "Authn", "Authz", "Container", "DataResource"], + "status": "Proposed", + "comment": "owner POST container [Content-Type: text/plain, Slug: shoppinglist.txt] -> 201 { Location, Link: [linkset, up, type=DataResource] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#create-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "POST", + "url": "/alice/notes/", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "contentType": "text/plain", + "slug": "shoppinglist.txt", + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Content-Length", "headerValue": "47" } + ], + "bodyURL": "resources/shoppinglist.txt" + }, + "response": { + "statusCode": 201, + "locationURL": "/alice/notes/shoppinglist.txt", + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/shoppinglist.txt.meta", + "mediaType": "application/linkset+json" + }, + { "rel": "up", "hrefTemplate": "/alice/notes/" }, + { "rel": "type", "href": "https://www.w3.org/ns/lws#DataResource" } + ], + "otherHeaders": [ + { "headerName": "Content-Length", "headerValue": "0" } + ] + } + }, + + { + "@type": "NegativeTest", + "@id": "#createDataResource-unauthorized", + "name": "createDataResource-unauthorized", + "traits": ["Post", "Authz", "Container"], + "status": "Proposed", + "comment": "unauthn POST container -> 401 { WWW-Authenticate: [Bearer, as_uri, realm] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "POST", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "contentType": "text/plain", + "linkHeaders": [], + "otherHeaders": [], + "body": "some content" + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/" + } + } + }, + + { + "@type": "ValidationTest", + "@id": "#readDataResource", + "name": "readDataResource", + "traits": ["Get", "Public", "DataResource"], + "status": "Proposed", + "comment": "public GET resource -> 200 text/plain { Link: [linkset, up, type=DataResource, storageDescription] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#read-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/shoppinglist.txt", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "contentType": "text/plain", + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/shoppinglist.txt.meta", + "mediaType": "application/linkset+json" + }, + { "rel": "up", "hrefTemplate": "/alice/notes/" }, + { "rel": "type", "href": "https://www.w3.org/ns/lws#DataResource" }, + { + "rel": "storageDescription", + "hrefTemplate": "/alice/description" + } + ], + "bodyURL": "resources/shoppinglist.txt" + } + }, + + { + "@type": "ValidationTest", + "@id": "#updateDataResource", + "name": "updateDataResource", + "traits": ["Put", "Authn", "Authz", "DataResource"], + "status": "Proposed", + "comment": "owner PUT resource [Content-Type: text/plain] -> 200 { Link: [up] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#update-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "PUT", + "url": "/alice/notes/shoppinglist.txt", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "contentType": "text/plain", + "linkHeaders": [], + "otherHeaders": [], + "body": "milk\neggs\nbread\n" + }, + "response": { + "statusCode": 200, + "linkHeaders": [ + { "rel": "up", "hrefTemplate": "/alice/notes/" } + ], + "otherHeaders": [] + } + }, + + { + "@type": "ValidationTest", + "@id": "#deleteDataResource", + "name": "deleteDataResource", + "traits": ["Delete", "Authn", "Authz", "DataResource"], + "status": "Proposed", + "comment": "owner DELETE resource -> 200", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#delete-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "DELETE", + "url": "/alice/notes/shoppinglist.txt", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "otherHeaders": [] + } + }, + + { + "@type": "NegativeTest", + "@id": "#deleteDataResource-unauthorized", + "name": "deleteDataResource-unauthorized", + "traits": ["Delete", "Authz", "DataResource"], + "status": "Proposed", + "comment": "unauthn DELETE resource -> 401 { WWW-Authenticate: [Bearer, as_uri, realm] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "DELETE", + "url": "/alice/notes/shoppinglist.txt", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/" + } + } + }, + + { + "@type": "ValidationTest", + "@id": "#getLinkset", + "name": "getLinkset", + "traits": ["Get", "Public", "Container"], + "status": "Proposed", + "comment": "public GET /.meta [Accept: linkset+json] -> 200 linkset+json", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#logical-resource-organization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/.meta", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/linkset+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/linkset+json", + "bodyURL": "linksets/alice-notes.meta.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#getContainer-containmentIntegrity", + "name": "getContainer-containmentIntegrity", + "traits": ["Get", "Public", "Container"], + "status": "Proposed", + "comment": "public GET container [Accept: lws+json] -> 200 lws+json { body.items ∋ member-resource }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#container-representation", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + }, + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/lws+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/lws+json", + "comment": "The response body MUST include shoppinglist.txt in items.", + "bodyURL": "containers/alice-notes.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#authz-server-metadata-well-known", + "name": "authz-server-metadata-well-known", + "traits": ["Get", "Discovery", "Public"], + "status": "Proposed", + "comment": "public GET AS:/.well-known/lws-configuration [Accept: json] -> 200 json", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization-server-discovery", + "prereqs": { + "hierarchy": [], + "authentication": null + }, + "request": { + "method": "GET", + "url": "https://authorization.example/.well-known/lws-configuration", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "resources/authorization-server-metadata.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#authz-token-exchange-valid", + "name": "authz-token-exchange-valid", + "traits": ["Post", "Authn", "Authz"], + "status": "Proposed", + "comment": "POST AS:/token [grant_type=token-exchange, subject_token_type=id_token, valid subject_token] -> 200 json { access_token }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [], + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=" + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "resources/token-response-200.json" + } + }, + + { + "@type": "NegativeTest", + "@id": "#authz-token-exchange-invalid-resource", + "name": "authz-token-exchange-invalid-resource", + "traits": ["Post", "Authz"], + "status": "Proposed", + "comment": "POST AS:/token [grant_type=token-exchange, resource=unknown] -> 400 json { error }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [], + "authentication": null + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "linkHeaders": [], + "otherHeaders": [], + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Funknown.example%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=" + }, + "response": { + "statusCode": 400, + "bodyURL": "resources/token-response-400.json" + } + }, + + { + "@type": "NegativeTest", + "@id": "#authz-expired-token-rejected", + "name": "authz-expired-token-rejected", + "traits": ["Get", "Authz", "Private"], + "status": "Proposed", + "comment": "GET [Authorization: Bearer ] -> 401 { WWW-Authenticate: [Bearer, as_uri, realm, error=invalid_token] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "comment": "The Bearer token in the Authorization header is expired.", + "linkHeaders": [], + "otherHeaders": [ + { + "headerName": "Authorization", + "headerValue": "Bearer " + } + ] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/", + "error": "invalid_token" + } + } + }, + + { + "@type": "ValidationTest", + "@id": "#createContainer", + "name": "createContainer", + "traits": ["Post", "Authn", "Authz", "Container"], + "status": "Proposed", + "comment": "owner POST container [Content-Type: lws+json, Link: type=Container, Slug: notes] -> 201 { Location, Link: [linkset, up, type=Container] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#create-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "POST", + "url": "/alice/", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "contentType": "application/lws+json", + "slug": "notes", + "linkHeaders": [ + { + "rel": "type", + "href": "https://www.w3.org/ns/lws#Container" + } + ], + "otherHeaders": [], + "body": "" + }, + "response": { + "statusCode": 201, + "locationURL": "/alice/notes/", + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/.meta", + "mediaType": "application/linkset+json" + }, + { "rel": "up", "hrefTemplate": "/alice/" }, + { "rel": "type", "href": "https://www.w3.org/ns/lws#Container" } + ], + "otherHeaders": [] + } + } + + ] + } + ] +} diff --git a/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/did-key/manifest.jsonld b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/did-key/manifest.jsonld new file mode 100644 index 0000000..caa1ced --- /dev/null +++ b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/did-key/manifest.jsonld @@ -0,0 +1,89 @@ +{ + "@context": [ + { "@base": "https://raw.githubusercontent.com/w3c/lws-protocol/main/tests/auth/did-key/" }, + "../../context.jsonld" + ], + "@graph": [ + { + "@id": "", + "@type": "Manifest", + "label": "LWS Authentication: Self-signed Identity using did:key", + "comment": "Tests for the LWS 1.0 Authentication Suite: Self-signed Identity using did:key (https://www.w3.org/TR/lws10-authn-ssi-did-key/)", + "entries": [ + { + "@id": "#authn-didkey-valid-credential", + "@type": "ValidationTest", + "name": "authn-didkey-valid-credential", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A valid did:key authentication credential is accepted by the authorization server.", + "prereqs": { + "authentication": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "../../resources/token-response-200.json" + } + }, + { + "@id": "#authn-didkey-invalid-signature", + "@type": "NegativeTest", + "name": "authn-didkey-invalid-signature", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A did:key credential with an invalid signature is rejected.", + "prereqs": { + "authentication": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 400, + "bodyURL": "../../resources/token-response-400.json" + } + }, + { + "@id": "#authn-didkey-missing-credential", + "@type": "NegativeTest", + "name": "authn-didkey-missing-credential", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A token exchange request with no subject_token is rejected.", + "prereqs": { + "authentication": null, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 400, + "bodyURL": "../../resources/token-response-400.json" + } + } + ] + } + ] +} diff --git a/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/oidc/manifest.jsonld b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/oidc/manifest.jsonld new file mode 100644 index 0000000..a8bcd93 --- /dev/null +++ b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/oidc/manifest.jsonld @@ -0,0 +1,116 @@ +{ + "@context": [ + { "@base": "https://raw.githubusercontent.com/w3c/lws-protocol/main/tests/auth/oidc/" }, + "../../context.jsonld" + ], + "@graph": [ + { + "@id": "", + "@type": "Manifest", + "label": "LWS Authentication: OpenID Connect", + "comment": "Tests for the LWS 1.0 Authentication Suite: OpenID Connect (https://www.w3.org/TR/lws10-authn-openid/)", + "entries": [ + { + "@id": "#authn-oidc-valid-id-token", + "@type": "ValidationTest", + "name": "authn-oidc-valid-id-token", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A valid OIDC ID token is accepted by the authorization server at the token exchange endpoint.", + "prereqs": { + "authentication": { + "type": "OIDCIdToken", + "issuer": "https://identity.example", + "subject": "https://identity.example/users/alice" + }, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "../../resources/token-response-200.json" + } + }, + { + "@id": "#authn-oidc-expired-id-token", + "@type": "NegativeTest", + "name": "authn-oidc-expired-id-token", + "traits": ["Authn"], + "status": "Proposed", + "comment": "An expired OIDC ID token is rejected by the authorization server.", + "prereqs": { + "authentication": { + "type": "OIDCIdToken", + "issuer": "https://identity.example", + "subject": "https://identity.example/users/alice", + "expired": true + }, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 400, + "bodyURL": "../../resources/token-response-400.json" + } + }, + { + "@id": "#authn-oidc-server-metadata-discovery", + "@type": "ValidationTest", + "name": "authn-oidc-server-metadata-discovery", + "traits": ["Authn", "Discovery"], + "status": "Proposed", + "comment": "A 401 response from a storage server includes WWW-Authenticate with as_uri and realm, enabling OIDC-based auth server discovery.", + "prereqs": { + "authentication": null, + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ] + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/" + }, + "linkHeaders": [ + { + "rel": "https://www.w3.org/ns/lws#storageDescription", + "href": "/alice/description" + } + ] + } + } + ] + } + ] +} diff --git a/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/saml/manifest.jsonld b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/saml/manifest.jsonld new file mode 100644 index 0000000..3dc43e9 --- /dev/null +++ b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/saml/manifest.jsonld @@ -0,0 +1,74 @@ +{ + "@context": [ + { "@base": "https://raw.githubusercontent.com/w3c/lws-protocol/main/tests/auth/saml/" }, + "../../context.jsonld" + ], + "@graph": [ + { + "@id": "", + "@type": "Manifest", + "label": "LWS Authentication: SAML 2.0", + "comment": "Tests for the LWS 1.0 Authentication Suite: SAML 2.0 (https://www.w3.org/TR/lws10-authn-saml/)", + "entries": [ + { + "@id": "#authn-saml-valid-assertion", + "@type": "ValidationTest", + "name": "authn-saml-valid-assertion", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A valid SAML 2.0 assertion is accepted at the token exchange endpoint.", + "prereqs": { + "authentication": { + "type": "SAML2Assertion", + "issuer": "https://idp.example", + "subject": "https://idp.example/users/alice" + }, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Asaml2&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "../../resources/token-response-200.json" + } + }, + { + "@id": "#authn-saml-invalid-signature", + "@type": "NegativeTest", + "name": "authn-saml-invalid-signature", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A SAML 2.0 assertion with a tampered signature is rejected.", + "prereqs": { + "authentication": { + "type": "SAML2Assertion", + "issuer": "https://idp.example", + "subject": "https://idp.example/users/alice", + "tampered": true + }, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Asaml2&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 400, + "bodyURL": "../../resources/token-response-400.json" + } + } + ] + } + ] +} diff --git a/lws10-test-suite/resources/authorization-server-metadata.json b/lws10-test-suite/resources/authorization-server-metadata.json new file mode 100644 index 0000000..31aa2f6 --- /dev/null +++ b/lws10-test-suite/resources/authorization-server-metadata.json @@ -0,0 +1,14 @@ +{ + "issuer": "https://authorization.example", + "grant_types_supported": [ + "urn:ietf:params:oauth:grant-type:token-exchange" + ], + "token_endpoint": "https://authorization.example/token", + "jwks_uri": "https://authorization.example/jwks", + "claims_supported": ["sub", "iss", "client_id", "aud"], + "response_types_supported": ["token"], + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:jwt", + "urn:ietf:params:oauth:token-type:id-token" + ] +} diff --git a/lws10-test-suite/resources/shoppinglist.txt b/lws10-test-suite/resources/shoppinglist.txt new file mode 100644 index 0000000..b188fd3 --- /dev/null +++ b/lws10-test-suite/resources/shoppinglist.txt @@ -0,0 +1,6 @@ +milk +eggs +bread +butter +apples +orange juice diff --git a/lws10-test-suite/resources/storage-description.json b/lws10-test-suite/resources/storage-description.json new file mode 100644 index 0000000..9f09e86 --- /dev/null +++ b/lws10-test-suite/resources/storage-description.json @@ -0,0 +1,19 @@ +{ + "@context": "https://www.w3.org/ns/lws/v1", + "id": "https://storage.example/alice/", + "type": "Storage", + "service": [ + { + "type": "StorageDescription", + "serviceEndpoint": "https://storage.example/alice/description" + }, + { + "type": "AuthorizationServer", + "serviceEndpoint": "https://authorization.example" + } + ], + "capability": [ + { "type": "Notifications" }, + { "type": "ProfileNegotiation" } + ] +} diff --git a/lws10-test-suite/resources/token-response-200.json b/lws10-test-suite/resources/token-response-200.json new file mode 100644 index 0000000..8c643fb --- /dev/null +++ b/lws10-test-suite/resources/token-response-200.json @@ -0,0 +1,5 @@ +{ + "access_token": "", + "token_type": "Bearer", + "expires_in": 300 +} diff --git a/lws10-test-suite/resources/token-response-400.json b/lws10-test-suite/resources/token-response-400.json new file mode 100644 index 0000000..648dd2a --- /dev/null +++ b/lws10-test-suite/resources/token-response-400.json @@ -0,0 +1,3 @@ +{ + "error": "invalid_request" +}