diff --git a/.componentsignore b/.componentsignore
index ea3c04a90..fc0c67764 100644
--- a/.componentsignore
+++ b/.componentsignore
@@ -11,6 +11,7 @@
"ChangeMap",
"CredentialSet",
"Dict",
+ "EmptyObject",
"Error",
"EventEmitter",
"FetchDocumentLoader",
@@ -21,6 +22,7 @@
"IndexTypeCollection",
"IdentifierMap",
"IdentifierSetMultiMap",
+ "interactionPolicy.DefaultPolicy",
"NodeJS.Dict",
"NotificationChannelType",
"PermissionMap",
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 5d3d234b6..000d9c104 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -5,13 +5,26 @@
### New features
- The minimum supported Node version is now v18.
+- Account management and everything related to it have been drastically changed,
+ see the [usage documentation](https://communitysolidserver.github.io/CommunitySolidServer/7.x/usage/identity-provider/)
+ for an overview of the new features,
+ and the [architecture documentation](http://communitysolidserver.github.io/CommunitySolidServer/7.x/architecture/features/accounts/overview/)
+ for an overview of the new structure.
+ Creating an account now requires multiple steps, but allows you to have multiple pods or WebIDs for 1 account.
+ The architecture has been updated to be more easily extensible.
+- Pod seeding has been updated to account for the new account management, with an update CLI parameter `--seedConfig`,
+ see the [updated documentation](https://communitysolidserver.github.io/CommunitySolidServer/7.x/usage/seeding-pods/)
+ for more details.
+- Due to the changes in account management, setup has been removed completely.
+ The `*-no-setup.json` configurations have been renamed to `*-root.json` to indicate their focus on the root container.
- The `StaticAssetHandler` can now be used to link static pages to containers.
This can be used to set a static page for the root container of a server.
See the `/config/app/init/static-root.json` config for an example.
### Data migration
-No actions are required to migrate data.
+Old account data will need to be migrated as described in the
+[documentation](https://communitysolidserver.github.io/CommunitySolidServer/7.x/usage/account/migration/).
### Configuration changes
@@ -23,16 +36,21 @@ The `@context` needs to be updated to
The following changes pertain to the imports in the default configs:
- There is a new `static-root.json` import option for `app/init`, setting a static page for the root container.
+- There is a new set of imports `identity/interaction` to determine the IDP features.
+- There is a new set of imports `storage/location` to determine where the root storage of the server is located.
+- The `app/setup`and `identity/registration` imports have been removed.
The following changes are relevant for v6 custom configs that replaced certain features.
+- All configurations that had a reference to setup have been updated.
- `/app/init/*` imports have changed. Functionality remained the same though.
- All imports that define storages have been updated with new storage classes.
- `/http/notifications/base/storage.json`
- - `/identity/*`
- `/storage/keyvalue/storages/storages.json`
- All identifiers containing the string "WebHook" have been renamed to instead use "Webhook"
to be consistent with the notification type.
+- `/identity/*` configurations have drastically changed due to the account management update.
+- `/http/static/default.json` has been updated to allow easier overriding of the static resources.
### Interface changes
@@ -45,6 +63,10 @@ These changes are relevant if you wrote custom modules for the server that depen
`HashEncodingPathStorage` has similarly been replaced by introducing `HashEncodingStorage`.
- All classes with the name `WebHook*` have been renamed to `Webhook*`
to be consistent with the corresponding notification type.
+- Most classes related to the IDP have been changed.
+- All classes related to setup have been removed.
+- The `StaticAssetHandler` has bene updated to support the new functionality.
+- `SeededPodInitializer` has been renamed to `SeededAccountInitializer`.
## v6.1.0
diff --git a/config/app/README.md b/config/app/README.md
index a47b59f72..89492860e 100644
--- a/config/app/README.md
+++ b/config/app/README.md
@@ -8,8 +8,9 @@ Contains a list of initializer that need to be run when starting the server.
* *default*: The default setup. The ParallelHandler can be used to add custom Initializers.
* *initialize-root*: Makes sure the root container has the necessary resources to function properly.
- This is only relevant if setup is disabled but root container access is still required.
-* *initialize-prefilled-root*: Similar to `initialize-root` but adds some introductory resources to the root container.
+* *initialize-prefilled-root*: Similar to `initialize-root` but adds an index page to the root container.
+* *initialize-intro*: Similar to `initialize-prefilled-root` but adds an index page
+ specific to the memory-based server of the default configuration.
* *static-root*: Shows a static introduction page at the server root. This is not a Solid resource.
## Main
diff --git a/config/app/init/base/init.json b/config/app/init/base/init.json
index 76d7bdbae..0729e8306 100644
--- a/config/app/init/base/init.json
+++ b/config/app/init/base/init.json
@@ -4,7 +4,7 @@
"css:config/app/init/initializers/base-url.json",
"css:config/app/init/initializers/logger.json",
"css:config/app/init/initializers/server.json",
- "css:config/app/init/initializers/seeded-pod.json",
+ "css:config/app/init/initializers/seeding.json",
"css:config/app/init/initializers/version.json",
"css:config/app/init/initializers/workers.json"
],
@@ -33,7 +33,7 @@
{ "@id": "urn:solid-server:default:CleanupInitializer"},
{ "@id": "urn:solid-server:default:BaseUrlVerifier" },
{ "@id": "urn:solid-server:default:PrimaryParallelInitializer" },
- { "@id": "urn:solid-server:default:SeededPodInitializer" },
+ { "@id": "urn:solid-server:default:SeededAccountInitializer" },
{ "@id": "urn:solid-server:default:ModuleVersionVerifier" },
{ "@id": "urn:solid-server:default:WorkerManager" }
]
diff --git a/config/app/init/initialize-intro.json b/config/app/init/initialize-intro.json
new file mode 100644
index 000000000..004214967
--- /dev/null
+++ b/config/app/init/initialize-intro.json
@@ -0,0 +1,22 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/app/init/default.json",
+ "css:config/app/init/initializers/root.json"
+ ],
+ "@graph": [
+ {
+ "comment": "Initializes the root container resource.",
+ "@id": "urn:solid-server:default:PrimaryParallelInitializer",
+ "@type": "ParallelHandler",
+ "handlers": [
+ { "@id": "urn:solid-server:default:RootInitializer" }
+ ]
+ },
+ {
+ "@id": "urn:solid-server:default:RootFolderGenerator",
+ "@type": "StaticFolderGenerator",
+ "templateFolder": "@css:templates/root/intro"
+ }
+ ]
+}
diff --git a/config/app/init/initializers/seeded-pod.json b/config/app/init/initializers/seeded-pod.json
deleted file mode 100644
index a7988863a..000000000
--- a/config/app/init/initializers/seeded-pod.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Separate manager from the RegistrationHandler in case registration is disabled.",
- "@id": "urn:solid-server:default:SeededPodRegistrationManager",
- "@type": "RegistrationManager",
- "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
- "args_webIdSuffix": "/profile/card#me",
- "args_identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" },
- "args_ownershipValidator": { "@id": "urn:solid-server:auth:password:OwnershipValidator" },
- "args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
- "args_podManager": { "@id": "urn:solid-server:default:PodManager" }
- },
- {
- "comment": "Initializer that instantiates all the seeded accounts and pods.",
- "@id": "urn:solid-server:default:SeededPodInitializer",
- "@type": "SeededPodInitializer",
- "registrationManager": { "@id": "urn:solid-server:default:SeededPodRegistrationManager" },
- "configFilePath": { "@id": "urn:solid-server:default:variable:seededPodConfigJson" }
- }
- ]
-}
diff --git a/config/app/init/initializers/seeding.json b/config/app/init/initializers/seeding.json
new file mode 100644
index 000000000..fa02a675c
--- /dev/null
+++ b/config/app/init/initializers/seeding.json
@@ -0,0 +1,14 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Initializer that instantiates all the seeded accounts and pods.",
+ "@id": "urn:solid-server:default:SeededAccountInitializer",
+ "@type": "SeededAccountInitializer",
+ "accountHandler": { "@id": "urn:solid-server:default:CreateAccountHandler" },
+ "passwordHandler": { "@id": "urn:solid-server:default:CreatePasswordHandler" },
+ "podHandler": { "@id": "urn:solid-server:default:CreatePodHandler" },
+ "configFilePath": { "@id": "urn:solid-server:default:variable:seedConfig" }
+ }
+ ]
+}
diff --git a/config/app/variables/cli/cli.json b/config/app/variables/cli/cli.json
index 47d385f6e..119e2a698 100644
--- a/config/app/variables/cli/cli.json
+++ b/config/app/variables/cli/cli.json
@@ -105,11 +105,11 @@
},
{
"@type": "YargsParameter",
- "name": "seededPodConfigJson",
+ "name": "seedConfig",
"options": {
"requiresArg": true,
"type": "string",
- "describe": "Path to the file that will be used to seed pods."
+ "describe": "Path to the file that will be used to seed accounts and pods."
}
},
{
diff --git a/config/app/variables/resolver/resolver.json b/config/app/variables/resolver/resolver.json
index 92fef320a..54e961ab4 100644
--- a/config/app/variables/resolver/resolver.json
+++ b/config/app/variables/resolver/resolver.json
@@ -68,10 +68,10 @@
}
},
{
- "CombinedShorthandResolver:_resolvers_key": "urn:solid-server:default:variable:seededPodConfigJson",
+ "CombinedShorthandResolver:_resolvers_key": "urn:solid-server:default:variable:seedConfig",
"CombinedShorthandResolver:_resolvers_value": {
"@type": "AssetPathExtractor",
- "key": "seededPodConfigJson"
+ "key": "seedConfig"
}
},
{
diff --git a/config/default.json b/config/default.json
index 73034cdae..42da36f12 100644
--- a/config/default.json
+++ b/config/default.json
@@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
- "css:config/app/init/initialize-prefilled-root.json",
+ "css:config/app/init/initialize-intro.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/dynamic.json b/config/dynamic.json
index 84b53a552..9780c7d40 100644
--- a/config/dynamic.json
+++ b/config/dynamic.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/dynamic.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/example-https-file.json b/config/example-https-file.json
index 5ba745e8f..f0c37f0a0 100644
--- a/config/example-https-file.json
+++ b/config/example-https-file.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/file-acp.json b/config/file-acp.json
index 58ecb5a31..efbd5b01c 100644
--- a/config/file-acp.json
+++ b/config/file-acp.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/acp.json",
"css:config/ldp/handler/default.json",
diff --git a/config/file-root.json b/config/file-root.json
index 43e50a492..1b8961400 100644
--- a/config/file-root.json
+++ b/config/file-root.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/file.json b/config/file.json
index 14d4fdec4..7a8dde0fb 100644
--- a/config/file.json
+++ b/config/file.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/http/notifications/base/description.json b/config/http/notifications/base/description.json
index 640c414cf..6e1d1fbd8 100644
--- a/config/http/notifications/base/description.json
+++ b/config/http/notifications/base/description.json
@@ -21,7 +21,10 @@
"comment": "The root URL of all Notification subscription routes.",
"@id": "urn:solid-server:default:NotificationRoute",
"@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:default:variable:baseUrl" },
+ "base": {
+ "@type": "AbsolutePathInteractionRoute",
+ "path": { "@id": "urn:solid-server:default:variable:baseUrl" }
+ },
"relativePath": "/.notifications/"
}
]
diff --git a/config/http/notifications/webhooks/routes.json b/config/http/notifications/webhooks/routes.json
index aefceff16..59fa23aec 100644
--- a/config/http/notifications/webhooks/routes.json
+++ b/config/http/notifications/webhooks/routes.json
@@ -11,7 +11,8 @@
"@id": "urn:solid-server:default:WebhookWebIdRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:default:WebhookRoute" },
- "relativePath": "/webId"
+ "relativePath": "/webId",
+ "ensureSlash": false
},
{
diff --git a/config/https-file-cli.json b/config/https-file-cli.json
index 49bab6b2b..a96ae265c 100644
--- a/config/https-file-cli.json
+++ b/config/https-file-cli.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/identity/README.md b/config/identity/README.md
index 74370d1be..3335355cb 100644
--- a/config/identity/README.md
+++ b/config/identity/README.md
@@ -27,8 +27,15 @@ Necessary for sending e-mail when using IDP.
Contains everything needed for setting up the Identity Provider.
-* *default*: As of writing there is not much customization possible.
- This contains everything needed.
+* *default*: Contains all the core components of the IDP.
+
+## Interaction
+
+Everything related to the JSON API and its routing.
+
+* *default*: Everything enabled.
+* *no-accounts*: Disables the creation of new accounts.
+* *no-pods*: Disables the creation of new pods.
## Ownership
@@ -44,10 +51,3 @@ What to use for pod creation.
* *dynamic*: Every created pod has its own Components.js config for its ResourceStore,
which can differ from the others.
* *static*: All pod data is stored in separate containers in the same ResourceStore.
-
-## Registration
-
-If users should be able to register on the server.
-
-* *enabled*: Enables registration.
-* *disabled*: Disables registration.
diff --git a/config/identity/access/initializers/idp.json b/config/identity/access/initializers/idp.json
index 8c60aeba3..89c58c12e 100644
--- a/config/identity/access/initializers/idp.json
+++ b/config/identity/access/initializers/idp.json
@@ -11,7 +11,7 @@
"source": {
"@type": "ContainerInitializer",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
- "args_path": "/idp/",
+ "args_path": "/.account/",
"args_store": { "@id": "urn:solid-server:default:ResourceStore" },
"args_generator": {
"@type": "StaticFolderGenerator",
diff --git a/config/identity/email/default.json b/config/identity/email/default.json
index 1bb7cd14b..888ecfbf8 100644
--- a/config/identity/email/default.json
+++ b/config/identity/email/default.json
@@ -4,7 +4,8 @@
{
"comment": "The default configuration does not contain credentials for an email client. In production systems, you likely want to set up your own.",
"@id": "urn:solid-server:default:EmailSender",
- "@type": "UnsupportedAsyncHandler"
+ "@type": "UnsupportedAsyncHandler",
+ "errorMessage": "No email server is configured."
}
]
}
diff --git a/config/identity/handler/account-store/default.json b/config/identity/handler/account-store/default.json
deleted file mode 100644
index 0ae3a0957..000000000
--- a/config/identity/handler/account-store/default.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "The storage adapter that persists usernames, passwords, etc.",
- "@id": "urn:solid-server:auth:password:AccountStore",
- "@type": "BaseAccountStore",
- "saltRounds": 10,
- "storage": {
- "@type": "Base64EncodingStorage",
- "source": {
- "@type": "ContainerPathStorage",
- "relativePath": "/accounts/",
- "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
- }
- },
- "forgotPasswordStorage": {
- "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage"
- }
- },
- {
- "comment": "Stores expiring data. This class has a `finalize` function that needs to be called after stopping the server.",
- "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage",
- "@type": "WrappedExpiringStorage",
- "source": {
- "@type": "Base64EncodingStorage",
- "source": {
- "@type": "ContainerPathStorage",
- "relativePath": "/forgot-password/",
- "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
- }
- }
- }
- ]
-}
diff --git a/config/identity/handler/adapter-factory/webid.json b/config/identity/handler/adapter-factory/webid.json
index 59d378735..b0388b8f0 100644
--- a/config/identity/handler/adapter-factory/webid.json
+++ b/config/identity/handler/adapter-factory/webid.json
@@ -5,10 +5,11 @@
"comment": "An adapter is responsible for storing all interaction metadata.",
"@id": "urn:solid-server:default:IdpAdapterFactory",
"@type": "ClientCredentialsAdapterFactory",
- "storage": { "@id": "urn:solid-server:auth:password:CredentialsStorage" },
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "clientCredentialsStore": { "@id": "urn:solid-server:default:ClientCredentialsStore" },
"source": {
"@type": "WebIdAdapterFactory",
- "converter": {"@id": "urn:solid-server:default:RepresentationConverter" },
+ "converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
"source": {
"@type": "ExpiringAdapterFactory",
"storage": {
diff --git a/config/identity/handler/default.json b/config/identity/handler/default.json
index 1dabe42e7..00fc69688 100644
--- a/config/identity/handler/default.json
+++ b/config/identity/handler/default.json
@@ -1,46 +1,46 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
- "css:config/identity/handler/account-store/default.json",
"css:config/identity/handler/adapter-factory/webid.json",
- "css:config/identity/handler/interaction/routes.json",
"css:config/identity/handler/jwks/default.json",
- "css:config/identity/handler/provider-factory/identity.json"
+ "css:config/identity/handler/provider-factory/identity.json",
+ "css:config/identity/handler/storage/default.json",
+ "css:config/identity/handler/storage/password.json"
],
"@graph": [
{
"comment": "Routes all IDP related requests to the relevant handlers.",
"@id": "urn:solid-server:default:IdentityProviderHandler",
"@type": "RouterHandler",
- "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
- "args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
- "args_allowedPathNames": [ "^/idp/.*" ],
- "args_handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" }
+ "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
+ "targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
+ "allowedPathNames": [ "^/.account/.*" ],
+ "handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" }
},
{
"comment": "Handles IDP input parsing.",
"@id": "urn:solid-server:default:IdentityProviderParsingHandler",
"@type": "ParsingHttpHandler",
- "args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
- "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
- "args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
- "args_operationHandler": {
+ "requestParser": { "@id": "urn:solid-server:default:RequestParser" },
+ "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
+ "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
+ "operationHandler": {
"comment": "Handles IDP input authorization. Permission reader should be set to allow all if no authorization is needed.",
"@type": "AuthorizingHttpHandler",
"@id": "urn:solid-server:default:IdentityProviderAuthorizingHandler",
- "args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
- "args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
- "args_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
- "args_operationHandler": { "@id": "urn:solid-server:default:IdentityProviderHttpHandler" }
+ "credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
+ "modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
+ "authorizer": { "@id": "urn:solid-server:default:Authorizer" },
+ "operationHandler": { "@id": "urn:solid-server:default:IdentityProviderHttpHandler" }
}
},
{
"comment": "Handles IDP handler behaviour.",
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
"@type": "IdentityProviderHttpHandler",
- "args_providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
- "args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
- "args_handler": { "@id": "urn:solid-server:default:InteractionHandler" }
+ "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
+ "cookieStore": { "@id": "urn:solid-server:default:CookieStore" },
+ "handler": { "@id": "urn:solid-server:default:InteractionHandler" }
}
]
}
diff --git a/config/identity/handler/interaction/routes.json b/config/identity/handler/interaction/routes.json
deleted file mode 100644
index 133f7640a..000000000
--- a/config/identity/handler/interaction/routes.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "import": [
- "css:config/identity/handler/interaction/routes/consent.json",
- "css:config/identity/handler/interaction/routes/credentials.json",
- "css:config/identity/handler/interaction/routes/forgot-password.json",
- "css:config/identity/handler/interaction/routes/index.json",
- "css:config/identity/handler/interaction/routes/login.json",
- "css:config/identity/handler/interaction/routes/prompt.json",
- "css:config/identity/handler/interaction/routes/reset-password.json",
- "css:config/identity/handler/interaction/views/controls.json",
- "css:config/identity/handler/interaction/views/html.json"
- ],
- "@graph": [
- {
- "@id": "urn:solid-server:default:InteractionHandler",
- "@type": "WaterfallHandler",
- "handlers": [
- {
- "comment": "Returns the relevant HTML pages for the interactions when needed",
- "@id": "urn:solid-server:auth:password:HtmlViewHandler"
- },
- {
- "comment": "Adds controls and API version to JSON responses.",
- "@id": "urn:solid-server:auth:password:ControlHandler",
- "@type": "ControlHandler",
- "source" : { "@id": "urn:solid-server:auth:password:LocationInteractionHandler" }
- }
- ]
- },
- {
- "comment": "Converts 3xx redirects to 200 JSON responses for consumption by browser scripts.",
- "@id": "urn:solid-server:auth:password:LocationInteractionHandler",
- "@type": "LocationInteractionHandler",
- "source" : { "@id": "urn:solid-server:auth:password:InteractionRouteHandler" }
- },
- {
- "comment": "Handles every interaction based on their route.",
- "@id": "urn:solid-server:auth:password:InteractionRouteHandler",
- "@type": "WaterfallHandler",
- "handlers": [
- { "@id": "urn:solid-server:auth:password:IndexRouteHandler" },
- { "@id": "urn:solid-server:auth:password:PromptRouteHandler" },
- { "@id": "urn:solid-server:auth:password:LoginRouteHandler" },
- { "@id": "urn:solid-server:auth:password:ConsentRouteHandler" },
- { "@id": "urn:solid-server:auth:password:ForgotPasswordRouteHandler" },
- { "@id": "urn:solid-server:auth:password:ResetPasswordRouteHandler" },
- { "@id": "urn:solid-server:auth:password:CredentialsRouteHandler" }
- ]
- }
- ]
-}
diff --git a/config/identity/handler/interaction/routes/consent.json b/config/identity/handler/interaction/routes/consent.json
deleted file mode 100644
index e6c7ba2cd..000000000
--- a/config/identity/handler/interaction/routes/consent.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Handles the interaction that occurs when a logged in user wants to authenticate with a new app.",
- "@id": "urn:solid-server:auth:password:ConsentRouteHandler",
- "@type":"InteractionRouteHandler",
- "route": {
- "@id": "urn:solid-server:auth:password:ConsentRoute",
- "@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
- "relativePath": "/consent/"
- },
- "source": {
- "@id": "urn:solid-server:auth:password:ConsentHandler",
- "@type": "ConsentHandler",
- "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
- }
- }
- ]
-}
diff --git a/config/identity/handler/interaction/routes/credentials.json b/config/identity/handler/interaction/routes/credentials.json
deleted file mode 100644
index 373e4c5ac..000000000
--- a/config/identity/handler/interaction/routes/credentials.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Stores all client credential tokens.",
- "@id": "urn:solid-server:auth:password:CredentialsStorage",
- "@type": "Base64EncodingStorage",
- "source": {
- "@type": "ContainerPathStorage",
- "relativePath": "/accounts/credentials/",
- "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
- }
- },
- {
- "comment": "Handles credential tokens. These can be used to automate clients. See documentation for more info.",
- "@id": "urn:solid-server:auth:password:CredentialsRouteHandler",
- "@type":"InteractionRouteHandler",
- "route": {
- "@id": "urn:solid-server:auth:password:CredentialsRoute",
- "@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
- "relativePath": "/credentials/"
- },
- "source": {
- "@id": "urn:solid-server:auth:password:CredentialsHandler",
- "@type": "EmailPasswordAuthorizer",
- "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
- "source": {
- "@type": "WaterfallHandler",
- "handlers": [
- {
- "@type": "CreateCredentialsHandler",
- "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
- "credentialStorage": { "@id": "urn:solid-server:auth:password:CredentialsStorage" }
- },
- {
- "@type": "DeleteCredentialsHandler",
- "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
- "credentialStorage": { "@id": "urn:solid-server:auth:password:CredentialsStorage" }
- },
- {
- "@type": "ListCredentialsHandler",
- "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
- }
- ]
- }
- }
- },
- {
-
- }
- ]
-}
diff --git a/config/identity/handler/interaction/routes/forgot-password.json b/config/identity/handler/interaction/routes/forgot-password.json
deleted file mode 100644
index 22dec3011..000000000
--- a/config/identity/handler/interaction/routes/forgot-password.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Handles the forgot password interaction",
- "@id": "urn:solid-server:auth:password:ForgotPasswordRouteHandler",
- "@type":"InteractionRouteHandler",
- "route": {
- "@id": "urn:solid-server:auth:password:ForgotPasswordRoute",
- "@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
- "relativePath": "/forgotpassword/"
- },
- "source": {
- "@id": "urn:solid-server:auth:password:ForgotPasswordHandler",
- "@type": "ForgotPasswordHandler",
- "args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
- "args_templateEngine": {
- "@type": "StaticTemplateEngine",
- "templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
- "template": "@css:templates/identity/email-password/reset-password-email.html.ejs"
- },
- "args_emailSender": { "@id": "urn:solid-server:default:EmailSender" },
- "args_resetRoute": { "@id": "urn:solid-server:auth:password:ResetPasswordRoute" }
- }
- }
- ]
-}
diff --git a/config/identity/handler/interaction/routes/index.json b/config/identity/handler/interaction/routes/index.json
deleted file mode 100644
index 0afc9a2a8..000000000
--- a/config/identity/handler/interaction/routes/index.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Root API entry. Returns an empty body so we can add controls pointing to other interaction routes.",
- "@id": "urn:solid-server:auth:password:IndexRouteHandler",
- "@type": "InteractionRouteHandler",
- "route": {
- "@id": "urn:solid-server:auth:password:IndexRoute",
- "@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:default:variable:baseUrl" },
- "relativePath": "/idp/"
- },
- "source": {
- "@id": "urn:solid-server:auth:password:IndexHandler",
- "@type": "FixedInteractionHandler",
- "response": {}
- }
- }
- ]
-}
diff --git a/config/identity/handler/interaction/routes/login.json b/config/identity/handler/interaction/routes/login.json
deleted file mode 100644
index edec4e6ca..000000000
--- a/config/identity/handler/interaction/routes/login.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Handles the login interaction",
- "@id": "urn:solid-server:auth:password:LoginRouteHandler",
- "@type": "InteractionRouteHandler",
- "route": {
- "@id": "urn:solid-server:auth:password:LoginRoute",
- "@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
- "relativePath": "/login/"
- },
- "source": {
- "@id": "urn:solid-server:auth:password:LoginHandler",
- "@type": "LoginHandler",
- "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
- }
- }
- ]
-}
diff --git a/config/identity/handler/interaction/routes/prompt.json b/config/identity/handler/interaction/routes/prompt.json
deleted file mode 100644
index 14a7bb7e5..000000000
--- a/config/identity/handler/interaction/routes/prompt.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Handles OIDC redirects containing a prompt, such as login or consent.",
- "@id": "urn:solid-server:auth:password:PromptRouteHandler",
- "@type": "InteractionRouteHandler",
- "route": {
- "@id": "urn:solid-server:auth:password:PromptRoute",
- "@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
- "relativePath": "/prompt/"
- },
- "source": {
- "@type": "PromptHandler",
- "@id": "urn:solid-server:auth:password:PromptHandler",
- "promptRoutes": [
- {
- "PromptHandler:_promptRoutes_key": "login",
- "PromptHandler:_promptRoutes_value": { "@id": "urn:solid-server:auth:password:LoginRoute" }
- },
- {
- "PromptHandler:_promptRoutes_key": "consent",
- "PromptHandler:_promptRoutes_value": { "@id": "urn:solid-server:auth:password:ConsentRoute" }
- }
- ]
- }
- }
- ]
-}
diff --git a/config/identity/handler/interaction/routes/reset-password.json b/config/identity/handler/interaction/routes/reset-password.json
deleted file mode 100644
index 50bc4a085..000000000
--- a/config/identity/handler/interaction/routes/reset-password.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Handles the reset password interaction",
- "@id": "urn:solid-server:auth:password:ResetPasswordRouteHandler",
- "@type": "InteractionRouteHandler",
- "route": {
- "@id": "urn:solid-server:auth:password:ResetPasswordRoute",
- "@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
- "relativePath": "/resetpassword/"
- },
- "source": {
- "@id": "urn:solid-server:auth:password:ResetPasswordHandler",
- "@type": "ResetPasswordHandler",
- "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
- }
- }
- ]
-}
diff --git a/config/identity/handler/interaction/views/controls.json b/config/identity/handler/interaction/views/controls.json
deleted file mode 100644
index 6d24e952c..000000000
--- a/config/identity/handler/interaction/views/controls.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "@id": "urn:solid-server:auth:password:ControlHandler",
- "@type": "ControlHandler",
- "controls": [
- {
- "ControlHandler:_controls_key": "index",
- "ControlHandler:_controls_value": { "@id": "urn:solid-server:auth:password:IndexRoute" }
- },
- {
- "ControlHandler:_controls_key": "prompt",
- "ControlHandler:_controls_value": { "@id": "urn:solid-server:auth:password:PromptRoute" }
- },
- {
- "ControlHandler:_controls_key": "login",
- "ControlHandler:_controls_value": { "@id": "urn:solid-server:auth:password:LoginRoute" }
- },
- {
- "ControlHandler:_controls_key": "forgotPassword",
- "ControlHandler:_controls_value": { "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" }
- },
- {
- "ControlHandler:_controls_key": "credentials",
- "ControlHandler:_controls_value": { "@id": "urn:solid-server:auth:password:CredentialsRoute" }
- }
- ]
- }
- ]
-}
diff --git a/config/identity/handler/interaction/views/html.json b/config/identity/handler/interaction/views/html.json
deleted file mode 100644
index e7615c63a..000000000
--- a/config/identity/handler/interaction/views/html.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "@id": "urn:solid-server:auth:password:HtmlViewHandler",
- "@type": "HtmlViewHandler",
- "index": { "@id": "urn:solid-server:auth:password:IndexRoute" },
- "templateEngine": {
- "comment": "Renders the specific page and embeds it into the main HTML body.",
- "@type": "ChainedTemplateEngine",
- "renderedName": "htmlBody",
- "engines": [
- {
- "comment": "Will be called with specific templates to generate HTML snippets.",
- "@id": "urn:solid-server:default:TemplateEngine"
- },
- {
- "comment": "Will embed the result of the first engine into the main HTML template.",
- "@type": "StaticTemplateEngine",
- "templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
- "template": "@css:templates/main.html.ejs"
- }
- ]
- },
- "templates": [
- {
- "HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/login.html.ejs",
- "HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:LoginRoute" }
- },
- {
- "HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/consent.html.ejs",
- "HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:ConsentRoute" }
- },
- {
- "HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/forgot-password.html.ejs",
- "HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" }
- },
- {
- "HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/reset-password.html.ejs",
- "HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:ResetPasswordRoute" }
- }
- ]
- }
- ]
-}
diff --git a/config/identity/handler/provider-factory/identity.json b/config/identity/handler/provider-factory/identity.json
index 47f92a7cb..b44a46fae 100644
--- a/config/identity/handler/provider-factory/identity.json
+++ b/config/identity/handler/provider-factory/identity.json
@@ -5,16 +5,28 @@
"comment": "Sets all the relevant Solid-OIDC parameters.",
"@id": "urn:solid-server:default:IdentityProviderFactory",
"@type": "IdentityProviderFactory",
+ "promptFactory": {
+ "@id": "urn:solid-server:default:PromptFactory",
+ "@type": "SequenceHandler",
+ "handlers": [
+ {
+ "@type": "AccountPromptFactory",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "cookieStore": { "@id": "urn:solid-server:default:CookieStore" },
+ "cookieName": { "@id": "urn:solid-server:default:value:accountCookieName" }
+ }
+ ]
+ },
"adapterFactory": { "@id": "urn:solid-server:default:IdpAdapterFactory" },
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"oidcPath": "/.oidc",
- "interactionHandler": { "@id": "urn:solid-server:auth:password:PromptHandler" },
- "credentialStorage": { "@id": "urn:solid-server:auth:password:CredentialsStorage" },
+ "clientCredentialsStore": { "@id": "urn:solid-server:default:ClientCredentialsStore" },
"storage": { "@id": "urn:solid-server:default:KeyStorage" },
"jwkGenerator": { "@id": "urn:solid-server:default:JwkGenerator" },
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
+ "interactionRoute": { "@id": "urn:solid-server:default:IndexRoute" },
"config": {
"claims": {
"openid": [ "azp" ],
diff --git a/config/identity/handler/storage/default.json b/config/identity/handler/storage/default.json
new file mode 100644
index 000000000..26d4b2e5f
--- /dev/null
+++ b/config/identity/handler/storage/default.json
@@ -0,0 +1,78 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "@id": "urn:solid-server:default:AccountStore",
+ "@type": "BaseAccountStore",
+ "storage": {
+ "@id": "urn:solid-server:default:AccountStorage",
+ "@type": "WrappedExpiringStorage",
+ "source": {
+ "@type": "Base64EncodingStorage",
+ "source": {
+ "@type": "ContainerPathStorage",
+ "relativePath": "/accounts/data/",
+ "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
+ }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:WebIdStore",
+ "@type": "BaseWebIdStore",
+ "webIdRoute": { "@id": "urn:solid-server:default:AccountWebIdLinkRoute" },
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "storage": {
+ "@id": "urn:solid-server:default:WebIdStorage",
+ "@type": "Base64EncodingStorage",
+ "source": {
+ "@type": "ContainerPathStorage",
+ "relativePath": "/accounts/webIds/",
+ "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:CookieStore",
+ "@type": "BaseCookieStore",
+ "storage": {
+ "@id": "urn:solid-server:default:CookieStorage",
+ "@type": "WrappedExpiringStorage",
+ "source": {
+ "@type": "Base64EncodingStorage",
+ "source": {
+ "@type": "ContainerPathStorage",
+ "relativePath": "/accounts/cookies/",
+ "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
+ }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:PodStore",
+ "@type": "BasePodStore",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "podRoute": { "@id": "urn:solid-server:default:AccountPodIdRoute" },
+ "manager": { "@id": "urn:solid-server:default:PodManager" }
+ },
+
+ {
+ "@id": "urn:solid-server:default:ClientCredentialsStore",
+ "@type": "BaseClientCredentialsStore",
+ "clientCredentialsRoute": { "@id": "urn:solid-server:default:AccountClientCredentialsIdRoute" },
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "storage": {
+ "@id": "urn:solid-server:default:ClientCredentialsStorage",
+ "@type": "Base64EncodingStorage",
+ "source": {
+ "@type": "ContainerPathStorage",
+ "relativePath": "/accounts/client-credentials/",
+ "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
+ }
+ }
+ }
+ ]
+}
diff --git a/config/identity/handler/storage/password.json b/config/identity/handler/storage/password.json
new file mode 100644
index 000000000..461a3dc79
--- /dev/null
+++ b/config/identity/handler/storage/password.json
@@ -0,0 +1,35 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "@id": "urn:solid-server:default:PasswordStore",
+ "@type": "BasePasswordStore",
+ "storage": {
+ "@id": "urn:solid-server:default:PasswordStorage",
+ "@type": "Base64EncodingStorage",
+ "source": {
+ "@type": "ContainerPathStorage",
+ "relativePath": "/accounts/logins/password/",
+ "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:ForgotPasswordStore",
+ "@type": "BaseForgotPasswordStore",
+ "storage": {
+ "@id": "urn:solid-server:default:ForgotPasswordStorage",
+ "@type": "WrappedExpiringStorage",
+ "source": {
+ "@type": "Base64EncodingStorage",
+ "source": {
+ "@type": "ContainerPathStorage",
+ "relativePath": "/accounts/logins/password/forgot/",
+ "source": { "@id": "urn:solid-server:default:KeyValueStorage" }
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/config/identity/interaction/default.json b/config/identity/interaction/default.json
new file mode 100644
index 000000000..42c50b204
--- /dev/null
+++ b/config/identity/interaction/default.json
@@ -0,0 +1,16 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/identity/interaction/routing/default.json",
+ "css:config/identity/interaction/enable/account.json",
+ "css:config/identity/interaction/enable/client-credentials.json",
+ "css:config/identity/interaction/enable/password.json",
+ "css:config/identity/interaction/enable/pod.json",
+ "css:config/identity/interaction/enable/webid.json"
+ ],
+ "@graph": [
+ {
+ "comment": "Enables all account-related features."
+ }
+ ]
+}
diff --git a/config/identity/interaction/enable/account.json b/config/identity/interaction/enable/account.json
new file mode 100644
index 000000000..4bed2cdd6
--- /dev/null
+++ b/config/identity/interaction/enable/account.json
@@ -0,0 +1,56 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Enable account creation."
+ },
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:AccountRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:AccountControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "create",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountRoute" }
+ }]
+ },
+
+ {
+ "comment": "The parts below are specific for password logins, but will not cause issues should password logins be disabled."
+ },
+ {
+ "comment": "Route only used for an HTML page (and its corresponding controls).",
+ "@id": "urn:solid-server:default:RegisterPasswordRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:LoginPasswordRoute" },
+ "relativePath": "register/"
+ },
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [
+ {
+ "@id": "urn:solid-server:default:RegisterPasswordAccountHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/password/register.html.ejs",
+ "route": { "@id": "urn:solid-server:default:RegisterPasswordRoute" }
+ }
+ ]
+ },
+ {
+ "@id": "urn:solid-server:default:PasswordHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "register",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:RegisterPasswordRoute" }
+ }
+ ]
+ }
+ ]
+}
+
diff --git a/config/identity/interaction/enable/client-credentials.json b/config/identity/interaction/enable/client-credentials.json
new file mode 100644
index 000000000..fde0e5feb
--- /dev/null
+++ b/config/identity/interaction/enable/client-credentials.json
@@ -0,0 +1,43 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Enable client credentials creation."
+ },
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:AccountClientCredentialsRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:AccountControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "clientCredentials",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountClientCredentialsRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:CreateClientCredentialsHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/account/create-client-credentials.html.ejs",
+ "route": { "@id": "urn:solid-server:default:AccountClientCredentialsRoute" }
+ }]
+ },
+ {
+ "ControlHandler:_controls_value": {
+ "@id": "urn:solid-server:default:AccountHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "createClientCredentials",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountClientCredentialsRoute" }
+ }]
+ }
+ }
+ ]
+}
diff --git a/config/identity/interaction/enable/password.json b/config/identity/interaction/enable/password.json
new file mode 100644
index 000000000..568804b80
--- /dev/null
+++ b/config/identity/interaction/enable/password.json
@@ -0,0 +1,52 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Enable creating email/password combinations as a login mechanism."
+ },
+ {
+ "@id": "urn:solid-server:default:ControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "password",
+ "ControlHandler:_controls_value": {
+ "comment": "All controls associated with the password login method.",
+ "@id": "urn:solid-server:default:PasswordControlHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }
+ ]
+ },
+
+ {
+ "comment": "Adds a link to the login page of this auth method to the list that contains all options",
+ "@id": "urn:solid-server:default:LoginHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "Email/password combination",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:LoginPasswordRoute" }
+ }
+ ]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "password",
+ "ControlHandler:_controls_value": {
+ "comment": "Contains the controls linking to all HTML pages related to password authentication.",
+ "@id": "urn:solid-server:default:PasswordHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }
+ ]
+ }
+ ]
+}
+
diff --git a/config/identity/interaction/enable/pod.json b/config/identity/interaction/enable/pod.json
new file mode 100644
index 000000000..a8fc343a9
--- /dev/null
+++ b/config/identity/interaction/enable/pod.json
@@ -0,0 +1,44 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Enable pod creation."
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ { "@id": "urn:solid-server:default:AccountPodRouter" }
+ ]
+ },
+
+ {
+ "@id": "urn:solid-server:default:AccountControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "pod",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountPodRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:CreatePodHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/account/create-pod.html.ejs",
+ "route": { "@id": "urn:solid-server:default:AccountPodRoute" }
+ }]
+ },
+ {
+ "@id": "urn:solid-server:default:AccountHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "createPod",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountPodRoute" }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/enable/webid.json b/config/identity/interaction/enable/webid.json
new file mode 100644
index 000000000..a8dffa40f
--- /dev/null
+++ b/config/identity/interaction/enable/webid.json
@@ -0,0 +1,44 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Enable linking WebIDs to an account."
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ { "@id": "urn:solid-server:default:AccountWebIdRouter" }
+ ]
+ },
+
+ {
+ "@id": "urn:solid-server:default:AccountControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "webId",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountWebIdRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:LinkWebIdHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/account/link-webid.html.ejs",
+ "route": { "@id": "urn:solid-server:default:AccountWebIdRoute" }
+ }]
+ },
+ {
+ "@id": "urn:solid-server:default:AccountHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "linkWebId",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountWebIdRoute" }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/no-accounts.json b/config/identity/interaction/no-accounts.json
new file mode 100644
index 000000000..4d25c2c35
--- /dev/null
+++ b/config/identity/interaction/no-accounts.json
@@ -0,0 +1,15 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/identity/interaction/routing/default.json",
+ "css:config/identity/interaction/enable/client-credentials.json",
+ "css:config/identity/interaction/enable/password.json",
+ "css:config/identity/interaction/enable/pod.json",
+ "css:config/identity/interaction/enable/webid.json"
+ ],
+ "@graph": [
+ {
+ "comment": "Disables account creation."
+ }
+ ]
+}
diff --git a/config/identity/interaction/no-pods.json b/config/identity/interaction/no-pods.json
new file mode 100644
index 000000000..92091ad3f
--- /dev/null
+++ b/config/identity/interaction/no-pods.json
@@ -0,0 +1,15 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/identity/interaction/routing/default.json",
+ "css:config/identity/interaction/enable/account.json",
+ "css:config/identity/interaction/enable/client-credentials.json",
+ "css:config/identity/interaction/enable/password.json",
+ "css:config/identity/interaction/enable/webid.json"
+ ],
+ "@graph": [
+ {
+ "comment": "Disabled pod creation."
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/account/create.json b/config/identity/interaction/routing/account/create.json
new file mode 100644
index 000000000..5b6515dac
--- /dev/null
+++ b/config/identity/interaction/routing/account/create.json
@@ -0,0 +1,37 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the account creation",
+ "@id": "urn:solid-server:default:AccountRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:IndexRoute" },
+ "relativePath": "account/"
+ },
+ "source": {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:CreateAccountHandler",
+ "@type": "CreateAccountHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "cookieStore": { "@id": "urn:solid-server:default:CookieStore" },
+ "accountRoute": { "@id": "urn:solid-server:default:AccountIdRoute" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:AccountHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/account/account.html.ejs",
+ "route": { "@id": "urn:solid-server:default:AccountRoute" }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/account/login.json b/config/identity/interaction/routing/account/login.json
new file mode 100644
index 000000000..bd5ac57b6
--- /dev/null
+++ b/config/identity/interaction/routing/account/login.json
@@ -0,0 +1,14 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Base account login route that specific login implementations can extend.",
+ "@id": "urn:solid-server:default:AccountLoginRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": {
+ "@id": "urn:solid-server:default:AccountIdRoute"
+ },
+ "relativePath": "login/"
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/account/logout.json b/config/identity/interaction/routing/account/logout.json
new file mode 100644
index 000000000..52db095e5
--- /dev/null
+++ b/config/identity/interaction/routing/account/logout.json
@@ -0,0 +1,40 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles logging a user out.",
+ "@id": "urn:solid-server:default:AccountLogoutRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountLogoutRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:AccountIdRoute" },
+ "relativePath": "logout/"
+ },
+ "source": {
+ "@type": "MethodFilterHandler",
+ "methods": [ "POST" ],
+ "source": {
+ "@id": "urn:solid-server:default:LogoutHandler",
+ "@type": "LogoutHandler",
+ "cookieStore": { "@id": "urn:solid-server:default:CookieStore" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:AccountLogoutRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:AccountControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "logout",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountLogoutRoute" }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/account/main.json b/config/identity/interaction/routing/account/main.json
new file mode 100644
index 000000000..9591bc533
--- /dev/null
+++ b/config/identity/interaction/routing/account/main.json
@@ -0,0 +1,42 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/identity/interaction/routing/account/create.json",
+ "css:config/identity/interaction/routing/account/login.json",
+ "css:config/identity/interaction/routing/account/logout.json",
+ "css:config/identity/interaction/routing/account/resource.json"
+ ],
+ "@graph": [
+ {
+ "@id": "urn:solid-server:default:ControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "account",
+ "ControlHandler:_controls_value": {
+ "comment": "All controls related to account management.",
+ "@id": "urn:solid-server:default:AccountControlHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }
+ ]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "account",
+ "ControlHandler:_controls_value": {
+ "comment": "Controls linking to account-related HTML pages.",
+ "@id": "urn:solid-server:default:AccountHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/account/resource.json b/config/identity/interaction/routing/account/resource.json
new file mode 100644
index 000000000..3121b5c0d
--- /dev/null
+++ b/config/identity/interaction/routing/account/resource.json
@@ -0,0 +1,60 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the account details.",
+ "@id": "urn:solid-server:default:AccountIdRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountIdRoute",
+ "@type": "BaseAccountIdRoute",
+ "base": { "@id": "urn:solid-server:default:AccountRoute" }
+ },
+ "source": {
+ "@id": "urn:solid-server:default:AccountResourceHandler",
+ "@type": "MethodFilterHandler",
+ "methods": [ "GET" ],
+ "source": {
+ "@type": "AccountDetailsHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:AccountIdRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:AccountControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "account",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountIdRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:AccountIdHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/account/resource.html.ejs",
+ "route": { "@id": "urn:solid-server:default:AccountIdRoute" }
+ }]
+ },
+ {
+ "@id": "urn:solid-server:default:AccountHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "account",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountIdRoute" }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/client-credentials/create.json b/config/identity/interaction/routing/client-credentials/create.json
new file mode 100644
index 000000000..a41a40717
--- /dev/null
+++ b/config/identity/interaction/routing/client-credentials/create.json
@@ -0,0 +1,25 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles credential tokens. These can be used to automate clients. See documentation for more info.",
+ "@id": "urn:solid-server:default:AccountClientCredentialsRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountClientCredentialsRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:AccountIdRoute" },
+ "relativePath": "client-credentials/"
+ },
+ "source": {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:CreateClientCredentialsHandler",
+ "@type": "CreateClientCredentialsHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "clientCredentialsStore": { "@id": "urn:solid-server:default:ClientCredentialsStore" }
+ }
+ }
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/client-credentials/resource.json b/config/identity/interaction/routing/client-credentials/resource.json
new file mode 100644
index 000000000..14c48da16
--- /dev/null
+++ b/config/identity/interaction/routing/client-credentials/resource.json
@@ -0,0 +1,45 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the client credentials link details such as deletion.",
+ "@id": "urn:solid-server:default:AccountClientCredentialsIdRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountClientCredentialsIdRoute",
+ "@type": "BaseClientCredentialsIdRoute",
+ "base": { "@id": "urn:solid-server:default:AccountClientCredentialsRoute" }
+ },
+ "source": {
+ "@id": "urn:solid-server:default:ClientCredentialsResourceHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ {
+ "@type": "MethodFilterHandler",
+ "methods": [ "GET" ],
+ "source": {
+ "@type": "ClientCredentialsDetailsHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "clientCredentialsStore": { "@id": "urn:solid-server:default:ClientCredentialsStore" }
+ }
+ },
+ {
+ "@type": "MethodFilterHandler",
+ "methods": [ "DELETE" ],
+ "source": {
+ "@type": "DeleteClientCredentialsHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "clientCredentialsStore": { "@id": "urn:solid-server:default:ClientCredentialsStore" }
+ }
+ }
+ ]
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:AccountClientCredentialsIdRouter" }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/core/index.json b/config/identity/interaction/routing/core/index.json
new file mode 100644
index 000000000..730eb0613
--- /dev/null
+++ b/config/identity/interaction/routing/core/index.json
@@ -0,0 +1,49 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Root API entry. Returns an empty body so we can add controls pointing to other interaction routes.",
+ "@id": "urn:solid-server:default:IndexRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:IndexRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": {
+ "@type": "AbsolutePathInteractionRoute",
+ "path": { "@id": "urn:solid-server:default:variable:baseUrl" }
+ },
+ "relativePath": ".account/"
+ },
+ "source": {
+ "@type": "StaticInteractionHandler",
+ "response": {}
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:IndexRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:MainControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "index",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:IndexRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:IndexHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/index.html.ejs",
+ "route": { "@id": "urn:solid-server:default:IndexRoute" }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/core/login.json b/config/identity/interaction/routing/core/login.json
new file mode 100644
index 000000000..82e3f5b64
--- /dev/null
+++ b/config/identity/interaction/routing/core/login.json
@@ -0,0 +1,72 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Returns the links of the HTML pages that can be used to log in with specific methods.",
+ "@id": "urn:solid-server:default:LoginRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:LoginRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:IndexRoute" },
+ "relativePath": "login/"
+ },
+ "source": {
+ "@type": "MethodFilterHandler",
+ "methods": [ "GET" ],
+ "source": {
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "logins",
+ "ControlHandler:_controls_value": {
+ "comment": "New login methods should add a link to their HTML login page here. This list can be used when multiple login methods exist to make a choice.",
+ "@id": "urn:solid-server:default:LoginHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }
+ ]
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:LoginRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:MainControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "logins",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:LoginRoute" }
+ }
+ ]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:LoginHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/login.html.ejs",
+ "route": { "@id": "urn:solid-server:default:LoginRoute" }
+ }]
+ },
+ {
+ "@id": "urn:solid-server:default:MainHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "login",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:LoginRoute" }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/core/main.json b/config/identity/interaction/routing/core/main.json
new file mode 100644
index 000000000..73978b3cb
--- /dev/null
+++ b/config/identity/interaction/routing/core/main.json
@@ -0,0 +1,38 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/identity/interaction/routing/core/index.json",
+ "css:config/identity/interaction/routing/core/login.json"
+ ],
+ "@graph": [
+ {
+ "@id": "urn:solid-server:default:ControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "main",
+ "ControlHandler:_controls_value": {
+ "comment": "Contains all general controls.",
+ "@id": "urn:solid-server:default:MainControlHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }
+ ]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "main",
+ "ControlHandler:_controls_value": {
+ "comment": "Controls all general HTML page controls.",
+ "@id": "urn:solid-server:default:MainHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/default.json b/config/identity/interaction/routing/default.json
new file mode 100644
index 000000000..ed5d6c9d1
--- /dev/null
+++ b/config/identity/interaction/routing/default.json
@@ -0,0 +1,85 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/identity/interaction/routing/account/main.json",
+ "css:config/identity/interaction/routing/client-credentials/create.json",
+ "css:config/identity/interaction/routing/client-credentials/resource.json",
+ "css:config/identity/interaction/routing/core/main.json",
+ "css:config/identity/interaction/routing/oidc/main.json",
+ "css:config/identity/interaction/routing/password/main.json",
+ "css:config/identity/interaction/routing/pod/create.json",
+ "css:config/identity/interaction/routing/pod/resource.json",
+ "css:config/identity/interaction/routing/webid/link.json",
+ "css:config/identity/interaction/routing/webid/resource.json",
+
+ "css:config/identity/interaction/routing/views/html.json"
+ ],
+ "@graph": [
+ {
+ "@id": "urn:solid-server:default:InteractionHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ {
+ "comment": "Returns the relevant HTML pages for the interactions when needed.",
+ "@id": "urn:solid-server:default:HtmlViewHandler"
+ },
+ {
+ "comment": "Ensures locks on authenticated requests.",
+ "@id": "urn:solid-server:default:LockingInteractionHandler",
+ "@type": "LockingInteractionHandler",
+ "locker": { "@id": "urn:solid-server:default:ResourceLocker" },
+ "accountRoute": { "@id": "urn:solid-server:default:AccountIdRoute" },
+ "source": { "@id": "urn:solid-server:default:JsonConversionHandler" }
+ }
+ ]
+ },
+ {
+ "comment": "Convert incoming requests to JSON operations.",
+ "@id": "urn:solid-server:default:JsonConversionHandler",
+ "@type": "JsonConversionHandler",
+ "source": { "@id": "urn:solid-server:default:VersionHandler" },
+ "converter": { "@id": "urn:solid-server:default:RepresentationConverter" }
+ },
+ {
+ "comment": "Adds the API version to the JSON response.",
+ "@id": "urn:solid-server:default:VersionHandler",
+ "@type": "VersionHandler",
+ "source" : { "@id": "urn:solid-server:default:CookieInteractionHandler" }
+ },
+ {
+ "comment": "Updates the cookie values as necessary.",
+ "@id": "urn:solid-server:default:CookieInteractionHandler",
+ "@type": "CookieInteractionHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "cookieStore": { "@id": "urn:solid-server:default:CookieStore" },
+ "source": { "@id": "urn:solid-server:default:RootControlHandler" }
+ },
+ {
+ "comment": "Adds controls to the JSON response.",
+ "@id": "urn:solid-server:default:RootControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "controls",
+ "ControlHandler:_controls_value": {
+ "comment": "The main controls object. All other controls should be added to this one.",
+ "@id": "urn:solid-server:default:ControlHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }],
+ "source" : { "@id": "urn:solid-server:default:LocationInteractionHandler" }
+ },
+ {
+ "comment": "Converts 3xx redirects to 200 JSON responses for consumption by browser scripts.",
+ "@id": "urn:solid-server:default:LocationInteractionHandler",
+ "@type": "LocationInteractionHandler",
+ "source" : { "@id": "urn:solid-server:default:InteractionRouteHandler" }
+ },
+ {
+ "comment": "Contains all JsonInteractionHandlers that can potentially handle the input request.",
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": []
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/oidc/cancel.json b/config/identity/interaction/routing/oidc/cancel.json
new file mode 100644
index 000000000..d60da3222
--- /dev/null
+++ b/config/identity/interaction/routing/oidc/cancel.json
@@ -0,0 +1,44 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Allows users to cancel an OIDC interaction, bringing them back to the original client.",
+ "@id": "urn:solid-server:default:OidcCancelRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:OidcCancelRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:OidcRoute" },
+ "relativePath": "cancel/"
+ },
+ "source": {
+ "@id": "urn:solid-server:default:CancelOidcHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ {
+ "@type": "MethodFilterHandler",
+ "methods": [ "POST" ],
+ "source": { "@type": "CancelOidcHandler" }
+ }
+ ]
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:OidcCancelRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:OidcControlHandler",
+ "@type": "OidcControlHandler",
+ "controls": [
+ {
+ "OidcControlHandler:_controls_key": "cancel",
+ "OidcControlHandler:_controls_value": { "@id": "urn:solid-server:default:OidcCancelRoute" }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/oidc/consent.json b/config/identity/interaction/routing/oidc/consent.json
new file mode 100644
index 000000000..941a5842b
--- /dev/null
+++ b/config/identity/interaction/routing/oidc/consent.json
@@ -0,0 +1,66 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the interaction that occurs when a logged in user wants to authenticate with a new app.",
+ "@id": "urn:solid-server:default:OidcConsentRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:OidcConsentRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:OidcRoute" },
+ "relativePath": "consent/"
+ },
+ "source": {
+ "@id": "urn:solid-server:default:ConsentHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ {
+ "@type": "MethodFilterHandler",
+ "methods": [ "GET" ],
+ "source": {
+ "@type": "ClientInfoHandler",
+ "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
+ }
+ },
+ {
+ "@type": "MethodFilterHandler",
+ "methods": [ "POST" ],
+ "source": {
+ "@type": "ConsentHandler",
+ "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
+ }
+ }
+ ]
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:OidcConsentRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:OidcControlHandler",
+ "@type": "OidcControlHandler",
+ "controls": [
+ {
+ "OidcControlHandler:_controls_key": "consent",
+ "OidcControlHandler:_controls_value": { "@id": "urn:solid-server:default:OidcConsentRoute" }
+ }
+ ]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:OidcConsentHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/oidc/consent.html.ejs",
+ "route": { "@id": "urn:solid-server:default:OidcConsentRoute" }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/oidc/forget-webid.json b/config/identity/interaction/routing/oidc/forget-webid.json
new file mode 100644
index 000000000..be8e18f05
--- /dev/null
+++ b/config/identity/interaction/routing/oidc/forget-webid.json
@@ -0,0 +1,47 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Allows the picked WebID to be forgotten in an OIDC interaction so the user can pick again.",
+ "@id": "urn:solid-server:default:OidcForgetWebIdRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:OidcForgetWebIDRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:OidcRoute" },
+ "relativePath": "forget-webid/"
+ },
+ "source": {
+ "@id": "urn:solid-server:default:ForgetWebIdHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ {
+ "@type": "MethodFilterHandler",
+ "methods": [ "POST" ],
+ "source": {
+ "@type": "ForgetWebIdHandler",
+ "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
+ }
+ }
+ ]
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:OidcForgetWebIdRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:OidcControlHandler",
+ "@type": "OidcControlHandler",
+ "controls": [
+ {
+ "OidcControlHandler:_controls_key": "forgetWebId",
+ "OidcControlHandler:_controls_value": { "@id": "urn:solid-server:default:OidcForgetWebIDRoute" }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/oidc/main.json b/config/identity/interaction/routing/oidc/main.json
new file mode 100644
index 000000000..72df29378
--- /dev/null
+++ b/config/identity/interaction/routing/oidc/main.json
@@ -0,0 +1,35 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/identity/interaction/routing/oidc/cancel.json",
+ "css:config/identity/interaction/routing/oidc/consent.json",
+ "css:config/identity/interaction/routing/oidc/forget-webid.json",
+ "css:config/identity/interaction/routing/oidc/prompt.json",
+ "css:config/identity/interaction/routing/oidc/pick-webid.json"
+ ],
+ "@graph": [
+ {
+ "comment": "Main OIDC route others can extend.",
+ "@id": "urn:solid-server:default:OidcRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:IndexRoute" },
+ "relativePath": "oidc/"
+ },
+
+ {
+ "@id": "urn:solid-server:default:ControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "oidc",
+ "ControlHandler:_controls_value": {
+ "comment": "Contains all OIDC controls.",
+ "@id": "urn:solid-server:default:OidcControlHandler",
+ "@type": "OidcControlHandler",
+ "controls": []
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/oidc/pick-webid.json b/config/identity/interaction/routing/oidc/pick-webid.json
new file mode 100644
index 000000000..4c7e48e30
--- /dev/null
+++ b/config/identity/interaction/routing/oidc/pick-webid.json
@@ -0,0 +1,44 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles picking a WebID during an OIDC interaction.",
+ "@id": "urn:solid-server:default:OidcPickWebIdRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:OidcPickWebIdRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:OidcRoute" },
+ "relativePath": "pick-webid/"
+ },
+ "source": {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@type": "PickWebIdHandler",
+ "@id": "urn:solid-server:default:PickWebIdHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ { "@id": "urn:solid-server:default:OidcPickWebIdRouter" }
+ ]
+ },
+
+ {
+ "@id": "urn:solid-server:default:OidcControlHandler",
+ "@type": "OidcControlHandler",
+ "controls": [
+ {
+ "OidcControlHandler:_controls_key": "webId",
+ "OidcControlHandler:_controls_value": { "@id": "urn:solid-server:default:OidcPickWebIdRoute" }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/oidc/prompt.json b/config/identity/interaction/routing/oidc/prompt.json
new file mode 100644
index 000000000..4b3ed401c
--- /dev/null
+++ b/config/identity/interaction/routing/oidc/prompt.json
@@ -0,0 +1,51 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the interaction that occurs when a logged in user wants to authenticate with a new app.",
+ "@id": "urn:solid-server:default:OidcPromptRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:OidcPromptRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:OidcRoute" },
+ "relativePath": "prompt/"
+ },
+ "source": {
+ "@id": "urn:solid-server:default:PromptHandler",
+ "@type": "PromptHandler",
+ "promptRoutes": [
+ {
+ "PromptHandler:_promptRoutes_key": "account",
+ "PromptHandler:_promptRoutes_value": { "@id": "urn:solid-server:default:LoginRoute" }
+ },
+ {
+ "PromptHandler:_promptRoutes_key": "login",
+ "PromptHandler:_promptRoutes_value": { "@id": "urn:solid-server:default:OidcConsentRoute" }
+ },
+ {
+ "PromptHandler:_promptRoutes_key": "consent",
+ "PromptHandler:_promptRoutes_value": { "@id": "urn:solid-server:default:OidcConsentRoute" }
+ }
+ ]
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:OidcPromptRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:OidcControlHandler",
+ "@type": "OidcControlHandler",
+ "controls": [
+ {
+ "OidcControlHandler:_controls_key": "prompt",
+ "OidcControlHandler:_controls_value": { "@id": "urn:solid-server:default:OidcPromptRoute" }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/password/create.json b/config/identity/interaction/routing/password/create.json
new file mode 100644
index 000000000..ed699c45b
--- /dev/null
+++ b/config/identity/interaction/routing/password/create.json
@@ -0,0 +1,62 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Allows adding the email/password login method to an account",
+ "@id": "urn:solid-server:default:AccountPasswordRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountPasswordRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:AccountLoginRoute" },
+ "relativePath": "password/"
+ },
+ "source": {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:CreatePasswordHandler",
+ "@type": "CreatePasswordHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "passwordStore": { "@id": "urn:solid-server:default:PasswordStore" },
+ "passwordRoute": { "@id": "urn:solid-server:default:AccountPasswordIdRoute" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:AccountPasswordRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:PasswordControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "create",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountPasswordRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:CreatePasswordHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/password/create.html.ejs",
+ "route": { "@id": "urn:solid-server:default:AccountPasswordRoute" }
+ }]
+ },
+ {
+ "ControlHandler:_controls_value": {
+ "@id": "urn:solid-server:default:PasswordHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "create",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountPasswordRoute" }
+ }]
+ }
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/password/forgot.json b/config/identity/interaction/routing/password/forgot.json
new file mode 100644
index 000000000..d42665eaa
--- /dev/null
+++ b/config/identity/interaction/routing/password/forgot.json
@@ -0,0 +1,68 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the forgot password interaction.",
+ "@id": "urn:solid-server:default:ForgotPasswordRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:ForgotPasswordRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:LoginPasswordRoute" },
+ "relativePath": "forgot/"
+ },
+ "source": {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:ForgotPasswordHandler",
+ "@type": "ForgotPasswordHandler",
+ "passwordStore": { "@id": "urn:solid-server:default:PasswordStore" },
+ "forgotPasswordStore": { "@id": "urn:solid-server:default:ForgotPasswordStore" },
+ "templateEngine": {
+ "@type": "StaticTemplateEngine",
+ "templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
+ "template": "@css:templates/identity/password/reset-email.html.ejs"
+ },
+ "emailSender": { "@id": "urn:solid-server:default:EmailSender" },
+ "resetRoute": { "@id": "urn:solid-server:default:ResetPasswordRoute" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:ForgotPasswordRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:PasswordControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "forgot",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:ForgotPasswordRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:ForgotPasswordHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/password/forgot.html.ejs",
+ "route": { "@id": "urn:solid-server:default:ForgotPasswordRoute" }
+ }]
+ },
+ {
+ "ControlHandler:_controls_value": {
+ "@id": "urn:solid-server:default:PasswordHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "forgot",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:ForgotPasswordRoute" }
+ }]
+ }
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/password/login.json b/config/identity/interaction/routing/password/login.json
new file mode 100644
index 000000000..bc17f6f6d
--- /dev/null
+++ b/config/identity/interaction/routing/password/login.json
@@ -0,0 +1,63 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the password login interaction.",
+ "@id": "urn:solid-server:default:LoginPasswordRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:LoginPasswordRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:LoginRoute" },
+ "relativePath": "password/"
+ },
+ "source": {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:PasswordLoginHandler",
+ "@type": "PasswordLoginHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "passwordStore": { "@id": "urn:solid-server:default:PasswordStore" },
+ "cookieStore": { "@id": "urn:solid-server:default:CookieStore" },
+ "accountRoute": { "@id": "urn:solid-server:default:AccountIdRoute" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:LoginPasswordRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:PasswordControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "login",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:LoginPasswordRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:PasswordLoginHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/password/login.html.ejs",
+ "route": { "@id": "urn:solid-server:default:LoginPasswordRoute" }
+ }]
+ },
+ {
+ "ControlHandler:_controls_value": {
+ "@id": "urn:solid-server:default:PasswordHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "login",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:LoginPasswordRoute" }
+ }]
+ }
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/password/main.json b/config/identity/interaction/routing/password/main.json
new file mode 100644
index 000000000..0e9a509e3
--- /dev/null
+++ b/config/identity/interaction/routing/password/main.json
@@ -0,0 +1,11 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "import": [
+ "css:config/identity/interaction/routing/password/create.json",
+ "css:config/identity/interaction/routing/password/forgot.json",
+ "css:config/identity/interaction/routing/password/login.json",
+ "css:config/identity/interaction/routing/password/reset.json",
+ "css:config/identity/interaction/routing/password/resource.json"
+ ],
+ "@graph": []
+}
diff --git a/config/identity/interaction/routing/password/reset.json b/config/identity/interaction/routing/password/reset.json
new file mode 100644
index 000000000..f83980015
--- /dev/null
+++ b/config/identity/interaction/routing/password/reset.json
@@ -0,0 +1,51 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the reset password interaction.",
+ "@id": "urn:solid-server:default:ResetPasswordRouter",
+ "@type": "InteractionRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:ResetPasswordRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:LoginPasswordRoute" },
+ "relativePath": "reset/"
+ },
+ "source": {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:ResetPasswordHandler",
+ "@type": "ResetPasswordHandler",
+ "passwordStore": { "@id": "urn:solid-server:default:PasswordStore" },
+ "forgotPasswordStore": { "@id": "urn:solid-server:default:ForgotPasswordStore" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:ResetPasswordRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:PasswordControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "reset",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:ResetPasswordRoute" }
+ }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:ResetPasswordHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/password/reset.html.ejs",
+ "route": { "@id": "urn:solid-server:default:ResetPasswordRoute" }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/password/resource.json b/config/identity/interaction/routing/password/resource.json
new file mode 100644
index 000000000..44f41552d
--- /dev/null
+++ b/config/identity/interaction/routing/password/resource.json
@@ -0,0 +1,55 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the password link details such as update and delete.",
+ "@id": "urn:solid-server:default:AccountPasswordIdRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountPasswordIdRoute",
+ "@type": "BasePasswordIdRoute",
+ "base": { "@id": "urn:solid-server:default:AccountPasswordRoute" }
+ },
+ "source": {
+ "@id": "urn:solid-server:default:PasswordResourceHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@type": "UpdatePasswordHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "passwordStore": { "@id": "urn:solid-server:default:PasswordStore" }
+ }
+ },
+ {
+ "@type": "MethodFilterHandler",
+ "methods": [ "DELETE" ],
+ "source": {
+ "@type": "DeletePasswordHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "passwordStore": { "@id": "urn:solid-server:default:PasswordStore" }
+ }
+ }
+ ]
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [{ "@id": "urn:solid-server:default:AccountPasswordIdRouter" }]
+ },
+
+ {
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:UpdatePasswordHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/password/update.html.ejs",
+ "route": { "@id": "urn:solid-server:default:AccountPasswordIdRoute" }
+ }]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/pod/create.json b/config/identity/interaction/routing/pod/create.json
new file mode 100644
index 000000000..dbf123189
--- /dev/null
+++ b/config/identity/interaction/routing/pod/create.json
@@ -0,0 +1,30 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles pod creation.",
+ "@id": "urn:solid-server:default:AccountPodRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountPodRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:AccountIdRoute" },
+ "relativePath": "pod/"
+ },
+ "source": {
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:CreatePodHandler",
+ "@type": "CreatePodHandler",
+ "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
+ "identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" },
+ "relativeWebIdPath": "/profile/card#me",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "webIdStore": { "@id": "urn:solid-server:default:WebIdStore" },
+ "podStore": { "@id": "urn:solid-server:default:PodStore" },
+ "allowRoot": false
+ }
+ }
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/pod/resource.json b/config/identity/interaction/routing/pod/resource.json
new file mode 100644
index 000000000..14efe3e59
--- /dev/null
+++ b/config/identity/interaction/routing/pod/resource.json
@@ -0,0 +1,11 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "This route is only used when creating new pod resources as no further interactions are supported.",
+ "@id": "urn:solid-server:default:AccountPodIdRoute",
+ "@type": "BasePodIdRoute",
+ "base": { "@id": "urn:solid-server:default:AccountPodRoute" }
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/views/html.json b/config/identity/interaction/routing/views/html.json
new file mode 100644
index 000000000..ea559f527
--- /dev/null
+++ b/config/identity/interaction/routing/views/html.json
@@ -0,0 +1,45 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Returns HTML pages if the URL matches and HTML is preferred.",
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "index": { "@id": "urn:solid-server:default:IndexRoute" },
+ "templateEngine": {
+ "comment": "Renders the specific page and embeds it into the main HTML body.",
+ "@type": "ChainedTemplateEngine",
+ "renderedName": "htmlBody",
+ "engines": [
+ {
+ "comment": "Will be called with specific templates to generate HTML snippets.",
+ "@id": "urn:solid-server:default:TemplateEngine"
+ },
+ {
+ "comment": "Will embed the result of the first engine into the main HTML template.",
+ "@type": "StaticTemplateEngine",
+ "templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
+ "template": "@css:templates/main.html.ejs"
+ }
+ ]
+ },
+ "templates": []
+ },
+
+ {
+ "@id": "urn:solid-server:default:ControlHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "html",
+ "ControlHandler:_controls_value": {
+ "comment": "Controls linking to HTML pages. These can be the same URLs as the JSON APIs, but can also be different.",
+ "@id": "urn:solid-server:default:HtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": []
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/webid/link.json b/config/identity/interaction/routing/webid/link.json
new file mode 100644
index 000000000..58d57c3bc
--- /dev/null
+++ b/config/identity/interaction/routing/webid/link.json
@@ -0,0 +1,29 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles linking a WebID to an account",
+ "@id": "urn:solid-server:default:AccountWebIdRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountWebIdRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:AccountIdRoute" },
+ "relativePath": "webid/"
+ },
+ "source": {
+ "@id": "urn:solid-server:default:WebIdHandler",
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:LinkWebIdHandler",
+ "@type": "LinkWebIdHandler",
+ "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
+ "ownershipValidator": { "@id": "urn:solid-server:default:OwnershipValidator" },
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "webIdStore": { "@id": "urn:solid-server:default:WebIdStore" },
+ "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }
+ }
+ }
+ }
+ ]
+}
diff --git a/config/identity/interaction/routing/webid/resource.json b/config/identity/interaction/routing/webid/resource.json
new file mode 100644
index 000000000..04a11fe73
--- /dev/null
+++ b/config/identity/interaction/routing/webid/resource.json
@@ -0,0 +1,33 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Handles the WebID link details such as deletion.",
+ "@id": "urn:solid-server:default:AccountWebIdLinkRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountWebIdLinkRoute",
+ "@type": "BaseWebIdLinkRoute",
+ "base": { "@id": "urn:solid-server:default:AccountWebIdRoute" }
+ },
+ "source": {
+ "@id": "urn:solid-server:default:WebIdLinkHandler",
+ "@type": "MethodFilterHandler",
+ "methods": [ "DELETE" ],
+ "source": {
+ "@type": "UnlinkWebIdHandler",
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "webIdStore": { "@id": "urn:solid-server:default:WebIdStore" }
+ }
+ }
+ },
+
+ {
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ { "@id": "urn:solid-server:default:AccountWebIdLinkRouter" }
+ ]
+ }
+ ]
+}
diff --git a/config/identity/ownership/token.json b/config/identity/ownership/token.json
index 2720d005c..b22eea23c 100644
--- a/config/identity/ownership/token.json
+++ b/config/identity/ownership/token.json
@@ -3,7 +3,7 @@
"@graph": [
{
"comment": "Determines WebID ownership by requesting a specific value to be added to the WebID document",
- "@id": "urn:solid-server:auth:password:OwnershipValidator",
+ "@id": "urn:solid-server:default:OwnershipValidator",
"@type": "TokenOwnershipValidator",
"storage": { "@id": "urn:solid-server:default:ExpiringTokenStorage" }
},
diff --git a/config/identity/ownership/unsafe-no-check.json b/config/identity/ownership/unsafe-no-check.json
index 09ac4f191..a9fcd1b2a 100644
--- a/config/identity/ownership/unsafe-no-check.json
+++ b/config/identity/ownership/unsafe-no-check.json
@@ -6,7 +6,7 @@
"DO NOT USE IN PRODUCTION. ONLY FOR DEVELOPMENT, TESTING, OR DEBUGGING.",
"Do no verification to determine WebID ownership."
],
- "@id": "urn:solid-server:auth:password:OwnershipValidator",
+ "@id": "urn:solid-server:default:OwnershipValidator",
"@type": "NoCheckOwnershipValidator"
}
]
diff --git a/config/identity/registration/disabled.json b/config/identity/registration/disabled.json
deleted file mode 100644
index ac7d0d1f6..000000000
--- a/config/identity/registration/disabled.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Disable registration by not attaching a registration handler."
- }
- ]
-}
diff --git a/config/identity/registration/enabled.json b/config/identity/registration/enabled.json
deleted file mode 100644
index 8569827b1..000000000
--- a/config/identity/registration/enabled.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "import": [
- "css:config/identity/registration/route/registration.json"
- ],
- "@graph": [
- {
- "@id": "urn:solid-server:auth:password:InteractionRouteHandler",
- "@type": "WaterfallHandler",
- "handlers": [
- { "@id": "urn:solid-server:auth:password:RegistrationRouteHandler" }
- ]
- },
- {
- "@id": "urn:solid-server:auth:password:ControlHandler",
- "@type": "ControlHandler",
- "controls": [
- {
- "ControlHandler:_controls_key": "register",
- "ControlHandler:_controls_value": { "@id": "urn:solid-server:auth:password:RegistrationRoute" }
- }
- ]
- },
- {
- "@id": "urn:solid-server:auth:password:HtmlViewHandler",
- "@type": "HtmlViewHandler",
- "templates": [
- {
- "HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/register.html.ejs",
- "HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:RegistrationRoute" }
- }
- ]
- }
- ]
-}
diff --git a/config/identity/registration/route/registration.json b/config/identity/registration/route/registration.json
deleted file mode 100644
index 40cc9400d..000000000
--- a/config/identity/registration/route/registration.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
- "@graph": [
- {
- "comment": "Handles the register interaction",
- "@id": "urn:solid-server:auth:password:RegistrationRouteHandler",
- "@type": "InteractionRouteHandler",
- "route": {
- "@id": "urn:solid-server:auth:password:RegistrationRoute",
- "@type": "RelativePathInteractionRoute",
- "base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
- "relativePath": "/register/"
- },
- "source": {
- "@id": "urn:solid-server:auth:password:RegistrationHandler",
- "@type": "RegistrationHandler",
- "registrationManager": {
- "@type": "RegistrationManager",
- "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
- "args_webIdSuffix": "/profile/card#me",
- "args_identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" },
- "args_ownershipValidator": { "@id": "urn:solid-server:auth:password:OwnershipValidator" },
- "args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
- "args_podManager": { "@id": "urn:solid-server:default:PodManager" }
- }
- }
- }
- ]
-}
diff --git a/config/ldp/authorization/readers/ownership.json b/config/ldp/authorization/readers/ownership.json
index cc744b0d7..445ec93be 100644
--- a/config/ldp/authorization/readers/ownership.json
+++ b/config/ldp/authorization/readers/ownership.json
@@ -5,7 +5,8 @@
"comment": "Allows pod owners to always edit permissions on the data.",
"@id": "urn:solid-server:default:OwnerPermissionReader",
"@type": "OwnerPermissionReader",
- "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
+ "webIdStore": { "@id": "urn:solid-server:default:WebIdStore" },
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
"identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }
}
]
diff --git a/config/ldp/metadata-parser/default.json b/config/ldp/metadata-parser/default.json
index c08773857..f26ed70a1 100644
--- a/config/ldp/metadata-parser/default.json
+++ b/config/ldp/metadata-parser/default.json
@@ -1,7 +1,9 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
+ "css:config/ldp/metadata-parser/parsers/authorization.json",
"css:config/ldp/metadata-parser/parsers/content-type.json",
+ "css:config/ldp/metadata-parser/parsers/cookie.json",
"css:config/ldp/metadata-parser/parsers/link.json",
"css:config/ldp/metadata-parser/parsers/plain-json-ld-filter.json",
"css:config/ldp/metadata-parser/parsers/slug.json"
@@ -12,6 +14,8 @@
"@id": "urn:solid-server:default:MetadataParser",
"@type": "ParallelHandler",
"handlers": [
+ { "@id": "urn:solid-server:default:AuthorizationParser" },
+ { "@id": "urn:solid-server:default:CookieParser" },
{ "@id": "urn:solid-server:default:ContentTypeParser" },
{ "@id": "urn:solid-server:default:LinkRelParser" },
{ "@id": "urn:solid-server:default:PlainJsonLdFilter" },
diff --git a/config/ldp/metadata-parser/parsers/authorization.json b/config/ldp/metadata-parser/parsers/authorization.json
new file mode 100644
index 000000000..c2a98c071
--- /dev/null
+++ b/config/ldp/metadata-parser/parsers/authorization.json
@@ -0,0 +1,16 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Converts the authorization header into RDF metadata",
+ "@id": "urn:solid-server:default:AuthorizationParser",
+ "@type": "AuthorizationParser",
+ "authMap": [
+ {
+ "AuthorizationParser:_authMap_key": "CSS-Account-Cookie",
+ "AuthorizationParser:_authMap_value": "urn:npm:solid:community-server:http:accountCookie"
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/ldp/metadata-parser/parsers/cookie.json b/config/ldp/metadata-parser/parsers/cookie.json
new file mode 100644
index 000000000..2824257ef
--- /dev/null
+++ b/config/ldp/metadata-parser/parsers/cookie.json
@@ -0,0 +1,21 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "The name of the cookie to identify being logged in with a CSS account.",
+ "@id": "urn:solid-server:default:value:accountCookieName",
+ "valueRaw": "css-account"
+ },
+ {
+ "comment": "Converts cookies into RDF metadata.",
+ "@id": "urn:solid-server:default:CookieParser",
+ "@type": "CookieParser",
+ "cookieMap": [
+ {
+ "CookieParser:_cookieMap_key": { "@id": "urn:solid-server:default:value:accountCookieName" },
+ "CookieParser:_cookieMap_value": "urn:npm:solid:community-server:http:accountCookie",
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/ldp/metadata-writer/default.json b/config/ldp/metadata-writer/default.json
index f90bf6344..23ca135e6 100644
--- a/config/ldp/metadata-writer/default.json
+++ b/config/ldp/metadata-writer/default.json
@@ -3,6 +3,7 @@
"import": [
"css:config/ldp/metadata-writer/writers/allow-accept.json",
"css:config/ldp/metadata-writer/writers/content-type.json",
+ "css:config/ldp/metadata-writer/writers/cookie.json",
"css:config/ldp/metadata-writer/writers/link-rel.json",
"css:config/ldp/metadata-writer/writers/link-rel-metadata.json",
"css:config/ldp/metadata-writer/writers/mapped.json",
@@ -21,6 +22,7 @@
{ "@id": "urn:solid-server:default:MetadataWriter_ContentType" },
{ "@id": "urn:solid-server:default:MetadataWriter_LinkRel" },
{ "@id": "urn:solid-server:default:MetadataWriter_LinkRelMetadata" },
+ { "@id": "urn:solid-server:default:MetadataWriter_Cookie" },
{ "@id": "urn:solid-server:default:MetadataWriter_Mapped" },
{ "@id": "urn:solid-server:default:MetadataWriter_Modified" },
{ "@id": "urn:solid-server:default:MetadataWriter_Range" },
diff --git a/config/ldp/metadata-writer/writers/cookie.json b/config/ldp/metadata-writer/writers/cookie.json
new file mode 100644
index 000000000..e6dc06124
--- /dev/null
+++ b/config/ldp/metadata-writer/writers/cookie.json
@@ -0,0 +1,17 @@
+{
+ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
+ "@graph": [
+ {
+ "comment": "Converts all triples with the given predicates to cookies.",
+ "@id": "urn:solid-server:default:MetadataWriter_Cookie",
+ "@type": "CookieMetadataWriter",
+ "cookieMap": [
+ {
+ "CookieMetadataWriter:_cookieMap_key": "urn:npm:solid:community-server:http:accountCookie",
+ "CookieMetadataWriter:_name": { "@id": "urn:solid-server:default:value:accountCookieName" },
+ "CookieMetadataWriter:_expirationUri": "urn:npm:solid:community-server:http:accountCookieExpiration"
+ }
+ ]
+ }
+ ]
+}
diff --git a/config/memory-subdomains.json b/config/memory-subdomains.json
index eac0a3fc6..ac65e72ba 100644
--- a/config/memory-subdomains.json
+++ b/config/memory-subdomains.json
@@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
- "css:config/app/init/initialize-root.json",
+ "css:config/app/init/static-root.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/path-routing.json b/config/path-routing.json
index 2993aad14..8e4722d96 100644
--- a/config/path-routing.json
+++ b/config/path-routing.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/quota-file.json b/config/quota-file.json
index a5efd9e2a..0fb9d4067 100644
--- a/config/quota-file.json
+++ b/config/quota-file.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/restrict-idp.json b/config/restrict-idp.json
index 4d62e350b..ddb5768cb 100644
--- a/config/restrict-idp.json
+++ b/config/restrict-idp.json
@@ -12,9 +12,9 @@
"css:config/identity/access/restricted.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/sparql-endpoint-root.json b/config/sparql-endpoint-root.json
index 318ca9d2f..9e494a0cc 100644
--- a/config/sparql-endpoint-root.json
+++ b/config/sparql-endpoint-root.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/sparql-endpoint.json b/config/sparql-endpoint.json
index fe04a91ae..77162ebea 100644
--- a/config/sparql-endpoint.json
+++ b/config/sparql-endpoint.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/sparql-file-storage.json b/config/sparql-file-storage.json
index 96ac60771..353c349bf 100644
--- a/config/sparql-file-storage.json
+++ b/config/sparql-file-storage.json
@@ -12,9 +12,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
+ "css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
- "css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
diff --git a/config/util/variables/default.json b/config/util/variables/default.json
index 7ed7b5f0c..fc4e00666 100644
--- a/config/util/variables/default.json
+++ b/config/util/variables/default.json
@@ -44,7 +44,7 @@
},
{
"comment": "Path to the JSON file used to seed pods.",
- "@id": "urn:solid-server:default:variable:seededPodConfigJson",
+ "@id": "urn:solid-server:default:variable:seedConfig",
"@type": "Variable"
},
{
diff --git a/documentation/markdown/README.md b/documentation/markdown/README.md
index 1c8500621..7c35a6cc0 100644
--- a/documentation/markdown/README.md
+++ b/documentation/markdown/README.md
@@ -32,7 +32,7 @@ the [changelog](https://github.com/CommunitySolidServer/CommunitySolidServer/blo
* [Quickly starting the server](usage/starting-server.md)
* [Basic example HTTP requests](usage/example-requests.md)
* [Editing the metadata of a resource](usage/metadata.md)
-* [How to use the Identity Provider](usage/identity-provider.md)
+* [How to use the Identity Provider and accounts](usage/identity-provider.md)
* [How to automate authentication](usage/client-credentials.md)
* [How to automatically seed pods on startup](usage/seeding-pods.md)
* [Receiving notifications when resources change](usage/notifications.md)
diff --git a/documentation/markdown/architecture/features/accounts/controls.md b/documentation/markdown/architecture/features/accounts/controls.md
new file mode 100644
index 000000000..62eaafa6c
--- /dev/null
+++ b/documentation/markdown/architecture/features/accounts/controls.md
@@ -0,0 +1,27 @@
+# JSON API controls
+
+A large part of every response of the JSON API is the `controls` block.
+These are generated by using nested `ControlHandler` objects.
+These take as input a key/value with the values being either routes or other interaction handlers.
+These will then be executed to determine the values of the output JSON object, with the same keys.
+By using other `ControlHandler`s in the input map, we can create nested objects.
+
+The default structure of these handlers is as follows:
+
+```mermaid
+flowchart LR
+ RootControlHandler("RootControlHandler
ControlHandler")
+ RootControlHandler --controls--> ControlHandler("ControlHandler
ControlHandler")
+ ControlHandler --main--> MainControlHandler("MainControlHandler
ControlHandler")
+ ControlHandler --account--> AccountControlHandler("AccountControlHandler
ControlHandler")
+ ControlHandler --password--> PasswordControlHandler("PasswordControlHandler
ControlHandler")
+ ControlHandler --"oidc"--> OidcControlHandler("OidcControlHandler
OidcControlHandler")
+ ControlHandler --html--> HtmlControlHandler("HtmlControlHandler
ControlHandler")
+
+ HtmlControlHandler --main--> MainHtmlControlHandler("MainHtmlControlHandler
ControlHandler")
+ HtmlControlHandler --account--> AccountHtmlControlHandler("AccountHtmlControlHandler
ControlHandler")
+ HtmlControlHandler --password--> PasswordHtmlControlHandler("PasswordHtmlControlHandler
ControlHandler")
+```
+
+Each of these control handlers then has a map of routes which link to the actual API endpoints.
+How to add these can be seen [here](routes.md#adding-the-necessary-controls).
diff --git a/documentation/markdown/architecture/features/accounts/overview.md b/documentation/markdown/architecture/features/accounts/overview.md
new file mode 100644
index 000000000..f0e3f99b1
--- /dev/null
+++ b/documentation/markdown/architecture/features/accounts/overview.md
@@ -0,0 +1,58 @@
+# Account management
+
+The main entry point is the `IdentityProviderHandler`,
+which routes all requests targeting a resource starting with `/.account/` into this handler,
+after which it goes through similar parsing handlers as described [here](../protocol/overview.md),
+the flow of which is shown below:
+
+```mermaid
+flowchart LR
+ Handler("IdentityProviderHandler
RouterHandler")
+ ParsingHandler("IdentityProviderParsingHandler
AuthorizingHttpHandler")
+ AuthorizingHandler("IdentityProviderAuthorizingHandler
AuthorizingHttpHandler")
+
+ Handler --> ParsingHandler
+ ParsingHandler --> AuthorizingHandler
+ AuthorizingHandler --> HttpHandler("IdentityProviderHttpHandler
IdentityProviderHttpHandler")
+```
+
+The `IdentityProviderHttpHandler` is where the actual differentiation of this component starts.
+It handles identifying the account based on the supplied cookie and determining the active OIDC interaction,
+after which it calls an `InteractionHandler` with this additional input.
+The `InteractionHandler` is many handlers chained together as follows:
+
+```mermaid
+flowchart TD
+ HttpHandler("IdentityProviderHttpHandler
IdentityProviderHttpHandler")
+ HttpHandler --> InteractionHandler("InteractionHandler
WaterfallHandler")
+ InteractionHandler --> InteractionHandlerArgs
+
+ subgraph InteractionHandlerArgs[" "]
+ HtmlViewHandler("HtmlViewHandler
HtmlViewHandler")
+ LockingInteractionHandler("LockingInteractionHandler
LockingInteractionHandler")
+ end
+
+ LockingInteractionHandler --> JsonConversionHandler("JsonConversionHandler
JsonConversionHandler")
+ JsonConversionHandler --> VersionHandler("VersionHandler
VersionHandler")
+ VersionHandler --> CookieInteractionHandler("CookieInteractionHandler
CookieInteractionHandler")
+ CookieInteractionHandler --> RootControlHandler("RootControlHandler
ControlHandler")
+ RootControlHandler --> LocationInteractionHandler("LocationInteractionHandler
LocationInteractionHandler")
+ LocationInteractionHandler --> InteractionRouteHandler("InteractionRouteHandler
WaterfallHandler")
+```
+
+The `HtmlViewHandler` catches all request that request an HTML output.
+This class keeps a list of HTML pages and their corresponding URL and returns them when needed.
+
+If the request is for the JSON API,
+the request goes through a chain of handlers, each responsible for a specific step in the API process.
+We'll list and summarize these here:
+
+* `LockingInteractionHandler`: In case the request is authenticated,
+ this requests a lock on that account to prevent simultaneous operations on the same account.
+* `JsonConversionHandler`: Converts the streaming input into a JSON object.
+* `VersionHandler`: Adds a version number to all output.
+* `CookieInteractionHandler`: Refreshes the cookie if necessary and adds relevant cookie metadata to the output.
+* `RootControlHandler`: Responsible for adding all the [controls](controls.md) to the output.
+ Will take as input multiple other control handlers which create the nested values in the `controls` field.
+* `LocationInteractionHandler`: Catches redirect errors and converts them to JSON objects with a `location` field.
+* `InteractionRouteHandler`: A `WaterfallHandler` containing an entry for every supported API [route](routes.md).
diff --git a/documentation/markdown/architecture/features/accounts/routes.md b/documentation/markdown/architecture/features/accounts/routes.md
new file mode 100644
index 000000000..15e6f557a
--- /dev/null
+++ b/documentation/markdown/architecture/features/accounts/routes.md
@@ -0,0 +1,126 @@
+# Account API routes
+
+All entries contained in the `urn:solid-server:default:InteractionRouteHandler` have a similar structure:
+an `InteractionRouteHandler`, or `AuthorizedRouteHandler` for authenticated requests,
+which checks if the request targets a specific URL
+and redirects the request to its source if there is a match.
+Its source is quite often a `ViewInteractionHandler`,
+which returns a specific view on GET requests and performs an operation on POST requests,
+but other handlers can also occur.
+
+Below we will give an example of one API route and all the components that are necessary to add it to the server.
+
+## Route handler
+
+```json
+{
+ "@id": "urn:solid-server:default:AccountWebIdRouter",
+ "@type": "AuthorizedRouteHandler",
+ "route": {
+ "@id": "urn:solid-server:default:AccountWebIdRoute",
+ "@type": "RelativePathInteractionRoute",
+ "base": { "@id": "urn:solid-server:default:AccountIdRoute" },
+ "relativePath": "webid/"
+ },
+ "source": { "@id": "urn:solid-server:default:WebIdHandler" }
+}
+```
+
+The main entry point is the route handler,
+which determines the URL necessary to reach this API.
+In this case we create a new route, relative to the `urn:solid-server:default:AccountIdRoute`.
+That route specifically matches URLs of the format `http://localhost:3000/.account/account//`.
+Here we create a route relative to that one by appending `webid`,
+so the resulting route would match `http://localhost:3000/.account/account//webid/`.
+Since an `AuthorizedRouteHandler` is used here,
+the request also needs to be authenticated using an account cookie.
+If there is match, the request will be sent to the `urn:solid-server:default:WebIdHandler`.
+
+## Interaction handler
+
+```json
+{
+ "@id": "urn:solid-server:default:WebIdHandler",
+ "@type": "ViewInteractionHandler",
+ "source": {
+ "@id": "urn:solid-server:default:LinkWebIdHandler",
+ "@type": "LinkWebIdHandler",
+ "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
+ "ownershipValidator": { "@id": "urn:solid-server:default:OwnershipValidator" },
+ "accountStore": { "@id": "urn:solid-server:default:AccountStore" },
+ "webIdStore": { "@id": "urn:solid-server:default:WebIdStore" },
+ "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }
+ }
+}
+```
+
+The interaction handler is the class that performs the necessary operation based on the request.
+Often these are wrapped in a `ViewInteractionHandler`,
+which allows classes to have different support for GET and POST requests.
+
+## Exposing the API
+
+```json
+{
+ "@id": "urn:solid-server:default:InteractionRouteHandler",
+ "@type": "WaterfallHandler",
+ "handlers": [
+ { "@id": "urn:solid-server:default:AccountWebIdRouter" }
+ ]
+}
+```
+
+To make sure the API can be accessed,
+it needs to be added to the list of `urn:solid-server:default:InteractionRouteHandler`.
+This is the main handler that contains entries for all the APIs.
+This block of Components.js adds the route handler defined above to that list.
+
+## Adding the necessary controls
+
+```json
+{
+ "@id": "urn:solid-server:default:AccountControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "webId",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountWebIdRoute" }
+ }]
+}
+```
+
+To make sure people can find the API,
+it is necessary to link it through the associated `controls` object.
+This API is related to account management,
+so we add its route in the account controls with the key `webId`.
+More information about controls can be found [here](controls.md).
+
+## Adding HTML
+
+```json
+{
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:LinkWebIdHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/account/link-webid.html.ejs",
+ "route": { "@id": "urn:solid-server:default:AccountWebIdRoute" }
+ }]
+}
+```
+
+Some API routes also have an associated HTML page,
+in which case the page needs to be added to the `urn:solid-server:default:HtmlViewHandler`,
+which is what we do here.
+Usually you will also want to add HTML controls so the page can be found.
+
+```json
+{
+ "@id": "urn:solid-server:default:AccountHtmlControlHandler",
+ "@type": "ControlHandler",
+ "controls": [{
+ "ControlHandler:_controls_key": "linkWebId",
+ "ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountWebIdRoute" }
+ }]
+}
+```
diff --git a/documentation/markdown/architecture/features/http-handler.md b/documentation/markdown/architecture/features/http-handler.md
index f9ad06582..7c2c5d5ed 100644
--- a/documentation/markdown/architecture/features/http-handler.md
+++ b/documentation/markdown/architecture/features/http-handler.md
@@ -88,8 +88,9 @@ More on this can be found in the [identity provider](../../../usage/identity-pro
The `urn:solid-server:default:IdentityProviderHttpHandler` handles everything
related to our custom identity provider API, such as registering, logging in, returning the relevant HTML pages, etc.
-All these requests are identified by being on the `/idp/` subpath.
+All these requests are identified by being on the `/.account/` subpath.
More information on the API can be found in the [identity provider](../../../usage/identity-provider) documentation
+The architectural overview can be found [here](accounts/overview.md).
## LdpHandler
diff --git a/documentation/markdown/architecture/features/protocol/overview.md b/documentation/markdown/architecture/features/protocol/overview.md
index 69a013f80..d1b0d5443 100644
--- a/documentation/markdown/architecture/features/protocol/overview.md
+++ b/documentation/markdown/architecture/features/protocol/overview.md
@@ -10,7 +10,7 @@ Below is a simplified view of how these handlers are linked.
```mermaid
flowchart LR
- LdpHandler("LdpHandler
ParsingHttphandler")
+ LdpHandler("LdpHandler
ParsingHttpHandler")
LdpHandler --> AuthorizingHttpHandler("
AuthorizingHttpHandler")
AuthorizingHttpHandler --> OperationHandler("OperationHandler
OperationHandler")
OperationHandler --> ResourceStore("ResourceStore
ResourceStore")
diff --git a/documentation/markdown/usage/account/json-api.md b/documentation/markdown/usage/account/json-api.md
new file mode 100644
index 000000000..94532d303
--- /dev/null
+++ b/documentation/markdown/usage/account/json-api.md
@@ -0,0 +1,281 @@
+# Account management JSON API
+
+Everything related to account management is done through a JSON API,
+of which we will describe all paths below.
+There are also HTML pages available to handle account management
+that use these APIs internally.
+Links to these can be found in the HTML controls
+All APIs expect JSON as input, and will return JSON objects as output.
+
+## Finding API URLs
+
+All URLs below are relative to the index account API URL, which by default is `http://localhost:3000/.account/`.
+Every response of an API request will contain a `controls` object,
+containing all the URLs of the other API endpoints.
+It is generally advised to make use of these controls instead of hardcoding the URLs.
+Only the initial index URL needs to be known then to find the controls.
+Certain controls will be missing if those features are disabled in the configuration.
+
+## API requests
+
+Many APIs require a POST request to perform an action.
+When doing a GET request on these APIs they will return an object describing what input is expected for the POST.
+
+## Authorization
+
+After logging in, the API will return a `set-cookie` header.
+This cookie is necessary to have access to many of the APIs.
+When including this cookie, the controls object will also be extended with new URLs that are now accessible.
+When logging in, the response body JSON body will also contain a `cookie` field containing the cookie value.
+Instead of using cookies,
+this value can also be used in an `Authorization` header with auth scheme `CSS-Account-Cookie`
+to achieve the same result.
+
+The expiration time of this cookie will be refreshed
+every time there is a successful request to the server with that cookie.
+
+## Redirecting
+
+As redirects through status codes 3xx can make working with JSON APIs more difficult,
+the API will never make use of this.
+Instead, if a redirect is required after an action,
+the response JSON object will return a `location` field.
+This is the next URL that should be fetched.
+This is mostly relevant in OIDC interactions as these cause the interaction to progress.
+
+## Controls
+
+Below is an overview of all the keys in a controls object returned by the server,
+with all features enabled.
+An example of what such an object looks like can be found at the [bottom](#example) of the page.
+
+### controls.main
+
+General controls that require no authentication.
+
+#### controls.main.index
+
+General entrypoint to the API.
+Returns an empty object, including the controls, on all GET requests.
+
+#### controls.main.logins
+
+Returns an overview of all login systems available on the server in `logins` object.
+Keys are a string description of the login system and values are links to their login pages.
+This can be used to let users choose how they want to log in.
+By default, the object only contains the email/password login system.
+
+### controls.account
+
+All controls related to account management.
+All of these require authorization, except for the create action.
+
+#### controls.account.create
+
+Creates a new account on empty POST requests.
+The response contains the necessary cookie values to log and a `resource` field containing the URL of the account.
+This account can not be used until a login method has been added to it.
+All other interactions will fail until this is the case.
+See the [controls.password.create](#controlspasswordcreate) section below for more information on how to do this.
+This account will expire after some time if no login method is added.
+
+#### controls.account.logout
+
+Logs the account out on an empty POST request.
+Invalidates the cookie that was used.
+
+#### controls.account.webId
+
+POST requests link a WebID to the account,
+allowing the account to identify as that WebID during an OIDC authentication interaction.
+Expected input is an object containing a `webId` field.
+
+If the chosen WebID is contained within a Solid pod associated with this account,
+the request will succeed immediately.
+If not, an error will be thrown,
+asking the user to add a specific triple to the WebID to confirm that they are the owner.
+After this triple is added, a second request will be successful.
+
+#### controls.account.pod
+
+Creates a Solid pod for the account on POST requests.
+The only required field is `name`, which will determine the name of the pod.
+
+Additionally, a `settings` object can be sent along,
+the values of which will be sent to the templates used when generating the pod.
+If this `settings` object contains a `webId` field,
+that WebID will be the WebID that has initial access to the pod.
+
+If no WebID value is provided,
+a WebID will be generated in the pod and immediately linked to the account
+as described in [controls.account.webID](#controlsaccountwebid).
+This WebID will then be the WebID that has initial access.
+
+#### controls.account.clientCredentials
+
+Creates a client credentials token on POST requests.
+More information on these tokens can be found [here](../client-credentials.md).
+Expected input is an object containing a `name` and `webId` field.
+The name is optional and will be used to name the token,
+the WebID determines which WebID you will identify as when using that token.
+It needs to be a WebID linked to the account as described in [controls.account.webID](#controlsaccountwebid).
+
+#### controls.account.account
+
+This value corresponds to the resource URL of the account you received when creating it.
+This returns all resources linked to this account, such as login methods, WebIDs, pods, and client credentials tokens.
+
+Below is an example response object:
+
+```json
+{
+ "logins": {
+ "password": {
+ "test@example.com": "http://localhost:3000/.account/account/c63c9e6f-48f8-40d0-8fec-238da893a7f2/login/password/test%40example.com/"
+ }
+ },
+ "pods": {
+ "http://localhost:3000/test/": "http://localhost:3000/.account/account/c63c9e6f-48f8-40d0-8fec-238da893a7f2/pod/7def7830df1161e422537db594ad2b7412ffb735e0e2320cf3e90db19cd969f9/"
+ },
+ "webIds": {
+ "http://localhost:3000/test/profile/card#me": "http://localhost:3000/.account/account/c63c9e6f-48f8-40d0-8fec-238da893a7f2/webid/5c1b70d3ffaa840394dda86889ed1569cf897ef3d6041fb4c9513f82144cbb7f/"
+ },
+ "clientCredentials": {
+ "token_562cdeb5-d4b2-4905-9e62-8969ac10daaa": "http://localhost:3000/.account/account/c63c9e6f-48f8-40d0-8fec-238da893a7f2/client-credentials/token_562cdeb5-d4b2-4905-9e62-8969ac10daaa/"
+ },
+ "settings": {}
+}
+```
+
+In each of the sub-objects, the key is always the unique identifier of whatever is being described,
+while the value is the resource URL that can potentially be used to modify the resource.
+Removing an entry can be done by sending a DELETE request to the resource URL,
+except for pods, which cannot be deleted.
+Login methods can only be deleted if the account has at least 1 login method remaining afterwards.
+
+The password login resource URL can also be used to modify the password,
+which can be done by sending a POST request to it with the body containing an `oldPassword` and a `newPassword` field.
+
+### controls.password
+
+Controls related to managing the email/password login method.
+
+#### controls.password.create
+
+POST requests create an email/password login and adds it to the account you are logged in as.
+Expects `email` and `password` fields.
+
+#### controls.password.login
+
+POST requests log a user in and return the relevant cookie values.
+Expected fields are `email`, `password`, and optionally a `remember` boolean.
+The `remember` value determines if the returned cookie is only valid for the session,
+or for a longer time.
+
+#### controls.password.forgot
+
+Can be used when a user forgets their password.
+POST requests with an `email` field will send an email with a link to reset the password.
+
+#### controls.password.reset
+
+Used to handle reset password URLs generated when a user forgets their password.
+Expected input values for the POST request are `recordId`,
+which was generated when sending the reset mail,
+and `password` with the new password value.
+
+### controls.oidc
+
+These controls are related to completing OIDC interactions.
+
+#### controls.oidc.cancel
+
+Sending a POST request to this API will cancel the OIDC interaction
+and return the user to the client that started the interaction.
+
+#### controls.oidc.prompt
+
+This API is used to determine what the next necessary step is in the OIDC interaction.
+The response will contain a `location` field,
+containing the URL to the next page the user should go to,
+and a `prompt` field,
+indicating the next step that is necessary to progress the OIDC interaction.
+The three possible prompts are the following:
+
+* **account**: The user needs to log in, so they have an account cookie.
+* **login**: The user needs to pick the WebID they want to use in the resulting OIDC token.
+* **consent**: The user needs to consent to the interaction.
+
+#### controls.oidc.webId
+
+Relevant for solving the **login** prompt.
+GET request will return a list of WebIDs the user can choose from.
+This is the same result as requesting the account information and looking at the linked WebIDs.
+The POST requests expects a `webId` value and optionally a `remember` boolean.
+The latter determines if the server should remember the picked WebID for later interactions.
+
+#### controls.oidc.forgetWebId
+
+POST requests to this API will cause the OIDC interaction to forget the picked WebID
+so a new one can be picked by the user.
+
+#### controls.oidc.consent
+
+A GET request to this API will return all the relevant information about the client doing the request.
+A POST requests causes the OIDC interaction to finish.
+It can have an optional `remember` value, which allows for refresh tokens if it is set to true.
+
+#### controls.html
+
+All these controls link to HTML pages and are thus mostly relevant to provide links to let the user navigate around.
+
+## Example
+
+Below is an example of a controls object in a response.
+
+```json
+{
+ "main": {
+ "index": "http://localhost:3000/.account/",
+ "logins": "http://localhost:3000/.account/login/"
+ },
+ "account": {
+ "create": "http://localhost:3000/.account/account/",
+ "logout": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/logout/",
+ "webId": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/webid/",
+ "pod": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/pod/",
+ "clientCredentials": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/client-credentials/",
+ "account": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/"
+ },
+ "password": {
+ "create": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/login/password/",
+ "login": "http://localhost:3000/.account/login/password/",
+ "forgot": "http://localhost:3000/.account/login/password/forgot/",
+ "reset": "http://localhost:3000/.account/login/password/reset/"
+ },
+ "oidc": {
+ "cancel": "http://localhost:3000/.account/oidc/cancel/",
+ "prompt": "http://localhost:3000/.account/oidc/prompt/",
+ "webId": "http://localhost:3000/.account/oidc/pick-webid/",
+ "forgetWebId": "http://localhost:3000/.account/oidc/forget-webid/",
+ "consent": "http://localhost:3000/.account/oidc/consent/"
+ },
+ "html": {
+ "main": {
+ "login": "http://localhost:3000/.account/login/"
+ },
+ "account": {
+ "createClientCredentials": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/client-credentials/",
+ "createPod": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/pod/",
+ "linkWebId": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/webid/",
+ "account": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/"
+ },
+ "password": {
+ "register": "http://localhost:3000/.account/login/password/register/",
+ "login": "http://localhost:3000/.account/login/password/",
+ "create": "http://localhost:3000/.account/account/ade5c046-e882-4b56-80f4-18cb16433360/login/password/",
+ "forgot": "http://localhost:3000/.account/login/password/forgot/"
+ }
+ }
+}
+```
diff --git a/documentation/markdown/usage/account/login-method.md b/documentation/markdown/usage/account/login-method.md
new file mode 100644
index 000000000..f5fe4f285
--- /dev/null
+++ b/documentation/markdown/usage/account/login-method.md
@@ -0,0 +1,118 @@
+# Adding a new login method
+
+By default, the server allows users to use email/password combinations to identify as the owner of their account.
+But, just like with many other parts of the server,
+this can be extended so other login methods can be used.
+Here we'll cover everything that is necessary.
+
+## Components
+
+These are the components that are needed for adding a new login method.
+Not all of these are mandatory,
+but they can make the life of the user easier when trying to find and use the new method.
+Also have a look at the general [structure](../../architecture/features/accounts/routes.md)
+of new API components to see what is expected of such a component.
+
+### Create component
+
+There needs to be one or more components that allow a user
+to create an instance of the new login method and assign it to their account.
+The `CreatePasswordHandler` can be used as an example.
+This does not necessarily have to happen in a single request,
+potentially multiple requests can be used if the user has to perform actions on an external site for example.
+The only thing that matters is that at the end there is a new entry in the account's `logins` object.
+
+When adding logins of your method a new key will need to be chosen to group these logins together.
+The email/password method uses `password` for example.
+
+A new storage will probably need to be created to storage relevant metadata about this login method entry.
+Below is an example of how the `PasswordStore` is created:
+
+```json
+{
+ "@id": "urn:solid-server:default:PasswordStore",
+ "@type": "BasePasswordStore",
+ "storage": {
+ "@id": "urn:solid-server:default:PasswordStorage",
+ "@type": "EncodingPathStorage",
+ "relativePath": "/accounts/logins/password/",
+ "source": {
+ "@id": "urn:solid-server:default:KeyValueStorage"
+ }
+ }
+}
+```
+
+### Login component
+
+After creating a login instance, a user needs to be able to log in using the new method.
+This can again be done with multiple API calls if necessary,
+but the final one needs to be one that handles the necessary actions
+such as creating a cookie and finishing the OIDC interaction if necessary.
+The `ResolveLoginHandler` can be extended to take care of most of this,
+the `PasswordLoginHandler` provides an example of this.
+
+### Additional components
+
+Besides creating a login instance and logging in,
+it is always possible to offer additional functionality specific to this login method.
+The email/password method, for example, also has components for password recovery and updating a password.
+
+### HTML pages
+
+To make the life easier for users,
+at the very least you probably want to make an HTML page which people can use
+to create an instance of your login method.
+Besides that you could also make a page where people can combine creating an account with creating a login instance.
+The `templates/identity` folder contains all the pages the server has by default,
+which can be used as inspiration.
+
+These pages need to be linked to the `urn:solid-server:default:HtmlViewHandler`.
+Below is an example of this:
+
+```json
+{
+ "@id": "urn:solid-server:default:HtmlViewHandler",
+ "@type": "HtmlViewHandler",
+ "templates": [{
+ "@id": "urn:solid-server:default:CreatePasswordHtml",
+ "@type": "HtmlViewEntry",
+ "filePath": "@css:templates/identity/password/create.html.ejs",
+ "route": {
+ "@id": "urn:solid-server:default:AccountPasswordRoute"
+ }
+ }]
+}
+```
+
+### Updating the login handler
+
+The `urn:solid-server:default:LoginHandler` returns a list of available login methods,
+which are used to offer users a choice of which login method they want to use on the default login page.
+If you want the new method to also be offered you will have to add similar Components.js configuration:
+
+```json
+{
+ "@id": "urn:solid-server:default:LoginHandler",
+ "@type": "ControlHandler",
+ "controls": [
+ {
+ "ControlHandler:_controls_key": "Email/password combination",
+ "ControlHandler:_controls_value": {
+ "@id": "urn:solid-server:default:LoginPasswordRoute"
+ }
+ }
+ ]
+}
+```
+
+### Controls
+
+All new relevant API endpoints should be added to the controls object,
+otherwise there is no way for users to find out where to send their requests.
+Similarly, links to the HTML pages should also be in the controls, so they can be navigated to.
+Examples of how to do this can be found [here](../../architecture/features/accounts/routes.md).
+
+The default account overview page makes some assumptions about the controls when building the page.
+Specifically, it checks if `controls.html..create` exists,
+if yes, it automatically creates a link on the page so users can create new login instances for their account.
diff --git a/documentation/markdown/usage/account/migration.md b/documentation/markdown/usage/account/migration.md
new file mode 100644
index 000000000..123994170
--- /dev/null
+++ b/documentation/markdown/usage/account/migration.md
@@ -0,0 +1,60 @@
+# Migrating account data from v6 to v7
+
+Below is a description of the changes that are necessary to migration account data from v6 to v7 of the server.
+Note that the resource identifier values are bas64 encoded before being appended to the storage location.
+
+* "Forgot password" records
+ * **Storage location**
+ * Old: `.internal/forgot-password/`
+ * New: `.internal/accounts/login/password/forgot/`
+ * **Resource identifiers**
+ * Old: `"forgot-password-resource-identifier/" + recordId`
+ * New: `recordId`
+ * **Data format**
+ * Old: `{ recordId, email }`
+ * New: `email`
+ * **Notes**
+ * Just deleting all existing records is an acceptable solution as these do not contain important information.
+* Client credentials tokens
+ * **Storage location**
+ * Old: `.internal/accounts/credentials/`
+ * New: `.internal/accounts/client-credentials/`
+ * **Resource identifiers**
+ * No change
+ * **Data format**
+ * Old: `{ webId, secret }`
+ * New: `{ accountId, webId, secret }`
+ * **Notes**
+ * Account IDs will need to be generated first before these can be transferred.
+* Account and password data
+ * **Storage location**
+ * Old: `.internal/accounts/`
+ * New: Split up over the following:
+ * `.internal/accounts/data/`
+ * `.internal/accounts/webIds/`
+ * `.internal/accounts/logins/password/`
+ * **Resource identifiers**
+ * Old: `"account/" + encodeURIComponent(email)` or `webId`
+ * New:
+ * `.internal/accounts/data/`: Newly generated account ID.
+ * `.internal/accounts/webIds/`: `webID`
+ * `.internal/accounts/logins/password/`: `encodeURIComponent(email.toLowerCase())`
+ * **Data format**
+ * Old: `{ webId, email, password, verified }` or `{ useIdp, podBaseUrl?, clientCredentials? }`
+ * New:
+ * `.internal/accounts/data/`: `{ id, logins: { password }, pods, webIds, clientCredentials }`
+ * `.internal/accounts/webIds/`: `accountId[]`
+ * `.internal/accounts/logins/password/`: `{ accountId, password, verified }`
+ * **Notes**
+ * First account IDs need to be generated,
+ then login/pod/webId/clientCredentials resources need to be generated,
+ and then the account needs to be updated with those resources.
+ * Resource URLs are generated as follows:
+ * Passwords: `/.account/account//login/password/`
+ * Pods: `/.account/account//pod/`
+ * WebIds: `/.account/account//webid/`
+ * Client Credentials: `/.account/account//client-credentials/`
+ * The above URLs are the values in all the account objects,
+ the keys are the corresponding (lowercase) email, pod base URL, webID, and token name.
+ * Only WebIDs where `useIdp` is `true` need to be linked to the account.
+ * In the previous version, a WebID will be linked to exactly 1 account.
diff --git a/documentation/markdown/usage/identity-provider.md b/documentation/markdown/usage/identity-provider.md
index efdc636b5..2acbe4b7b 100644
--- a/documentation/markdown/usage/identity-provider.md
+++ b/documentation/markdown/usage/identity-provider.md
@@ -8,48 +8,18 @@ It is recommended to use the latest version
of the [Solid authentication client](https://github.com/inrupt/solid-client-authn-js)
to interact with the server.
-The links here assume the server is hosted at `http://localhost:3000/`.
+It also provides account management options for creating pods and WebIDs to be used during authentication,
+which are discussed more in-depth below.
+The links on this page assume the server is hosted at `http://localhost:3000/`.
## Registering an account
-To register an account, you can go to `http://localhost:3000/idp/register/` if this feature is enabled,
-which it is on most configurations we provide.
-Currently our registration page ties 3 features together on the same page:
-
-* Creating an account on the server.
-* Creating or linking a WebID to your account.
-* Creating a pod on the server.
-
-### Account
-
-To create an account you need to provide an email address and password.
+To register an account, you can go to `http://localhost:3000/.account/password/register/`, if this feature is enabled.
+There you can create an account with the email/password login method.
The password will be salted and hashed before being stored.
-As of now, the account is only used to log in and identify yourself to the IDP
-when you want to do an authenticated request,
-but in future the plan is to also use this for account/pod management.
+Afterwards you will be redirected to the account page where you can create pods and link WebIDs to your account.
-### WebID
-
-We require each account to have a corresponding WebID.
-You can either let the server create a WebID for you in a pod,
-which will also need to be created then,
-or you can link an already existing WebID you have on an external server.
-
-In case you try to link your own WebID, you can choose if you want to be able
-to use this server as your IDP for this WebID.
-If not, you can still create a pod,
-but you will not be able to direct the authentication client to this server to identify yourself.
-
-Additionally, if you try to register with an external WebID,
-the first attempt will return an error indicating you need to add an identification triple to your WebID.
-After doing that you can try to register again.
-This is how we verify you are the owner of that WebID.
-After registration the next page will inform you
-that you have to add an additional triple to your WebID if you want to use the server as your IDP.
-
-All of the above is automated if you create the WebID on the server itself.
-
-### Pod
+### Creating a pod
To create a pod you simply have to fill in the name you want your pod to have.
This will then be used to generate the full URL of your pod.
@@ -57,23 +27,42 @@ For example, if you choose the name `test`,
your pod would be located at `http://localhost:3000/test/`
and your generated WebID would be `http://localhost:3000/test/profile/card#me`.
+If you fill in a WebID when creating the pod,
+that WebID will be the one that has access to all data in the pod.
+If you don't, a WebID will be created in the pod and immediately linked to your account,
+allowing you to use it for authentication and accessing the data in that pod
+
The generated name also depends on the configuration you chose for your server.
If you are using the subdomain feature,
-such as being done in the `config/memory-subdomains.json` configuration,
the generated pod URL would be `http://test.localhost:3000/`.
+### WebIDs
+
+To use Solid authentication,
+you need to link at least one WebID to your account.
+This can happen automatically when creating a pod as mentioned above,
+or can be done manually with external WebIDs.
+
+If you try to link an external WebID,
+the first attempt will return an error indicating you need to add an identification triple to your WebID.
+After doing that you can try to register again.
+This is how we verify you are the owner of that WebID.
+Afterwards the page will inform you
+that you have to add a triple to your WebID if you want to use the server as your IDP.
+
## Logging in
When using an authenticating client,
you will be redirected to a login screen asking for your email and password.
-After that you will be redirected to a page showing some basic information about the client.
-There you need to consent that this client is allowed to identify using your WebID.
+After that you will be redirected to a page showing some basic information about the client
+where you can pick the WebID you want to use.
+There you need to consent that this client is allowed to identify using that WebID.
As a result the server will send a token back to the client
that contains all the information needed to use your WebID.
## Forgot password
-If you forgot your password, you can recover it by going to `http://localhost:3000/idp/forgotpassword/`.
+If you forgot your password, you can recover it by going to `http://localhost:3000/.account/login/password/forgot/`.
There you can enter your email address to get a recovery mail to reset your password.
This feature only works if a mail server was configured,
which by default is not the case.
@@ -81,63 +70,11 @@ which by default is not the case.
## JSON API
All of the above happens through HTML pages provided by the server.
-By default, the server uses the templates found in `/templates/identity/email-password/`
+By default, the server uses the templates found in `/templates/identity/`
but different templates can be used through configuration.
These templates all make use of a JSON API exposed by the server.
-For example, when doing a GET request to `http://localhost:3000/idp/register/`
-with a JSON accept header, the following JSON is returned:
-
-```json
-{
- "required": {
- "email": "string",
- "password": "string",
- "confirmPassword": "string",
- "createWebId": "boolean",
- "register": "boolean",
- "createPod": "boolean",
- "rootPod": "boolean"
- },
- "optional": {
- "webId": "string",
- "podName": "string",
- "template": "string"
- },
- "controls": {
- "register": "http://localhost:3000/idp/register/",
- "index": "http://localhost:3000/idp/",
- "prompt": "http://localhost:3000/idp/prompt/",
- "login": "http://localhost:3000/idp/login/",
- "forgotPassword": "http://localhost:3000/idp/forgotpassword/"
- },
- "apiVersion": "0.3"
-}
-```
-
-The `required` and `optional` fields indicate which input fields are expected by the API.
-These correspond to the fields of the HTML registration page.
-To register a user, you can do a POST request with a JSON body containing the correct fields:
-
-```json
-{
- "email": "test@example.com",
- "password": "secret",
- "confirmPassword": "secret",
- "createWebId": true,
- "register": true,
- "createPod": true,
- "rootPod": false,
- "podName": "test"
-}
-```
-
-Two fields here that are not covered on the HTML page above are `rootPod` and `template`.
-`rootPod` tells the server to put the pod in the root of the server instead of a location based on the `podName`.
-By default the server will reject requests where this is `true`.
-`template` is only used by servers running the `config/dynamic.json` configuration,
-which is a very custom setup where every pod can have a different Components.js configuration,
-so this value can usually be ignored.
+A full description of this API can be found [here](account/json-api.md).
## IDP configuration
@@ -175,14 +112,31 @@ which you will need to copy over to your base configuration and then remove the
There is only one option here. This import contains all the core components necessary to make the IDP work.
In case you need to make some changes to core IDP settings, this is where you would have to look.
+### interaction
+
+Here you determine which features of account management are available.
+`default.json` allows everything, while `no-accounts.json` and `no-pods.json`
+disable account and pod creation respectively.
+Taking one of those latter options will disable the relevant JSON APIs and HTML pages.
+
### pod
The `pod` options determines how pods are created. `static.json` is the expected pod behaviour as described above.
`dynamic.json` is an experimental feature that allows users
to have a custom Components.js configuration for their own pod.
-When using such a setup, a JSON file will be written containing all the information of the user pods
+When using such a configuration, a JSON file will be written containing all the information of the user pods,
so they can be recreated when the server restarts.
-### registration
+## Adding a new login method to the server
-This setting allows you to enable/disable registration on the server.
+Due to its modular nature,
+it is possible to add new login methods to the server,
+allowing users to log in different ways than just the standard email/password combination.
+More information on what is required can be found [here](account/login-method.md).
+
+## Data migration
+
+Going from v6 to v7 of the server, the account management is completely rewritten,
+including how account data is stored on the server.
+More information about how account data of an existing server can be migrated to the newer version
+can be found [here](account/migration.md).
diff --git a/documentation/markdown/usage/seeding-pods.md b/documentation/markdown/usage/seeding-pods.md
index 35d47fb46..d2d0ec87a 100644
--- a/documentation/markdown/usage/seeding-pods.md
+++ b/documentation/markdown/usage/seeding-pods.md
@@ -1,39 +1,36 @@
# How to seed Accounts and Pods
If you need to seed accounts and pods,
-the `--seededPodConfigJson` command line option can be used
+the `--seedConfig` command line option can be used
with as value the path to a JSON file containing configurations for every required pod.
The file needs to contain an array of JSON objects,
-with each object containing at least a `podName`, `email`, and `password` field.
+with each object containing at least an `email`, and `password` field.
+Multiple pod objects can also be assigned to such an object in the `pods` array to create pods for the account,
+with contents being the same as its corresponding JSON [API](account/json-api.md#controlsaccountpod).
For example:
```json
[
{
- "podName": "example",
"email": "hello@example.com",
"password": "abc123"
- }
-]
-```
-
-You may optionally specify other parameters
-as described in the [Identity Provider documentation](identity-provider.md#json-api).
-
-For example, to set up a pod without registering the generated WebID with the Identity Provider:
-
-```json
-[
+ },
{
- "podName": "example",
- "email": "hello@example.com",
- "password": "abc123",
- "webId": "https://id.inrupt.com/example",
- "register": false
+ "email": "hello2@example.com",
+ "password": "123abc",
+ "pods": [
+ { "name": "pod1" },
+ { "name": "pod2" }
+ ]
}
]
```
This feature cannot be used to register pods with pre-existing WebIDs,
-which requires an interactive validation step.
+which requires an interactive validation step,
+unless you disable the WebID ownership check in your server configuration.
+
+Note that pod seeding is made for a default server setup with standard email/password login.
+If you [add a new login method](account/login-method.md)
+you will need to create a new implementation of pod seeding if you want to use it.
diff --git a/documentation/markdown/usage/starting-server.md b/documentation/markdown/usage/starting-server.md
index e1ac8fc8d..05ffc16fd 100644
--- a/documentation/markdown/usage/starting-server.md
+++ b/documentation/markdown/usage/starting-server.md
@@ -61,7 +61,7 @@ to some commonly used settings:
| `--sparqlEndpoint, -s` | | URL of the SPARQL endpoint, when using a quadstore-based configuration. |
| `--showStackTrace, -t` | false | Enables detailed logging on error output. |
| `--podConfigJson` | `./pod-config.json` | Path to the file that keeps track of dynamic Pod configurations. Only relevant when using `@css:config/dynamic.json`. |
-| `--seededPodConfigJson` | | Path to the file that keeps track of seeded Pod configurations. |
+| `--seedConfig` | | Path to the file that keeps track of seeded account configurations. |
| `--mainModulePath, -m` | | Path from where Components.js will start its lookup when initializing configurations. |
| `--workers, -w` | `1` | Run in multithreaded mode using workers. Special values are `-1` (scale to `num_cores-1`), `0` (scale to `num_cores`) and 1 (singlethreaded). |
diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml
index cfb52f689..ca45b3a07 100644
--- a/documentation/mkdocs.yml
+++ b/documentation/mkdocs.yml
@@ -80,10 +80,15 @@ nav:
- Usage:
- Example request: usage/example-requests.md
- Metadata: usage/metadata.md
- - Identity provider: usage/identity-provider.md
+ - Identity provider:
+ - Overview: usage/identity-provider.md
+ - JSON API: usage/account/json-api.md
+ - New login method: usage/account/login-method.md
+ - Data migration: usage/account/migration.md
- Client credentials: usage/client-credentials.md
- Seeding pods: usage/seeding-pods.md
- Notifications: usage/notifications.md
+ - Development server: usage/dev-configuration.md
- Architecture:
- Overview: architecture/overview.md
- Dependency injection: architecture/dependency-injection.md
@@ -97,6 +102,10 @@ nav:
- Parsing: architecture/features/protocol/parsing.md
- Authorization: architecture/features/protocol/authorization.md
- Resource Store: architecture/features/protocol/resource-store.md
+ - Account management:
+ - Overview: architecture/features/accounts/overview.md
+ - Controls: architecture/features/accounts/controls.md
+ - Routes: architecture/features/accounts/routes.md
- Notifications: architecture/features/notifications.md
- Contributing:
- Pull requests: contributing/making-changes.md
diff --git a/package-lock.json b/package-lock.json
index 3a1aa145d..f3290ae4b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@solid/access-token-verifier": "^2.0.5",
"@types/async-lock": "^1.4.0",
"@types/bcryptjs": "^2.4.2",
+ "@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/ejs": "^3.1.2",
"@types/end-of-stream": "^1.4.1",
@@ -39,7 +40,8 @@
"arrayify-stream": "^2.0.1",
"async-lock": "^1.4.0",
"bcryptjs": "^2.4.3",
- "componentsjs": "^5.3.2",
+ "componentsjs": "^5.4.2",
+ "cookie": "^0.4.2",
"cors": "^2.8.5",
"cross-fetch": "^4.0.0",
"ejs": "^3.1.9",
@@ -74,7 +76,8 @@
"winston": "^3.8.2",
"winston-transport": "^4.5.0",
"ws": "^8.13.0",
- "yargs": "^17.7.1"
+ "yargs": "^17.7.1",
+ "yup": "^1.0.2"
},
"bin": {
"community-solid-server": "bin/server.js"
@@ -4715,6 +4718,11 @@
"resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg=="
},
+ "node_modules/@types/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+ },
"node_modules/@types/cookiejar": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -6670,18 +6678,18 @@
"dev": true
},
"node_modules/componentsjs": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/componentsjs/-/componentsjs-5.3.2.tgz",
- "integrity": "sha512-wqXaHjrnT4UDQT8Eaou/Itd55OWE7wasBivPJ0qfSlRMi5zRAwp3+sEgGO7F5T7hs0rMsrGTnkWWcoSHmrM/8A==",
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/componentsjs/-/componentsjs-5.4.2.tgz",
+ "integrity": "sha512-qIeXLozDkvubl6qtiovWsIBRqUP80w1ImTbilB6QE3OQgaEExI8pYZ9MkZ10QDFtdoKUryztlqp0AWs49t4puA==",
"dependencies": {
"@rdfjs/types": "*",
"@types/minimist": "^1.2.0",
- "@types/node": "^14.14.7",
+ "@types/node": "^18.0.0",
"@types/semver": "^7.3.4",
"jsonld-context-parser": "^2.1.1",
"minimist": "^1.2.0",
"rdf-data-factory": "^1.1.0",
- "rdf-object": "^1.13.1",
+ "rdf-object": "^1.14.0",
"rdf-parse": "^2.0.0",
"rdf-quad": "^1.5.0",
"rdf-string": "^1.6.0",
@@ -6720,6 +6728,11 @@
"node": ">=12.0"
}
},
+ "node_modules/componentsjs/node_modules/@types/node": {
+ "version": "18.18.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz",
+ "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA=="
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -7223,6 +7236,14 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
+ "node_modules/cookie": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cookiejar": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
@@ -13240,6 +13261,11 @@
"signal-exit": "^3.0.2"
}
},
+ "node_modules/property-expr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
+ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
+ },
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -13439,9 +13465,9 @@
}
},
"node_modules/rdf-object": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/rdf-object/-/rdf-object-1.13.1.tgz",
- "integrity": "sha512-Sgq+GbsqdPsMYh+d4OZ4C9brXlzqa9MvfVHG4pkuT9p7o+AX39nqjTWE/8HVaXjjOZBIDe8T54WWTMWphu3BpA==",
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/rdf-object/-/rdf-object-1.14.0.tgz",
+ "integrity": "sha512-/KSUWr7onDtL7d81kOpcUzJ2vHYOYJc2KU9WzBZRYydBhK0Sksh5Hg4VCQNaxUEvYEgdrrTuq9SLpOOCmag0rQ==",
"dependencies": {
"@rdfjs/types": "*",
"jsonld-context-parser": "^2.0.2",
@@ -14805,6 +14831,11 @@
"readable-stream": "3"
}
},
+ "node_modules/tiny-case": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
+ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
+ },
"node_modules/tiny-glob": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
@@ -14850,6 +14881,11 @@
"node": ">=0.6"
}
},
+ "node_modules/toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
+ },
"node_modules/touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
@@ -15599,6 +15635,28 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/yup": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-1.1.1.tgz",
+ "integrity": "sha512-KfCGHdAErqFZWA5tZf7upSUnGKuTOnsI3hUsLr7fgVtx+DK04NPV01A68/FslI4t3s/ZWpvXJmgXhd7q6ICnag==",
+ "dependencies": {
+ "property-expr": "^2.0.5",
+ "tiny-case": "^1.0.3",
+ "toposort": "^2.0.2",
+ "type-fest": "^2.19.0"
+ }
+ },
+ "node_modules/yup/node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
},
"dependencies": {
@@ -19750,6 +19808,11 @@
"resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg=="
},
+ "@types/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+ },
"@types/cookiejar": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -21230,24 +21293,31 @@
"dev": true
},
"componentsjs": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/componentsjs/-/componentsjs-5.3.2.tgz",
- "integrity": "sha512-wqXaHjrnT4UDQT8Eaou/Itd55OWE7wasBivPJ0qfSlRMi5zRAwp3+sEgGO7F5T7hs0rMsrGTnkWWcoSHmrM/8A==",
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/componentsjs/-/componentsjs-5.4.2.tgz",
+ "integrity": "sha512-qIeXLozDkvubl6qtiovWsIBRqUP80w1ImTbilB6QE3OQgaEExI8pYZ9MkZ10QDFtdoKUryztlqp0AWs49t4puA==",
"requires": {
"@rdfjs/types": "*",
"@types/minimist": "^1.2.0",
- "@types/node": "^14.14.7",
+ "@types/node": "^18.0.0",
"@types/semver": "^7.3.4",
"jsonld-context-parser": "^2.1.1",
"minimist": "^1.2.0",
"rdf-data-factory": "^1.1.0",
- "rdf-object": "^1.13.1",
+ "rdf-object": "^1.14.0",
"rdf-parse": "^2.0.0",
"rdf-quad": "^1.5.0",
"rdf-string": "^1.6.0",
"rdf-terms": "^1.7.0",
"semver": "^7.3.2",
"winston": "^3.3.3"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "18.18.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz",
+ "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA=="
+ }
}
},
"componentsjs-generator": {
@@ -21672,6 +21742,11 @@
}
}
},
+ "cookie": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
+ },
"cookiejar": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
@@ -26214,6 +26289,11 @@
"signal-exit": "^3.0.2"
}
},
+ "property-expr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
+ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
+ },
"pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -26364,9 +26444,9 @@
}
},
"rdf-object": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/rdf-object/-/rdf-object-1.13.1.tgz",
- "integrity": "sha512-Sgq+GbsqdPsMYh+d4OZ4C9brXlzqa9MvfVHG4pkuT9p7o+AX39nqjTWE/8HVaXjjOZBIDe8T54WWTMWphu3BpA==",
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/rdf-object/-/rdf-object-1.14.0.tgz",
+ "integrity": "sha512-/KSUWr7onDtL7d81kOpcUzJ2vHYOYJc2KU9WzBZRYydBhK0Sksh5Hg4VCQNaxUEvYEgdrrTuq9SLpOOCmag0rQ==",
"requires": {
"@rdfjs/types": "*",
"jsonld-context-parser": "^2.0.2",
@@ -27468,6 +27548,11 @@
"readable-stream": "3"
}
},
+ "tiny-case": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
+ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
+ },
"tiny-glob": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
@@ -27504,6 +27589,11 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
+ "toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
+ },
"touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
@@ -28033,6 +28123,24 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
+ },
+ "yup": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-1.1.1.tgz",
+ "integrity": "sha512-KfCGHdAErqFZWA5tZf7upSUnGKuTOnsI3hUsLr7fgVtx+DK04NPV01A68/FslI4t3s/ZWpvXJmgXhd7q6ICnag==",
+ "requires": {
+ "property-expr": "^2.0.5",
+ "tiny-case": "^1.0.3",
+ "toposort": "^2.0.2",
+ "type-fest": "^2.19.0"
+ },
+ "dependencies": {
+ "type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 90de22317..9bdd0acbf 100644
--- a/package.json
+++ b/package.json
@@ -55,8 +55,8 @@
"release": "commit-and-tag-version",
"postrelease": "ts-node ./scripts/finalizeRelease.ts",
"start": "node ./bin/server.js",
- "start:file": "node ./bin/server.js -c config/file.json -f ./data",
- "start:file-root": "node ./bin/server.js -c config/file-root.json -f ./data",
+ "start:file": "node ./bin/server.js -c config/file.json -f ./.data",
+ "start:file-root": "node ./bin/server.js -c config/file-root.json -f ./.data",
"test": "npm run test:ts && npm run jest",
"test:deploy": "test/deploy/validate-configs.sh",
"test:ts": "tsc -p test --noEmit",
@@ -98,6 +98,7 @@
"@solid/access-token-verifier": "^2.0.5",
"@types/async-lock": "^1.4.0",
"@types/bcryptjs": "^2.4.2",
+ "@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/ejs": "^3.1.2",
"@types/end-of-stream": "^1.4.1",
@@ -121,7 +122,8 @@
"arrayify-stream": "^2.0.1",
"async-lock": "^1.4.0",
"bcryptjs": "^2.4.3",
- "componentsjs": "^5.3.2",
+ "componentsjs": "^5.4.2",
+ "cookie": "^0.4.2",
"cors": "^2.8.5",
"cross-fetch": "^4.0.0",
"ejs": "^3.1.9",
@@ -156,7 +158,8 @@
"winston": "^3.8.2",
"winston-transport": "^4.5.0",
"ws": "^8.13.0",
- "yargs": "^17.7.1"
+ "yargs": "^17.7.1",
+ "yup": "^1.0.2"
},
"devDependencies": {
"@commitlint/cli": "^17.6.1",
diff --git a/src/authorization/OwnerPermissionReader.ts b/src/authorization/OwnerPermissionReader.ts
index f4ef27aba..7a2cd35d8 100644
--- a/src/authorization/OwnerPermissionReader.ts
+++ b/src/authorization/OwnerPermissionReader.ts
@@ -1,7 +1,8 @@
import type { Credentials } from '../authentication/Credentials';
import type { AuxiliaryIdentifierStrategy } from '../http/auxiliary/AuxiliaryIdentifierStrategy';
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
-import type { AccountSettings, AccountStore } from '../identity/interaction/email-password/storage/AccountStore';
+import type { AccountStore } from '../identity/interaction/account/util/AccountStore';
+import type { WebIdStore } from '../identity/interaction/webid/util/WebIdStore';
import { getLoggerFor } from '../logging/LogUtil';
import { createErrorMessage } from '../util/errors/ErrorUtil';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
@@ -19,13 +20,15 @@ import type { PermissionMap } from './permissions/Permissions';
export class OwnerPermissionReader extends PermissionReader {
protected readonly logger = getLoggerFor(this);
+ private readonly webIdStore: WebIdStore;
private readonly accountStore: AccountStore;
private readonly authStrategy: AuxiliaryIdentifierStrategy;
private readonly identifierStrategy: IdentifierStrategy;
- public constructor(accountStore: AccountStore, authStrategy: AuxiliaryIdentifierStrategy,
+ public constructor(webIdStore: WebIdStore, accountStore: AccountStore, authStrategy: AuxiliaryIdentifierStrategy,
identifierStrategy: IdentifierStrategy) {
super();
+ this.webIdStore = webIdStore;
this.accountStore = accountStore;
this.authStrategy = authStrategy;
this.identifierStrategy = identifierStrategy;
@@ -40,16 +43,16 @@ export class OwnerPermissionReader extends PermissionReader {
return result;
}
- let podBaseUrl: ResourceIdentifier;
+ let podBaseUrls: ResourceIdentifier[];
try {
- podBaseUrl = await this.findPodBaseUrl(input.credentials);
+ podBaseUrls = await this.findPodBaseUrls(input.credentials);
} catch (error: unknown) {
this.logger.debug(`No pod owner Control permissions: ${createErrorMessage(error)}`);
return result;
}
for (const auth of auths) {
- if (this.identifierStrategy.contains(podBaseUrl, auth, true)) {
+ if (podBaseUrls.some((podBaseUrl): boolean => this.identifierStrategy.contains(podBaseUrl, auth, true))) {
this.logger.debug(`Granting Control permissions to owner on ${auth.path}`);
result.set(auth, {
read: true,
@@ -68,19 +71,26 @@ export class OwnerPermissionReader extends PermissionReader {
* Find the base URL of the pod the given credentials own.
* Will throw an error if none can be found.
*/
- private async findPodBaseUrl(credentials: Credentials): Promise {
+ private async findPodBaseUrls(credentials: Credentials): Promise {
if (!credentials.agent?.webId) {
throw new NotImplementedHttpError('Only authenticated agents could be owners');
}
- let settings: AccountSettings;
- try {
- settings = await this.accountStore.getSettings(credentials.agent.webId);
- } catch {
- throw new NotImplementedHttpError('No account registered for this WebID');
+
+ const accountIds = await this.webIdStore.get(credentials.agent.webId);
+ if (accountIds.length === 0) {
+ throw new NotImplementedHttpError('No account is linked to this WebID');
}
- if (!settings.podBaseUrl) {
- throw new NotImplementedHttpError('This agent has no pod on the server');
+
+ const baseUrls: ResourceIdentifier[] = [];
+ for (const accountId of accountIds) {
+ const account = await this.accountStore.get(accountId);
+ if (!account) {
+ this.logger.error(`Found invalid account ID ${accountId} through WebID ${credentials.agent.webId}`);
+ continue;
+ }
+ baseUrls.push(...Object.keys(account.pods).map((pod): ResourceIdentifier => ({ path: pod })));
}
- return { path: settings.podBaseUrl };
+
+ return baseUrls;
}
}
diff --git a/src/http/input/metadata/AuthorizationParser.ts b/src/http/input/metadata/AuthorizationParser.ts
new file mode 100644
index 000000000..89ebd008b
--- /dev/null
+++ b/src/http/input/metadata/AuthorizationParser.ts
@@ -0,0 +1,43 @@
+import { DataFactory } from 'n3';
+import type { NamedNode } from 'rdf-js';
+import type { HttpRequest } from '../../../server/HttpRequest';
+import { matchesAuthorizationScheme } from '../../../util/HeaderUtil';
+import { SOLID_META } from '../../../util/Vocabularies';
+import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
+import { MetadataParser } from './MetadataParser';
+import namedNode = DataFactory.namedNode;
+
+/**
+ * Parses specific authorization schemes and stores their value as metadata.
+ * The keys of the input `authMap` should be the schemes,
+ * and the values the corresponding predicate that should be used to store the value in the metadata.
+ * The scheme will be sliced off the value, after which it is used as the object in the metadata triple.
+ *
+ * This should be used for custom authorization schemes,
+ * for things like OIDC tokens a {@link CredentialsExtractor} should be used.
+ */
+export class AuthorizationParser extends MetadataParser {
+ private readonly authMap: Record;
+
+ public constructor(authMap: Record) {
+ super();
+ this.authMap = Object.fromEntries(
+ Object.entries(authMap).map(([ scheme, uri ]): [string, NamedNode] => [ scheme, namedNode(uri) ]),
+ );
+ }
+
+ public async handle(input: { request: HttpRequest; metadata: RepresentationMetadata }): Promise {
+ const authHeader = input.request.headers.authorization;
+ if (!authHeader) {
+ return;
+ }
+ for (const [ scheme, uri ] of Object.entries(this.authMap)) {
+ if (matchesAuthorizationScheme(scheme, authHeader)) {
+ // This metadata should not be stored
+ input.metadata.add(uri, authHeader.slice(scheme.length + 1), SOLID_META.ResponseMetadata);
+ // There can only be 1 match
+ return;
+ }
+ }
+ }
+}
diff --git a/src/http/input/metadata/CookieParser.ts b/src/http/input/metadata/CookieParser.ts
new file mode 100644
index 000000000..c3723cb66
--- /dev/null
+++ b/src/http/input/metadata/CookieParser.ts
@@ -0,0 +1,36 @@
+import { parse } from 'cookie';
+import { DataFactory } from 'n3';
+import type { NamedNode } from 'rdf-js';
+import type { HttpRequest } from '../../../server/HttpRequest';
+import { SOLID_META } from '../../../util/Vocabularies';
+import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
+import { MetadataParser } from './MetadataParser';
+import namedNode = DataFactory.namedNode;
+
+/**
+ * Parses the cookie header and stores their values as metadata.
+ * The keys of the input `cookieMap` should be the cookie names,
+ * and the values the corresponding predicate that should be used to store the value in the metadata.
+ * The values of the cookies will be used as objects in the generated triples
+ */
+export class CookieParser extends MetadataParser {
+ private readonly cookieMap: Record;
+
+ public constructor(cookieMap: Record) {
+ super();
+ this.cookieMap = Object.fromEntries(
+ Object.entries(cookieMap).map(([ name, uri ]): [string, NamedNode] => [ name, namedNode(uri) ]),
+ );
+ }
+
+ public async handle(input: { request: HttpRequest; metadata: RepresentationMetadata }): Promise {
+ const cookies = parse(input.request.headers.cookie ?? '');
+ for (const [ name, uri ] of Object.entries(this.cookieMap)) {
+ const value = cookies[name];
+ if (value) {
+ // This metadata should not be stored
+ input.metadata.add(uri, value, SOLID_META.ResponseMetadata);
+ }
+ }
+ }
+}
diff --git a/src/http/output/metadata/CookieMetadataWriter.ts b/src/http/output/metadata/CookieMetadataWriter.ts
new file mode 100644
index 000000000..6f16906ce
--- /dev/null
+++ b/src/http/output/metadata/CookieMetadataWriter.ts
@@ -0,0 +1,50 @@
+import { serialize } from 'cookie';
+import type { NamedNode } from 'n3';
+import { DataFactory } from 'n3';
+import type { HttpResponse } from '../../../server/HttpResponse';
+import { addHeader } from '../../../util/HeaderUtil';
+import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
+import { MetadataWriter } from './MetadataWriter';
+
+/**
+ * Generates the necessary `Set-Cookie` header if a cookie value is detected in the metadata.
+ * The keys of the input `cookieMap` should be the URIs of the predicates
+ * used in the metadata when the object is a cookie value.
+ * The value of the map are objects that contain the name of the cookie,
+ * and the URI that is used to store the expiration date in the metadata, if any.
+ * If no expiration date is found in the metadata, none will be set for the cookie,
+ * causing it to be a session cookie.
+ */
+export class CookieMetadataWriter extends MetadataWriter {
+ private readonly cookieMap: Map;
+
+ public constructor(cookieMap: Record) {
+ super();
+ this.cookieMap = new Map(Object.entries(cookieMap)
+ .map(([ uri, { name, expirationUri }]): [ NamedNode, { name: string; expirationUri?: NamedNode } ] =>
+ [
+ DataFactory.namedNode(uri),
+ {
+ name,
+ expirationUri: expirationUri ? DataFactory.namedNode(expirationUri) : undefined,
+ },
+ ]));
+ }
+
+ public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise {
+ const { response, metadata } = input;
+ for (const [ uri, { name, expirationUri }] of this.cookieMap.entries()) {
+ const value = metadata.get(uri)?.value;
+ if (value) {
+ const expiration = expirationUri && metadata.get(expirationUri)?.value;
+ const expires = typeof expiration === 'string' ? new Date(expiration) : undefined;
+ // Not setting secure flag since not all tools realize those cookies are also valid for http://localhost.
+ // Not setting the httpOnly flag as that would prevent JS API access.
+ // SameSite: Lax makes it so the cookie gets sent if the origin is the server,
+ // or if the browser navigates there from another site.
+ // Setting the path to `/` so it applies to the entire server.
+ addHeader(response, 'Set-Cookie', serialize(name, value, { path: '/', sameSite: 'lax', expires }));
+ }
+ }
+ }
+}
diff --git a/src/identity/IdentityProviderHttpHandler.ts b/src/identity/IdentityProviderHttpHandler.ts
index aa9f146ef..4706322ea 100644
--- a/src/identity/IdentityProviderHttpHandler.ts
+++ b/src/identity/IdentityProviderHttpHandler.ts
@@ -3,13 +3,11 @@ import type { ResponseDescription } from '../http/output/response/ResponseDescri
import { getLoggerFor } from '../logging/LogUtil';
import type { OperationHttpHandlerInput } from '../server/OperationHttpHandler';
import { OperationHttpHandler } from '../server/OperationHttpHandler';
-import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
-import { APPLICATION_JSON } from '../util/ContentTypes';
+import { createErrorMessage } from '../util/errors/ErrorUtil';
+import { SOLID_HTTP } from '../util/Vocabularies';
import type { ProviderFactory } from './configuration/ProviderFactory';
-import type {
- InteractionHandler,
- Interaction,
-} from './interaction/InteractionHandler';
+import type { CookieStore } from './interaction/account/util/CookieStore';
+import type { InteractionHandler, Interaction } from './interaction/InteractionHandler';
export interface IdentityProviderHttpHandlerArgs {
/**
@@ -17,9 +15,9 @@ export interface IdentityProviderHttpHandlerArgs {
*/
providerFactory: ProviderFactory;
/**
- * Used for converting the input data.
+ * Used to determine the account of the requesting agent.
*/
- converter: RepresentationConverter;
+ cookieStore: CookieStore;
/**
* Handles the requests.
*/
@@ -27,24 +25,22 @@ export interface IdentityProviderHttpHandlerArgs {
}
/**
- * Generates the active Interaction object if there is an ongoing OIDC interaction
- * and sends it to the {@link InteractionHandler}.
+ * Generates the active Interaction object if there is an ongoing OIDC interaction.
+ * Finds the account ID if there is cookie metadata.
*
- * Input data will first be converted to JSON.
- *
- * Only GET and POST methods are accepted.
+ * Calls the stored {@link InteractionHandler} with that information and returns the result.
*/
export class IdentityProviderHttpHandler extends OperationHttpHandler {
protected readonly logger = getLoggerFor(this);
private readonly providerFactory: ProviderFactory;
- private readonly converter: RepresentationConverter;
+ private readonly cookieStore: CookieStore;
private readonly handler: InteractionHandler;
public constructor(args: IdentityProviderHttpHandlerArgs) {
super();
this.providerFactory = args.providerFactory;
- this.converter = args.converter;
+ this.cookieStore = args.cookieStore;
this.handler = args.handler;
}
@@ -55,27 +51,18 @@ export class IdentityProviderHttpHandler extends OperationHttpHandler {
const provider = await this.providerFactory.getProvider();
oidcInteraction = await provider.interactionDetails(request, response);
this.logger.debug('Found an active OIDC interaction.');
- } catch {
- this.logger.debug('No active OIDC interaction found.');
+ } catch (error: unknown) {
+ this.logger.debug(`No active OIDC interaction found: ${createErrorMessage(error)}`);
}
- // Convert input data to JSON
- // Allows us to still support form data
- const { contentType } = operation.body.metadata;
- if (contentType && contentType !== APPLICATION_JSON) {
- this.logger.debug(`Converting input ${contentType} to ${APPLICATION_JSON}`);
- const args = {
- representation: operation.body,
- preferences: { type: { [APPLICATION_JSON]: 1 }},
- identifier: operation.target,
- };
- operation = {
- ...operation,
- body: await this.converter.handleSafe(args),
- };
+ // Determine account
+ let accountId: string | undefined;
+ const cookie = operation.body.metadata.get(SOLID_HTTP.terms.accountCookie)?.value;
+ if (cookie) {
+ accountId = await this.cookieStore.get(cookie);
}
- const representation = await this.handler.handleSafe({ operation, oidcInteraction });
+ const representation = await this.handler.handleSafe({ operation, oidcInteraction, accountId });
return new OkResponseDescription(representation.metadata, representation.data);
}
}
diff --git a/src/identity/IdentityUtil.ts b/src/identity/IdentityUtil.ts
new file mode 100644
index 000000000..e99eea616
--- /dev/null
+++ b/src/identity/IdentityUtil.ts
@@ -0,0 +1,21 @@
+import type { CanBePromise } from '../../templates/types/oidc-provider';
+
+/**
+ * Import the OIDC-provider package.
+ *
+ * As oidc-provider is an ESM package and CSS is CJS, we have to use a dynamic import here.
+ * Unfortunately, there is a Node/Jest bug that causes segmentation faults when doing such an import in Jest:
+ * https://github.com/nodejs/node/issues/35889
+ * To work around that, we do the import differently, in case we are in a Jest test run.
+ * This can be detected via the env variables: https://jestjs.io/docs/environment-variables.
+ * There have been reports of `JEST_WORKER_ID` being undefined, so to be sure we check both.
+ */
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+export function importOidcProvider(): CanBePromise {
+ // eslint-disable-next-line no-process-env
+ if (process.env.JEST_WORKER_ID ?? process.env.NODE_ENV === 'test') {
+ // eslint-disable-next-line no-undef
+ return jest.requireActual('oidc-provider');
+ }
+ return import('oidc-provider');
+}
diff --git a/src/identity/configuration/AccountPromptFactory.ts b/src/identity/configuration/AccountPromptFactory.ts
new file mode 100644
index 000000000..22eb7dc77
--- /dev/null
+++ b/src/identity/configuration/AccountPromptFactory.ts
@@ -0,0 +1,91 @@
+import type { interactionPolicy, KoaContextWithOIDC } from '../../../templates/types/oidc-provider';
+import { getLoggerFor } from '../../logging/LogUtil';
+import { InternalServerError } from '../../util/errors/InternalServerError';
+import { importOidcProvider } from '../IdentityUtil';
+import type { AccountStore } from '../interaction/account/util/AccountStore';
+import type { CookieStore } from '../interaction/account/util/CookieStore';
+import { ACCOUNT_PROMPT } from '../interaction/InteractionUtil';
+import { PromptFactory } from './PromptFactory';
+
+type OIDCContext = NonNullable;
+type ExtendedContext = OIDCContext & { internalAccountId?: string };
+
+/**
+ * Creates the prompt necessary to ensure a user is logged in with their account when doing an OIDC interaction.
+ * This is done by checking the presence of the account-related cookie.
+ *
+ * Adds a Check to the login policy that verifies if the stored accountId, which corresponds to the chosen WebID,
+ * belongs to the currently logged in account.
+ */
+export class AccountPromptFactory extends PromptFactory {
+ protected readonly logger = getLoggerFor(this);
+
+ private readonly accountStore: AccountStore;
+ private readonly cookieStore: CookieStore;
+ private readonly cookieName: string;
+
+ public constructor(accountStore: AccountStore, cookieStore: CookieStore, cookieName: string) {
+ super();
+ this.accountStore = accountStore;
+ this.cookieStore = cookieStore;
+ this.cookieName = cookieName;
+ }
+
+ public async handle(policy: interactionPolicy.DefaultPolicy): Promise {
+ const { interactionPolicy: ip } = await importOidcProvider();
+ this.addAccountPrompt(policy, ip);
+ this.addWebIdVerificationPrompt(policy, ip);
+ }
+
+ private addAccountPrompt(policy: interactionPolicy.DefaultPolicy, ip: typeof interactionPolicy): void {
+ const check = new ip.Check('no_account', 'An account cookie is required.', async(ctx): Promise => {
+ const cookie = ctx.cookies.get(this.cookieName);
+ let accountId: string | undefined;
+ if (cookie) {
+ accountId = await this.cookieStore.get(cookie);
+ // This is an ugly way to pass a value to the other prompts/checks,
+ // but the oidc-provider library does similar things internally.
+ (ctx.oidc as ExtendedContext).internalAccountId = accountId;
+ }
+ this.logger.debug(`Found account cookie ${cookie} and accountID ${accountId}`);
+
+ // Check needs to return true if the prompt has to trigger
+ return !accountId;
+ });
+ const accountPrompt = new ip.Prompt({ name: ACCOUNT_PROMPT, requestable: true }, check);
+ policy.add(accountPrompt, 0);
+ }
+
+ private addWebIdVerificationPrompt(policy: interactionPolicy.DefaultPolicy, ip: typeof interactionPolicy): void {
+ const check = new ip.Check('no_webid_ownserhip',
+ 'The stored WebID does not belong to the account.',
+ async(ctx): Promise => {
+ if (!ctx.oidc.session?.accountId) {
+ return false;
+ }
+
+ const accountId = (ctx.oidc as ExtendedContext).internalAccountId;
+ if (!accountId) {
+ this.logger.error(`Missing 'internalAccountId' value in OIDC context`);
+ return false;
+ }
+
+ const account = await this.accountStore.get(accountId);
+ if (!account) {
+ this.logger.error(`Invalid account ID ${accountId}`);
+ return false;
+ }
+
+ const owner = account.webIds[ctx.oidc.session.accountId];
+ this.logger.debug(`Session has WebID ${ctx.oidc.session.accountId
+ }, which ${owner ? 'belongs' : 'does not belong'} to the authenticated account`);
+
+ return !owner;
+ });
+ const loginPrompt = policy.get('login');
+ if (!loginPrompt) {
+ throw new InternalServerError('Missing default login policy');
+ }
+ loginPrompt.checks.add(check);
+ }
+}
diff --git a/src/identity/configuration/IdentityProviderFactory.ts b/src/identity/configuration/IdentityProviderFactory.ts
index 1db9ed269..b33504111 100644
--- a/src/identity/configuration/IdentityProviderFactory.ts
+++ b/src/identity/configuration/IdentityProviderFactory.ts
@@ -12,27 +12,29 @@ import type { Account,
UnknownObject,
errors } from '../../../templates/types/oidc-provider';
import type Provider from '../../../templates/types/oidc-provider';
-import type { Operation } from '../../http/Operation';
import type { ErrorHandler } from '../../http/output/error/ErrorHandler';
import type { ResponseWriter } from '../../http/output/ResponseWriter';
-import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
import { getLoggerFor } from '../../logging/LogUtil';
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import type { HttpError } from '../../util/errors/HttpError';
import { errorTermsToMetadata } from '../../util/errors/HttpErrorUtil';
-import { InternalServerError } from '../../util/errors/InternalServerError';
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
-import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
import { guardStream } from '../../util/GuardedStream';
import { joinUrl } from '../../util/PathUtil';
-import type { ClientCredentials } from '../interaction/email-password/credentials/ClientCredentialsAdapterFactory';
-import type { InteractionHandler } from '../interaction/InteractionHandler';
+import { importOidcProvider } from '../IdentityUtil';
+import type { ClientCredentialsStore } from '../interaction/client-credentials/util/ClientCredentialsStore';
+import type { InteractionRoute } from '../interaction/routing/InteractionRoute';
import type { AdapterFactory } from '../storage/AdapterFactory';
import type { AlgJwk, JwkGenerator } from './JwkGenerator';
+import type { PromptFactory } from './PromptFactory';
import type { ProviderFactory } from './ProviderFactory';
export interface IdentityProviderFactoryArgs {
+ /**
+ * Used to generate new prompt that are needed in addition to the defaults prompts.
+ */
+ promptFactory: PromptFactory;
/**
* Factory that creates the adapter used for OIDC data storage.
*/
@@ -46,13 +48,13 @@ export interface IdentityProviderFactoryArgs {
*/
oidcPath: string;
/**
- * The handler responsible for redirecting interaction requests to the correct URL.
+ * The route where requests should be redirected to in case of an OIDC interaction.
*/
- interactionHandler: InteractionHandler;
+ interactionRoute: InteractionRoute;
/**
- * Storage containing the generated client credentials with their associated WebID.
+ * Store containing the generated client credentials with their associated WebID.
*/
- credentialStorage: KeyValueStorage;
+ clientCredentialsStore: ClientCredentialsStore;
/**
* Storage used to store cookie keys so they can be re-used in case of multithreading.
*/
@@ -87,12 +89,13 @@ const COOKIES_KEY = 'cookie-secret';
export class IdentityProviderFactory implements ProviderFactory {
protected readonly logger = getLoggerFor(this);
+ private readonly promptFactory: PromptFactory;
private readonly config: Configuration;
private readonly adapterFactory: AdapterFactory;
private readonly baseUrl: string;
private readonly oidcPath: string;
- private readonly interactionHandler: InteractionHandler;
- private readonly credentialStorage: KeyValueStorage;
+ private readonly interactionRoute: InteractionRoute;
+ private readonly clientCredentialsStore: ClientCredentialsStore;
private readonly storage: KeyValueStorage;
private readonly jwkGenerator: JwkGenerator;
private readonly showStackTrace: boolean;
@@ -108,11 +111,12 @@ export class IdentityProviderFactory implements ProviderFactory {
public constructor(config: Configuration, args: IdentityProviderFactoryArgs) {
this.config = config;
+ this.promptFactory = args.promptFactory;
this.adapterFactory = args.adapterFactory;
this.baseUrl = args.baseUrl;
this.oidcPath = args.oidcPath;
- this.interactionHandler = args.interactionHandler;
- this.credentialStorage = args.credentialStorage;
+ this.interactionRoute = args.interactionRoute;
+ this.clientCredentialsStore = args.clientCredentialsStore;
this.storage = args.storage;
this.jwkGenerator = args.jwkGenerator;
this.showStackTrace = args.showStackTrace;
@@ -145,21 +149,14 @@ export class IdentityProviderFactory implements ProviderFactory {
// Render errors with our own error handler
this.configureErrors(config);
- // As oidc-provider is an ESM package and CSS is CJS, we have to use a dynamic import here.
- // Unfortunately, there is a Node/Jest bug that causes segmentation faults when doing such an import in Jest:
- // https://github.com/nodejs/node/issues/35889
- // To work around that, we do the import differently, in case we are in a Jest test run.
- // This can be detected via the env variables: https://jestjs.io/docs/environment-variables.
- // There have been reports of `JEST_WORKER_ID` being undefined, so to be sure we check both.
- let ctr: { default: new(issuer: string, configuration?: Configuration) => Provider };
- // eslint-disable-next-line no-process-env
- if (process.env.JEST_WORKER_ID ?? process.env.NODE_ENV === 'test') {
- // eslint-disable-next-line no-undef
- ctr = jest.requireActual('oidc-provider');
- } else {
- ctr = await import('oidc-provider');
- }
- const provider = new ctr.default(this.baseUrl, config);
+ const oidcImport = await importOidcProvider();
+
+ // Adds new prompts
+ const policy = oidcImport.interactionPolicy.base();
+ await this.promptFactory.handleSafe(policy);
+ config.interactions!.policy = policy;
+
+ const provider = new oidcImport.default(this.baseUrl, config);
// Allow provider to interpret reverse proxy headers.
provider.proxy = true;
@@ -283,7 +280,7 @@ export class IdentityProviderFactory implements ProviderFactory {
config.extraTokenClaims = async(ctx, token): Promise =>
this.isAccessToken(token) ?
{ webid: token.accountId } :
- { webid: token.client && (await this.credentialStorage.get(token.client.clientId))?.webId };
+ { webid: token.client && (await this.clientCredentialsStore.get(token.client.clientId))?.webId };
config.features = {
...config.features,
@@ -300,7 +297,7 @@ export class IdentityProviderFactory implements ProviderFactory {
getResourceServerInfo: (): ResourceServer => ({
// The scopes of the Resource Server.
// These get checked when requesting client credentials.
- scope: 'webid',
+ scope: '',
audience: 'solid',
accessTokenFormat: 'jwt',
jwt: {
@@ -329,26 +326,7 @@ export class IdentityProviderFactory implements ProviderFactory {
// it will resolve the interactions.url helper function and redirect the User-Agent to that url.
// Another requirement is that `features.userinfo` is disabled in the configuration.
config.interactions = {
- url: async(ctx, oidcInteraction): Promise => {
- const operation: Operation = {
- method: ctx.method,
- target: { path: ctx.request.href },
- preferences: {},
- body: new BasicRepresentation(),
- };
-
- // Instead of sending a 3xx redirect to the client (via a RedirectHttpError),
- // we need to pass the location URL to the OIDC library
- try {
- await this.interactionHandler.handleSafe({ operation, oidcInteraction });
- } catch (error: unknown) {
- if (RedirectHttpError.isInstance(error)) {
- return error.location;
- }
- throw error;
- }
- throw new InternalServerError('Could not correctly redirect for the given interaction.');
- },
+ url: async(): Promise => this.interactionRoute.getPath(),
};
config.routes = {
diff --git a/src/identity/configuration/PromptFactory.ts b/src/identity/configuration/PromptFactory.ts
new file mode 100644
index 000000000..22257a04f
--- /dev/null
+++ b/src/identity/configuration/PromptFactory.ts
@@ -0,0 +1,7 @@
+import type { interactionPolicy } from '../../../templates/types/oidc-provider';
+import { AsyncHandler } from '../../util/handlers/AsyncHandler';
+
+/**
+ * Used to generate custom {@link interactionPolicy.Prompt}s.
+ */
+export abstract class PromptFactory extends AsyncHandler {}
diff --git a/src/identity/interaction/BaseInteractionHandler.ts b/src/identity/interaction/BaseInteractionHandler.ts
deleted file mode 100644
index 1f192c10a..000000000
--- a/src/identity/interaction/BaseInteractionHandler.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
-import type { Representation } from '../../http/representation/Representation';
-import { APPLICATION_JSON } from '../../util/ContentTypes';
-import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
-import type { InteractionHandlerInput } from './InteractionHandler';
-import { InteractionHandler } from './InteractionHandler';
-
-/**
- * Abstract implementation for handlers that always return a fixed JSON view on a GET.
- * POST requests are passed to an abstract function.
- * Other methods will be rejected.
- */
-export abstract class BaseInteractionHandler extends InteractionHandler {
- private readonly view: string;
-
- protected constructor(view: Record) {
- super();
- this.view = JSON.stringify(view);
- }
-
- public async canHandle(input: InteractionHandlerInput): Promise {
- await super.canHandle(input);
- const { method } = input.operation;
- if (method !== 'GET' && method !== 'POST') {
- throw new MethodNotAllowedHttpError([ method ], 'Only GET/POST requests are supported.');
- }
- }
-
- public async handle(input: InteractionHandlerInput): Promise {
- switch (input.operation.method) {
- case 'GET': return this.handleGet(input);
- case 'POST': return this.handlePost(input);
- default: throw new MethodNotAllowedHttpError([ input.operation.method ]);
- }
- }
-
- /**
- * Returns a fixed JSON view.
- * @param input - Input parameters, only the operation target is used.
- */
- protected async handleGet(input: InteractionHandlerInput): Promise {
- return new BasicRepresentation(this.view, input.operation.target, APPLICATION_JSON);
- }
-
- /**
- * Function that will be called for POST requests.
- * Input data remains unchanged.
- * @param input - Input operation and OidcInteraction if it exists.
- */
- protected abstract handlePost(input: InteractionHandlerInput): Promise;
-}
diff --git a/src/identity/interaction/ConsentHandler.ts b/src/identity/interaction/ConsentHandler.ts
deleted file mode 100644
index 140317f26..000000000
--- a/src/identity/interaction/ConsentHandler.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import type {
- AllClientMetadata,
- InteractionResults,
- KoaContextWithOIDC,
- UnknownObject,
-} from '../../../templates/types/oidc-provider';
-import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
-import type { Representation } from '../../http/representation/Representation';
-import { APPLICATION_JSON } from '../../util/ContentTypes';
-import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
-import { FoundHttpError } from '../../util/errors/FoundHttpError';
-import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
-import { readJsonStream } from '../../util/StreamUtil';
-import type { ProviderFactory } from '../configuration/ProviderFactory';
-import { BaseInteractionHandler } from './BaseInteractionHandler';
-import type { Interaction, InteractionHandlerInput } from './InteractionHandler';
-
-type Grant = NonNullable;
-
-/**
- * Handles the OIDC consent prompts where the user confirms they want to log in for the given client.
- *
- * Returns all the relevant Client metadata on GET requests.
- */
-export class ConsentHandler extends BaseInteractionHandler {
- private readonly providerFactory: ProviderFactory;
-
- public constructor(providerFactory: ProviderFactory) {
- super({});
- this.providerFactory = providerFactory;
- }
-
- public async canHandle(input: InteractionHandlerInput): Promise {
- await super.canHandle(input);
- if (input.operation.method === 'POST' && !input.oidcInteraction) {
- throw new BadRequestHttpError(
- 'This action can only be performed as part of an OIDC authentication flow.',
- { errorCode: 'E0002' },
- );
- }
- }
-
- protected async handleGet(input: Required): Promise {
- const { operation, oidcInteraction } = input;
- const provider = await this.providerFactory.getProvider();
- const client = await provider.Client.find(oidcInteraction.params.client_id as string);
- const metadata: AllClientMetadata = client?.metadata() ?? {};
-
- // Only extract specific fields to prevent leaking information
- // Based on https://www.w3.org/ns/solid/oidc-context.jsonld
- const keys = [ 'client_id', 'client_uri', 'logo_uri', 'policy_uri',
- 'client_name', 'contacts', 'grant_types', 'scope' ];
-
- const jsonLd = Object.fromEntries(
- keys.filter((key): boolean => key in metadata)
- .map((key): [ string, unknown ] => [ key, metadata[key] ]),
- );
- jsonLd['@context'] = 'https://www.w3.org/ns/solid/oidc-context.jsonld';
- const json = { webId: oidcInteraction.session?.accountId, client: jsonLd };
-
- return new BasicRepresentation(JSON.stringify(json), operation.target, APPLICATION_JSON);
- }
-
- protected async handlePost({ operation, oidcInteraction }: InteractionHandlerInput): Promise {
- const { remember, logOut } = await readJsonStream(operation.body.data);
-
- if (logOut) {
- const provider = await this.providerFactory.getProvider();
- const session = (await provider.Session.find(oidcInteraction!.session!.cookie))!;
- delete session.accountId;
- await session.save(session.exp - Math.floor(Date.now() / 1000));
-
- throw new FoundHttpError(oidcInteraction!.returnTo);
- }
-
- const grant = await this.getGrant(oidcInteraction!);
- this.updateGrant(grant, oidcInteraction!.prompt.details, remember);
-
- const location = await this.updateInteraction(oidcInteraction!, grant);
-
- throw new FoundHttpError(location);
- }
-
- /**
- * Either returns the grant associated with the given interaction or creates a new one if it does not exist yet.
- */
- private async getGrant(oidcInteraction: Interaction): Promise {
- if (!oidcInteraction.session) {
- throw new NotImplementedHttpError('Only interactions with a valid session are supported.');
- }
-
- const { params, session: { accountId }, grantId } = oidcInteraction;
- const provider = await this.providerFactory.getProvider();
- let grant: Grant;
- if (grantId) {
- grant = (await provider.Grant.find(grantId))!;
- } else {
- grant = new provider.Grant({
- accountId,
- clientId: params.client_id as string,
- });
- }
- return grant;
- }
-
- /**
- * Updates the grant with all the missing scopes and claims requested by the interaction.
- *
- * Will reject the `offline_access` scope if `remember` is false.
- */
- private updateGrant(grant: Grant, details: UnknownObject, remember: boolean): void {
- // Reject the offline_access scope if the user does not want to be remembered
- if (!remember) {
- grant.rejectOIDCScope('offline_access');
- }
-
- // Grant all the requested scopes and claims
- if (details.missingOIDCScope) {
- grant.addOIDCScope((details.missingOIDCScope as string[]).join(' '));
- }
- if (details.missingOIDCClaims) {
- grant.addOIDCClaims(details.missingOIDCClaims as string[]);
- }
- if (details.missingResourceScopes) {
- for (const [ indicator, scopes ] of Object.entries(details.missingResourceScopes as Record)) {
- grant.addResourceScope(indicator, scopes.join(' '));
- }
- }
- }
-
- /**
- * Updates the interaction with the new grant and returns the resulting redirect URL.
- */
- private async updateInteraction(oidcInteraction: Interaction, grant: Grant): Promise {
- const grantId = await grant.save();
-
- const consent: InteractionResults['consent'] = {};
- // Only need to update the grantId if it is new
- if (!oidcInteraction.grantId) {
- consent.grantId = grantId;
- }
-
- const result: InteractionResults = { consent };
-
- // Need to merge with previous submission
- oidcInteraction.result = { ...oidcInteraction.lastSubmission, ...result };
- await oidcInteraction.save(oidcInteraction.exp - Math.floor(Date.now() / 1000));
-
- return oidcInteraction.returnTo;
- }
-}
diff --git a/src/identity/interaction/ControlHandler.ts b/src/identity/interaction/ControlHandler.ts
index 97b26d8ef..390fea28b 100644
--- a/src/identity/interaction/ControlHandler.ts
+++ b/src/identity/interaction/ControlHandler.ts
@@ -1,43 +1,118 @@
-import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
-import type { Representation } from '../../http/representation/Representation';
-import { APPLICATION_JSON } from '../../util/ContentTypes';
-import { InternalServerError } from '../../util/errors/InternalServerError';
-import { readJsonStream } from '../../util/StreamUtil';
-import type { InteractionHandlerInput } from './InteractionHandler';
-import { InteractionHandler } from './InteractionHandler';
+import { ACCOUNT_ID_KEY } from './account/AccountIdRoute';
+import type { Json, JsonRepresentation } from './InteractionUtil';
+import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
+import { JsonInteractionHandler } from './JsonInteractionHandler';
import type { InteractionRoute } from './routing/InteractionRoute';
-
-const INTERNAL_API_VERSION = '0.4';
+import Dict = NodeJS.Dict;
/**
- * Adds `controls` and `apiVersion` fields to the output of its source handler,
- * such that clients can predictably find their way to other resources.
- * Control paths are determined by the input routes.
+ * Creates an object with the keys matching those of the input `controls`,
+ * and the values being the results received by the matching values in the same input.
+ *
+ * If `source` is defined, the controls will be added to the output of that handler after passing the input.
+ * In case the control keys conflict with a key already present in the resulting object,
+ * the results will be merged.
*/
-export class ControlHandler extends InteractionHandler {
- private readonly source: InteractionHandler;
- private readonly controls: Record;
+export class ControlHandler extends JsonInteractionHandler {
+ private readonly controls: Record;
+ private readonly source?: JsonInteractionHandler;
- public constructor(source: InteractionHandler, controls: Record) {
+ public constructor(controls: Record,
+ source?: JsonInteractionHandler) {
super();
+ this.controls = controls;
this.source = source;
- this.controls = Object.fromEntries(
- Object.entries(controls).map(([ control, route ]): [ string, string ] => [ control, route.getPath() ]),
- );
}
- public async canHandle(input: InteractionHandlerInput): Promise {
- await this.source.canHandle(input);
+ public async canHandle(input: JsonInteractionHandlerInput): Promise {
+ await this.source?.canHandle(input);
}
- public async handle(input: InteractionHandlerInput): Promise {
- const result = await this.source.handle(input);
- if (result.metadata.contentType !== APPLICATION_JSON) {
- throw new InternalServerError('Source handler should return application/json.');
+ public async handle(input: JsonInteractionHandlerInput): Promise {
+ const result = await this.source?.handle(input);
+ const controls = await this.generateControls(input);
+
+ const json = this.mergeControls(result?.json, controls) as Dict;
+
+ return {
+ json,
+ metadata: result?.metadata,
+ };
+ }
+
+ protected isRoute(value: InteractionRoute | JsonInteractionHandler): value is InteractionRoute {
+ return Boolean((value as InteractionRoute).getPath);
+ }
+
+ /**
+ * Generate the controls for all the stored keys.
+ */
+ protected async generateControls(input: JsonInteractionHandlerInput): Promise> {
+ let controls: Record = {};
+
+ for (const [ key, value ] of Object.entries(this.controls)) {
+ const controlSet = await this.generateControlSet(input, value);
+ if (controlSet) {
+ controls = this.mergeControls(controls, { [key]: controlSet }) as Record;
+ }
}
- const json = await readJsonStream(result.data);
- json.controls = this.controls;
- json.apiVersion = INTERNAL_API_VERSION;
- return new BasicRepresentation(JSON.stringify(json), result.metadata);
+
+ return controls;
+ }
+
+ protected async generateControlSet(input: JsonInteractionHandlerInput,
+ value: InteractionRoute | JsonInteractionHandler): Promise {
+ if (this.isRoute(value)) {
+ try {
+ return value.getPath({ [ACCOUNT_ID_KEY]: input.accountId });
+ } catch {
+ // Path required an account ID which is missing
+ return;
+ }
+ }
+ const { json } = await value.handleSafe(input);
+ if (Array.isArray(json) && json.length === 0) {
+ return;
+ }
+ if (typeof json === 'object' && Object.keys(json).length === 0) {
+ return;
+ }
+ return json;
+ }
+
+ /**
+ * Merge the two objects.
+ * Generally this will probably not be necessary, or be very simple merges,
+ * but this ensures that we handle all possibilities.
+ */
+ protected mergeControls(original?: Json, controls?: Json): Json {
+ if (typeof original === 'undefined') {
+ return controls!;
+ }
+
+ if (typeof controls === 'undefined') {
+ return original;
+ }
+
+ if (typeof original !== 'object' || typeof controls !== 'object') {
+ return original;
+ }
+
+ if (Array.isArray(original)) {
+ if (Array.isArray(controls)) {
+ return [ ...original, ...controls ];
+ }
+ return original;
+ }
+
+ if (Array.isArray(controls)) {
+ return original;
+ }
+
+ const result: Record = {};
+ for (const key of new Set([ ...Object.keys(original), ...Object.keys(controls) ])) {
+ result[key] = this.mergeControls(original[key], controls[key]);
+ }
+ return result;
}
}
diff --git a/src/identity/interaction/CookieInteractionHandler.ts b/src/identity/interaction/CookieInteractionHandler.ts
new file mode 100644
index 000000000..5b2c45b82
--- /dev/null
+++ b/src/identity/interaction/CookieInteractionHandler.ts
@@ -0,0 +1,70 @@
+import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
+import { SOLID_HTTP } from '../../util/Vocabularies';
+import { ACCOUNT_SETTINGS_REMEMBER_LOGIN } from './account/util/Account';
+import type { AccountStore } from './account/util/AccountStore';
+import type { CookieStore } from './account/util/CookieStore';
+import type { JsonRepresentation } from './InteractionUtil';
+import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
+import { JsonInteractionHandler } from './JsonInteractionHandler';
+
+/**
+ * Handles all the necessary steps for having cookies.
+ * Refreshes the cookie expiration if there was a successful account interaction.
+ * Adds the cookie and cookie expiration data to the output metadata,
+ * unless it is already present in that metadata.
+ * Checks the account settings to see if the cookie needs to be remembered.
+ */
+export class CookieInteractionHandler extends JsonInteractionHandler {
+ private readonly source: JsonInteractionHandler;
+ private readonly accountStore: AccountStore;
+ private readonly cookieStore: CookieStore;
+
+ public constructor(source: JsonInteractionHandler, accountStore: AccountStore, cookieStore: CookieStore) {
+ super();
+ this.source = source;
+ this.accountStore = accountStore;
+ this.cookieStore = cookieStore;
+ }
+
+ public async canHandle(input: JsonInteractionHandlerInput): Promise {
+ return this.source.canHandle(input);
+ }
+
+ public async handle(input: JsonInteractionHandlerInput): Promise {
+ const output = await this.source.handle(input);
+
+ let { metadata: outputMetadata } = output;
+
+ // The cookie could be new, in the output, or the one received in the input if no new cookie is made
+ const cookie = outputMetadata?.get(SOLID_HTTP.terms.accountCookie)?.value ??
+ input.metadata.get(SOLID_HTTP.terms.accountCookie)?.value;
+ // Only update the expiration if it wasn't set by the source handler,
+ // as that might have a specific reason, such as logging out.
+ if (!cookie || outputMetadata?.has(SOLID_HTTP.terms.accountCookieExpiration)) {
+ return output;
+ }
+ // Not reusing the account ID from the input,
+ // as that could potentially belong to a different account if this is a new login action.
+ const accountId = await this.cookieStore.get(cookie);
+
+ // Only refresh the cookie if it points to an account that exists and wants to be remembered
+ if (!accountId) {
+ return output;
+ }
+ const account = await this.accountStore.get(accountId);
+ if (!account?.settings[ACCOUNT_SETTINGS_REMEMBER_LOGIN]) {
+ return output;
+ }
+
+ // Refresh the cookie, could be undefined if it was deleted by the operation
+ const expiration = await this.cookieStore.refresh(cookie);
+ if (expiration) {
+ outputMetadata = outputMetadata ?? new RepresentationMetadata(input.target);
+ outputMetadata.set(SOLID_HTTP.terms.accountCookie, cookie);
+ outputMetadata.set(SOLID_HTTP.terms.accountCookieExpiration, expiration.toISOString());
+ output.metadata = outputMetadata;
+ }
+
+ return output;
+ }
+}
diff --git a/src/identity/interaction/FixedInteractionHandler.ts b/src/identity/interaction/FixedInteractionHandler.ts
deleted file mode 100644
index 5237de868..000000000
--- a/src/identity/interaction/FixedInteractionHandler.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable tsdoc/syntax */
-// tsdoc/syntax cannot handle `@range`
-import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
-import type { Representation } from '../../http/representation/Representation';
-import { APPLICATION_JSON } from '../../util/ContentTypes';
-import type { InteractionHandlerInput } from './InteractionHandler';
-import { InteractionHandler } from './InteractionHandler';
-
-/**
- * An {@link InteractionHandler} that always returns the same JSON response on all requests.
- */
-export class FixedInteractionHandler extends InteractionHandler {
- private readonly response: string;
-
- /**
- * @param response - @range {json}
- */
- public constructor(response: Record) {
- super();
- this.response = JSON.stringify(response);
- }
-
- public async handle({ operation }: InteractionHandlerInput): Promise {
- return new BasicRepresentation(this.response, operation.target, APPLICATION_JSON);
- }
-}
diff --git a/src/identity/interaction/HtmlViewHandler.ts b/src/identity/interaction/HtmlViewHandler.ts
index 76cd59c8a..ea567ca77 100644
--- a/src/identity/interaction/HtmlViewHandler.ts
+++ b/src/identity/interaction/HtmlViewHandler.ts
@@ -10,14 +10,26 @@ import type { InteractionHandlerInput } from './InteractionHandler';
import { InteractionHandler } from './InteractionHandler';
import type { InteractionRoute } from './routing/InteractionRoute';
+/**
+ * Used to link file paths and URLs together.
+ * The reason we use a separate object instead of a key/value Record,
+ * is that this makes it easier to override the values in Components.js,
+ * which can be useful if someone wants to replace the HTML for certain URLs.
+ */
+export class HtmlViewEntry {
+ public constructor(
+ public readonly route: InteractionRoute,
+ public readonly filePath: string,
+ ) { }
+}
+
/**
* Stores the HTML templates associated with specific InteractionRoutes.
- * Template keys should be file paths to the templates,
- * values should be the corresponding routes.
*
- * Will only handle GET operations for which there is a matching template if HTML is more preferred than JSON.
- * Reason for doing it like this instead of a standard content negotiation flow
- * is because we only want to return the HTML pages on GET requests. *
+ * This class will only handle GET operations for which there is a matching template,
+ * if HTML is more preferred than JSON.
+ * The reason for doing it like this instead of a standard content negotiation flow,
+ * is because we only want to return the HTML pages on GET requests.
*
* Templates will receive the parameter `idpIndex` in their context pointing to the root index URL of the IDP API
* and an `authenticating` parameter indicating if this is an active OIDC interaction.
@@ -25,37 +37,47 @@ import type { InteractionRoute } from './routing/InteractionRoute';
export class HtmlViewHandler extends InteractionHandler {
private readonly idpIndex: string;
private readonly templateEngine: TemplateEngine;
- private readonly templates: Record;
+ private readonly templates: HtmlViewEntry[];
- public constructor(index: InteractionRoute, templateEngine: TemplateEngine,
- templates: Record) {
+ public constructor(index: InteractionRoute, templateEngine: TemplateEngine, templates: HtmlViewEntry[]) {
super();
this.idpIndex = index.getPath();
this.templateEngine = templateEngine;
- this.templates = Object.fromEntries(
- Object.entries(templates).map(([ template, route ]): [ string, string ] => [ route.getPath(), template ]),
- );
+ this.templates = templates;
}
public async canHandle({ operation }: InteractionHandlerInput): Promise {
if (operation.method !== 'GET') {
throw new MethodNotAllowedHttpError([ operation.method ]);
}
- if (!this.templates[operation.target.path]) {
- throw new NotFoundHttpError();
- }
+
const preferences = cleanPreferences(operation.preferences.type);
const htmlWeight = getTypeWeight(TEXT_HTML, preferences);
const jsonWeight = getTypeWeight(APPLICATION_JSON, preferences);
if (jsonWeight >= htmlWeight) {
throw new NotImplementedHttpError('HTML views are only returned when they are preferred.');
}
+
+ // Will throw error if no match is found
+ this.findTemplate(operation.target.path);
}
public async handle({ operation, oidcInteraction }: InteractionHandlerInput): Promise {
- const template = this.templates[operation.target.path];
+ const template = this.findTemplate(operation.target.path);
const contents = { idpIndex: this.idpIndex, authenticating: Boolean(oidcInteraction) };
const result = await this.templateEngine.handleSafe({ contents, template: { templateFile: template }});
return new BasicRepresentation(result, operation.target, TEXT_HTML);
}
+
+ /**
+ * Finds the template for the given URL.
+ */
+ private findTemplate(target: string): string {
+ for (const template of this.templates) {
+ if (template.route.matchPath(target)) {
+ return template.filePath;
+ }
+ }
+ throw new NotFoundHttpError();
+ }
}
diff --git a/src/identity/interaction/InteractionHandler.ts b/src/identity/interaction/InteractionHandler.ts
index b044df895..7964a6a18 100644
--- a/src/identity/interaction/InteractionHandler.ts
+++ b/src/identity/interaction/InteractionHandler.ts
@@ -1,8 +1,6 @@
import type { KoaContextWithOIDC } from '../../../templates/types/oidc-provider';
import type { Operation } from '../../http/Operation';
import type { Representation } from '../../http/representation/Representation';
-import { APPLICATION_JSON } from '../../util/ContentTypes';
-import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
// OIDC library does not directly export the Interaction type
@@ -18,17 +16,13 @@ export interface InteractionHandlerInput {
* such as logging a user in.
*/
oidcInteraction?: Interaction;
+ /**
+ * The account id of the agent doing the request if one could be found.
+ */
+ accountId?: string;
}
/**
* Handler used for IDP interactions.
- * Only supports JSON data.
*/
-export abstract class InteractionHandler extends AsyncHandler {
- public async canHandle({ operation }: InteractionHandlerInput): Promise {
- const { contentType } = operation.body.metadata;
- if (contentType && contentType !== APPLICATION_JSON) {
- throw new NotImplementedHttpError('Only application/json data is supported.');
- }
- }
-}
+export abstract class InteractionHandler extends AsyncHandler { }
diff --git a/src/identity/interaction/InteractionUtil.ts b/src/identity/interaction/InteractionUtil.ts
new file mode 100644
index 000000000..c86481e0e
--- /dev/null
+++ b/src/identity/interaction/InteractionUtil.ts
@@ -0,0 +1,86 @@
+import type { InteractionResults } from '../../../templates/types/oidc-provider';
+import type Provider from '../../../templates/types/oidc-provider';
+import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
+import { getLoggerFor } from '../../logging/LogUtil';
+import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
+import type { Interaction } from './InteractionHandler';
+import Dict = NodeJS.Dict;
+
+const logger = getLoggerFor('AccountUtil');
+
+/**
+ * A JSON object.
+ */
+export type Json = string | number | boolean | Dict | Json[];
+
+/**
+ * Contains a JSON object and any associated metadata.
+ * Similar to a {@link Representation} but with all the data in memory instead of as a stream
+ * and specific to JSON.
+ */
+export interface JsonRepresentation = Dict> {
+ json: T;
+ metadata?: RepresentationMetadata;
+}
+
+/**
+ * Asserts `oidcInteraction` is defined, throws the correct error in case this is not the case.
+ * The error contains the relevant error code that can be used to explain more extensively what the issue is
+ * and why an OIDC interaction is needed.
+ *
+ * @param oidcInteraction - Interaction object to check.
+ */
+export function assertOidcInteraction(oidcInteraction?: Interaction): asserts oidcInteraction is Interaction {
+ if (!oidcInteraction) {
+ logger.warn(`Trying to perform OIDC operation without being in an OIDC authentication flow`);
+ throw new BadRequestHttpError(
+ 'This action can only be performed as part of an OIDC authentication flow.',
+ { errorCode: 'E0002' },
+ );
+ }
+}
+
+/**
+ * The prompt that is used to track the account ID of a user during an OIDC interaction.
+ * The already existing `login` prompt in the {@link InteractionResults}
+ * is used to track the WebID that is chosen in an OIDC interaction.
+ */
+export const ACCOUNT_PROMPT = 'account';
+/**
+ * {@link InteractionResults} extended with our custom key for tracking a user's account ID.
+ */
+export type AccountInteractionResults = { [ACCOUNT_PROMPT]?: string } & InteractionResults;
+
+/**
+ * Updates the `oidcInteraction` object with the necessary data in case a prompt gets updated.
+ * @param oidcInteraction - Interaction to update.
+ * @param result - New data to add to the interaction.
+ * @param mergeWithLastSubmission - If this new data needs to be merged with already existing data in the interaction.
+ */
+export async function finishInteraction(oidcInteraction: Interaction, result: AccountInteractionResults,
+ mergeWithLastSubmission: boolean): Promise {
+ if (mergeWithLastSubmission) {
+ result = { ...oidcInteraction.lastSubmission, ...result };
+ }
+
+ oidcInteraction.result = result;
+ await oidcInteraction.persist();
+
+ return oidcInteraction.returnTo;
+}
+
+/**
+ * Removes the WebID, the `accountId`, from the OIDC session object,
+ * allowing us to replace it with a new value.
+ * If there is no session in the Interaction, nothing will happen.
+ * @param provider - The OIDC provider.
+ * @param oidcInteraction - The current interaction.
+ */
+export async function forgetWebId(provider: Provider, oidcInteraction: Interaction): Promise {
+ if (oidcInteraction.session) {
+ const session = (await provider.Session.find(oidcInteraction.session.cookie))!;
+ logger.debug(`Forgetting WebID ${session.accountId} in active session`);
+ delete session.accountId;
+ await session.persist();
+ }
+}
diff --git a/src/identity/interaction/JsonConversionHandler.ts b/src/identity/interaction/JsonConversionHandler.ts
new file mode 100644
index 000000000..80af8ada1
--- /dev/null
+++ b/src/identity/interaction/JsonConversionHandler.ts
@@ -0,0 +1,72 @@
+import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
+import type { Representation } from '../../http/representation/Representation';
+import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
+import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
+import { APPLICATION_JSON } from '../../util/ContentTypes';
+import { readJsonStream } from '../../util/StreamUtil';
+import type { InteractionHandlerInput } from './InteractionHandler';
+import { InteractionHandler } from './InteractionHandler';
+import type { Json } from './InteractionUtil';
+import type { JsonInteractionHandler, JsonInteractionHandlerInput } from './JsonInteractionHandler';
+
+/**
+ * An {@link InteractionHandler} that sits in-between
+ * an {@link InteractionHandler} and a {@link JsonInteractionHandler}.
+ * It converts the input data stream into a JSON object to be used by the stored handler.
+ *
+ * Since the JSON body is only made during the `handle` call, it can not be used during the `canHandle`,
+ * so the `canHandle` call of the stored handler is not called,
+ * meaning this class accepts all input that can be converted to JSON.
+ */
+export class JsonConversionHandler extends InteractionHandler {
+ private readonly source: JsonInteractionHandler;
+ private readonly converter: RepresentationConverter;
+
+ public constructor(source: JsonInteractionHandler, converter: RepresentationConverter) {
+ super();
+ this.source = source;
+ this.converter = converter;
+ }
+
+ public async canHandle({ operation }: InteractionHandlerInput): Promise {
+ if (!operation.body.isEmpty) {
+ await this.converter.canHandle({
+ identifier: operation.target,
+ preferences: { type: { [APPLICATION_JSON]: 1 }},
+ representation: operation.body,
+ });
+ }
+ }
+
+ public async handle({ operation, oidcInteraction, accountId }: InteractionHandlerInput): Promise {
+ let json: Json = {};
+ let jsonMetadata = operation.body.metadata;
+
+ // Convert to JSON and read out if there is a body
+ if (!operation.body.isEmpty) {
+ const converted = await this.converter.handle({
+ identifier: operation.target,
+ preferences: { type: { [APPLICATION_JSON]: 1 }},
+ representation: operation.body,
+ });
+ json = await readJsonStream(converted.data);
+ jsonMetadata = converted.metadata;
+ }
+
+ // Input for the handler
+ const input: JsonInteractionHandlerInput = {
+ method: operation.method,
+ target: operation.target,
+ metadata: jsonMetadata,
+ json,
+ oidcInteraction,
+ accountId,
+ };
+
+ const result = await this.source.handleSafe(input);
+
+ // Convert the response JSON back to a Representation
+ const responseMetadata = result.metadata ?? new RepresentationMetadata(operation.target);
+ return new BasicRepresentation(JSON.stringify(result.json), responseMetadata, APPLICATION_JSON);
+ }
+}
diff --git a/src/identity/interaction/JsonInteractionHandler.ts b/src/identity/interaction/JsonInteractionHandler.ts
new file mode 100644
index 000000000..e2e1e65eb
--- /dev/null
+++ b/src/identity/interaction/JsonInteractionHandler.ts
@@ -0,0 +1,41 @@
+import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
+import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
+import { AsyncHandler } from '../../util/handlers/AsyncHandler';
+import type { Interaction } from './InteractionHandler';
+import type { Json, JsonRepresentation } from './InteractionUtil';
+import Dict = NodeJS.Dict;
+
+export interface JsonInteractionHandlerInput {
+ /**
+ * The operation to execute.
+ */
+ method: string;
+ /**
+ * The resource that is being targeted.
+ */
+ target: ResourceIdentifier;
+ /**
+ * The JSON body of the request.
+ */
+ json: unknown;
+ /**
+ * The metadata of the request.
+ */
+ metadata: RepresentationMetadata;
+ /**
+ * Will be defined if the OIDC library expects us to resolve an interaction it can't handle itself,
+ * such as logging a user in.
+ */
+ oidcInteraction?: Interaction;
+ /**
+ * The account id of the agent doing the request if one could be found.
+ */
+ accountId?: string;
+}
+
+/**
+ * A handler that consumes and returns a JSON object,
+ * designed to be used for IDP/OIDC interactions.
+ */
+export abstract class JsonInteractionHandler = Dict>
+ extends AsyncHandler> { }
diff --git a/src/identity/interaction/JsonView.ts b/src/identity/interaction/JsonView.ts
new file mode 100644
index 000000000..e7cb28602
--- /dev/null
+++ b/src/identity/interaction/JsonView.ts
@@ -0,0 +1,10 @@
+import type { JsonRepresentation } from './InteractionUtil';
+import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
+
+/**
+ * An interface that can be used by classes that can provide a view besides doing an action.
+ * Designed to be used by a {@link JsonInteractionHandler} that has a view explaining what JSON input it supports.
+ */
+export interface JsonView {
+ getView: (input: JsonInteractionHandlerInput) => Promise;
+}
diff --git a/src/identity/interaction/LocationInteractionHandler.ts b/src/identity/interaction/LocationInteractionHandler.ts
index 392307139..02b6ddb31 100644
--- a/src/identity/interaction/LocationInteractionHandler.ts
+++ b/src/identity/interaction/LocationInteractionHandler.ts
@@ -1,9 +1,10 @@
-import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
-import type { Representation } from '../../http/representation/Representation';
-import { APPLICATION_JSON } from '../../util/ContentTypes';
+import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
+import { getLoggerFor } from '../../logging/LogUtil';
import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
-import type { InteractionHandlerInput } from './InteractionHandler';
-import { InteractionHandler } from './InteractionHandler';
+import { SOLID_HTTP } from '../../util/Vocabularies';
+import type { JsonRepresentation } from './InteractionUtil';
+import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
+import { JsonInteractionHandler } from './JsonInteractionHandler';
/**
* Transforms an HTTP redirect into a hypermedia document with a redirection link,
@@ -22,25 +23,29 @@ import { InteractionHandler } from './InteractionHandler';
* with an explicit link to the next page,
* enabling the script to move the user to the next page.
*/
-export class LocationInteractionHandler extends InteractionHandler {
- private readonly source: InteractionHandler;
+export class LocationInteractionHandler extends JsonInteractionHandler {
+ private readonly logger = getLoggerFor(this);
- public constructor(source: InteractionHandler) {
+ private readonly source: JsonInteractionHandler;
+
+ public constructor(source: JsonInteractionHandler) {
super();
this.source = source;
}
- public async canHandle(input: InteractionHandlerInput): Promise {
+ public async canHandle(input: JsonInteractionHandlerInput): Promise {
await this.source.canHandle(input);
}
- public async handle(input: InteractionHandlerInput): Promise {
+ public async handle(input: JsonInteractionHandlerInput): Promise {
try {
return await this.source.handle(input);
} catch (error: unknown) {
if (RedirectHttpError.isInstance(error)) {
- const body = JSON.stringify({ location: error.location });
- return new BasicRepresentation(body, input.operation.target, APPLICATION_JSON);
+ this.logger.debug(`Converting redirect error to location field in JSON body with location ${error.location}`);
+ const metadata = new RepresentationMetadata(input.target);
+ metadata.set(SOLID_HTTP.terms.location, error.location);
+ return { json: { location: error.location }, metadata };
}
throw error;
}
diff --git a/src/identity/interaction/LockingInteractionHandler.ts b/src/identity/interaction/LockingInteractionHandler.ts
new file mode 100644
index 000000000..9e85010a1
--- /dev/null
+++ b/src/identity/interaction/LockingInteractionHandler.ts
@@ -0,0 +1,44 @@
+import type { Representation } from '../../http/representation/Representation';
+import type { ReadWriteLocker } from '../../util/locking/ReadWriteLocker';
+import type { AccountIdRoute } from './account/AccountIdRoute';
+import type { InteractionHandlerInput } from './InteractionHandler';
+import { InteractionHandler } from './InteractionHandler';
+
+const READ_METHODS = new Set([ 'OPTIONS', 'HEAD', 'GET' ]);
+
+/**
+ * An {@link InteractionHandler} that locks the path generated with the stored route during an operation.
+ * If the route is the base account route, this can be used to prevent multiple operations on the same account.
+ */
+export class LockingInteractionHandler extends InteractionHandler {
+ private readonly locker: ReadWriteLocker;
+ private readonly accountRoute: AccountIdRoute;
+ private readonly source: InteractionHandler;
+
+ public constructor(locker: ReadWriteLocker, accountRoute: AccountIdRoute, source: InteractionHandler) {
+ super();
+ this.locker = locker;
+ this.accountRoute = accountRoute;
+ this.source = source;
+ }
+
+ public async canHandle(input: InteractionHandlerInput): Promise {
+ return this.source.canHandle(input);
+ }
+
+ public async handle(input: InteractionHandlerInput): Promise {
+ const { accountId, operation } = input;
+
+ // No lock if there is no account
+ if (!accountId) {
+ return this.source.handle(input);
+ }
+
+ const identifier = { path: this.accountRoute.getPath({ accountId }) };
+ if (READ_METHODS.has(operation.method)) {
+ return this.locker.withReadLock(identifier, (): Promise => this.source.handle(input));
+ }
+
+ return this.locker.withWriteLock(identifier, (): Promise => this.source.handle(input));
+ }
+}
diff --git a/src/identity/interaction/OidcControlHandler.ts b/src/identity/interaction/OidcControlHandler.ts
new file mode 100644
index 000000000..7da91bc8c
--- /dev/null
+++ b/src/identity/interaction/OidcControlHandler.ts
@@ -0,0 +1,16 @@
+import { ControlHandler } from './ControlHandler';
+import type { Json } from './InteractionUtil';
+import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
+
+/**
+ * A {@link ControlHandler} that only returns results if there is an active OIDC interaction.
+ */
+export class OidcControlHandler extends ControlHandler {
+ protected async generateControls(input: JsonInteractionHandlerInput): Promise> {
+ if (!input.oidcInteraction) {
+ return {};
+ }
+
+ return super.generateControls(input);
+ }
+}
diff --git a/src/identity/interaction/PromptHandler.ts b/src/identity/interaction/PromptHandler.ts
deleted file mode 100644
index 1eb838204..000000000
--- a/src/identity/interaction/PromptHandler.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
-import { FoundHttpError } from '../../util/errors/FoundHttpError';
-import { InteractionHandler } from './InteractionHandler';
-import type { InteractionHandlerInput } from './InteractionHandler';
-import type { InteractionRoute } from './routing/InteractionRoute';
-
-/**
- * Redirects requests based on the OIDC Interaction prompt.
- * Errors in case no match was found.
- */
-export class PromptHandler extends InteractionHandler {
- private readonly promptRoutes: Record;
-
- public constructor(promptRoutes: Record) {
- super();
- this.promptRoutes = promptRoutes;
- }
-
- public async handle({ oidcInteraction }: InteractionHandlerInput): Promise {
- // We also want to redirect on GET so no method check is needed
- const prompt = oidcInteraction?.prompt.name;
- if (prompt && this.promptRoutes[prompt]) {
- const location = this.promptRoutes[prompt].getPath();
- throw new FoundHttpError(location);
- }
- throw new BadRequestHttpError(`Unsupported prompt: ${prompt}`);
- }
-}
diff --git a/src/identity/interaction/StaticInteractionHandler.ts b/src/identity/interaction/StaticInteractionHandler.ts
new file mode 100644
index 000000000..686680bef
--- /dev/null
+++ b/src/identity/interaction/StaticInteractionHandler.ts
@@ -0,0 +1,23 @@
+/* eslint-disable tsdoc/syntax */
+import type { Json, JsonRepresentation } from './InteractionUtil';
+// Tsdoc/syntax cannot handle `@range`
+import { JsonInteractionHandler } from './JsonInteractionHandler';
+
+/**
+ * An {@link JsonInteractionHandler} that always returns the same JSON response on all requests.
+ */
+export class StaticInteractionHandler extends JsonInteractionHandler {
+ private readonly response: Record;
+
+ /**
+ * @param response - @range {json}
+ */
+ public constructor(response: Record) {
+ super();
+ this.response = response;
+ }
+
+ public async handle(): Promise {
+ return { json: this.response };
+ }
+}
diff --git a/src/identity/interaction/VersionHandler.ts b/src/identity/interaction/VersionHandler.ts
new file mode 100644
index 000000000..4aaf9b479
--- /dev/null
+++ b/src/identity/interaction/VersionHandler.ts
@@ -0,0 +1,28 @@
+import type { JsonRepresentation } from './InteractionUtil';
+import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
+import { JsonInteractionHandler } from './JsonInteractionHandler';
+
+const INTERNAL_API_VERSION = '0.5';
+
+/**
+ * Adds the current version of the API to the JSON output.
+ * This version number should be updated every time the API changes.
+ */
+export class VersionHandler extends JsonInteractionHandler {
+ private readonly source: JsonInteractionHandler;
+
+ public constructor(source: JsonInteractionHandler) {
+ super();
+ this.source = source;
+ }
+
+ public async canHandle(input: JsonInteractionHandlerInput): Promise {
+ await this.source.canHandle(input);
+ }
+
+ public async handle(input: JsonInteractionHandlerInput): Promise {
+ const result = await this.source.handle(input);
+ result.json.version = INTERNAL_API_VERSION;
+ return result;
+ }
+}
diff --git a/src/identity/interaction/ViewInteractionHandler.ts b/src/identity/interaction/ViewInteractionHandler.ts
new file mode 100644
index 000000000..23ac47576
--- /dev/null
+++ b/src/identity/interaction/ViewInteractionHandler.ts
@@ -0,0 +1,40 @@
+import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
+import type { JsonRepresentation } from './InteractionUtil';
+import type { JsonInteractionHandlerInput } from './JsonInteractionHandler';
+import { JsonInteractionHandler } from './JsonInteractionHandler';
+import type { JsonView } from './JsonView';
+
+/**
+ * Utility class for the common case of a {@link JsonInteractionHandler}
+ * describing the expected input on a GET request which is needed to do a POST request.
+ *
+ * Returns the result of a {@link JsonView} on GET requests.
+ * POST requests are sent to the {@link JsonInteractionHandler}.
+ * Other methods will be rejected.
+ */
+export class ViewInteractionHandler extends JsonInteractionHandler {
+ private readonly source: JsonInteractionHandler & JsonView;
+
+ public constructor(source: JsonInteractionHandler & JsonView) {
+ super();
+ this.source = source;
+ }
+
+ public async canHandle(input: JsonInteractionHandlerInput): Promise {
+ const { method } = input;
+ if (method !== 'GET' && method !== 'POST') {
+ throw new MethodNotAllowedHttpError([ method ], 'Only GET/POST requests are supported.');
+ }
+
+ if (method === 'POST') {
+ await this.source.canHandle(input);
+ }
+ }
+
+ public async handle(input: JsonInteractionHandlerInput): Promise {
+ if (input.method === 'GET') {
+ return this.source.getView(input);
+ }
+ return this.source.handle(input);
+ }
+}
diff --git a/src/identity/interaction/YupUtil.ts b/src/identity/interaction/YupUtil.ts
new file mode 100644
index 000000000..bf4059527
--- /dev/null
+++ b/src/identity/interaction/YupUtil.ts
@@ -0,0 +1,68 @@
+import { string } from 'yup';
+import type { ObjectSchema, Schema, ValidateOptions } from 'yup';
+import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
+import { createErrorMessage } from '../../util/errors/ErrorUtil';
+import { isUrl } from '../../util/StringUtil';
+import type { Json } from './InteractionUtil';
+import Dict = NodeJS.Dict;
+
+// The builtin `url` validator of `yup` does not support localhost URLs, so we create a custom one here.
+// The reason for having a URL validator on the WebID is to prevent us from generating invalid ACL,
+// which would break the pod creation causing us to have an incomplete pod.
+export const URL_SCHEMA = string().trim().optional().test({
+ name: 'url',
+ message: (value): string => `"${value.value}" is not a valid URL`,
+ test(value): boolean {
+ if (!value) {
+ return true;
+ }
+ return isUrl(value);
+ },
+});
+
+function isObjectSchema(schema: Schema): schema is ObjectSchema {
+ return schema.type === 'object';
+}
+
+// `T` can't extend Schema since it could also be a Reference, which is a type `yup` doesn't export
+type SchemaType = T extends ObjectSchema ? ObjectType : { required: boolean; type: string };
+// The type of the fields in an object schema
+type FieldType> = T extends { fields: Record } ? R : never;
+// Simplified type we use to represent yup objects
+type ObjectType> =
+ { required: boolean; type: 'object'; fields: {[ K in FieldType ]: SchemaType }};
+
+/**
+ * Recursive function used when generating yup schema representations.
+ */
+function parseSchemaDescription(schema: T): SchemaType {
+ const result: Dict = { required: !schema.spec.optional, type: schema.type };
+ if (isObjectSchema(schema)) {
+ result.fields = {};
+ for (const [ field, description ] of Object.entries(schema.fields)) {
+ // We never use references so this cast is fine
+ result.fields[field] = parseSchemaDescription(description as Schema);
+ }
+ }
+ return result as SchemaType;
+}
+
+/**
+ * Generates a simplified representation of a yup schema.
+ */
+export function parseSchema>(schema: T): Pick, 'fields'> {
+ const result = parseSchemaDescription(schema);
+ return { fields: result.fields };
+}
+
+/**
+ * Same functionality as the yup validate function, but throws a {@link BadRequestHttpError} if there is an error.
+ */
+export async function validateWithError>(schema: T, data: unknown,
+ options?: ValidateOptions): Promise {
+ try {
+ return await schema.validate(data, options);
+ } catch (error: unknown) {
+ throw new BadRequestHttpError(createErrorMessage(error));
+ }
+}
diff --git a/src/identity/interaction/account/AccountDetailsHandler.ts b/src/identity/interaction/account/AccountDetailsHandler.ts
new file mode 100644
index 000000000..c60493441
--- /dev/null
+++ b/src/identity/interaction/account/AccountDetailsHandler.ts
@@ -0,0 +1,29 @@
+import type { Json, JsonRepresentation } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+import type { Account } from './util/Account';
+import type { AccountStore } from './util/AccountStore';
+import { getRequiredAccount } from './util/AccountUtil';
+import Dict = NodeJS.Dict;
+
+/**
+ * Outputs a JSON description of the account details.
+ */
+export class AccountDetailsHandler extends JsonInteractionHandler {
+ private readonly accountStore: AccountStore;
+
+ public constructor(accountStore: AccountStore) {
+ super();
+ this.accountStore = accountStore;
+ }
+
+ public async handle({ accountId }: JsonInteractionHandlerInput): Promise> {
+ const account = await getRequiredAccount(this.accountStore, accountId);
+
+ // The ID does not need to be in the JSON
+ const json: Dict = account;
+ delete json.id;
+
+ return { json: account };
+ }
+}
diff --git a/src/identity/interaction/account/AccountIdRoute.ts b/src/identity/interaction/account/AccountIdRoute.ts
new file mode 100644
index 000000000..d47e441c3
--- /dev/null
+++ b/src/identity/interaction/account/AccountIdRoute.ts
@@ -0,0 +1,21 @@
+import { IdInteractionRoute } from '../routing/IdInteractionRoute';
+import type { InteractionRoute } from '../routing/InteractionRoute';
+
+// AccountIdKey = typeof ACCOUNT_ID_KEY does not work because Components.js doesn't support typeof like that
+
+export type AccountIdKey = 'accountId';
+export const ACCOUNT_ID_KEY: AccountIdKey = 'accountId';
+
+/**
+ * A route that includes an account identifier.
+ */
+export type AccountIdRoute = InteractionRoute;
+
+/**
+ * Implementation of an {@link AccountIdRoute} that adds the identifier relative to a base {@link InteractionRoute}.
+ */
+export class BaseAccountIdRoute extends IdInteractionRoute implements AccountIdRoute {
+ public constructor(base: InteractionRoute) {
+ super(base, 'accountId');
+ }
+}
diff --git a/src/identity/interaction/account/CreateAccountHandler.ts b/src/identity/interaction/account/CreateAccountHandler.ts
new file mode 100644
index 000000000..e42ca9e9b
--- /dev/null
+++ b/src/identity/interaction/account/CreateAccountHandler.ts
@@ -0,0 +1,27 @@
+import type { EmptyObject } from '../../../util/map/MapUtil';
+import type { JsonRepresentation } from '../InteractionUtil';
+import type { JsonView } from '../JsonView';
+import type { LoginOutputType } from '../login/ResolveLoginHandler';
+import { ResolveLoginHandler } from '../login/ResolveLoginHandler';
+import type { AccountIdRoute } from './AccountIdRoute';
+import type { AccountStore } from './util/AccountStore';
+import type { CookieStore } from './util/CookieStore';
+
+/**
+ * Creates new accounts using an {@link AccountStore};
+ */
+export class CreateAccountHandler extends ResolveLoginHandler implements JsonView {
+ public constructor(accountStore: AccountStore, cookieStore: CookieStore, accountRoute: AccountIdRoute) {
+ super(accountStore, cookieStore, accountRoute);
+ }
+
+ public async getView(): Promise> {
+ return { json: {}};
+ }
+
+ public async login(): Promise> {
+ const account = await this.accountStore.create();
+
+ return { json: { accountId: account.id }};
+ }
+}
diff --git a/src/identity/interaction/account/util/Account.ts b/src/identity/interaction/account/util/Account.ts
new file mode 100644
index 000000000..c001c17ae
--- /dev/null
+++ b/src/identity/interaction/account/util/Account.ts
@@ -0,0 +1,57 @@
+import type { Json } from '../../InteractionUtil';
+import Dict = NodeJS.Dict;
+
+/**
+ * Settings parameter used to determine if the user wants the login to be remembered.
+ */
+export const ACCOUNT_SETTINGS_REMEMBER_LOGIN = 'rememberLogin';
+
+/**
+ * Object used to keep track of all the relevant account data.
+ * All key/value objects stored in this are expected to have the same similar structure:
+ * the keys should be the unique value relevant for that type of data,
+ * while the values should be the URL of the corresponding resource that can be used to potentially modify this entry.
+ */
+export type Account = {
+ /**
+ * A unique identifier for this account.
+ */
+ readonly id: string;
+ /**
+ * All login methods that can be used to identify as this account.
+ * As one login method can have multiple entries, this is a nested map.
+ * You could have several different e-mail addresses to log in with for example.
+ * The keys of the first map are the unique identifiers of the login methods.
+ * The keys of the second map are the unique identifiers of the entry within that login method.
+ *
+ * For example, assume we have a login method `password` that uses e-mail addresses to identify entries,
+ * this could look as follows:
+ * `{ logins: { password: { ['test@example.com']: 'http://localhost:3000/.account/123/logins/password/123' } } }`.
+ *
+ * Implementations should make sure it is not possible to have an account without login method,
+ * as that would make the account inaccessible.
+ */
+ readonly logins: Dict>;
+ /**
+ * The pods this account is the owner of.
+ * The keys are the base URLs of those pods.
+ */
+ readonly pods: Dict;
+ /**
+ * All WebIDs registered to this account,
+ * meaning this account can identify as any of these WebIDs after logging in.
+ * The keys are the actual WebIDs.
+ */
+ readonly webIds: Dict;
+ /**
+ * The client credentials stored for this account.
+ * The keys are the IDs of the tokens.
+ */
+ readonly clientCredentials: Dict;
+ /**
+ * Various settings of the account.
+ * This is an open-ended object that can be used for any settings that need to be tracked on an account,
+ * hence there are no strict typings on the values.
+ */
+ readonly settings: Dict;
+};
diff --git a/src/identity/interaction/account/util/AccountStore.ts b/src/identity/interaction/account/util/AccountStore.ts
new file mode 100644
index 000000000..16e20e20b
--- /dev/null
+++ b/src/identity/interaction/account/util/AccountStore.ts
@@ -0,0 +1,28 @@
+import type { Account } from './Account';
+
+/**
+ * Used to store account data.
+ */
+export interface AccountStore {
+ /**
+ * Creates a new and completely empty account.
+ * Since this account will not yet have a login method,
+ * implementations should restrict what is possible with this account,
+ * and should potentially have something in place to clean these accounts up if they are unused.
+ */
+ create: () => Promise;
+ /**
+ * Finds the account with the given identifier.
+ * @param id - The account identifier.
+ */
+ get: (id: string) => Promise;
+ /**
+ * Updates the account with the given values.
+ * The account will be completely overwritten with the provided {@link Account} object.
+ *
+ * It should not be possible to update an account to have no login methods.
+ *
+ * @param account - The new values for the account.
+ */
+ update: (account: Account) => Promise;
+}
diff --git a/src/identity/interaction/account/util/AccountUtil.ts b/src/identity/interaction/account/util/AccountUtil.ts
new file mode 100644
index 000000000..c14448431
--- /dev/null
+++ b/src/identity/interaction/account/util/AccountUtil.ts
@@ -0,0 +1,93 @@
+import { getLoggerFor } from '../../../../logging/LogUtil';
+import { NotFoundHttpError } from '../../../../util/errors/NotFoundHttpError';
+import type { Account } from './Account';
+import type { AccountStore } from './AccountStore';
+import Dict = NodeJS.Dict;
+
+const logger = getLoggerFor('AccountUtil');
+
+/**
+ * Finds the account in the store for the given `accountId`.
+ * Throws a {@link NotFoundHttpError} if no account is found.
+ *
+ * @param accountStore - Account store to look in.
+ * @param accountId - Identifier to look up.
+ */
+export async function getRequiredAccount(accountStore: AccountStore, accountId?: string): Promise {
+ const account = accountId && await accountStore.get(accountId);
+ if (!account) {
+ logger.debug('Missing account');
+ throw new NotFoundHttpError();
+ }
+ return account;
+}
+
+/**
+ * Looks for the key in the provided `data` object with `resource` as value.
+ * This was designed specifically for working with {@link Account} data where you have a resource
+ * but don't know which key it is associated with.
+ *
+ * @param data - Object to look in.
+ * @param resource - The resource URL.
+ *
+ * @throws A {@link NotFoundHttpError} if no match could be found.
+ */
+export function ensureResource(data?: Dict, resource?: string): string {
+ if (!data || !resource) {
+ throw new NotFoundHttpError();
+ }
+ const token = Object.keys(data).find((key): boolean => data[key] === resource);
+ if (!token) {
+ logger.debug(`Missing resource ${resource}`);
+ throw new NotFoundHttpError();
+ }
+ return token;
+}
+
+/**
+ * Adds a login entry for a specific login method to the account data.
+ *
+ * @param account - {@link Account} to update.
+ * @param method - Name of the login method.
+ * @param key - Key of the entry.
+ * @param resource - Resource associated with the entry.
+ */
+export function addLoginEntry(account: Account, method: string, key: string, resource: string): void {
+ const logins = account.logins[method] ?? {};
+ account.logins[method] = logins;
+ logins[key] = resource;
+}
+
+/**
+ * Updates {@link Account} data in such a way to minimize chances of incomplete updates
+ * when multiple storages have to be updated simultaneously.
+ *
+ * First the `accountStore` will be used to update the account, then the `operation` function will be executed.
+ * If that latter call fails, the updates done to the account will be reverted.
+ * In both success and failure, the result of calling `operation` will be returned.
+ *
+ * @param account - The account object with the new data. If the `operation` call fails,
+ * this object will be updated to contain the original account data.
+ * @param accountStore - Store used to update the account.
+ * @param operation - Function to execute safely.
+ */
+export async function safeUpdate(account: Account, accountStore: AccountStore, operation: () => Promise):
+Promise {
+ const oldAccount = await accountStore.get(account.id);
+ if (!oldAccount) {
+ throw new NotFoundHttpError();
+ }
+
+ await accountStore.update(account);
+ try {
+ return await operation();
+ } catch (error: unknown) {
+ logger.warn(`Error while updating account ${account.id}, reverting operation.`);
+ await accountStore.update(oldAccount);
+ // Update the keys of the input `account` variable to make sure it matches what is now stored again.
+ // This is relevant if the error thrown here is caught and the account object is still used for some reason.
+ Object.assign(account, oldAccount);
+
+ throw error;
+ }
+}
diff --git a/src/identity/interaction/account/util/BaseAccountStore.ts b/src/identity/interaction/account/util/BaseAccountStore.ts
new file mode 100644
index 000000000..ad6d6add8
--- /dev/null
+++ b/src/identity/interaction/account/util/BaseAccountStore.ts
@@ -0,0 +1,69 @@
+import { v4 } from 'uuid';
+import { getLoggerFor } from '../../../../logging/LogUtil';
+import type { ExpiringStorage } from '../../../../storage/keyvalue/ExpiringStorage';
+import { BadRequestHttpError } from '../../../../util/errors/BadRequestHttpError';
+import { NotFoundHttpError } from '../../../../util/errors/NotFoundHttpError';
+import type { Account } from './Account';
+import type { AccountStore } from './AccountStore';
+
+/**
+ * A {@link AccountStore} that uses an {@link ExpiringStorage} to keep track of the accounts.
+ * Created accounts will be removed after the chosen expiration in seconds, default 30 minutes,
+ * if no login method gets added.
+ *
+ * New accounts can not be updated unless the update includes at least 1 login method.
+ */
+export class BaseAccountStore implements AccountStore {
+ private readonly logger = getLoggerFor(this);
+
+ private readonly storage: ExpiringStorage;
+ private readonly expiration: number;
+
+ public constructor(storage: ExpiringStorage, expiration = 30 * 60) {
+ this.storage = storage;
+ this.expiration = expiration * 1000;
+ }
+
+ public async create(): Promise {
+ const id = v4();
+ const account: Account = {
+ id,
+ logins: {},
+ pods: {},
+ webIds: {},
+ clientCredentials: {},
+ settings: {},
+ };
+
+ // Expire accounts after some time if no login gets added
+ await this.storage.set(id, account, this.expiration);
+ this.logger.debug(`Created new account ${id}`);
+
+ return account;
+ }
+
+ public async get(id: string): Promise {
+ return this.storage.get(id);
+ }
+
+ public async update(account: Account): Promise {
+ const oldAccount = await this.get(account.id);
+ // Make sure the account exists
+ if (!oldAccount) {
+ this.logger.warn(`Trying to update account ${account.id} which does not exist`);
+ throw new NotFoundHttpError();
+ }
+
+ // Ensure there is at least 1 login method
+ const logins = Object.values(account.logins);
+ if (!logins.some((specificLogins): boolean => Object.keys(specificLogins ?? {}).length > 0)) {
+ this.logger.warn(`Trying to update account ${account.id} without login methods`);
+ throw new BadRequestHttpError('An account needs at least 1 login method.');
+ }
+
+ // This will disable the expiration if there still was one
+ await this.storage.set(account.id, account);
+
+ this.logger.debug(`Updated account ${account.id}`);
+ }
+}
diff --git a/src/identity/interaction/account/util/BaseCookieStore.ts b/src/identity/interaction/account/util/BaseCookieStore.ts
new file mode 100644
index 000000000..633b22f0f
--- /dev/null
+++ b/src/identity/interaction/account/util/BaseCookieStore.ts
@@ -0,0 +1,40 @@
+import { v4 } from 'uuid';
+import type { ExpiringStorage } from '../../../../storage/keyvalue/ExpiringStorage';
+import type { CookieStore } from './CookieStore';
+
+/**
+ * A {@link CookieStore} that uses an {@link ExpiringStorage} to keep track of the stored cookies.
+ * Cookies have a specified time to live in seconds, default is 14 days,
+ * after which they will be removed.
+ */
+export class BaseCookieStore implements CookieStore {
+ private readonly storage: ExpiringStorage;
+ private readonly ttl: number;
+
+ public constructor(storage: ExpiringStorage, ttl = 14 * 24 * 60 * 60) {
+ this.storage = storage;
+ this.ttl = ttl * 1000;
+ }
+
+ public async generate(accountId: string): Promise {
+ const cookie = v4();
+ await this.storage.set(cookie, accountId, this.ttl);
+ return cookie;
+ }
+
+ public async get(cookie: string): Promise {
+ return await this.storage.get(cookie);
+ }
+
+ public async refresh(cookie: string): Promise {
+ const accountId = await this.storage.get(cookie);
+ if (accountId) {
+ await this.storage.set(cookie, accountId, this.ttl);
+ return new Date(Date.now() + this.ttl);
+ }
+ }
+
+ public async delete(cookie: string): Promise {
+ return this.storage.delete(cookie);
+ }
+}
diff --git a/src/identity/interaction/account/util/CookieStore.ts b/src/identity/interaction/account/util/CookieStore.ts
new file mode 100644
index 000000000..d6ea3c9e8
--- /dev/null
+++ b/src/identity/interaction/account/util/CookieStore.ts
@@ -0,0 +1,31 @@
+/**
+ * Used to generate and store cookies.
+ */
+export interface CookieStore {
+ /**
+ * Generates and stores a new cookie for the given accountId.
+ * This does not replace previously generated cookies.
+ * @param accountId - Account to create a cookie for.
+ *
+ * @returns The generated cookie.
+ */
+ generate: (accountId: string) => Promise;
+
+ /**
+ * Return the accountID associated with the given cookie.
+ * @param cookie - Cookie to find the account for.
+ */
+ get: (cookie: string) => Promise;
+
+ /**
+ * Refreshes the cookie expiration and returns when it will expire if the cookie exists.
+ * @param cookie - Cookie to refresh.
+ */
+ refresh: (cookie: string) => Promise;
+
+ /**
+ * Deletes the given cookie.
+ * @param cookie - Cookie to delete.
+ */
+ delete: (cookie: string) => Promise;
+}
diff --git a/src/identity/interaction/client-credentials/ClientCredentialsAdapterFactory.ts b/src/identity/interaction/client-credentials/ClientCredentialsAdapterFactory.ts
new file mode 100644
index 000000000..af06862e8
--- /dev/null
+++ b/src/identity/interaction/client-credentials/ClientCredentialsAdapterFactory.ts
@@ -0,0 +1,80 @@
+import type { Adapter, AdapterPayload } from '../../../../templates/types/oidc-provider';
+import { getLoggerFor } from '../../../logging/LogUtil';
+import type { AdapterFactory } from '../../storage/AdapterFactory';
+import { PassthroughAdapterFactory, PassthroughAdapter } from '../../storage/PassthroughAdapterFactory';
+import type { AccountStore } from '../account/util/AccountStore';
+import type { ClientCredentialsStore } from './util/ClientCredentialsStore';
+
+/**
+ * A {@link PassthroughAdapter} that overrides the `find` function
+ * by checking if there are stored client credentials for the given ID
+ * if no payload is found in the source.
+ */
+export class ClientCredentialsAdapter extends PassthroughAdapter {
+ protected readonly logger = getLoggerFor(this);
+
+ private readonly accountStore: AccountStore;
+ private readonly clientCredentialsStore: ClientCredentialsStore;
+
+ public constructor(name: string, source: Adapter, accountStore: AccountStore,
+ clientCredentialsStore: ClientCredentialsStore) {
+ super(name, source);
+ this.accountStore = accountStore;
+ this.clientCredentialsStore = clientCredentialsStore;
+ }
+
+ public async find(id: string): Promise {
+ let payload = await this.source.find(id);
+
+ if (!payload && this.name === 'Client') {
+ const credentials = await this.clientCredentialsStore.get(id);
+ if (credentials) {
+ // Make sure the WebID is still linked to the account.
+ // Unlinking a WebID does not necessarily delete the corresponding credential tokens.
+ const account = await this.accountStore.get(credentials.accountId);
+ if (!account) {
+ this.logger.error(`Storage contains credentials ${id} with unknown account ID ${credentials.accountId}`);
+ return;
+ }
+
+ if (!account.webIds[credentials.webId]) {
+ this.logger.warn(
+ `Client credentials token ${id} contains WebID that is no longer linked to the account. Removing...`,
+ );
+ await this.clientCredentialsStore.delete(id, account);
+ return;
+ }
+
+ this.logger.debug(`Authenticating as ${credentials.webId} using client credentials`);
+
+ /* eslint-disable @typescript-eslint/naming-convention */
+ payload = {
+ client_id: id,
+ client_secret: credentials.secret,
+ grant_types: [ 'client_credentials' ],
+ redirect_uris: [],
+ response_types: [],
+ };
+ /* eslint-enable @typescript-eslint/naming-convention */
+ }
+ }
+ return payload;
+ }
+}
+
+export class ClientCredentialsAdapterFactory extends PassthroughAdapterFactory {
+ private readonly accountStore: AccountStore;
+ private readonly clientCredentialsStore: ClientCredentialsStore;
+
+ public constructor(source: AdapterFactory, accountStore: AccountStore,
+ clientCredentialsStore: ClientCredentialsStore) {
+ super(source);
+ this.accountStore = accountStore;
+ this.clientCredentialsStore = clientCredentialsStore;
+ }
+
+ public createStorageAdapter(name: string): Adapter {
+ const adapter = this.source.createStorageAdapter(name);
+ return new ClientCredentialsAdapter(name, adapter, this.accountStore, this.clientCredentialsStore);
+ }
+}
diff --git a/src/identity/interaction/client-credentials/ClientCredentialsDetailsHandler.ts b/src/identity/interaction/client-credentials/ClientCredentialsDetailsHandler.ts
new file mode 100644
index 000000000..e790f883d
--- /dev/null
+++ b/src/identity/interaction/client-credentials/ClientCredentialsDetailsHandler.ts
@@ -0,0 +1,48 @@
+import { getLoggerFor } from '../../../logging/LogUtil';
+import { InternalServerError } from '../../../util/errors/InternalServerError';
+import type { AccountStore } from '../account/util/AccountStore';
+import { ensureResource, getRequiredAccount } from '../account/util/AccountUtil';
+import type { JsonRepresentation } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+import type { ClientCredentialsStore } from './util/ClientCredentialsStore';
+
+type OutType = {
+ id: string;
+ webId: string;
+};
+
+/**
+ * Provides a view on a client credentials token, indicating the token identifier and its associated WebID.
+ */
+export class ClientCredentialsDetailsHandler extends JsonInteractionHandler {
+ protected readonly logger = getLoggerFor(this);
+
+ private readonly accountStore: AccountStore;
+ private readonly clientCredentialsStore: ClientCredentialsStore;
+
+ public constructor(accountStore: AccountStore, clientCredentialsStore: ClientCredentialsStore) {
+ super();
+ this.accountStore = accountStore;
+ this.clientCredentialsStore = clientCredentialsStore;
+ }
+
+ public async handle({ target, accountId }: JsonInteractionHandlerInput): Promise> {
+ const account = await getRequiredAccount(this.accountStore, accountId);
+
+ const id = ensureResource(account.clientCredentials, target.path);
+
+ const credentials = await this.clientCredentialsStore.get(id);
+ if (!credentials) {
+ this.logger.error(
+ `Data inconsistency between account and credentials data for account ${account.id} and token ${id}.`,
+ );
+ throw new InternalServerError('Data inconsistency between account and client credentials data.');
+ }
+
+ return { json: {
+ id,
+ webId: credentials.webId,
+ }};
+ }
+}
diff --git a/src/identity/interaction/client-credentials/CreateClientCredentialsHandler.ts b/src/identity/interaction/client-credentials/CreateClientCredentialsHandler.ts
new file mode 100644
index 000000000..88ca2ec9a
--- /dev/null
+++ b/src/identity/interaction/client-credentials/CreateClientCredentialsHandler.ts
@@ -0,0 +1,52 @@
+import { v4 } from 'uuid';
+import { object, string } from 'yup';
+import { sanitizeUrlPart } from '../../../util/StringUtil';
+import type { AccountStore } from '../account/util/AccountStore';
+import { getRequiredAccount } from '../account/util/AccountUtil';
+import type { JsonRepresentation } from '../InteractionUtil';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import type { JsonView } from '../JsonView';
+import { parseSchema, validateWithError } from '../YupUtil';
+import type { ClientCredentialsStore } from './util/ClientCredentialsStore';
+
+const inSchema = object({
+ name: string().trim().optional(),
+ webId: string().trim().required(),
+});
+
+type OutType = {
+ id: string;
+ secret: string;
+ resource: string;
+};
+
+/**
+ * Handles the creation of client credential tokens.
+ */
+export class CreateClientCredentialsHandler extends JsonInteractionHandler implements JsonView {
+ private readonly accountStore: AccountStore;
+ private readonly clientCredentialsStore: ClientCredentialsStore;
+
+ public constructor(accountStore: AccountStore, clientCredentialsStore: ClientCredentialsStore) {
+ super();
+ this.accountStore = accountStore;
+ this.clientCredentialsStore = clientCredentialsStore;
+ }
+
+ public async getView(): Promise {
+ return { json: parseSchema(inSchema) };
+ }
+
+ public async handle({ accountId, json }: JsonInteractionHandlerInput): Promise> {
+ const account = await getRequiredAccount(this.accountStore, accountId);
+
+ const { name, webId } = await validateWithError(inSchema, json);
+ const cleanedName = name ? sanitizeUrlPart(name.trim()) : '';
+ const id = `${cleanedName}_${v4()}`;
+
+ const { secret, resource } = await this.clientCredentialsStore.add(id, webId, account);
+
+ return { json: { id, secret, resource }};
+ }
+}
diff --git a/src/identity/interaction/client-credentials/DeleteClientCredentialsHandler.ts b/src/identity/interaction/client-credentials/DeleteClientCredentialsHandler.ts
new file mode 100644
index 000000000..d001d2cc4
--- /dev/null
+++ b/src/identity/interaction/client-credentials/DeleteClientCredentialsHandler.ts
@@ -0,0 +1,32 @@
+import type { EmptyObject } from '../../../util/map/MapUtil';
+import type { AccountStore } from '../account/util/AccountStore';
+import { ensureResource, getRequiredAccount } from '../account/util/AccountUtil';
+import type { JsonRepresentation } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+import type { ClientCredentialsStore } from './util/ClientCredentialsStore';
+
+/**
+ * Handles the deletion of client credentials tokens.
+ */
+export class DeleteClientCredentialsHandler extends JsonInteractionHandler {
+ private readonly accountStore: AccountStore;
+ private readonly clientCredentialsStore: ClientCredentialsStore;
+
+ public constructor(accountStore: AccountStore, clientCredentialsStore: ClientCredentialsStore) {
+ super();
+ this.accountStore = accountStore;
+ this.clientCredentialsStore = clientCredentialsStore;
+ }
+
+ public async handle({ target, accountId }: JsonInteractionHandlerInput): Promise> {
+ const account = await getRequiredAccount(this.accountStore, accountId);
+
+ const id = ensureResource(account.clientCredentials, target.path);
+
+ // This also deletes it from the account
+ await this.clientCredentialsStore.delete(id, account);
+
+ return { json: {}};
+ }
+}
diff --git a/src/identity/interaction/client-credentials/util/BaseClientCredentialsStore.ts b/src/identity/interaction/client-credentials/util/BaseClientCredentialsStore.ts
new file mode 100644
index 000000000..120708684
--- /dev/null
+++ b/src/identity/interaction/client-credentials/util/BaseClientCredentialsStore.ts
@@ -0,0 +1,63 @@
+import { randomBytes } from 'crypto';
+import { getLoggerFor } from '../../../../logging/LogUtil';
+import type { KeyValueStorage } from '../../../../storage/keyvalue/KeyValueStorage';
+import { BadRequestHttpError } from '../../../../util/errors/BadRequestHttpError';
+import type { Account } from '../../account/util/Account';
+import type { AccountStore } from '../../account/util/AccountStore';
+import { safeUpdate } from '../../account/util/AccountUtil';
+import type { ClientCredentialsIdRoute } from './ClientCredentialsIdRoute';
+import type { ClientCredentials, ClientCredentialsStore } from './ClientCredentialsStore';
+
+/**
+ * A {@link ClientCredentialsStore} that uses a {@link KeyValueStorage} for storing the tokens.
+ */
+export class BaseClientCredentialsStore implements ClientCredentialsStore {
+ private readonly logger = getLoggerFor(this);
+
+ private readonly clientCredentialsRoute: ClientCredentialsIdRoute;
+ private readonly accountStore: AccountStore;
+ private readonly storage: KeyValueStorage;
+
+ public constructor(clientCredentialsRoute: ClientCredentialsIdRoute, accountStore: AccountStore,
+ storage: KeyValueStorage) {
+ this.clientCredentialsRoute = clientCredentialsRoute;
+ this.accountStore = accountStore;
+ this.storage = storage;
+ }
+
+ public async get(id: string): Promise {
+ return this.storage.get(id);
+ }
+
+ public async add(id: string, webId: string, account: Account): Promise<{ secret: string; resource: string }> {
+ if (typeof account.webIds[webId] !== 'string') {
+ this.logger.warn(`Trying to create token for ${webId} which does not belong to account ${account.id}`);
+ throw new BadRequestHttpError('WebID does not belong to this account.');
+ }
+
+ const secret = randomBytes(64).toString('hex');
+ const resource = this.clientCredentialsRoute.getPath({ accountId: account.id, clientCredentialsId: id });
+
+ account.clientCredentials[id] = resource;
+ await safeUpdate(account,
+ this.accountStore,
+ (): Promise => this.storage.set(id, { accountId: account.id, secret, webId }));
+
+ this.logger.debug(`Created client credentials token ${id} for WebID ${webId} and account ${account.id}`);
+
+ return { secret, resource };
+ }
+
+ public async delete(id: string, account: Account): Promise {
+ const link = account.clientCredentials[id];
+
+ if (link) {
+ delete account.clientCredentials[id];
+ await safeUpdate(account,
+ this.accountStore,
+ (): Promise => this.storage.delete(id));
+
+ this.logger.debug(`Deleted client credentials token ${id} for account ${account.id}`);
+ }
+ }
+}
diff --git a/src/identity/interaction/client-credentials/util/ClientCredentialsIdRoute.ts b/src/identity/interaction/client-credentials/util/ClientCredentialsIdRoute.ts
new file mode 100644
index 000000000..a9044e03f
--- /dev/null
+++ b/src/identity/interaction/client-credentials/util/ClientCredentialsIdRoute.ts
@@ -0,0 +1,20 @@
+import type { AccountIdKey, AccountIdRoute } from '../../account/AccountIdRoute';
+import { IdInteractionRoute } from '../../routing/IdInteractionRoute';
+import type { ExtendedRoute } from '../../routing/InteractionRoute';
+
+export type CredentialsIdKey = 'clientCredentialsId';
+
+/**
+ * An {@link AccountIdRoute} that also includes a credentials identifier.
+ */
+export type ClientCredentialsIdRoute = ExtendedRoute;
+
+/**
+ * Implementation of an {@link ClientCredentialsIdRoute}
+ * that adds the identifier relative to a base {@link AccountIdRoute}.
+ */
+export class BaseClientCredentialsIdRoute extends IdInteractionRoute {
+ public constructor(base: AccountIdRoute) {
+ super(base, 'clientCredentialsId');
+ }
+}
diff --git a/src/identity/interaction/client-credentials/util/ClientCredentialsStore.ts b/src/identity/interaction/client-credentials/util/ClientCredentialsStore.ts
new file mode 100644
index 000000000..2088d6b97
--- /dev/null
+++ b/src/identity/interaction/client-credentials/util/ClientCredentialsStore.ts
@@ -0,0 +1,47 @@
+import type { Account } from '../../account/util/Account';
+
+/**
+ * A client credentials token.
+ * If at some point the WebID is no longer registered to the account stored in this token,
+ * the token should be invalidated.
+ */
+export interface ClientCredentials {
+ /**
+ * The identifier of the account that created the token.
+ */
+ accountId: string;
+ /**
+ * The secret of the token.
+ */
+ secret: string;
+ /**
+ * The WebID users will be identified as after using the token.
+ */
+ webId: string;
+}
+
+/**
+ * Stores and creates {@link ClientCredentials}.
+ */
+export interface ClientCredentialsStore {
+ /**
+ * Find the {@link ClientCredentials} with the given label. Undefined if there is no match.
+ * @param label - Label of the credentials.
+ */
+ get: (label: string) => Promise;
+ /**
+ * Creates new {@link ClientCredentials} and adds a reference to the account.
+ * Will error if the WebID is not registered to the account.
+ *
+ * @param label - Identifier to use for the new credentials.
+ * @param webId - WebID to identify as when using this token.
+ * @param account - Account that is associated with this token.
+ */
+ add: (label: string, webId: string, account: Account) => Promise<{ secret: string; resource: string }>;
+ /**
+ * Deletes the token with the given identifier and removes the reference from the account.
+ * @param label - Identifier of the token.
+ * @param account - Account this token belongs to.
+ */
+ delete: (label: string, account: Account) => Promise;
+}
diff --git a/src/identity/interaction/email-password/EmailPasswordUtil.ts b/src/identity/interaction/email-password/EmailPasswordUtil.ts
deleted file mode 100644
index 8c6954af4..000000000
--- a/src/identity/interaction/email-password/EmailPasswordUtil.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import assert from 'assert';
-
-/**
- * Asserts that `password` is a string that matches `confirmPassword`.
- * Will throw an Error otherwise.
- * @param password - Password to assert.
- * @param confirmPassword - Confirmation of password to match.
- */
-export function assertPassword(password: any, confirmPassword: any): asserts password is string {
- assert(
- typeof password === 'string' && password.length > 0,
- 'Please enter a password.',
- );
- assert(
- typeof confirmPassword === 'string' && confirmPassword.length > 0,
- 'Please confirm your password.',
- );
- assert(
- password === confirmPassword,
- 'Your password and confirmation did not match.',
- );
-}
diff --git a/src/identity/interaction/email-password/credentials/ClientCredentialsAdapterFactory.ts b/src/identity/interaction/email-password/credentials/ClientCredentialsAdapterFactory.ts
deleted file mode 100644
index 4ed6d77d2..000000000
--- a/src/identity/interaction/email-password/credentials/ClientCredentialsAdapterFactory.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { AdapterPayload, Adapter } from '../../../../../templates/types/oidc-provider';
-import type { KeyValueStorage } from '../../../../storage/keyvalue/KeyValueStorage';
-import type { AdapterFactory } from '../../../storage/AdapterFactory';
-import { PassthroughAdapterFactory, PassthroughAdapter } from '../../../storage/PassthroughAdapterFactory';
-
-export interface ClientCredentials {
- secret: string;
- webId: string;
-}
-
-/**
- * A {@link PassthroughAdapter} that overrides the `find` function
- * by checking if there are stored client credentials for the given ID
- * if no payload is found in the source.
- */
-export class ClientCredentialsAdapter extends PassthroughAdapter {
- private readonly storage: KeyValueStorage;
-
- public constructor(name: string, source: Adapter, storage: KeyValueStorage) {
- super(name, source);
- this.storage = storage;
- }
-
- public async find(id: string): Promise {
- let payload = await this.source.find(id);
-
- if (!payload && this.name === 'Client') {
- const credentials = await this.storage.get(id);
- if (credentials) {
- /* eslint-disable @typescript-eslint/naming-convention */
- payload = {
- client_id: id,
- client_secret: credentials.secret,
- grant_types: [ 'client_credentials' ],
- redirect_uris: [],
- response_types: [],
- };
- /* eslint-enable @typescript-eslint/naming-convention */
- }
- }
- return payload;
- }
-}
-
-export class ClientCredentialsAdapterFactory extends PassthroughAdapterFactory {
- private readonly storage: KeyValueStorage;
-
- public constructor(source: AdapterFactory, storage: KeyValueStorage) {
- super(source);
- this.storage = storage;
- }
-
- public createStorageAdapter(name: string): Adapter {
- return new ClientCredentialsAdapter(name, this.source.createStorageAdapter(name), this.storage);
- }
-}
diff --git a/src/identity/interaction/email-password/credentials/CreateCredentialsHandler.ts b/src/identity/interaction/email-password/credentials/CreateCredentialsHandler.ts
deleted file mode 100644
index 2cfe6615e..000000000
--- a/src/identity/interaction/email-password/credentials/CreateCredentialsHandler.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { randomBytes } from 'crypto';
-import { v4 } from 'uuid';
-import { BasicRepresentation } from '../../../../http/representation/BasicRepresentation';
-import type { Representation } from '../../../../http/representation/Representation';
-import type { KeyValueStorage } from '../../../../storage/keyvalue/KeyValueStorage';
-import { APPLICATION_JSON } from '../../../../util/ContentTypes';
-import { BadRequestHttpError } from '../../../../util/errors/BadRequestHttpError';
-import { NotImplementedHttpError } from '../../../../util/errors/NotImplementedHttpError';
-import type { AccountStore } from '../storage/AccountStore';
-import type { ClientCredentials } from './ClientCredentialsAdapterFactory';
-import type { CredentialsHandlerInput } from './CredentialsHandler';
-import { CredentialsHandler } from './CredentialsHandler';
-
-/**
- * Handles the creation of credential tokens.
- * Requires a `name` field in the input JSON body,
- * that will be used to generate the ID token.
- */
-export class CreateCredentialsHandler extends CredentialsHandler {
- private readonly accountStore: AccountStore;
- private readonly credentialStorage: KeyValueStorage;
-
- public constructor(accountStore: AccountStore, credentialStorage: KeyValueStorage) {
- super();
- this.accountStore = accountStore;
- this.credentialStorage = credentialStorage;
- }
-
- public async canHandle({ body }: CredentialsHandlerInput): Promise {
- if (typeof body.name !== 'string') {
- throw new NotImplementedHttpError();
- }
- }
-
- public async handle({ operation, body: { webId, name }}: CredentialsHandlerInput): Promise {
- const settings = await this.accountStore.getSettings(webId);
-
- if (!settings.useIdp) {
- throw new BadRequestHttpError('This server is not an identity provider for this account.');
- }
-
- const id = `${(name as string).replace(/\W/gu, '-')}_${v4()}`;
- const secret = randomBytes(64).toString('hex');
-
- // Store the credentials, and point to them from the account
- settings.clientCredentials = settings.clientCredentials ?? [];
- settings.clientCredentials.push(id);
- await this.accountStore.updateSettings(webId, settings);
- await this.credentialStorage.set(id, { secret, webId });
-
- const response = { id, secret };
- return new BasicRepresentation(JSON.stringify(response), operation.target, APPLICATION_JSON);
- }
-}
diff --git a/src/identity/interaction/email-password/credentials/CredentialsHandler.ts b/src/identity/interaction/email-password/credentials/CredentialsHandler.ts
deleted file mode 100644
index 44e605efc..000000000
--- a/src/identity/interaction/email-password/credentials/CredentialsHandler.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { Operation } from '../../../../http/Operation';
-import type { Representation } from '../../../../http/representation/Representation';
-import { AsyncHandler } from '../../../../util/handlers/AsyncHandler';
-
-export interface CredentialsHandlerBody extends Record {
- email: string;
- webId: string;
-}
-
-/**
- * `body` is the parsed JSON from `operation.body.data` with the WebID of the account having been added.
- * This means that the data stream in the Operation can not be read again.
- */
-export interface CredentialsHandlerInput {
- operation: Operation;
- body: CredentialsHandlerBody;
-}
-
-/**
- * Handles a request after the user has been authenticated
- * by providing a valid email/password combination in the JSON body.
- */
-export abstract class CredentialsHandler extends AsyncHandler { }
diff --git a/src/identity/interaction/email-password/credentials/DeleteCredentialsHandler.ts b/src/identity/interaction/email-password/credentials/DeleteCredentialsHandler.ts
deleted file mode 100644
index 2dd94a306..000000000
--- a/src/identity/interaction/email-password/credentials/DeleteCredentialsHandler.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { BasicRepresentation } from '../../../../http/representation/BasicRepresentation';
-import type { Representation } from '../../../../http/representation/Representation';
-import type { KeyValueStorage } from '../../../../storage/keyvalue/KeyValueStorage';
-import { APPLICATION_JSON } from '../../../../util/ContentTypes';
-import { BadRequestHttpError } from '../../../../util/errors/BadRequestHttpError';
-import { NotImplementedHttpError } from '../../../../util/errors/NotImplementedHttpError';
-import type { AccountStore } from '../storage/AccountStore';
-import type { ClientCredentials } from './ClientCredentialsAdapterFactory';
-import type { CredentialsHandlerInput } from './CredentialsHandler';
-import { CredentialsHandler } from './CredentialsHandler';
-
-/**
- * Handles the deletion of credential tokens.
- * Expects the JSON body to have a `delete` field with as value the ID of the token to be deleted.
- * This should be replaced to be an actual DELETE request once the API supports it.
- */
-export class DeleteCredentialsHandler extends CredentialsHandler {
- private readonly accountStore: AccountStore;
- private readonly credentialStorage: KeyValueStorage;
-
- public constructor(accountStore: AccountStore, credentialStorage: KeyValueStorage) {
- super();
- this.accountStore = accountStore;
- this.credentialStorage = credentialStorage;
- }
-
- public async canHandle({ body }: CredentialsHandlerInput): Promise {
- if (typeof body.delete !== 'string') {
- throw new NotImplementedHttpError();
- }
- }
-
- public async handle({ operation, body }: CredentialsHandlerInput): Promise {
- const id = body.delete as string;
- const settings = await this.accountStore.getSettings(body.webId);
- settings.clientCredentials = settings.clientCredentials ?? [];
- const idx = settings.clientCredentials.indexOf(id);
- if (idx < 0) {
- throw new BadRequestHttpError('No credential with this ID exists for this account.');
- }
-
- await this.credentialStorage.delete(id);
- settings.clientCredentials.splice(idx, 1);
- await this.accountStore.updateSettings(body.webId, settings);
- return new BasicRepresentation(JSON.stringify({}), operation.target, APPLICATION_JSON);
- }
-}
diff --git a/src/identity/interaction/email-password/credentials/EmailPasswordAuthorizer.ts b/src/identity/interaction/email-password/credentials/EmailPasswordAuthorizer.ts
deleted file mode 100644
index 255a68628..000000000
--- a/src/identity/interaction/email-password/credentials/EmailPasswordAuthorizer.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import assert from 'assert';
-import type { Representation } from '../../../../http/representation/Representation';
-import { MethodNotAllowedHttpError } from '../../../../util/errors/MethodNotAllowedHttpError';
-import { readJsonStream } from '../../../../util/StreamUtil';
-import type { InteractionHandlerInput } from '../../InteractionHandler';
-import { InteractionHandler } from '../../InteractionHandler';
-import type { AccountStore } from '../storage/AccountStore';
-import type { CredentialsHandler } from './CredentialsHandler';
-
-/**
- * Authenticates a user by the email/password in a JSON POST body.
- * Passes the body and the WebID associated with that account to the source handler.
- */
-export class EmailPasswordAuthorizer extends InteractionHandler {
- private readonly accountStore: AccountStore;
- private readonly source: CredentialsHandler;
-
- public constructor(accountStore: AccountStore, source: CredentialsHandler) {
- super();
- this.accountStore = accountStore;
- this.source = source;
- }
-
- public async handle({ operation }: InteractionHandlerInput): Promise {
- if (operation.method !== 'POST') {
- throw new MethodNotAllowedHttpError([ operation.method ], 'Only POST requests are supported.');
- }
- const json = await readJsonStream(operation.body.data);
- const { email, password } = json;
- assert(typeof email === 'string' && email.length > 0, 'Email required');
- assert(typeof password === 'string' && password.length > 0, 'Password required');
- const webId = await this.accountStore.authenticate(email, password);
- // Password no longer needed from this point on
- delete json.password;
- return this.source.handleSafe({ operation, body: { ...json, email, webId }});
- }
-}
diff --git a/src/identity/interaction/email-password/credentials/ListCredentialsHandler.ts b/src/identity/interaction/email-password/credentials/ListCredentialsHandler.ts
deleted file mode 100644
index 275ebc8e4..000000000
--- a/src/identity/interaction/email-password/credentials/ListCredentialsHandler.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { BasicRepresentation } from '../../../../http/representation/BasicRepresentation';
-import type { Representation } from '../../../../http/representation/Representation';
-import { APPLICATION_JSON } from '../../../../util/ContentTypes';
-import type { AccountStore } from '../storage/AccountStore';
-import type { CredentialsHandlerInput } from './CredentialsHandler';
-import { CredentialsHandler } from './CredentialsHandler';
-
-/**
- * Returns a list of all credential tokens associated with this account.
- * Note that this only returns the ID tokens, not the associated secrets.
- */
-export class ListCredentialsHandler extends CredentialsHandler {
- private readonly accountStore: AccountStore;
-
- public constructor(accountStore: AccountStore) {
- super();
- this.accountStore = accountStore;
- }
-
- public async handle({ operation, body: { webId }}: CredentialsHandlerInput): Promise {
- const credentials = (await this.accountStore.getSettings(webId)).clientCredentials ?? [];
- return new BasicRepresentation(JSON.stringify(credentials), operation.target, APPLICATION_JSON);
- }
-}
diff --git a/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts b/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts
deleted file mode 100644
index 7c9a2fd06..000000000
--- a/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import assert from 'assert';
-import { BasicRepresentation } from '../../../../http/representation/BasicRepresentation';
-import type { Representation } from '../../../../http/representation/Representation';
-import { getLoggerFor } from '../../../../logging/LogUtil';
-import { APPLICATION_JSON } from '../../../../util/ContentTypes';
-import { readJsonStream } from '../../../../util/StreamUtil';
-import type { TemplateEngine } from '../../../../util/templates/TemplateEngine';
-import { BaseInteractionHandler } from '../../BaseInteractionHandler';
-import type { InteractionHandlerInput } from '../../InteractionHandler';
-import type { InteractionRoute } from '../../routing/InteractionRoute';
-import type { AccountStore } from '../storage/AccountStore';
-import type { EmailSender } from '../util/EmailSender';
-
-const forgotPasswordView = {
- required: {
- email: 'string',
- },
-} as const;
-
-export interface ForgotPasswordHandlerArgs {
- accountStore: AccountStore;
- templateEngine: TemplateEngine<{ resetLink: string }>;
- emailSender: EmailSender;
- resetRoute: InteractionRoute;
-}
-
-/**
- * Handles the submission of the ForgotPassword form
- */
-export class ForgotPasswordHandler extends BaseInteractionHandler {
- protected readonly logger = getLoggerFor(this);
-
- private readonly accountStore: AccountStore;
- private readonly templateEngine: TemplateEngine<{ resetLink: string }>;
- private readonly emailSender: EmailSender;
- private readonly resetRoute: InteractionRoute;
-
- public constructor(args: ForgotPasswordHandlerArgs) {
- super(forgotPasswordView);
- this.accountStore = args.accountStore;
- this.templateEngine = args.templateEngine;
- this.emailSender = args.emailSender;
- this.resetRoute = args.resetRoute;
- }
-
- public async handlePost({ operation }: InteractionHandlerInput): Promise {
- // Validate incoming data
- const { email } = await readJsonStream(operation.body.data);
- assert(typeof email === 'string' && email.length > 0, 'Email required');
-
- await this.resetPassword(email);
- return new BasicRepresentation(JSON.stringify({ email }), operation.target, APPLICATION_JSON);
- }
-
- /**
- * Generates a record to reset the password for the given email address and then mails it.
- * In case there is no account, no error wil be thrown for privacy reasons.
- * Instead nothing will happen instead.
- */
- private async resetPassword(email: string): Promise {
- let recordId: string;
- try {
- recordId = await this.accountStore.generateForgotPasswordRecord(email);
- } catch {
- // Don't emit an error for privacy reasons
- this.logger.warn(`Password reset request for unknown email ${email}`);
- return;
- }
- await this.sendResetMail(recordId, email);
- }
-
- /**
- * Generates the link necessary for resetting the password and mails it to the given email address.
- */
- private async sendResetMail(recordId: string, email: string): Promise {
- this.logger.info(`Sending password reset to ${email}`);
- const resetLink = `${this.resetRoute.getPath()}?rid=${encodeURIComponent(recordId)}`;
- const renderedEmail = await this.templateEngine.handleSafe({ contents: { resetLink }});
- await this.emailSender.handleSafe({
- recipient: email,
- subject: 'Reset your password',
- text: `To reset your password, go to this link: ${resetLink}`,
- html: renderedEmail,
- });
- }
-}
diff --git a/src/identity/interaction/email-password/handler/LoginHandler.ts b/src/identity/interaction/email-password/handler/LoginHandler.ts
deleted file mode 100644
index 42afb3ef4..000000000
--- a/src/identity/interaction/email-password/handler/LoginHandler.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import assert from 'assert';
-import type { InteractionResults } from '../../../../../templates/types/oidc-provider';
-import type { Operation } from '../../../../http/Operation';
-import { getLoggerFor } from '../../../../logging/LogUtil';
-import { BadRequestHttpError } from '../../../../util/errors/BadRequestHttpError';
-import { FoundHttpError } from '../../../../util/errors/FoundHttpError';
-import { readJsonStream } from '../../../../util/StreamUtil';
-import { BaseInteractionHandler } from '../../BaseInteractionHandler';
-import type { InteractionHandlerInput } from '../../InteractionHandler';
-import type { AccountStore } from '../storage/AccountStore';
-
-const loginView = {
- required: {
- email: 'string',
- password: 'string',
- remember: 'boolean',
- },
-} as const;
-
-interface LoginInput {
- email: string;
- password: string;
- remember: boolean;
-}
-
-/**
- * Handles the submission of the Login Form and logs the user in.
- * Will throw a RedirectHttpError on success.
- */
-export class LoginHandler extends BaseInteractionHandler {
- protected readonly logger = getLoggerFor(this);
-
- private readonly accountStore: AccountStore;
-
- public constructor(accountStore: AccountStore) {
- super(loginView);
- this.accountStore = accountStore;
- }
-
- public async canHandle(input: InteractionHandlerInput): Promise {
- await super.canHandle(input);
- if (input.operation.method === 'POST' && !input.oidcInteraction) {
- throw new BadRequestHttpError(
- 'This action can only be performed as part of an OIDC authentication flow.',
- { errorCode: 'E0002' },
- );
- }
- }
-
- public async handlePost({ operation, oidcInteraction }: InteractionHandlerInput): Promise {
- const { email, password, remember } = await this.parseInput(operation);
- // Try to log in, will error if email/password combination is invalid
- const webId = await this.accountStore.authenticate(email, password);
- const settings = await this.accountStore.getSettings(webId);
- if (!settings.useIdp) {
- // There is an account but is not used for identification with the IDP
- throw new BadRequestHttpError('This server is not an identity provider for this account.');
- }
- this.logger.debug(`Logging in user ${email}`);
-
- // Update the interaction to get the redirect URL
- const login: InteractionResults['login'] = {
- accountId: webId,
- remember,
- };
- oidcInteraction!.result = { login };
- await oidcInteraction!.save(oidcInteraction!.exp - Math.floor(Date.now() / 1000));
-
- throw new FoundHttpError(oidcInteraction!.returnTo);
- }
-
- /**
- * Validates the input data. Also makes sure remember is a boolean.
- * Will throw an error in case something is wrong.
- */
- private async parseInput(operation: Operation): Promise {
- const { email, password, remember } = await readJsonStream(operation.body.data);
- assert(typeof email === 'string' && email.length > 0, 'Email required');
- assert(typeof password === 'string' && password.length > 0, 'Password required');
- return { email, password, remember: Boolean(remember) };
- }
-}
diff --git a/src/identity/interaction/email-password/handler/RegistrationHandler.ts b/src/identity/interaction/email-password/handler/RegistrationHandler.ts
deleted file mode 100644
index a8ae66b5c..000000000
--- a/src/identity/interaction/email-password/handler/RegistrationHandler.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { BasicRepresentation } from '../../../../http/representation/BasicRepresentation';
-import type { Representation } from '../../../../http/representation/Representation';
-import { getLoggerFor } from '../../../../logging/LogUtil';
-import { APPLICATION_JSON } from '../../../../util/ContentTypes';
-import { readJsonStream } from '../../../../util/StreamUtil';
-import { BaseInteractionHandler } from '../../BaseInteractionHandler';
-import type { InteractionHandlerInput } from '../../InteractionHandler';
-import type { RegistrationManager } from '../util/RegistrationManager';
-
-const registrationView = {
- required: {
- email: 'string',
- password: 'string',
- confirmPassword: 'string',
- createWebId: 'boolean',
- register: 'boolean',
- createPod: 'boolean',
- rootPod: 'boolean',
- },
- optional: {
- webId: 'string',
- podName: 'string',
- template: 'string',
- },
-} as const;
-
-/**
- * Supports registration based on the `RegistrationManager` behaviour.
- */
-export class RegistrationHandler extends BaseInteractionHandler {
- protected readonly logger = getLoggerFor(this);
-
- private readonly registrationManager: RegistrationManager;
-
- public constructor(registrationManager: RegistrationManager) {
- super(registrationView);
- this.registrationManager = registrationManager;
- }
-
- public async handlePost({ operation }: InteractionHandlerInput): Promise {
- const data = await readJsonStream(operation.body.data);
- const validated = this.registrationManager.validateInput(data, false);
- const details = await this.registrationManager.register(validated, false);
- return new BasicRepresentation(JSON.stringify(details), operation.target, APPLICATION_JSON);
- }
-}
diff --git a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts
deleted file mode 100644
index 5f9fe520e..000000000
--- a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import assert from 'assert';
-import { BasicRepresentation } from '../../../../http/representation/BasicRepresentation';
-import type { Representation } from '../../../../http/representation/Representation';
-import { getLoggerFor } from '../../../../logging/LogUtil';
-import { APPLICATION_JSON } from '../../../../util/ContentTypes';
-import { readJsonStream } from '../../../../util/StreamUtil';
-import { BaseInteractionHandler } from '../../BaseInteractionHandler';
-import type { InteractionHandlerInput } from '../../InteractionHandler';
-import { assertPassword } from '../EmailPasswordUtil';
-import type { AccountStore } from '../storage/AccountStore';
-
-const resetPasswordView = {
- required: {
- password: 'string',
- confirmPassword: 'string',
- recordId: 'string',
- },
-} as const;
-
-/**
- * Resets a password if a valid `recordId` is provided,
- * which should have been generated by a different handler.
- */
-export class ResetPasswordHandler extends BaseInteractionHandler {
- protected readonly logger = getLoggerFor(this);
-
- private readonly accountStore: AccountStore;
-
- public constructor(accountStore: AccountStore) {
- super(resetPasswordView);
- this.accountStore = accountStore;
- }
-
- public async handlePost({ operation }: InteractionHandlerInput): Promise {
- // Validate input data
- const { password, confirmPassword, recordId } = await readJsonStream(operation.body.data);
- assert(
- typeof recordId === 'string' && recordId.length > 0,
- 'Invalid request. Open the link from your email again',
- );
- assertPassword(password, confirmPassword);
-
- await this.resetPassword(recordId, password);
- return new BasicRepresentation(JSON.stringify({}), operation.target, APPLICATION_JSON);
- }
-
- /**
- * Resets the password for the account associated with the given recordId.
- */
- private async resetPassword(recordId: string, newPassword: string): Promise {
- const email = await this.accountStore.getForgotPasswordRecord(recordId);
- assert(email, 'This reset password link is no longer valid.');
- await this.accountStore.deleteForgotPasswordRecord(recordId);
- await this.accountStore.changePassword(email, newPassword);
- this.logger.debug(`Resetting password for user ${email}`);
- }
-}
diff --git a/src/identity/interaction/email-password/storage/AccountStore.ts b/src/identity/interaction/email-password/storage/AccountStore.ts
deleted file mode 100644
index bdbf3dcf8..000000000
--- a/src/identity/interaction/email-password/storage/AccountStore.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * Options that can be set on an account.
- */
-export interface AccountSettings {
- /**
- * If this account can be used to identify as the corresponding WebID in the IDP.
- */
- useIdp: boolean;
- /**
- * The base URL of the pod associated with this account, if there is one.
- */
- podBaseUrl?: string;
- /**
- * All credential tokens associated with this account.
- */
- clientCredentials?: string[];
-}
-
-/**
- * Storage needed for the email-password interaction
- */
-export interface AccountStore {
- /**
- * Authenticate if the username and password are correct and return the WebID
- * if it is. Throw an error if it is not.
- * @param email - The user's email.
- * @param password - This user's password.
- * @returns The user's WebID.
- */
- authenticate: (email: string, password: string) => Promise;
-
- /**
- * Creates a new account.
- * @param email - Account email.
- * @param webId - Account WebID.
- * @param password - Account password.
- * @param settings - Specific settings for the account.
- */
- create: (email: string, webId: string, password: string, settings: AccountSettings) => Promise;
-
- /**
- * Verifies the account creation. This can be used with, for example, e-mail verification.
- * The account can only be used after it is verified.
- * In case verification is not required, this should be called immediately after the `create` call.
- * @param email - The account email.
- */
- verify: (email: string) => Promise;
-
- /**
- * Changes the password.
- * @param email - The user's email.
- * @param password - The user's password.
- */
- changePassword: (email: string, password: string) => Promise;
-
- /**
- * Gets the settings associated with this account.
- * Errors if there is no matching account.
- * @param webId - The account WebID.
- */
- getSettings: (webId: string) => Promise;
-
- /**
- * Updates the settings associated with this account.
- * @param webId - The account WebID.
- * @param settings - New settings for the account.
- */
- updateSettings: (webId: string, settings: AccountSettings) => Promise;
-
- /**
- * Delete the account.
- * @param email - The user's email.
- */
- deleteAccount: (email: string) => Promise;
-
- /**
- * Creates a Forgot Password Confirmation Record. This will be to remember that
- * a user has made a request to reset a password. Throws an error if the email doesn't
- * exist
- * @param email - The user's email.
- * @returns The record id. This should be included in the reset password link.
- */
- generateForgotPasswordRecord: (email: string) => Promise;
-
- /**
- * Gets the email associated with the forgot password confirmation record or undefined
- * if it's not present
- * @param recordId - The record id retrieved from the link.
- * @returns The user's email.
- */
- getForgotPasswordRecord: (recordId: string) => Promise;
-
- /**
- * Deletes the Forgot Password Confirmation Record
- * @param recordId - The record id of the forgot password confirmation record.
- */
- deleteForgotPasswordRecord: (recordId: string) => Promise;
-}
diff --git a/src/identity/interaction/email-password/storage/BaseAccountStore.ts b/src/identity/interaction/email-password/storage/BaseAccountStore.ts
deleted file mode 100644
index 7bc511c4f..000000000
--- a/src/identity/interaction/email-password/storage/BaseAccountStore.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import assert from 'assert';
-import { hash, compare } from 'bcryptjs';
-import { v4 } from 'uuid';
-import type { ExpiringStorage } from '../../../../storage/keyvalue/ExpiringStorage';
-import type { KeyValueStorage } from '../../../../storage/keyvalue/KeyValueStorage';
-import type { AccountSettings, AccountStore } from './AccountStore';
-
-/**
- * A payload to persist a user account
- */
-export interface AccountPayload {
- webId: string;
- email: string;
- password: string;
- verified: boolean;
-}
-
-/**
- * A payload to persist the fact that a user
- * has requested to reset their password
- */
-export interface ForgotPasswordPayload {
- email: string;
- recordId: string;
-}
-
-export type EmailPasswordData = AccountPayload | ForgotPasswordPayload | AccountSettings;
-
-/**
- * A EmailPasswordStore that uses a KeyValueStorage to persist its information and an
- * ExpiringStorage to persist ForgotPassword records.
- *
- * `forgotPasswordExpiration` parameter is how long the ForgotPassword record should be
- * stored in minutes. *(defaults to 15 minutes)*
- */
-export class BaseAccountStore implements AccountStore {
- private readonly storage: KeyValueStorage;
- private readonly forgotPasswordStorage: ExpiringStorage;
- private readonly saltRounds: number;
- private readonly forgotPasswordExpiration: number;
-
- public constructor(storage: KeyValueStorage,
- forgotPasswordStorage: ExpiringStorage,
- saltRounds: number,
- forgotPasswordExpiration = 15) {
- this.storage = storage;
- this.forgotPasswordStorage = forgotPasswordStorage;
- this.forgotPasswordExpiration = forgotPasswordExpiration * 60 * 1000;
- this.saltRounds = saltRounds;
- }
-
- /**
- * Generates a ResourceIdentifier to store data for the given email.
- */
- private getAccountResourceIdentifier(email: string): string {
- return `account/${encodeURIComponent(email)}`;
- }
-
- /**
- * Generates a ResourceIdentifier to store data for the given recordId.
- */
- private getForgotPasswordRecordResourceIdentifier(recordId: string): string {
- return `forgot-password-resource-identifier/${encodeURIComponent(recordId)}`;
- }
-
- /* eslint-disable lines-between-class-members */
- /**
- * Helper function that converts the given e-mail to an account identifier
- * and retrieves the account data from the internal storage.
- *
- * Will error if `checkExistence` is true and the account does not exist.
- */
- private async getAccountPayload(email: string, checkExistence: true):
- Promise<{ key: string; account: AccountPayload }>;
- private async getAccountPayload(email: string, checkExistence: false):
- Promise<{ key: string; account?: AccountPayload }>;
- private async getAccountPayload(email: string, checkExistence: boolean):
- Promise<{ key: string; account?: AccountPayload }> {
- const key = this.getAccountResourceIdentifier(email);
- const account = await this.storage.get(key) as AccountPayload | undefined;
- assert(!checkExistence || account, 'Account does not exist');
- return { key, account };
- }
- /* eslint-enable lines-between-class-members */
-
- public async authenticate(email: string, password: string): Promise {
- const { account } = await this.getAccountPayload(email, true);
- assert(account.verified, 'Account still needs to be verified');
- assert(await compare(password, account.password), 'Incorrect password');
- return account.webId;
- }
-
- public async create(email: string, webId: string, password: string, settings: AccountSettings): Promise {
- const { key, account } = await this.getAccountPayload(email, false);
- assert(!account, 'Account already exists');
- // Make sure there is no other account for this WebID
- const storedSettings = await this.storage.get(webId);
- assert(!storedSettings, 'There already is an account for this WebID');
- const payload: AccountPayload = {
- email,
- password: await hash(password, this.saltRounds),
- verified: false,
- webId,
- };
- await this.storage.set(key, payload);
- await this.storage.set(webId, settings);
- }
-
- public async verify(email: string): Promise {
- const { key, account } = await this.getAccountPayload(email, true);
- account.verified = true;
- await this.storage.set(key, account);
- }
-
- public async changePassword(email: string, password: string): Promise {
- const { key, account } = await this.getAccountPayload(email, true);
- account.password = await hash(password, this.saltRounds);
- await this.storage.set(key, account);
- }
-
- public async getSettings(webId: string): Promise {
- const settings = await this.storage.get(webId) as AccountSettings | undefined;
- assert(settings, 'Account does not exist');
- return settings;
- }
-
- public async updateSettings(webId: string, settings: AccountSettings): Promise {
- const oldSettings = await this.storage.get(webId);
- assert(oldSettings, 'Account does not exist');
- await this.storage.set(webId, settings);
- }
-
- public async deleteAccount(email: string): Promise {
- const { key, account } = await this.getAccountPayload(email, false);
- if (account) {
- await this.storage.delete(key);
- await this.storage.delete(account.webId);
- }
- }
-
- public async generateForgotPasswordRecord(email: string): Promise {
- const recordId = v4();
- await this.getAccountPayload(email, true);
- await this.forgotPasswordStorage.set(
- this.getForgotPasswordRecordResourceIdentifier(recordId),
- { recordId, email },
- this.forgotPasswordExpiration,
- );
- return recordId;
- }
-
- public async getForgotPasswordRecord(recordId: string): Promise {
- const identifier = this.getForgotPasswordRecordResourceIdentifier(recordId);
- const forgotPasswordRecord = await this.forgotPasswordStorage.get(identifier) as ForgotPasswordPayload | undefined;
- return forgotPasswordRecord?.email;
- }
-
- public async deleteForgotPasswordRecord(recordId: string): Promise {
- await this.forgotPasswordStorage.delete(this.getForgotPasswordRecordResourceIdentifier(recordId));
- }
-}
diff --git a/src/identity/interaction/email-password/util/RegistrationManager.ts b/src/identity/interaction/email-password/util/RegistrationManager.ts
deleted file mode 100644
index 1abc9908d..000000000
--- a/src/identity/interaction/email-password/util/RegistrationManager.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-import assert from 'assert';
-import type { ResourceIdentifier } from '../../../../http/representation/ResourceIdentifier';
-import { getLoggerFor } from '../../../../logging/LogUtil';
-import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGenerator';
-import type { PodManager } from '../../../../pods/PodManager';
-import type { PodSettings } from '../../../../pods/settings/PodSettings';
-import { hasScheme } from '../../../../util/HeaderUtil';
-import { joinUrl } from '../../../../util/PathUtil';
-import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
-import { assertPassword } from '../EmailPasswordUtil';
-import type { AccountSettings, AccountStore } from '../storage/AccountStore';
-
-export interface RegistrationManagerArgs {
- /**
- * Used to set the `oidcIssuer` value of newly registered pods.
- */
- baseUrl: string;
- /**
- * Appended to the generated pod identifier to create the corresponding WebID.
- */
- webIdSuffix: string;
- /**
- * Generates identifiers for new pods.
- */
- identifierGenerator: IdentifierGenerator;
- /**
- * Verifies the user is the owner of the WebID they provide.
- */
- ownershipValidator: OwnershipValidator;
- /**
- * Stores all the registered account information.
- */
- accountStore: AccountStore;
- /**
- * Creates the new pods.
- */
- podManager: PodManager;
-}
-
-/**
- * The parameters expected for registration.
- */
-export interface RegistrationParams {
- email: string;
- webId?: string;
- password: string;
- podName?: string;
- template?: string;
- createWebId: boolean;
- register: boolean;
- createPod: boolean;
- rootPod: boolean;
-}
-
-/**
- * The result of a registration action.
- */
-export interface RegistrationResponse {
- email: string;
- webId?: string;
- oidcIssuer?: string;
- podBaseUrl?: string;
- createWebId: boolean;
- register: boolean;
- createPod: boolean;
-}
-
-const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/u;
-
-/**
- * Supports IDP registration and pod creation based on input parameters.
- *
- * The above behaviour is combined in the two class functions.
- * `validateInput` will make sure all incoming data is correct and makes sense.
- * `register` will call all the correct handlers based on the requirements of the validated parameters.
- */
-export class RegistrationManager {
- protected readonly logger = getLoggerFor(this);
-
- private readonly baseUrl: string;
- private readonly webIdSuffix: string;
- private readonly identifierGenerator: IdentifierGenerator;
- private readonly ownershipValidator: OwnershipValidator;
- private readonly accountStore: AccountStore;
- private readonly podManager: PodManager;
-
- public constructor(args: RegistrationManagerArgs) {
- this.baseUrl = args.baseUrl;
- this.webIdSuffix = args.webIdSuffix;
- this.identifierGenerator = args.identifierGenerator;
- this.ownershipValidator = args.ownershipValidator;
- this.accountStore = args.accountStore;
- this.podManager = args.podManager;
- }
-
- /**
- * Trims the input if it is a string, returns `undefined` otherwise.
- */
- private trimString(input: unknown): string | undefined {
- if (typeof input === 'string') {
- return input.trim();
- }
- }
-
- /**
- * Makes sure the input conforms to the following requirements when relevant:
- * * At least one option needs to be chosen.
- * * In case a new WebID needs to be created, the other 2 steps will be set to true.
- * * Valid email/WebID/password/podName when required.
- * * Only create a root pod when allowed.
- *
- * @param input - Input parameters for the registration procedure.
- * @param allowRootPod - If creating a pod in the root container should be allowed.
- *
- * @returns A cleaned up version of the input parameters.
- * Only (trimmed) parameters that are relevant to the registration procedure will be retained.
- */
- public validateInput(input: NodeJS.Dict, allowRootPod: boolean): RegistrationParams {
- const {
- email, password, confirmPassword, webId, podName, register, createPod, createWebId, template, rootPod,
- } = input;
-
- // Parse email
- const trimmedEmail = this.trimString(email);
- assert(trimmedEmail && emailRegex.test(trimmedEmail), 'Please enter a valid e-mail address.');
-
- assertPassword(password, confirmPassword);
-
- const validated: RegistrationParams = {
- email: trimmedEmail,
- password,
- register: Boolean(register) || Boolean(createWebId),
- createPod: Boolean(createPod) || Boolean(createWebId),
- createWebId: Boolean(createWebId),
- rootPod: Boolean(rootPod),
- };
- assert(validated.register || validated.createPod, 'Please register for a WebID or create a Pod.');
- assert(allowRootPod || !validated.rootPod, 'Creating a root pod is not supported.');
-
- // Parse WebID
- if (!validated.createWebId) {
- const trimmedWebId = this.trimString(webId);
- assert(trimmedWebId && hasScheme(trimmedWebId, 'http', 'https'), 'Please enter a valid WebID.');
- validated.webId = trimmedWebId;
- }
-
- // Parse Pod name
- if (validated.createPod && !validated.rootPod) {
- const trimmedPodName = this.trimString(podName);
- assert(trimmedPodName && trimmedPodName.length > 0, 'Please specify a Pod name.');
- validated.podName = trimmedPodName;
- }
-
- // Parse template if there is one
- if (template) {
- validated.template = this.trimString(template);
- }
-
- return validated;
- }
-
- /**
- * Handles the 3 potential steps of the registration process:
- * 1. Generating a new WebID.
- * 2. Registering a WebID with the IDP.
- * 3. Creating a new pod for a given WebID.
- *
- * All of these steps are optional and will be determined based on the input parameters.
- *
- * This includes the following steps:
- * * Ownership will be verified when the WebID is provided.
- * * When registering and creating a pod, the base URL will be used as oidcIssuer value.
- */
- public async register(input: RegistrationParams, allowRootPod: boolean): Promise {
- // This is only used when createWebId and/or createPod are true
- let podBaseUrl: ResourceIdentifier | undefined;
- if (input.createPod) {
- if (input.rootPod) {
- podBaseUrl = { path: this.baseUrl };
- } else {
- podBaseUrl = this.identifierGenerator.generate(input.podName!);
- }
- }
-
- // Create or verify the WebID
- if (input.createWebId) {
- input.webId = joinUrl(podBaseUrl!.path, this.webIdSuffix);
- } else {
- await this.ownershipValidator.handleSafe({ webId: input.webId! });
- }
-
- // Register the account
- const settings: AccountSettings = {
- useIdp: input.register,
- podBaseUrl: podBaseUrl?.path,
- clientCredentials: [],
- };
- await this.accountStore.create(input.email, input.webId!, input.password, settings);
-
- // Create the pod
- if (input.createPod) {
- const podSettings: PodSettings = {
- email: input.email,
- webId: input.webId!,
- template: input.template,
- podBaseUrl: podBaseUrl!.path,
- };
-
- // Set the OIDC issuer to our server when registering with the IDP
- if (input.register) {
- podSettings.oidcIssuer = this.baseUrl;
- }
-
- try {
- // Only allow overwrite for root pods
- await this.podManager.createPod(podBaseUrl!, podSettings, allowRootPod);
- } catch (error: unknown) {
- await this.accountStore.deleteAccount(input.email);
- throw error;
- }
- }
-
- // Verify the account
- // This prevents there being a small timeframe where the account can be used before the pod creation is finished.
- // That timeframe could potentially be used by malicious users.
- await this.accountStore.verify(input.email);
-
- return {
- webId: input.webId,
- email: input.email,
- oidcIssuer: this.baseUrl,
- podBaseUrl: podBaseUrl?.path,
- createWebId: input.createWebId,
- register: input.register,
- createPod: input.createPod,
- };
- }
-}
-
diff --git a/src/identity/interaction/login/LogoutHandler.ts b/src/identity/interaction/login/LogoutHandler.ts
new file mode 100644
index 000000000..b29422acd
--- /dev/null
+++ b/src/identity/interaction/login/LogoutHandler.ts
@@ -0,0 +1,43 @@
+import { RepresentationMetadata } from '../../../http/representation/RepresentationMetadata';
+import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
+import type { EmptyObject } from '../../../util/map/MapUtil';
+import { SOLID_HTTP } from '../../../util/Vocabularies';
+import type { CookieStore } from '../account/util/CookieStore';
+import type { JsonRepresentation } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+
+/**
+ * Responsible for logging a user out.
+ * In practice this means making sure the cookie is no longer valid.
+ */
+export class LogoutHandler extends JsonInteractionHandler {
+ private readonly cookieStore: CookieStore;
+
+ public constructor(cookieStore: CookieStore) {
+ super();
+ this.cookieStore = cookieStore;
+ }
+
+ public async handle(input: JsonInteractionHandlerInput): Promise> {
+ const { metadata, accountId, target } = input;
+ const cookie = metadata.get(SOLID_HTTP.terms.accountCookie)?.value;
+ if (cookie) {
+ // Make sure the cookie belongs to the logged-in user.
+ const foundId = await this.cookieStore.get(cookie);
+ if (foundId !== accountId) {
+ throw new BadRequestHttpError('Invalid cookie.');
+ }
+
+ await this.cookieStore.delete(cookie);
+
+ // Setting the expiration time of a cookie to somewhere in the past causes browsers to delete that cookie
+ const outputMetadata = new RepresentationMetadata(target);
+ outputMetadata.set(SOLID_HTTP.terms.accountCookie, cookie);
+ outputMetadata.set(SOLID_HTTP.terms.accountCookieExpiration, new Date(0).toISOString());
+ return { json: {}, metadata: outputMetadata };
+ }
+
+ return { json: {}};
+ }
+}
diff --git a/src/identity/interaction/login/ResolveLoginHandler.ts b/src/identity/interaction/login/ResolveLoginHandler.ts
new file mode 100644
index 000000000..8826cef41
--- /dev/null
+++ b/src/identity/interaction/login/ResolveLoginHandler.ts
@@ -0,0 +1,114 @@
+import { RepresentationMetadata } from '../../../http/representation/RepresentationMetadata';
+import { getLoggerFor } from '../../../logging/LogUtil';
+import { InternalServerError } from '../../../util/errors/InternalServerError';
+import { SOLID_HTTP } from '../../../util/Vocabularies';
+import type { AccountIdRoute } from '../account/AccountIdRoute';
+import { ACCOUNT_SETTINGS_REMEMBER_LOGIN } from '../account/util/Account';
+import type { AccountStore } from '../account/util/AccountStore';
+import type { CookieStore } from '../account/util/CookieStore';
+import type { Json, JsonRepresentation } from '../InteractionUtil';
+import { finishInteraction } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+
+/**
+ * Output type that is expected of handlers logging an account in.
+ */
+export type LoginOutputType = {
+ /**
+ * The ID of the account that logged in.
+ */
+ accountId: string;
+ /**
+ * If this account should be remembered or not.
+ * Setting this to `undefined` will keep the setting as it currently is.
+ */
+ remember?: boolean;
+};
+
+/**
+ * A handler that takes care of all the necessary steps when logging a user in,
+ * such as generating a cookie and setting the necessary OIDC information.
+ * It also sets the `resource` field of the response to the account URL.
+ * Classes that resolve login methods should extend this class and implement the `login` method.
+ */
+export abstract class ResolveLoginHandler extends JsonInteractionHandler {
+ protected readonly logger = getLoggerFor(this);
+
+ protected readonly accountStore: AccountStore;
+ protected readonly cookieStore: CookieStore;
+ protected readonly accountRoute: AccountIdRoute;
+
+ protected constructor(accountStore: AccountStore, cookieStore: CookieStore, accountRoute: AccountIdRoute) {
+ super();
+ this.accountStore = accountStore;
+ this.cookieStore = cookieStore;
+ this.accountRoute = accountRoute;
+ }
+
+ public async handle(input: JsonInteractionHandlerInput): Promise {
+ const result = await this.login(input);
+ const { accountId, remember } = result.json;
+
+ const json: Json = {
+ ...result.json,
+ resource: this.accountRoute.getPath({ accountId }),
+ };
+
+ // There is no need to output these fields in the response JSON
+ delete json.accountId;
+ delete json.remember;
+
+ // The cookie that is used to identify that a user has logged in.
+ // Putting it in the metadata, so it can be converted into an HTTP response header.
+ // Putting it in the response JSON so users can also use it in an Authorization header.
+ const metadata = result.metadata ?? new RepresentationMetadata(input.target);
+ json.cookie = await this.cookieStore.generate(accountId);
+ metadata.add(SOLID_HTTP.terms.accountCookie, json.cookie);
+
+ // Delete the old cookie if there was one, to prevent unused cookies from being stored.
+ // We are not reusing this cookie as it could be associated with a different account.
+ const oldCookie = input.metadata.get(SOLID_HTTP.terms.accountCookie)?.value;
+ if (oldCookie) {
+ this.logger.debug(`Replacing old cookie ${oldCookie} with ${json.cookie}`);
+ await this.cookieStore.delete(oldCookie);
+ }
+
+ // Update the account settings
+ await this.updateRememberSetting(accountId, remember);
+
+ // Not throwing redirect error otherwise the cookie metadata would be lost.
+ // See {@link LocationInteractionHandler} why this field is added.
+ if (input.oidcInteraction) {
+ // Finish the interaction so the policies are checked again, where they will find the new cookie
+ json.location = await finishInteraction(input.oidcInteraction, {}, true);
+ }
+
+ return { json, metadata };
+ }
+
+ /**
+ * Updates the account setting that determines if the login status needs to be remembered.
+ * @param accountId - ID of the account.
+ * @param remember - If the account should be remembered or not. The setting will not be updated if this is undefined.
+ */
+ protected async updateRememberSetting(accountId: string, remember?: boolean): Promise {
+ if (typeof remember === 'boolean') {
+ // Store the setting indicating if the user wants the cookie to persist
+ const account = await this.accountStore.get(accountId);
+ if (!account) {
+ this.logger.error(`Unable to find account ${accountId} that just logged in.`);
+ throw new InternalServerError('Unable to find account');
+ }
+ account.settings[ACCOUNT_SETTINGS_REMEMBER_LOGIN] = remember;
+ await this.accountStore.update(account);
+ this.logger.debug(`Updating account remember setting to ${remember}`);
+ }
+ }
+
+ /**
+ * Takes the necessary steps to log a user in.
+ * @param input - Same input that was passed to the handle function.
+ */
+ public abstract login(input: JsonInteractionHandlerInput): Promise>;
+}
diff --git a/src/identity/interaction/oidc/CancelOidcHandler.ts b/src/identity/interaction/oidc/CancelOidcHandler.ts
new file mode 100644
index 000000000..d81c38d0a
--- /dev/null
+++ b/src/identity/interaction/oidc/CancelOidcHandler.ts
@@ -0,0 +1,23 @@
+import { FoundHttpError } from '../../../util/errors/FoundHttpError';
+import { assertOidcInteraction, finishInteraction } from '../InteractionUtil';
+import type { JsonRepresentation } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+
+/**
+ * Cancel an active OIDC interaction.
+ */
+export class CancelOidcHandler extends JsonInteractionHandler {
+ public async handle({ oidcInteraction }: JsonInteractionHandlerInput): Promise> {
+ assertOidcInteraction(oidcInteraction);
+ const error = {
+ error: 'access_denied',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ error_description: 'User cancelled the interaction.',
+ };
+
+ const location = await finishInteraction(oidcInteraction, error, false);
+
+ throw new FoundHttpError(location);
+ }
+}
diff --git a/src/identity/interaction/oidc/ClientInfoHandler.ts b/src/identity/interaction/oidc/ClientInfoHandler.ts
new file mode 100644
index 000000000..8be1efc6d
--- /dev/null
+++ b/src/identity/interaction/oidc/ClientInfoHandler.ts
@@ -0,0 +1,52 @@
+import type { AllClientMetadata } from '../../../../templates/types/oidc-provider';
+import type { ArrayElement } from '../../../util/map/MapUtil';
+import type { ProviderFactory } from '../../configuration/ProviderFactory';
+import type { JsonRepresentation } from '../InteractionUtil';
+import { assertOidcInteraction } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+
+// Only extract specific fields to prevent leaking information
+// Based on https://www.w3.org/ns/solid/oidc-context.jsonld
+const CLIENT_KEYS = [ 'client_id', 'client_uri', 'logo_uri', 'policy_uri',
+ 'client_name', 'contacts', 'grant_types', 'scope' ] as const;
+
+// Possible keys in client metadata
+type KeyType = ArrayElement;
+// Possible values for client metadata
+type ValType = AllClientMetadata[KeyType];
+// Simplified to keep Components.js happy
+type OutType = {
+ client: Record;
+ webId?: string;
+};
+
+/**
+ * Returns a JSON representation with metadata of the client that is requesting the OIDC interaction.
+ */
+export class ClientInfoHandler extends JsonInteractionHandler {
+ private readonly providerFactory: ProviderFactory;
+
+ public constructor(providerFactory: ProviderFactory) {
+ super();
+ this.providerFactory = providerFactory;
+ }
+
+ public async handle({ oidcInteraction }: JsonInteractionHandlerInput): Promise> {
+ assertOidcInteraction(oidcInteraction);
+ const provider = await this.providerFactory.getProvider();
+ const client = await provider.Client.find(oidcInteraction.params.client_id as string);
+ const metadata: AllClientMetadata = client?.metadata() ?? {};
+
+ const jsonLd = Object.fromEntries(
+ CLIENT_KEYS.filter((key): boolean => key in metadata)
+ .map((key): [ KeyType, ValType ] => [ key, metadata[key] ]),
+ ) as Record & { '@context': 'https://www.w3.org/ns/solid/oidc-context.jsonld' };
+ jsonLd['@context'] = 'https://www.w3.org/ns/solid/oidc-context.jsonld';
+
+ // Note: this is the `accountId` from the OIDC library, in which we store the WebID
+ const webId = oidcInteraction?.session?.accountId;
+
+ return { json: { client: jsonLd, webId }};
+ }
+}
diff --git a/src/identity/interaction/oidc/ConsentHandler.ts b/src/identity/interaction/oidc/ConsentHandler.ts
new file mode 100644
index 000000000..d67c0eca1
--- /dev/null
+++ b/src/identity/interaction/oidc/ConsentHandler.ts
@@ -0,0 +1,104 @@
+import { boolean, object } from 'yup';
+import type { InteractionResults, KoaContextWithOIDC, UnknownObject } from '../../../../templates/types/oidc-provider';
+import { FoundHttpError } from '../../../util/errors/FoundHttpError';
+import { NotImplementedHttpError } from '../../../util/errors/NotImplementedHttpError';
+import type { ProviderFactory } from '../../configuration/ProviderFactory';
+import type { Interaction } from '../InteractionHandler';
+import { assertOidcInteraction, finishInteraction } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+import { validateWithError } from '../YupUtil';
+
+type Grant = NonNullable;
+
+const inSchema = object({
+ remember: boolean().default(false),
+});
+
+/**
+ * Handles the OIDC consent prompts where the user confirms they want to log in for the given client.
+ */
+export class ConsentHandler extends JsonInteractionHandler {
+ private readonly providerFactory: ProviderFactory;
+
+ public constructor(providerFactory: ProviderFactory) {
+ super();
+ this.providerFactory = providerFactory;
+ }
+
+ public async handle({ oidcInteraction, json }: JsonInteractionHandlerInput): Promise {
+ assertOidcInteraction(oidcInteraction);
+
+ const { remember } = await validateWithError(inSchema, json);
+
+ const grant = await this.getGrant(oidcInteraction);
+ this.updateGrant(grant, oidcInteraction.prompt.details, remember);
+
+ const location = await this.updateInteraction(oidcInteraction, grant);
+
+ throw new FoundHttpError(location);
+ }
+
+ /**
+ * Either returns the grant associated with the given interaction or creates a new one if it does not exist yet.
+ */
+ private async getGrant(oidcInteraction: Interaction): Promise {
+ if (!oidcInteraction.session) {
+ throw new NotImplementedHttpError('Only interactions with a valid session are supported.');
+ }
+
+ const { params, session: { accountId }, grantId } = oidcInteraction;
+ const provider = await this.providerFactory.getProvider();
+
+ let grant: Grant;
+ if (grantId) {
+ grant = (await provider.Grant.find(grantId))!;
+ } else {
+ grant = new provider.Grant({
+ accountId,
+ clientId: params.client_id as string,
+ });
+ }
+ return grant;
+ }
+
+ /**
+ * Updates the grant with all the missing scopes and claims requested by the interaction.
+ *
+ * Will reject the `offline_access` scope if `remember` is false.
+ */
+ private updateGrant(grant: Grant, details: UnknownObject, remember: boolean): void {
+ // Reject the offline_access scope if the user does not want to be remembered
+ if (!remember) {
+ grant.rejectOIDCScope('offline_access');
+ }
+
+ // Grant all the requested scopes and claims
+ if (details.missingOIDCScope) {
+ grant.addOIDCScope((details.missingOIDCScope as string[]).join(' '));
+ }
+ if (details.missingOIDCClaims) {
+ grant.addOIDCClaims(details.missingOIDCClaims as string[]);
+ }
+ if (details.missingResourceScopes) {
+ for (const [ indicator, scopes ] of Object.entries(details.missingResourceScopes as Record)) {
+ grant.addResourceScope(indicator, scopes.join(' '));
+ }
+ }
+ }
+
+ /**
+ * Updates the interaction with the new grant and returns the resulting redirect URL.
+ */
+ private async updateInteraction(oidcInteraction: Interaction, grant: Grant): Promise {
+ const grantId = await grant.save();
+
+ const consent: InteractionResults['consent'] = {};
+ // Only need to update the grantId if it is new
+ if (!oidcInteraction.grantId) {
+ consent.grantId = grantId;
+ }
+
+ return finishInteraction(oidcInteraction, { consent }, true);
+ }
+}
diff --git a/src/identity/interaction/oidc/ForgetWebIdHandler.ts b/src/identity/interaction/oidc/ForgetWebIdHandler.ts
new file mode 100644
index 000000000..912381328
--- /dev/null
+++ b/src/identity/interaction/oidc/ForgetWebIdHandler.ts
@@ -0,0 +1,30 @@
+import { FoundHttpError } from '../../../util/errors/FoundHttpError';
+import type { ProviderFactory } from '../../configuration/ProviderFactory';
+import { assertOidcInteraction, finishInteraction, forgetWebId } from '../InteractionUtil';
+import type { JsonRepresentation } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+
+/**
+ * Forgets the chosen WebID in an OIDC interaction,
+ * causing the next policy trigger to be one where a new WebID has to be chosen.
+ */
+export class ForgetWebIdHandler extends JsonInteractionHandler {
+ private readonly providerFactory: ProviderFactory;
+
+ public constructor(providerFactory: ProviderFactory) {
+ super();
+ this.providerFactory = providerFactory;
+ }
+
+ public async handle({ oidcInteraction }: JsonInteractionHandlerInput): Promise> {
+ assertOidcInteraction(oidcInteraction);
+
+ await forgetWebId(await this.providerFactory.getProvider(), oidcInteraction);
+
+ // Finish the interaction so the policies get checked again
+ const location = await finishInteraction(oidcInteraction, {}, false);
+
+ throw new FoundHttpError(location);
+ }
+}
diff --git a/src/identity/interaction/oidc/PickWebIdHandler.ts b/src/identity/interaction/oidc/PickWebIdHandler.ts
new file mode 100644
index 000000000..981c3668c
--- /dev/null
+++ b/src/identity/interaction/oidc/PickWebIdHandler.ts
@@ -0,0 +1,72 @@
+import { boolean, object, string } from 'yup';
+import type { InteractionResults } from '../../../../templates/types/oidc-provider';
+import { getLoggerFor } from '../../../logging/LogUtil';
+import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
+import { FoundHttpError } from '../../../util/errors/FoundHttpError';
+import type { ProviderFactory } from '../../configuration/ProviderFactory';
+import type { AccountStore } from '../account/util/AccountStore';
+import { getRequiredAccount } from '../account/util/AccountUtil';
+import type { JsonRepresentation } from '../InteractionUtil';
+import { assertOidcInteraction, finishInteraction, forgetWebId } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+import type { JsonView } from '../JsonView';
+import { parseSchema, validateWithError } from '../YupUtil';
+
+const inSchema = object({
+ webId: string().trim().required(),
+ remember: boolean().default(false),
+});
+
+/**
+ * Allows users to choose which WebID they want to authenticate as during an OIDC interaction.
+ *
+ * One of the main reason picking a WebID is a separate class/request from consenting to the OIDC interaction,
+ * is because the OIDC-provider will only give the information we need for consent
+ * once we have added an accountId to the OIDC interaction, which we do in this class.
+ * The library also really wants to use that accountId as the value that you use for generating the tokens,
+ * meaning we can't just use another value there, so we need to assign the WebID to it,
+ * unless we use a hacky workaround.
+ */
+export class PickWebIdHandler extends JsonInteractionHandler implements JsonView {
+ private readonly logger = getLoggerFor(this);
+
+ private readonly accountStore: AccountStore;
+ private readonly providerFactory: ProviderFactory;
+
+ public constructor(accountStore: AccountStore, providerFactory: ProviderFactory) {
+ super();
+ this.accountStore = accountStore;
+ this.providerFactory = providerFactory;
+ }
+
+ public async getView({ accountId }: JsonInteractionHandlerInput): Promise {
+ const account = await getRequiredAccount(this.accountStore, accountId);
+ const description = parseSchema(inSchema);
+ return { json: { ...description, webIds: Object.keys(account.webIds) }};
+ }
+
+ public async handle({ oidcInteraction, accountId, json }: JsonInteractionHandlerInput): Promise {
+ assertOidcInteraction(oidcInteraction);
+ const account = await getRequiredAccount(this.accountStore, accountId);
+
+ const { webId, remember } = await validateWithError(inSchema, json);
+ if (!account.webIds[webId]) {
+ this.logger.warn(`Trying to pick WebID ${webId} which does not belong to account ${accountId}`);
+ throw new BadRequestHttpError('WebID does not belong to this account.');
+ }
+
+ // We need to explicitly forget the WebID from the session or the library won't allow us to update the value
+ await forgetWebId(await this.providerFactory.getProvider(), oidcInteraction);
+
+ // Update the interaction to get the redirect URL
+ const login: InteractionResults['login'] = {
+ // Note that `accountId` here is unrelated to our user accounts but is part of the OIDC library
+ accountId: webId,
+ remember,
+ };
+
+ const location = await finishInteraction(oidcInteraction, { login }, true);
+ throw new FoundHttpError(location);
+ }
+}
diff --git a/src/identity/interaction/oidc/PromptHandler.ts b/src/identity/interaction/oidc/PromptHandler.ts
new file mode 100644
index 000000000..20a0fed74
--- /dev/null
+++ b/src/identity/interaction/oidc/PromptHandler.ts
@@ -0,0 +1,41 @@
+import { getLoggerFor } from '../../../logging/LogUtil';
+import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
+import type { JsonRepresentation } from '../InteractionUtil';
+import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
+import { JsonInteractionHandler } from '../JsonInteractionHandler';
+import type { InteractionRoute } from '../routing/InteractionRoute';
+
+type OutType = { location: string; prompt: string };
+
+/**
+ * Redirects requests based on the OIDC Interaction prompt.
+ * Errors in case no match was found.
+ *
+ * The reason we use this intermediate handler
+ * instead of letting the OIDC library redirect directly to the correct page,
+ * is because that library creates a cookie with of scope of the target page.
+ * By having the library always redirect to the index page,
+ * the cookie is relevant for all pages and other pages can see if we are still in an interaction.
+ */
+export class PromptHandler extends JsonInteractionHandler {
+ private readonly logger = getLoggerFor(this);
+
+ private readonly promptRoutes: Record