mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Moved IDP response and template behaviour to single class
This commit is contained in:
parent
2a82c4f06e
commit
9d337ba80c
15
config/identity/handler/account-store/default.json
Normal file
15
config/identity/handler/account-store/default.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"comment": "The storage adapter that persists usernames, passwords, etc.",
|
||||||
|
"@id": "urn:solid-server:auth:password:AccountStore",
|
||||||
|
"@type": "BaseAccountStore",
|
||||||
|
"args_storageName": "/idp/email-password-db",
|
||||||
|
"args_saltRounds": 10,
|
||||||
|
"args_storage": {
|
||||||
|
"@id": "urn:solid-server:default:IdpStorage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
"import": [
|
"import": [
|
||||||
|
"files-scs:config/identity/handler/account-store/default.json",
|
||||||
"files-scs:config/identity/handler/adapter-factory/webid.json",
|
"files-scs:config/identity/handler/adapter-factory/webid.json",
|
||||||
"files-scs:config/identity/handler/interaction/handler.json",
|
"files-scs:config/identity/handler/interaction/routes.json",
|
||||||
"files-scs:config/identity/handler/key-value/storage.json",
|
"files-scs:config/identity/handler/key-value/storage.json",
|
||||||
"files-scs:config/identity/handler/provider-factory/identity.json"
|
"files-scs:config/identity/handler/provider-factory/identity.json"
|
||||||
],
|
],
|
||||||
@ -18,8 +19,17 @@
|
|||||||
{
|
{
|
||||||
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
||||||
"@type": "IdentityProviderHttpHandler",
|
"@type": "IdentityProviderHttpHandler",
|
||||||
|
"idpPath": "/idp",
|
||||||
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
|
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
|
||||||
"interactionHttpHandler": { "@id": "urn:solid-server:auth:password:InteractionHttpHandler" },
|
"templateHandler": {
|
||||||
|
"@type": "TemplateHandler",
|
||||||
|
"templateEngine": { "@type": "EjsTemplateEngine" }
|
||||||
|
},
|
||||||
|
"interactionCompleter": {
|
||||||
|
"comment": "Responsible for finishing OIDC interactions.",
|
||||||
|
"@type": "InteractionCompleter",
|
||||||
|
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
|
||||||
|
},
|
||||||
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||||
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
||||||
}
|
}
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
|
||||||
"import": [
|
|
||||||
"files-scs:config/identity/handler/interaction/handlers/forgot-password.json",
|
|
||||||
"files-scs:config/identity/handler/interaction/handlers/initial.json",
|
|
||||||
"files-scs:config/identity/handler/interaction/handlers/login.json",
|
|
||||||
"files-scs:config/identity/handler/interaction/handlers/reset-password.json",
|
|
||||||
"files-scs:config/identity/handler/interaction/handlers/session.json"
|
|
||||||
],
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"comment": "Http handler to take care of all routing on for the email password interaction",
|
|
||||||
"@id": "urn:solid-server:auth:password:InteractionHttpHandler",
|
|
||||||
"@type": "WaterfallHandler",
|
|
||||||
"handlers": [
|
|
||||||
{ "@id": "urn:solid-server:auth:password:InitialInteractionHandler" },
|
|
||||||
{ "@id": "urn:solid-server:auth:password:LoginInteractionHandler" },
|
|
||||||
{ "@id": "urn:solid-server:auth:password:SessionInteractionHandler" },
|
|
||||||
{ "@id": "urn:solid-server:auth:password:ForgotPasswordInteractionHandler" },
|
|
||||||
{ "@id": "urn:solid-server:auth:password:ResetPasswordInteractionHandler" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"comment": "Below are extra classes used by the handlers."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"comment": "The storage adapter that persists usernames, passwords, etc.",
|
|
||||||
"@id": "urn:solid-server:auth:password:AccountStore",
|
|
||||||
"@type": "BaseAccountStore",
|
|
||||||
"args_storageName": "/idp/email-password-db",
|
|
||||||
"args_saltRounds": 10,
|
|
||||||
"args_storage": { "@id": "urn:solid-server:default:IdpStorage" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comment": "Responsible for completing an OIDC interaction after login or registration",
|
|
||||||
"@id": "urn:solid-server:auth:password:InteractionCompleter",
|
|
||||||
"@type": "InteractionCompleter"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"comment": "Handles all functionality on the forgot password page",
|
|
||||||
"@id": "urn:solid-server:auth:password:ForgotPasswordInteractionHandler",
|
|
||||||
"@type": "IdpRouteController",
|
|
||||||
"pathName": "^/idp/forgotpassword/?$",
|
|
||||||
"postHandler": {
|
|
||||||
"@type": "ForgotPasswordHandler",
|
|
||||||
"args_messageRenderHandler": { "@id": "urn:solid-server:auth:password:EmailSentRenderHandler" },
|
|
||||||
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
|
||||||
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
|
||||||
"args_idpPath": "/idp",
|
|
||||||
"args_templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/reset-password-email.html.ejs"
|
|
||||||
},
|
|
||||||
"args_emailSender": { "@id": "urn:solid-server:default:EmailSender" }
|
|
||||||
},
|
|
||||||
"renderHandler": { "@id": "urn:solid-server:auth:password:ForgotPasswordRenderHandler" }
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"comment": "Renders the Email Sent message page",
|
|
||||||
"@id": "urn:solid-server:auth:password:EmailSentRenderHandler",
|
|
||||||
"@type": "TemplateHandler",
|
|
||||||
"templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/email-sent.html.ejs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comment": "Renders the forgot password page",
|
|
||||||
"@id": "urn:solid-server:auth:password:ForgotPasswordRenderHandler",
|
|
||||||
"@type": "TemplateHandler",
|
|
||||||
"templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/forgot-password.html.ejs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"comment": "Handles the initial route when the user is directed from their app to the IdP",
|
|
||||||
"@id": "urn:solid-server:auth:password:InitialInteractionHandler",
|
|
||||||
"@type": "RouterHandler",
|
|
||||||
"allowedMethods": [ "GET" ],
|
|
||||||
"allowedPathNames": [ "^/idp/?$" ],
|
|
||||||
"handler": {
|
|
||||||
"@type": "InitialInteractionHandler",
|
|
||||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
|
||||||
"redirectMap": [
|
|
||||||
{
|
|
||||||
"InitialInteractionHandler:_redirectMap_key": "consent",
|
|
||||||
"InitialInteractionHandler:_redirectMap_value": "/idp/confirm"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"redirectMap_default": "/idp/login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"comment": "Handles all functionality on the Login Page",
|
|
||||||
"@id": "urn:solid-server:auth:password:LoginInteractionHandler",
|
|
||||||
"@type": "IdpRouteController",
|
|
||||||
"pathName": "^/idp/login/?$",
|
|
||||||
"postHandler": {
|
|
||||||
"@type": "LoginHandler",
|
|
||||||
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
|
||||||
"args_interactionCompleter": { "@id": "urn:solid-server:auth:password:InteractionCompleter" }
|
|
||||||
},
|
|
||||||
"renderHandler": { "@id": "urn:solid-server:auth:password:LoginRenderHandler" }
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"comment": "Renders the login page",
|
|
||||||
"@id": "urn:solid-server:auth:password:LoginRenderHandler",
|
|
||||||
"@type": "TemplateHandler",
|
|
||||||
"templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/login.html.ejs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
|
||||||
"comment": "Exports 2 handlers: one for viewing the page and one for doing the reset.",
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"comment": "Handles the reset password page submission",
|
|
||||||
"@id": "urn:solid-server:auth:password:ResetPasswordInteractionHandler",
|
|
||||||
"@type": "IdpRouteController",
|
|
||||||
"pathName": "^/idp/resetpassword/[^/]+$",
|
|
||||||
"postHandler": {
|
|
||||||
"@type": "ResetPasswordHandler",
|
|
||||||
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
|
||||||
"args_messageRenderHandler": { "@id": "urn:solid-server:auth:password:MessageRenderHandler" }
|
|
||||||
},
|
|
||||||
"renderHandler": { "@id": "urn:solid-server:auth:password:ResetPasswordRenderHandler" }
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"comment": "Renders the reset password page",
|
|
||||||
"@id": "urn:solid-server:auth:password:ResetPasswordRenderHandler",
|
|
||||||
"@type": "TemplateHandler",
|
|
||||||
"templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/reset-password.html.ejs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comment": "Renders a generic page that says a message",
|
|
||||||
"@id": "urn:solid-server:auth:password:MessageRenderHandler",
|
|
||||||
"@type": "TemplateHandler",
|
|
||||||
"templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/message.html.ejs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"comment": "Handles confirm requests",
|
|
||||||
"@id": "urn:solid-server:auth:password:SessionInteractionHandler",
|
|
||||||
"@type": "IdpRouteController",
|
|
||||||
"pathName": "^/idp/confirm/?$",
|
|
||||||
"postHandler": {
|
|
||||||
"@type": "SessionHttpHandler",
|
|
||||||
"interactionCompleter": { "@id": "urn:solid-server:auth:password:InteractionCompleter" }
|
|
||||||
},
|
|
||||||
"renderHandler": { "@id": "urn:solid-server:auth:password:ConfirmRenderHandler" }
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"comment": "Renders the confirmation page",
|
|
||||||
"@id": "urn:solid-server:auth:password:ConfirmRenderHandler",
|
|
||||||
"@type": "TemplateHandler",
|
|
||||||
"templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/confirm.html.ejs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
20
config/identity/handler/interaction/routes.json
Normal file
20
config/identity/handler/interaction/routes.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
|
"import": [
|
||||||
|
"files-scs:config/identity/handler/interaction/routes/forgot-password.json",
|
||||||
|
"files-scs:config/identity/handler/interaction/routes/login.json",
|
||||||
|
"files-scs:config/identity/handler/interaction/routes/reset-password.json",
|
||||||
|
"files-scs:config/identity/handler/interaction/routes/session.json"
|
||||||
|
],
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
||||||
|
"IdentityProviderHttpHandler:_interactionRoutes": [
|
||||||
|
{ "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" },
|
||||||
|
{ "@id": "urn:solid-server:auth:password:LoginRoute" },
|
||||||
|
{ "@id": "urn:solid-server:auth:password:ResetPasswordRoute" },
|
||||||
|
{ "@id": "urn:solid-server:auth:password:SessionRoute" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"comment": "Handles all functionality on the forgot password page",
|
||||||
|
"@id": "urn:solid-server:auth:password:ForgotPasswordRoute",
|
||||||
|
"@type": "InteractionRoute",
|
||||||
|
"route": "^/forgotpassword/?$",
|
||||||
|
"viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/forgot-password.html.ejs",
|
||||||
|
"responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/email-sent.html.ejs",
|
||||||
|
"handler": {
|
||||||
|
"@type": "ForgotPasswordHandler",
|
||||||
|
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
||||||
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
|
"args_idpPath": "/idp",
|
||||||
|
"args_templateEngine": {
|
||||||
|
"@type": "EjsTemplateEngine",
|
||||||
|
"template": "$PACKAGE_ROOT/templates/identity/email-password/reset-password-email.html.ejs"
|
||||||
|
},
|
||||||
|
"args_emailSender": { "@id": "urn:solid-server:default:EmailSender" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
17
config/identity/handler/interaction/routes/login.json
Normal file
17
config/identity/handler/interaction/routes/login.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"comment": "Handles all functionality on the Login Page",
|
||||||
|
"@id": "urn:solid-server:auth:password:LoginRoute",
|
||||||
|
"@type": "InteractionRoute",
|
||||||
|
"route": "^/login/?$",
|
||||||
|
"prompt": "default",
|
||||||
|
"viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/login.html.ejs",
|
||||||
|
"handler": {
|
||||||
|
"@type": "LoginHandler",
|
||||||
|
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
|
"comment": "Exports 2 handlers: one for viewing the page and one for doing the reset.",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"comment": "Handles the reset password page submission",
|
||||||
|
"@id": "urn:solid-server:auth:password:ResetPasswordRoute",
|
||||||
|
"@type": "InteractionRoute",
|
||||||
|
"route": "^/resetpassword(/[^/]*)?$",
|
||||||
|
"viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/reset-password.html.ejs",
|
||||||
|
"responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/message.html.ejs",
|
||||||
|
"handler": {
|
||||||
|
"@type": "ResetPasswordHandler",
|
||||||
|
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
17
config/identity/handler/interaction/routes/session.json
Normal file
17
config/identity/handler/interaction/routes/session.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"comment": "Handles confirm requests",
|
||||||
|
"@id": "urn:solid-server:auth:password:SessionRoute",
|
||||||
|
"@type": "InteractionRoute",
|
||||||
|
"route": "^/confirm/?$",
|
||||||
|
"prompt": "consent",
|
||||||
|
"viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/confirm.html.ejs",
|
||||||
|
"handler": {
|
||||||
|
"@type": "SessionHttpHandler",
|
||||||
|
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
"import": [
|
"import": [
|
||||||
"files-scs:config/identity/registration/handler/registration.json"
|
"files-scs:config/identity/registration/route/registration.json"
|
||||||
],
|
],
|
||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
"comment": "Enable registration by adding a registration handler to the list of interaction handlers.",
|
"comment": "Enable registration by adding a registration handler to the list of interaction routes.",
|
||||||
"@id": "urn:solid-server:auth:password:InteractionHttpHandler",
|
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
||||||
"WaterfallHandler:_handlers": [
|
"IdentityProviderHttpHandler:_interactionRoutes": [
|
||||||
{ "@id": "urn:solid-server:auth:password:RegistrationInteractionHandler" }
|
{ "@id": "urn:solid-server:auth:password:RegistrationRoute" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"comment": "Handles all functionality on the register page",
|
|
||||||
"@id": "urn:solid-server:auth:password:RegistrationInteractionHandler",
|
|
||||||
"@type": "IdpRouteController",
|
|
||||||
"pathName": "^/idp/register/?$",
|
|
||||||
"postHandler": {
|
|
||||||
"@type": "RegistrationHandler",
|
|
||||||
"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" },
|
|
||||||
"args_responseHandler": { "@id": "urn:solid-server:auth:password:RegisterResponseRenderHandler" }
|
|
||||||
},
|
|
||||||
"renderHandler": { "@id": "urn:solid-server:auth:password:RegisterRenderHandler" }
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"comment": "Renders the register page",
|
|
||||||
"@id": "urn:solid-server:auth:password:RegisterRenderHandler",
|
|
||||||
"@type": "TemplateHandler",
|
|
||||||
"templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/register.html.ejs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comment": "Renders the successful registration page",
|
|
||||||
"@id": "urn:solid-server:auth:password:RegisterResponseRenderHandler",
|
|
||||||
"@type": "TemplateHandler",
|
|
||||||
"templateEngine": {
|
|
||||||
"@type": "EjsTemplateEngine",
|
|
||||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/register-response.html.ejs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
22
config/identity/registration/route/registration.json
Normal file
22
config/identity/registration/route/registration.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"comment": "Handles all functionality on the register page",
|
||||||
|
"@id": "urn:solid-server:auth:password:RegistrationRoute",
|
||||||
|
"@type": "InteractionRoute",
|
||||||
|
"route": "^/register/?$",
|
||||||
|
"viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/register.html.ejs",
|
||||||
|
"responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/register-response.html.ejs",
|
||||||
|
"handler": {
|
||||||
|
"@type": "RegistrationHandler",
|
||||||
|
"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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -3,16 +3,57 @@ import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
|||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
import type { HttpHandlerInput } from '../server/HttpHandler';
|
||||||
import { HttpHandler } from '../server/HttpHandler';
|
import { HttpHandler } from '../server/HttpHandler';
|
||||||
import { assertError } from '../util/errors/ErrorUtil';
|
import type { HttpRequest } from '../server/HttpRequest';
|
||||||
|
import type { HttpResponse } from '../server/HttpResponse';
|
||||||
|
import type { TemplateHandler } from '../server/util/TemplateHandler';
|
||||||
|
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
|
||||||
|
import { assertError, createErrorMessage } from '../util/errors/ErrorUtil';
|
||||||
|
import { InternalServerError } from '../util/errors/InternalServerError';
|
||||||
|
import { trimTrailingSlashes } from '../util/PathUtil';
|
||||||
import type { ProviderFactory } from './configuration/ProviderFactory';
|
import type { ProviderFactory } from './configuration/ProviderFactory';
|
||||||
import type { InteractionHttpHandler } from './interaction/InteractionHttpHandler';
|
import type { InteractionHandler,
|
||||||
|
InteractionHandlerResult } from './interaction/email-password/handler/InteractionHandler';
|
||||||
|
|
||||||
|
import { IdpInteractionError } from './interaction/util/IdpInteractionError';
|
||||||
|
import type { InteractionCompleter } from './interaction/util/InteractionCompleter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All the information that is required to handle a request to a custom IDP path.
|
||||||
|
*/
|
||||||
|
export class InteractionRoute {
|
||||||
|
public readonly route: RegExp;
|
||||||
|
public readonly handler: InteractionHandler;
|
||||||
|
public readonly viewTemplate: string;
|
||||||
|
public readonly prompt?: string;
|
||||||
|
public readonly responseTemplate?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param route - Regex to match this route.
|
||||||
|
* @param viewTemplate - Template to render on GET requests.
|
||||||
|
* @param handler - Handler to call on POST requests.
|
||||||
|
* @param prompt - In case of requests to the IDP entry point, the session prompt will be compared to this.
|
||||||
|
* One entry should have a value of "default" here in case there are no prompt matches.
|
||||||
|
* @param responseTemplate - Template to render as a response to POST requests when required.
|
||||||
|
*/
|
||||||
|
public constructor(route: string,
|
||||||
|
viewTemplate: string,
|
||||||
|
handler: InteractionHandler,
|
||||||
|
prompt?: string,
|
||||||
|
responseTemplate?: string) {
|
||||||
|
this.route = new RegExp(route, 'u');
|
||||||
|
this.viewTemplate = viewTemplate;
|
||||||
|
this.handler = handler;
|
||||||
|
this.prompt = prompt;
|
||||||
|
this.responseTemplate = responseTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all requests relevant for the entire IDP interaction,
|
* Handles all requests relevant for the entire IDP interaction,
|
||||||
* by sending them to either the stored {@link InteractionHttpHandler},
|
* by sending them to either a matching {@link InteractionRoute},
|
||||||
* or the generated Provider from the {@link ProviderFactory} if the first does not support the request.
|
* or the generated Provider from the {@link ProviderFactory} if there is no match.
|
||||||
*
|
*
|
||||||
* The InteractionHttpHandler would handle all requests where we need custom behaviour,
|
* The InteractionRoutes handle all requests where we need custom behaviour,
|
||||||
* such as everything related to generating and validating an account.
|
* such as everything related to generating and validating an account.
|
||||||
* The Provider handles all the default request such as the initial handshake.
|
* The Provider handles all the default request such as the initial handshake.
|
||||||
*
|
*
|
||||||
@ -22,42 +63,158 @@ import type { InteractionHttpHandler } from './interaction/InteractionHttpHandle
|
|||||||
export class IdentityProviderHttpHandler extends HttpHandler {
|
export class IdentityProviderHttpHandler extends HttpHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
|
private readonly idpPath: string;
|
||||||
private readonly providerFactory: ProviderFactory;
|
private readonly providerFactory: ProviderFactory;
|
||||||
private readonly interactionHttpHandler: InteractionHttpHandler;
|
private readonly interactionRoutes: InteractionRoute[];
|
||||||
|
private readonly templateHandler: TemplateHandler;
|
||||||
|
private readonly interactionCompleter: InteractionCompleter;
|
||||||
private readonly errorHandler: ErrorHandler;
|
private readonly errorHandler: ErrorHandler;
|
||||||
private readonly responseWriter: ResponseWriter;
|
private readonly responseWriter: ResponseWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param idpPath - Relative path of the IDP entry point.
|
||||||
|
* @param providerFactory - Used to generate the OIDC provider.
|
||||||
|
* @param interactionRoutes - All routes handling the custom IDP behaviour.
|
||||||
|
* @param templateHandler - Used for rendering responses.
|
||||||
|
* @param interactionCompleter - Used for POST requests that need to be handled by the OIDC library.
|
||||||
|
* @param errorHandler - Converts errors to responses.
|
||||||
|
* @param responseWriter - Renders error responses.
|
||||||
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
|
idpPath: string,
|
||||||
providerFactory: ProviderFactory,
|
providerFactory: ProviderFactory,
|
||||||
interactionHttpHandler: InteractionHttpHandler,
|
interactionRoutes: InteractionRoute[],
|
||||||
|
templateHandler: TemplateHandler,
|
||||||
|
interactionCompleter: InteractionCompleter,
|
||||||
errorHandler: ErrorHandler,
|
errorHandler: ErrorHandler,
|
||||||
responseWriter: ResponseWriter,
|
responseWriter: ResponseWriter,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
if (!idpPath.startsWith('/')) {
|
||||||
|
throw new Error('idpPath needs to start with a /');
|
||||||
|
}
|
||||||
|
// Trimming trailing slashes so the relative URL starts with a slash after slicing this off
|
||||||
|
this.idpPath = trimTrailingSlashes(idpPath);
|
||||||
this.providerFactory = providerFactory;
|
this.providerFactory = providerFactory;
|
||||||
this.interactionHttpHandler = interactionHttpHandler;
|
this.interactionRoutes = interactionRoutes;
|
||||||
|
this.templateHandler = templateHandler;
|
||||||
|
this.interactionCompleter = interactionCompleter;
|
||||||
this.errorHandler = errorHandler;
|
this.errorHandler = errorHandler;
|
||||||
this.responseWriter = responseWriter;
|
this.responseWriter = responseWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: HttpHandlerInput): Promise<void> {
|
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
||||||
const provider = await this.providerFactory.getProvider();
|
|
||||||
|
|
||||||
// If our own interaction handler does not support the input, it must be a request for the OIDC library
|
|
||||||
try {
|
try {
|
||||||
await this.interactionHttpHandler.canHandle({ ...input, provider });
|
await this.handleRequest(request, response);
|
||||||
} catch {
|
|
||||||
this.logger.debug(`Sending request to oidc-provider: ${input.request.url}`);
|
|
||||||
return provider.callback(input.request, input.response);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.interactionHttpHandler.handle({ ...input, provider });
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
assertError(error);
|
assertError(error);
|
||||||
// Setting preferences to text/plain since we didn't parse accept headers, see #764
|
// Setting preferences to text/plain since we didn't parse accept headers, see #764
|
||||||
const result = await this.errorHandler.handleSafe({ error, preferences: { type: { 'text/plain': 1 }}});
|
const result = await this.errorHandler.handleSafe({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||||
await this.responseWriter.handleSafe({ response: input.response, result });
|
await this.responseWriter.handleSafe({ response, result });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the matching route and resolves the request.
|
||||||
|
*/
|
||||||
|
private async handleRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
||||||
|
// If our own interaction handler does not support the input, it is either invalid or a request for the OIDC library
|
||||||
|
const route = await this.findRoute(request, response);
|
||||||
|
if (!route) {
|
||||||
|
const provider = await this.providerFactory.getProvider();
|
||||||
|
this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
|
||||||
|
return provider.callback(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.resolveRoute(request, response, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a route that supports the given request.
|
||||||
|
*/
|
||||||
|
private async findRoute(request: HttpRequest, response: HttpResponse): Promise<InteractionRoute | undefined> {
|
||||||
|
if (!request.url || !request.url.startsWith(this.idpPath)) {
|
||||||
|
// This is either an invalid request or a call to the .well-known configuration
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = request.url.slice(this.idpPath.length);
|
||||||
|
let route = this.getRouteMatch(url);
|
||||||
|
|
||||||
|
// In case the request targets the IDP entry point the prompt determines where to go
|
||||||
|
if (!route && (url === '/' || url === '')) {
|
||||||
|
const provider = await this.providerFactory.getProvider();
|
||||||
|
const interactionDetails = await provider.interactionDetails(request, response);
|
||||||
|
route = this.getPromptMatch(interactionDetails.prompt.name);
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the behaviour of an InteractionRoute.
|
||||||
|
* Will error if the route does not support the given request.
|
||||||
|
*
|
||||||
|
* GET requests go to the templateHandler, POST requests to the specific InteractionHandler of the route.
|
||||||
|
*/
|
||||||
|
private async resolveRoute(request: HttpRequest, response: HttpResponse, route: InteractionRoute): Promise<void> {
|
||||||
|
if (request.method === 'GET') {
|
||||||
|
// .ejs templates errors on undefined variables
|
||||||
|
return await this.handleTemplateResponse(response, route.viewTemplate, { errorMessage: '', prefilled: {}});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === 'POST') {
|
||||||
|
let result: InteractionHandlerResult;
|
||||||
|
try {
|
||||||
|
result = await route.handler.handleSafe({ request, response });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Render error in the view
|
||||||
|
const prefilled = IdpInteractionError.isInstance(error) ? error.prefilled : {};
|
||||||
|
const errorMessage = createErrorMessage(error);
|
||||||
|
return await this.handleTemplateResponse(response, route.viewTemplate, { errorMessage, prefilled });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.type === 'complete') {
|
||||||
|
return await this.interactionCompleter.handleSafe({ ...result.details, request, response });
|
||||||
|
}
|
||||||
|
if (result.type === 'response' && route.responseTemplate) {
|
||||||
|
return await this.handleTemplateResponse(response, route.responseTemplate, result.details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BadRequestHttpError(`Unsupported request: ${request.method} ${request.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleTemplateResponse(response: HttpResponse, templateFile: string, contents: NodeJS.Dict<any>):
|
||||||
|
Promise<void> {
|
||||||
|
await this.templateHandler.handleSafe({ response, templateFile, contents });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a route by matching the URL.
|
||||||
|
*/
|
||||||
|
private getRouteMatch(url: string): InteractionRoute | undefined {
|
||||||
|
for (const route of this.interactionRoutes) {
|
||||||
|
if (route.route.test(url)) {
|
||||||
|
return route;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a route by matching the prompt.
|
||||||
|
*/
|
||||||
|
private getPromptMatch(prompt: string): InteractionRoute {
|
||||||
|
let def: InteractionRoute | undefined;
|
||||||
|
for (const route of this.interactionRoutes) {
|
||||||
|
if (route.prompt === prompt) {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
if (route.prompt === 'default') {
|
||||||
|
def = route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!def) {
|
||||||
|
throw new InternalServerError('No handler for the default session prompt has been configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import type { Provider } from 'oidc-provider';
|
|
||||||
import type { HttpHandlerInput } from '../../server/HttpHandler';
|
|
||||||
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
|
||||||
|
|
||||||
export type InteractionHttpHandlerInput = HttpHandlerInput & {
|
|
||||||
provider: Provider;
|
|
||||||
};
|
|
||||||
|
|
||||||
export abstract class InteractionHttpHandler extends AsyncHandler<InteractionHttpHandlerInput> {}
|
|
@ -1,24 +1,29 @@
|
|||||||
|
import type { HttpHandlerInput } from '../../server/HttpHandler';
|
||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import type { InteractionHttpHandlerInput } from './InteractionHttpHandler';
|
import type { ProviderFactory } from '../configuration/ProviderFactory';
|
||||||
import { InteractionHttpHandler } from './InteractionHttpHandler';
|
import { InteractionHandler } from './email-password/handler/InteractionHandler';
|
||||||
import type { InteractionCompleter } from './util/InteractionCompleter';
|
import type { InteractionCompleteResult } from './email-password/handler/InteractionHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple InteractionHttpHandler that sends the session accountId to the InteractionCompleter as webId.
|
* Simple InteractionHttpHandler that sends the session accountId to the InteractionCompleter as webId.
|
||||||
*/
|
*/
|
||||||
export class SessionHttpHandler extends InteractionHttpHandler {
|
export class SessionHttpHandler extends InteractionHandler {
|
||||||
private readonly interactionCompleter: InteractionCompleter;
|
private readonly providerFactory: ProviderFactory;
|
||||||
|
|
||||||
public constructor(interactionCompleter: InteractionCompleter) {
|
public constructor(providerFactory: ProviderFactory) {
|
||||||
super();
|
super();
|
||||||
this.interactionCompleter = interactionCompleter;
|
this.providerFactory = providerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: InteractionHttpHandlerInput): Promise<void> {
|
public async handle(input: HttpHandlerInput): Promise<InteractionCompleteResult> {
|
||||||
const details = await input.provider.interactionDetails(input.request, input.response);
|
const provider = await this.providerFactory.getProvider();
|
||||||
|
const details = await provider.interactionDetails(input.request, input.response);
|
||||||
if (!details.session || !details.session.accountId) {
|
if (!details.session || !details.session.accountId) {
|
||||||
throw new NotImplementedHttpError('Only confirm actions with a session and accountId are supported');
|
throw new NotImplementedHttpError('Only confirm actions with a session and accountId are supported');
|
||||||
}
|
}
|
||||||
await this.interactionCompleter.handleSafe({ ...input, webId: details.session.accountId as any });
|
return {
|
||||||
|
type: 'complete',
|
||||||
|
details: { webId: details.session.accountId },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import urljoin from 'url-join';
|
import urljoin from 'url-join';
|
||||||
import { getLoggerFor } from '../../../../logging/LogUtil';
|
import { getLoggerFor } from '../../../../logging/LogUtil';
|
||||||
import type { HttpResponse } from '../../../../server/HttpResponse';
|
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
|
||||||
import { ensureTrailingSlash } from '../../../../util/PathUtil';
|
import { ensureTrailingSlash } from '../../../../util/PathUtil';
|
||||||
import type { TemplateEngine } from '../../../../util/templates/TemplateEngine';
|
import type { TemplateEngine } from '../../../../util/templates/TemplateEngine';
|
||||||
import type { InteractionHttpHandlerInput } from '../../InteractionHttpHandler';
|
|
||||||
import { InteractionHttpHandler } from '../../InteractionHttpHandler';
|
|
||||||
import type { EmailSender } from '../../util/EmailSender';
|
import type { EmailSender } from '../../util/EmailSender';
|
||||||
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
||||||
import type { IdpRenderHandler } from '../../util/IdpRenderHandler';
|
|
||||||
import { throwIdpInteractionError } from '../EmailPasswordUtil';
|
import { throwIdpInteractionError } from '../EmailPasswordUtil';
|
||||||
import type { AccountStore } from '../storage/AccountStore';
|
import type { AccountStore } from '../storage/AccountStore';
|
||||||
|
import { InteractionHandler } from './InteractionHandler';
|
||||||
|
import type { InteractionResponseResult } from './InteractionHandler';
|
||||||
|
|
||||||
export interface ForgotPasswordHandlerArgs {
|
export interface ForgotPasswordHandlerArgs {
|
||||||
messageRenderHandler: IdpRenderHandler;
|
|
||||||
accountStore: AccountStore;
|
accountStore: AccountStore;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
idpPath: string;
|
idpPath: string;
|
||||||
@ -24,10 +22,9 @@ export interface ForgotPasswordHandlerArgs {
|
|||||||
/**
|
/**
|
||||||
* Handles the submission of the ForgotPassword form
|
* Handles the submission of the ForgotPassword form
|
||||||
*/
|
*/
|
||||||
export class ForgotPasswordHandler extends InteractionHttpHandler {
|
export class ForgotPasswordHandler extends InteractionHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly messageRenderHandler: IdpRenderHandler;
|
|
||||||
private readonly accountStore: AccountStore;
|
private readonly accountStore: AccountStore;
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
private readonly idpPath: string;
|
private readonly idpPath: string;
|
||||||
@ -36,7 +33,6 @@ export class ForgotPasswordHandler extends InteractionHttpHandler {
|
|||||||
|
|
||||||
public constructor(args: ForgotPasswordHandlerArgs) {
|
public constructor(args: ForgotPasswordHandlerArgs) {
|
||||||
super();
|
super();
|
||||||
this.messageRenderHandler = args.messageRenderHandler;
|
|
||||||
this.accountStore = args.accountStore;
|
this.accountStore = args.accountStore;
|
||||||
this.baseUrl = ensureTrailingSlash(args.baseUrl);
|
this.baseUrl = ensureTrailingSlash(args.baseUrl);
|
||||||
this.idpPath = args.idpPath;
|
this.idpPath = args.idpPath;
|
||||||
@ -44,14 +40,14 @@ export class ForgotPasswordHandler extends InteractionHttpHandler {
|
|||||||
this.emailSender = args.emailSender;
|
this.emailSender = args.emailSender;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: InteractionHttpHandlerInput): Promise<void> {
|
public async handle(input: HttpHandlerInput): Promise<InteractionResponseResult<{ email: string }>> {
|
||||||
try {
|
try {
|
||||||
// Validate incoming data
|
// Validate incoming data
|
||||||
const { email } = await getFormDataRequestBody(input.request);
|
const { email } = await getFormDataRequestBody(input.request);
|
||||||
assert(typeof email === 'string' && email.length > 0, 'Email required');
|
assert(typeof email === 'string' && email.length > 0, 'Email required');
|
||||||
|
|
||||||
await this.resetPassword(email);
|
await this.resetPassword(email);
|
||||||
await this.sendResponse(input.response, email);
|
return { type: 'response', details: { email }};
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
throwIdpInteractionError(err, {});
|
throwIdpInteractionError(err, {});
|
||||||
}
|
}
|
||||||
@ -88,22 +84,4 @@ export class ForgotPasswordHandler extends InteractionHttpHandler {
|
|||||||
html: renderedEmail,
|
html: renderedEmail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a response through the messageRenderHandler.
|
|
||||||
* @param response - HttpResponse to send to.
|
|
||||||
* @param email - Will be inserted in `prefilled` for the template.
|
|
||||||
*/
|
|
||||||
private async sendResponse(response: HttpResponse, email: string): Promise<void> {
|
|
||||||
// Send response
|
|
||||||
await this.messageRenderHandler.handleSafe({
|
|
||||||
response,
|
|
||||||
contents: {
|
|
||||||
errorMessage: '',
|
|
||||||
prefilled: {
|
|
||||||
email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
|
||||||
|
import { AsyncHandler } from '../../../../util/handlers/AsyncHandler';
|
||||||
|
import type { InteractionCompleterParams } from '../../util/InteractionCompleter';
|
||||||
|
|
||||||
|
export type InteractionHandlerResult = InteractionResponseResult | InteractionCompleteResult;
|
||||||
|
|
||||||
|
export interface InteractionResponseResult<T = NodeJS.Dict<any>> {
|
||||||
|
type: 'response';
|
||||||
|
details: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractionCompleteResult {
|
||||||
|
type: 'complete';
|
||||||
|
details: InteractionCompleterParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler used for IDP interactions.
|
||||||
|
*/
|
||||||
|
export abstract class InteractionHandler extends AsyncHandler<HttpHandlerInput, InteractionHandlerResult> {}
|
@ -1,40 +1,36 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { getLoggerFor } from '../../../../logging/LogUtil';
|
import { getLoggerFor } from '../../../../logging/LogUtil';
|
||||||
|
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
|
||||||
import type { HttpRequest } from '../../../../server/HttpRequest';
|
import type { HttpRequest } from '../../../../server/HttpRequest';
|
||||||
import type { InteractionHttpHandlerInput } from '../../InteractionHttpHandler';
|
|
||||||
import { InteractionHttpHandler } from '../../InteractionHttpHandler';
|
|
||||||
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
||||||
import type { InteractionCompleter } from '../../util/InteractionCompleter';
|
|
||||||
import { throwIdpInteractionError } from '../EmailPasswordUtil';
|
import { throwIdpInteractionError } from '../EmailPasswordUtil';
|
||||||
import type { AccountStore } from '../storage/AccountStore';
|
import type { AccountStore } from '../storage/AccountStore';
|
||||||
|
import { InteractionHandler } from './InteractionHandler';
|
||||||
export interface LoginHandlerArgs {
|
import type { InteractionCompleteResult } from './InteractionHandler';
|
||||||
accountStore: AccountStore;
|
|
||||||
interactionCompleter: InteractionCompleter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the submission of the Login Form and logs the user in.
|
* Handles the submission of the Login Form and logs the user in.
|
||||||
*/
|
*/
|
||||||
export class LoginHandler extends InteractionHttpHandler {
|
export class LoginHandler extends InteractionHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly accountStore: AccountStore;
|
private readonly accountStore: AccountStore;
|
||||||
private readonly interactionCompleter: InteractionCompleter;
|
|
||||||
|
|
||||||
public constructor(args: LoginHandlerArgs) {
|
public constructor(accountStore: AccountStore) {
|
||||||
super();
|
super();
|
||||||
this.accountStore = args.accountStore;
|
this.accountStore = accountStore;
|
||||||
this.interactionCompleter = args.interactionCompleter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: InteractionHttpHandlerInput): Promise<void> {
|
public async handle(input: HttpHandlerInput): Promise<InteractionCompleteResult> {
|
||||||
const { email, password, remember } = await this.parseInput(input.request);
|
const { email, password, remember } = await this.parseInput(input.request);
|
||||||
try {
|
try {
|
||||||
// Try to log in, will error if email/password combination is invalid
|
// Try to log in, will error if email/password combination is invalid
|
||||||
const webId = await this.accountStore.authenticate(email, password);
|
const webId = await this.accountStore.authenticate(email, password);
|
||||||
await this.interactionCompleter.handleSafe({ ...input, webId, shouldRemember: Boolean(remember) });
|
|
||||||
this.logger.debug(`Logging in user ${email}`);
|
this.logger.debug(`Logging in user ${email}`);
|
||||||
|
return {
|
||||||
|
type: 'complete',
|
||||||
|
details: { webId, shouldRemember: Boolean(remember) },
|
||||||
|
};
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
throwIdpInteractionError(err, { email });
|
throwIdpInteractionError(err, { email });
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,13 @@ import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGe
|
|||||||
import type { PodManager } from '../../../../pods/PodManager';
|
import type { PodManager } from '../../../../pods/PodManager';
|
||||||
import type { PodSettings } from '../../../../pods/settings/PodSettings';
|
import type { PodSettings } from '../../../../pods/settings/PodSettings';
|
||||||
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
|
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
|
||||||
import { HttpHandler } from '../../../../server/HttpHandler';
|
|
||||||
import type { HttpRequest } from '../../../../server/HttpRequest';
|
import type { HttpRequest } from '../../../../server/HttpRequest';
|
||||||
import type { TemplateHandler } from '../../../../server/util/TemplateHandler';
|
|
||||||
import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
|
import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
|
||||||
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
||||||
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
|
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
|
||||||
import type { AccountStore } from '../storage/AccountStore';
|
import type { AccountStore } from '../storage/AccountStore';
|
||||||
|
import type { InteractionResponseResult } from './InteractionHandler';
|
||||||
|
import { InteractionHandler } from './InteractionHandler';
|
||||||
|
|
||||||
const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/u;
|
const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/u;
|
||||||
|
|
||||||
@ -41,10 +41,6 @@ export interface RegistrationHandlerArgs {
|
|||||||
* Creates the new pods.
|
* Creates the new pods.
|
||||||
*/
|
*/
|
||||||
podManager: PodManager;
|
podManager: PodManager;
|
||||||
/**
|
|
||||||
* Renders the response when registration is successful.
|
|
||||||
*/
|
|
||||||
responseHandler: TemplateHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,7 +83,7 @@ interface RegistrationResponse {
|
|||||||
* * Ownership will be verified when the WebID is provided.
|
* * Ownership will be verified when the WebID is provided.
|
||||||
* * When registering and creating a pod, the base URL will be used as oidcIssuer value.
|
* * When registering and creating a pod, the base URL will be used as oidcIssuer value.
|
||||||
*/
|
*/
|
||||||
export class RegistrationHandler extends HttpHandler {
|
export class RegistrationHandler extends InteractionHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
@ -96,7 +92,6 @@ export class RegistrationHandler extends HttpHandler {
|
|||||||
private readonly ownershipValidator: OwnershipValidator;
|
private readonly ownershipValidator: OwnershipValidator;
|
||||||
private readonly accountStore: AccountStore;
|
private readonly accountStore: AccountStore;
|
||||||
private readonly podManager: PodManager;
|
private readonly podManager: PodManager;
|
||||||
private readonly responseHandler: TemplateHandler;
|
|
||||||
|
|
||||||
public constructor(args: RegistrationHandlerArgs) {
|
public constructor(args: RegistrationHandlerArgs) {
|
||||||
super();
|
super();
|
||||||
@ -106,15 +101,14 @@ export class RegistrationHandler extends HttpHandler {
|
|||||||
this.ownershipValidator = args.ownershipValidator;
|
this.ownershipValidator = args.ownershipValidator;
|
||||||
this.accountStore = args.accountStore;
|
this.accountStore = args.accountStore;
|
||||||
this.podManager = args.podManager;
|
this.podManager = args.podManager;
|
||||||
this.responseHandler = args.responseHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
public async handle({ request }: HttpHandlerInput): Promise<InteractionResponseResult<RegistrationResponse>> {
|
||||||
const result = await this.parseInput(request);
|
const result = await this.parseInput(request);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contents = await this.register(result);
|
const details = await this.register(result);
|
||||||
await this.responseHandler.handleSafe({ response, contents });
|
return { type: 'response', details };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Don't expose the password field
|
// Don't expose the password field
|
||||||
delete result.password;
|
delete result.password;
|
||||||
|
@ -1,34 +1,27 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { getLoggerFor } from '../../../../logging/LogUtil';
|
import { getLoggerFor } from '../../../../logging/LogUtil';
|
||||||
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
|
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
|
||||||
import { HttpHandler } from '../../../../server/HttpHandler';
|
|
||||||
import type { TemplateHandler } from '../../../../server/util/TemplateHandler';
|
|
||||||
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
||||||
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
|
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
|
||||||
import type { AccountStore } from '../storage/AccountStore';
|
import type { AccountStore } from '../storage/AccountStore';
|
||||||
|
import type { InteractionResponseResult } from './InteractionHandler';
|
||||||
export interface ResetPasswordHandlerArgs {
|
import { InteractionHandler } from './InteractionHandler';
|
||||||
accountStore: AccountStore;
|
|
||||||
messageRenderHandler: TemplateHandler<{ message: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the submission of the ResetPassword form:
|
* Handles the submission of the ResetPassword form:
|
||||||
* this is the form that is linked in the reset password email.
|
* this is the form that is linked in the reset password email.
|
||||||
*/
|
*/
|
||||||
export class ResetPasswordHandler extends HttpHandler {
|
export class ResetPasswordHandler extends InteractionHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly accountStore: AccountStore;
|
private readonly accountStore: AccountStore;
|
||||||
private readonly messageRenderHandler: TemplateHandler<{ message: string }>;
|
|
||||||
|
|
||||||
public constructor(args: ResetPasswordHandlerArgs) {
|
public constructor(accountStore: AccountStore) {
|
||||||
super();
|
super();
|
||||||
this.accountStore = args.accountStore;
|
this.accountStore = accountStore;
|
||||||
this.messageRenderHandler = args.messageRenderHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: HttpHandlerInput): Promise<void> {
|
public async handle(input: HttpHandlerInput): Promise<InteractionResponseResult> {
|
||||||
try {
|
try {
|
||||||
// Extract record ID from request URL
|
// Extract record ID from request URL
|
||||||
const recordId = /\/([^/]+)$/u.exec(input.request.url!)?.[1];
|
const recordId = /\/([^/]+)$/u.exec(input.request.url!)?.[1];
|
||||||
@ -41,12 +34,7 @@ export class ResetPasswordHandler extends HttpHandler {
|
|||||||
assertPassword(password, confirmPassword);
|
assertPassword(password, confirmPassword);
|
||||||
|
|
||||||
await this.resetPassword(recordId, password);
|
await this.resetPassword(recordId, password);
|
||||||
await this.messageRenderHandler.handleSafe({
|
return { type: 'response', details: { message: 'Your password was successfully reset.' }};
|
||||||
response: input.response,
|
|
||||||
contents: {
|
|
||||||
message: 'Your password was successfully reset.',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
throwIdpInteractionError(error);
|
throwIdpInteractionError(error);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { TemplateHandler } from '../../../server/util/TemplateHandler';
|
|
||||||
|
|
||||||
export interface IdpRenderHandlerProps {
|
|
||||||
errorMessage?: string;
|
|
||||||
prefilled?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A special Render Handler that renders an IDP form.
|
|
||||||
* Contains an error message if something was wrong and prefilled values for forms.
|
|
||||||
*/
|
|
||||||
export abstract class IdpRenderHandler extends TemplateHandler<IdpRenderHandlerProps> {}
|
|
@ -1,43 +0,0 @@
|
|||||||
import type { HttpHandler } from '../../../server/HttpHandler';
|
|
||||||
import { RouterHandler } from '../../../server/util/RouterHandler';
|
|
||||||
import { createErrorMessage } from '../../../util/errors/ErrorUtil';
|
|
||||||
import type { InteractionHttpHandlerInput } from '../InteractionHttpHandler';
|
|
||||||
import { IdpInteractionError } from './IdpInteractionError';
|
|
||||||
import type { IdpRenderHandler } from './IdpRenderHandler';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an IDP interaction route.
|
|
||||||
* All routes render their UI on a GET and accept POST requests to handle the interaction.
|
|
||||||
*/
|
|
||||||
export class IdpRouteController extends RouterHandler {
|
|
||||||
private readonly renderHandler: IdpRenderHandler;
|
|
||||||
|
|
||||||
public constructor(pathName: string, renderHandler: IdpRenderHandler, postHandler: HttpHandler) {
|
|
||||||
super(postHandler, [ 'GET', 'POST' ], [ pathName ]);
|
|
||||||
this.renderHandler = renderHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the renderHandler to render using the given response and props.
|
|
||||||
*/
|
|
||||||
private async render(input: InteractionHttpHandlerInput, errorMessage = '', prefilled = {}):
|
|
||||||
Promise<void> {
|
|
||||||
return this.renderHandler.handleSafe({
|
|
||||||
response: input.response,
|
|
||||||
contents: { errorMessage, prefilled },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handle(input: InteractionHttpHandlerInput): Promise<void> {
|
|
||||||
if (input.request.method === 'GET') {
|
|
||||||
await this.render(input);
|
|
||||||
} else if (input.request.method === 'POST') {
|
|
||||||
try {
|
|
||||||
await this.handler.handleSafe(input);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const prefilled = IdpInteractionError.isInstance(err) ? err.prefilled : {};
|
|
||||||
await this.render(input, createErrorMessage(err), prefilled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import urljoin from 'url-join';
|
|
||||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
|
||||||
import type { InteractionHttpHandlerInput } from '../InteractionHttpHandler';
|
|
||||||
import { InteractionHttpHandler } from '../InteractionHttpHandler';
|
|
||||||
|
|
||||||
export interface RedirectMap {
|
|
||||||
[key: string]: string;
|
|
||||||
default: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link InteractionHttpHandler} that redirects requests based on their prompt.
|
|
||||||
* A list of possible prompts can be found at https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
||||||
* In case there is no prompt or there is no match in the input map,
|
|
||||||
* the `default` redirect will be used.
|
|
||||||
*
|
|
||||||
* Specifically, this is used to redirect the client to the correct way to login,
|
|
||||||
* such as a login page, or a confirmation page if a login procedure already succeeded previously.
|
|
||||||
*/
|
|
||||||
export class InitialInteractionHandler extends InteractionHttpHandler {
|
|
||||||
protected readonly logger = getLoggerFor(this);
|
|
||||||
|
|
||||||
private readonly baseUrl: string;
|
|
||||||
private readonly redirectMap: RedirectMap;
|
|
||||||
|
|
||||||
public constructor(baseUrl: string, redirectMap: RedirectMap) {
|
|
||||||
super();
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.redirectMap = redirectMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handle({ request, response, provider }: InteractionHttpHandlerInput): Promise<void> {
|
|
||||||
// Find the matching redirect in the map or take the default
|
|
||||||
const interactionDetails = await provider.interactionDetails(request, response);
|
|
||||||
const name = interactionDetails.prompt.name in this.redirectMap ? interactionDetails.prompt.name : 'default';
|
|
||||||
|
|
||||||
// Create a valid redirect URL
|
|
||||||
const location = urljoin(this.baseUrl, this.redirectMap[name]);
|
|
||||||
this.logger.debug(`Redirecting ${name} prompt to ${location}.`);
|
|
||||||
|
|
||||||
// Redirect to the result
|
|
||||||
response.writeHead(302, { location });
|
|
||||||
response.end();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +1,28 @@
|
|||||||
import type { InteractionResults } from 'oidc-provider';
|
import type { InteractionResults } from 'oidc-provider';
|
||||||
|
import type { HttpHandlerInput } from '../../../server/HttpHandler';
|
||||||
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
|
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
|
||||||
import type { InteractionHttpHandlerInput } from '../InteractionHttpHandler';
|
import type { ProviderFactory } from '../../configuration/ProviderFactory';
|
||||||
|
|
||||||
export interface InteractionCompleterInput extends InteractionHttpHandlerInput {
|
export interface InteractionCompleterParams {
|
||||||
webId: string;
|
webId: string;
|
||||||
shouldRemember?: boolean;
|
shouldRemember?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InteractionCompleterInput = HttpHandlerInput & InteractionCompleterParams;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completes an IDP interaction, logging the user in.
|
* Completes an IDP interaction, logging the user in.
|
||||||
*/
|
*/
|
||||||
export class InteractionCompleter extends AsyncHandler<InteractionCompleterInput> {
|
export class InteractionCompleter extends AsyncHandler<InteractionCompleterInput> {
|
||||||
|
private readonly providerFactory: ProviderFactory;
|
||||||
|
|
||||||
|
public constructor(providerFactory: ProviderFactory) {
|
||||||
|
super();
|
||||||
|
this.providerFactory = providerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
public async handle(input: InteractionCompleterInput): Promise<void> {
|
public async handle(input: InteractionCompleterInput): Promise<void> {
|
||||||
|
const provider = await this.providerFactory.getProvider();
|
||||||
const result: InteractionResults = {
|
const result: InteractionResults = {
|
||||||
login: {
|
login: {
|
||||||
account: input.webId,
|
account: input.webId,
|
||||||
@ -23,6 +34,6 @@ export class InteractionCompleter extends AsyncHandler<InteractionCompleterInput
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return input.provider.interactionFinished(input.request, input.response, result);
|
return provider.interactionFinished(input.request, input.response, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ export * from './identity/configuration/IdentityProviderFactory';
|
|||||||
export * from './identity/configuration/ProviderFactory';
|
export * from './identity/configuration/ProviderFactory';
|
||||||
|
|
||||||
// Identity/Interaction/Email-Password/Handler
|
// Identity/Interaction/Email-Password/Handler
|
||||||
|
export * from './identity/interaction/email-password/handler/InteractionHandler';
|
||||||
export * from './identity/interaction/email-password/handler/ForgotPasswordHandler';
|
export * from './identity/interaction/email-password/handler/ForgotPasswordHandler';
|
||||||
export * from './identity/interaction/email-password/handler/LoginHandler';
|
export * from './identity/interaction/email-password/handler/LoginHandler';
|
||||||
export * from './identity/interaction/email-password/handler/RegistrationHandler';
|
export * from './identity/interaction/email-password/handler/RegistrationHandler';
|
||||||
@ -39,13 +40,9 @@ export * from './identity/interaction/util/BaseEmailSender';
|
|||||||
export * from './identity/interaction/util/EmailSender';
|
export * from './identity/interaction/util/EmailSender';
|
||||||
export * from './identity/interaction/util/FormDataUtil';
|
export * from './identity/interaction/util/FormDataUtil';
|
||||||
export * from './identity/interaction/util/IdpInteractionError';
|
export * from './identity/interaction/util/IdpInteractionError';
|
||||||
export * from './identity/interaction/util/IdpRenderHandler';
|
|
||||||
export * from './identity/interaction/util/IdpRouteController';
|
|
||||||
export * from './identity/interaction/util/InitialInteractionHandler';
|
|
||||||
export * from './identity/interaction/util/InteractionCompleter';
|
export * from './identity/interaction/util/InteractionCompleter';
|
||||||
|
|
||||||
// Identity/Interaction
|
// Identity/Interaction
|
||||||
export * from './identity/interaction/InteractionHttpHandler';
|
|
||||||
export * from './identity/interaction/SessionHttpHandler';
|
export * from './identity/interaction/SessionHttpHandler';
|
||||||
|
|
||||||
// Identity/Ownership
|
// Identity/Ownership
|
||||||
|
@ -7,7 +7,7 @@ import Dict = NodeJS.Dict;
|
|||||||
* A Render Handler that uses a template engine to render a response.
|
* A Render Handler that uses a template engine to render a response.
|
||||||
*/
|
*/
|
||||||
export class TemplateHandler<T extends Dict<any> = Dict<any>>
|
export class TemplateHandler<T extends Dict<any> = Dict<any>>
|
||||||
extends AsyncHandler<{ response: HttpResponse; contents: T }> {
|
extends AsyncHandler<{ response: HttpResponse; templateFile: string; contents: T }> {
|
||||||
private readonly templateEngine: TemplateEngine;
|
private readonly templateEngine: TemplateEngine;
|
||||||
private readonly contentType: string;
|
private readonly contentType: string;
|
||||||
|
|
||||||
@ -17,8 +17,9 @@ export class TemplateHandler<T extends Dict<any> = Dict<any>>
|
|||||||
this.contentType = contentType;
|
this.contentType = contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ response, contents }: { response: HttpResponse; contents: T }): Promise<void> {
|
public async handle({ response, templateFile, contents }:
|
||||||
const rendered = await this.templateEngine.render(contents);
|
{ response: HttpResponse; templateFile: string; contents: T }): Promise<void> {
|
||||||
|
const rendered = await this.templateEngine.render(contents, { templateFile });
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
response.writeHead(200, { 'Content-Type': this.contentType });
|
response.writeHead(200, { 'Content-Type': this.contentType });
|
||||||
response.end(rendered);
|
response.end(rendered);
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<p>If your account exists, an email has been sent with a link to reset your password.</p>
|
<p>If your account exists, an email has been sent with a link to reset your password.</p>
|
||||||
<p>If you do not receive your email in a couple of minutes, check your spam folder or click the link below to send another email.</p>
|
<p>If you do not receive your email in a couple of minutes, check your spam folder or click the link below to send another email.</p>
|
||||||
|
|
||||||
<input type="hidden" name="email" value="<%= prefilled.email %>" />
|
<input type="hidden" name="email" value="<%= email %>" />
|
||||||
|
|
||||||
<p class="actions"><a href="/idp/login">Back to Log In</a></p>
|
<p class="actions"><a href="/idp/login">Back to Log In</a></p>
|
||||||
|
|
||||||
|
@ -87,12 +87,7 @@ export class IdentityTestState {
|
|||||||
expect(nextUrl.startsWith(this.oidcIssuer)).toBeTruthy();
|
expect(nextUrl.startsWith(this.oidcIssuer)).toBeTruthy();
|
||||||
|
|
||||||
// Need to catch the redirect so we can copy the cookies
|
// Need to catch the redirect so we can copy the cookies
|
||||||
let res = await this.fetchIdp(nextUrl);
|
const res = await this.fetchIdp(nextUrl);
|
||||||
expect(res.status).toBe(302);
|
|
||||||
nextUrl = res.headers.get('location')!;
|
|
||||||
|
|
||||||
// Redirect from main page to specific page (login or confirmation)
|
|
||||||
res = await this.fetchIdp(nextUrl);
|
|
||||||
expect(res.status).toBe(302);
|
expect(res.status).toBe(302);
|
||||||
nextUrl = res.headers.get('location')!;
|
nextUrl = res.headers.get('location')!;
|
||||||
|
|
||||||
|
@ -1,92 +1,210 @@
|
|||||||
import type { Provider } from 'oidc-provider';
|
import type { Provider } from 'oidc-provider';
|
||||||
import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory';
|
import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory';
|
||||||
import { IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler';
|
import { InteractionRoute, IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler';
|
||||||
import type { InteractionHttpHandler } from '../../../src/identity/interaction/InteractionHttpHandler';
|
import type { InteractionHandler } from '../../../src/identity/interaction/email-password/handler/InteractionHandler';
|
||||||
|
import { IdpInteractionError } from '../../../src/identity/interaction/util/IdpInteractionError';
|
||||||
|
import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter';
|
||||||
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
|
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
|
||||||
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
|
||||||
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
||||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
|
import type { TemplateHandler } from '../../../src/server/util/TemplateHandler';
|
||||||
|
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
||||||
|
import { InternalServerError } from '../../../src/util/errors/InternalServerError';
|
||||||
|
|
||||||
describe('An IdentityProviderHttpHandler', (): void => {
|
describe('An IdentityProviderHttpHandler', (): void => {
|
||||||
const request: HttpRequest = {} as any;
|
const idpPath = '/idp';
|
||||||
|
let request: HttpRequest;
|
||||||
const response: HttpResponse = {} as any;
|
const response: HttpResponse = {} as any;
|
||||||
let providerFactory: jest.Mocked<ProviderFactory>;
|
let providerFactory: jest.Mocked<ProviderFactory>;
|
||||||
let interactionHttpHandler: jest.Mocked<InteractionHttpHandler>;
|
let routes: { response: InteractionRoute; complete: InteractionRoute };
|
||||||
|
let interactionCompleter: jest.Mocked<InteractionCompleter>;
|
||||||
|
let templateHandler: jest.Mocked<TemplateHandler>;
|
||||||
let errorHandler: jest.Mocked<ErrorHandler>;
|
let errorHandler: jest.Mocked<ErrorHandler>;
|
||||||
let responseWriter: jest.Mocked<ResponseWriter>;
|
let responseWriter: jest.Mocked<ResponseWriter>;
|
||||||
let provider: jest.Mocked<Provider>;
|
let provider: jest.Mocked<Provider>;
|
||||||
let handler: IdentityProviderHttpHandler;
|
let handler: IdentityProviderHttpHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
request = { url: '/idp', method: 'GET' } as any;
|
||||||
|
|
||||||
provider = {
|
provider = {
|
||||||
callback: jest.fn(),
|
callback: jest.fn(),
|
||||||
|
interactionDetails: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
providerFactory = {
|
providerFactory = {
|
||||||
getProvider: jest.fn().mockResolvedValue(provider),
|
getProvider: jest.fn().mockResolvedValue(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
interactionHttpHandler = {
|
const handlers: InteractionHandler[] = [
|
||||||
canHandle: jest.fn(),
|
{ handleSafe: jest.fn().mockResolvedValue({ type: 'response', details: { key: 'val' }}) } as any,
|
||||||
handle: jest.fn(),
|
{ handleSafe: jest.fn().mockResolvedValue({ type: 'complete', details: { webId: 'webId' }}) } as any,
|
||||||
} as any;
|
];
|
||||||
|
|
||||||
|
routes = {
|
||||||
|
response: new InteractionRoute('/routeResponse', '/view1', handlers[0], 'default', '/response1'),
|
||||||
|
complete: new InteractionRoute('/routeComplete', '/view2', handlers[1], 'other', '/response2'),
|
||||||
|
};
|
||||||
|
|
||||||
|
templateHandler = { handleSafe: jest.fn() } as any;
|
||||||
|
|
||||||
|
interactionCompleter = { handleSafe: jest.fn() } as any;
|
||||||
|
|
||||||
errorHandler = { handleSafe: jest.fn() } as any;
|
errorHandler = { handleSafe: jest.fn() } as any;
|
||||||
|
|
||||||
responseWriter = { handleSafe: jest.fn() } as any;
|
responseWriter = { handleSafe: jest.fn() } as any;
|
||||||
|
|
||||||
handler = new IdentityProviderHttpHandler(
|
handler = new IdentityProviderHttpHandler(
|
||||||
|
idpPath,
|
||||||
providerFactory,
|
providerFactory,
|
||||||
interactionHttpHandler,
|
Object.values(routes),
|
||||||
|
templateHandler,
|
||||||
|
interactionCompleter,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
responseWriter,
|
responseWriter,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the provider if there is no matching handler.', async(): Promise<void> => {
|
it('errors if the idpPath does not start with a slash.', async(): Promise<void> => {
|
||||||
(interactionHttpHandler.canHandle as jest.Mock).mockRejectedValueOnce(new Error('error!'));
|
expect((): any => new IdentityProviderHttpHandler(
|
||||||
|
'idp', providerFactory, [], templateHandler, interactionCompleter, errorHandler, responseWriter,
|
||||||
|
)).toThrow('idpPath needs to start with a /');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the provider if there is no matching route.', async(): Promise<void> => {
|
||||||
|
request.url = 'invalid';
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
expect(provider.callback).toHaveBeenCalledTimes(1);
|
expect(provider.callback).toHaveBeenCalledTimes(1);
|
||||||
expect(provider.callback).toHaveBeenLastCalledWith(request, response);
|
expect(provider.callback).toHaveBeenLastCalledWith(request, response);
|
||||||
expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(0);
|
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the interaction handler if it can handle the input.', async(): Promise<void> => {
|
it('calls the templateHandler for matching GET requests.', async(): Promise<void> => {
|
||||||
|
request.url = '/idp/routeResponse';
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
expect(provider.callback).toHaveBeenCalledTimes(0);
|
expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(1);
|
expect(templateHandler.handleSafe).toHaveBeenLastCalledWith(
|
||||||
expect(interactionHttpHandler.handle).toHaveBeenLastCalledWith({ request, response, provider });
|
{ response, templateFile: routes.response.viewTemplate, contents: { errorMessage: '', prefilled: {}}},
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an error response if there was an issue with the interaction handler.', async(): Promise<void> => {
|
it('calls the templateHandler for InteractionResponseResults.', async(): Promise<void> => {
|
||||||
const error = new Error('error!');
|
request.url = '/idp/routeResponse';
|
||||||
const errorResponse: ResponseDescription = { statusCode: 500 };
|
request.method = 'POST';
|
||||||
interactionHttpHandler.handle.mockRejectedValueOnce(error);
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
errorHandler.handleSafe.mockResolvedValueOnce(errorResponse);
|
expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, response });
|
||||||
|
expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(templateHandler.handleSafe).toHaveBeenLastCalledWith(
|
||||||
|
{ response, templateFile: routes.response.responseTemplate, contents: { key: 'val' }},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the interactionCompleter for InteractionCompleteResults.', async(): Promise<void> => {
|
||||||
|
request.url = '/idp/routeComplete';
|
||||||
|
request.method = 'POST';
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request, response });
|
||||||
|
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ request, response, webId: 'webId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches paths based on prompt for requests to the root IDP.', async(): Promise<void> => {
|
||||||
|
request.url = '/idp';
|
||||||
|
request.method = 'POST';
|
||||||
|
provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any);
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
|
expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the default route for requests to the root IDP without (matching) prompt.', async(): Promise<void> => {
|
||||||
|
request.url = '/idp';
|
||||||
|
request.method = 'POST';
|
||||||
|
provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'notSupported' }} as any);
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the viewTemplate again in case of POST errors.', async(): Promise<void> => {
|
||||||
|
request.url = '/idp/routeResponse';
|
||||||
|
request.method = 'POST';
|
||||||
|
(routes.response.handler.handleSafe as any)
|
||||||
|
.mockRejectedValueOnce(new IdpInteractionError(500, 'handle error', { name: 'name' }));
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(templateHandler.handleSafe).toHaveBeenLastCalledWith({
|
||||||
|
response,
|
||||||
|
templateFile: routes.response.viewTemplate,
|
||||||
|
contents: { errorMessage: 'handle error', prefilled: { name: 'name' }},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to an empty prefilled object in case of POST errors.', async(): Promise<void> => {
|
||||||
|
request.url = '/idp/routeResponse';
|
||||||
|
request.method = 'POST';
|
||||||
|
(routes.response.handler.handleSafe as any).mockRejectedValueOnce(new Error('handle error'));
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(templateHandler.handleSafe).toHaveBeenLastCalledWith({
|
||||||
|
response,
|
||||||
|
templateFile: routes.response.viewTemplate,
|
||||||
|
contents: { errorMessage: 'handle error', prefilled: { }},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the errorHandler if there is a problem resolving the request.', async(): Promise<void> => {
|
||||||
|
request.url = '/idp/routeResponse';
|
||||||
|
request.method = 'GET';
|
||||||
|
const error = new Error('bad template');
|
||||||
|
templateHandler.handleSafe.mockRejectedValueOnce(error);
|
||||||
|
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
expect(provider.callback).toHaveBeenCalledTimes(0);
|
|
||||||
expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(1);
|
|
||||||
expect(interactionHttpHandler.handle).toHaveBeenLastCalledWith({ request, response, provider });
|
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('re-throws the error if it is not a native Error.', async(): Promise<void> => {
|
it('can only resolve GET/POST requests.', async(): Promise<void> => {
|
||||||
interactionHttpHandler.handle.mockRejectedValueOnce('apple!');
|
request.url = '/idp/routeResponse';
|
||||||
await expect(handler.handle({ request, response })).rejects.toEqual('apple!');
|
request.method = 'DELETE';
|
||||||
|
const error = new BadRequestHttpError('Unsupported request: DELETE /idp/routeResponse');
|
||||||
|
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if there is an issue creating the provider.', async(): Promise<void> => {
|
it('can only resolve InteractionResponseResult responses if a responseTemplate is set.', async(): Promise<void> => {
|
||||||
const error = new Error('error!');
|
request.url = '/idp/routeResponse';
|
||||||
providerFactory.getProvider.mockRejectedValueOnce(error);
|
request.method = 'POST';
|
||||||
await expect(handler.handle({ request, response })).rejects.toThrow(error);
|
(routes.response as any).responseTemplate = undefined;
|
||||||
|
const error = new BadRequestHttpError('Unsupported request: POST /idp/routeResponse');
|
||||||
|
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||||
|
});
|
||||||
|
|
||||||
providerFactory.getProvider.mockRejectedValueOnce('apple');
|
it('errors if no route is configured for the default prompt.', async(): Promise<void> => {
|
||||||
await expect(handler.handle({ request, response })).rejects.toBe('apple');
|
handler = new IdentityProviderHttpHandler(
|
||||||
|
idpPath, providerFactory, [], templateHandler, interactionCompleter, errorHandler, responseWriter,
|
||||||
|
);
|
||||||
|
request.url = '/idp';
|
||||||
|
provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any);
|
||||||
|
const error = new InternalServerError('No handler for the default session prompt has been configured.');
|
||||||
|
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Provider } from 'oidc-provider';
|
import type { Provider } from 'oidc-provider';
|
||||||
|
import type { ProviderFactory } from '../../../../src/identity/configuration/ProviderFactory';
|
||||||
import { SessionHttpHandler } from '../../../../src/identity/interaction/SessionHttpHandler';
|
import { SessionHttpHandler } from '../../../../src/identity/interaction/SessionHttpHandler';
|
||||||
import type { InteractionCompleter } from '../../../../src/identity/interaction/util/InteractionCompleter';
|
|
||||||
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
||||||
import type { HttpResponse } from '../../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../../src/server/HttpResponse';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
@ -11,7 +11,6 @@ describe('A SessionHttpHandler', (): void => {
|
|||||||
const webId = 'http://test.com/id#me';
|
const webId = 'http://test.com/id#me';
|
||||||
let details: any = {};
|
let details: any = {};
|
||||||
let provider: Provider;
|
let provider: Provider;
|
||||||
let oidcInteractionCompleter: InteractionCompleter;
|
|
||||||
let handler: SessionHttpHandler;
|
let handler: SessionHttpHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -20,31 +19,27 @@ describe('A SessionHttpHandler', (): void => {
|
|||||||
interactionDetails: jest.fn().mockResolvedValue(details),
|
interactionDetails: jest.fn().mockResolvedValue(details),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
oidcInteractionCompleter = {
|
const factory: ProviderFactory = {
|
||||||
handleSafe: jest.fn(),
|
getProvider: jest.fn().mockResolvedValue(provider),
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
handler = new SessionHttpHandler(oidcInteractionCompleter);
|
handler = new SessionHttpHandler(factory);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requires a session and accountId.', async(): Promise<void> => {
|
it('requires a session and accountId.', async(): Promise<void> => {
|
||||||
details.session = undefined;
|
details.session = undefined;
|
||||||
await expect(handler.handle({ request, response, provider })).rejects.toThrow(NotImplementedHttpError);
|
await expect(handler.handle({ request, response })).rejects.toThrow(NotImplementedHttpError);
|
||||||
|
|
||||||
details.session = { accountId: undefined };
|
details.session = { accountId: undefined };
|
||||||
await expect(handler.handle({ request, response, provider })).rejects.toThrow(NotImplementedHttpError);
|
await expect(handler.handle({ request, response })).rejects.toThrow(NotImplementedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the oidc completer with the webId in the session.', async(): Promise<void> => {
|
it('calls the oidc completer with the webId in the session.', async(): Promise<void> => {
|
||||||
await expect(handler.handle({ request, response, provider })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toEqual({
|
||||||
|
details: { webId },
|
||||||
|
type: 'complete',
|
||||||
|
});
|
||||||
expect(provider.interactionDetails).toHaveBeenCalledTimes(1);
|
expect(provider.interactionDetails).toHaveBeenCalledTimes(1);
|
||||||
expect(provider.interactionDetails).toHaveBeenLastCalledWith(request, response);
|
expect(provider.interactionDetails).toHaveBeenLastCalledWith(request, response);
|
||||||
expect(oidcInteractionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(oidcInteractionCompleter.handleSafe).toHaveBeenLastCalledWith({
|
|
||||||
request,
|
|
||||||
response,
|
|
||||||
provider,
|
|
||||||
webId,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import type { Provider } from 'oidc-provider';
|
|
||||||
import {
|
import {
|
||||||
ForgotPasswordHandler,
|
ForgotPasswordHandler,
|
||||||
} from '../../../../../../src/identity/interaction/email-password/handler/ForgotPasswordHandler';
|
} from '../../../../../../src/identity/interaction/email-password/handler/ForgotPasswordHandler';
|
||||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||||
import type { EmailSender } from '../../../../../../src/identity/interaction/util/EmailSender';
|
import type { EmailSender } from '../../../../../../src/identity/interaction/util/EmailSender';
|
||||||
import type { IdpRenderHandler } from '../../../../../../src/identity/interaction/util/IdpRenderHandler';
|
|
||||||
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
||||||
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
|
||||||
import type { TemplateEngine } from '../../../../../../src/util/templates/TemplateEngine';
|
import type { TemplateEngine } from '../../../../../../src/util/templates/TemplateEngine';
|
||||||
@ -16,9 +14,6 @@ describe('A ForgotPasswordHandler', (): void => {
|
|||||||
const email = 'test@test.email';
|
const email = 'test@test.email';
|
||||||
const recordId = '123456';
|
const recordId = '123456';
|
||||||
const html = `<a href="/base/idp/resetpassword/${recordId}">Reset Password</a>`;
|
const html = `<a href="/base/idp/resetpassword/${recordId}">Reset Password</a>`;
|
||||||
const renderParams = { response, contents: { errorMessage: '', prefilled: { email }}};
|
|
||||||
const provider: Provider = {} as any;
|
|
||||||
let messageRenderHandler: IdpRenderHandler;
|
|
||||||
let accountStore: AccountStore;
|
let accountStore: AccountStore;
|
||||||
const baseUrl = 'http://test.com/base/';
|
const baseUrl = 'http://test.com/base/';
|
||||||
const idpPath = '/idp';
|
const idpPath = '/idp';
|
||||||
@ -29,10 +24,6 @@ describe('A ForgotPasswordHandler', (): void => {
|
|||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
request = createPostFormRequest({ email });
|
request = createPostFormRequest({ email });
|
||||||
|
|
||||||
messageRenderHandler = {
|
|
||||||
handleSafe: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
accountStore = {
|
accountStore = {
|
||||||
generateForgotPasswordRecord: jest.fn().mockResolvedValue(recordId),
|
generateForgotPasswordRecord: jest.fn().mockResolvedValue(recordId),
|
||||||
} as any;
|
} as any;
|
||||||
@ -46,7 +37,6 @@ describe('A ForgotPasswordHandler', (): void => {
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
handler = new ForgotPasswordHandler({
|
handler = new ForgotPasswordHandler({
|
||||||
messageRenderHandler,
|
|
||||||
accountStore,
|
accountStore,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
idpPath,
|
idpPath,
|
||||||
@ -57,21 +47,21 @@ describe('A ForgotPasswordHandler', (): void => {
|
|||||||
|
|
||||||
it('errors on non-string emails.', async(): Promise<void> => {
|
it('errors on non-string emails.', async(): Promise<void> => {
|
||||||
request = createPostFormRequest({});
|
request = createPostFormRequest({});
|
||||||
await expect(handler.handle({ request, response, provider })).rejects.toThrow('Email required');
|
await expect(handler.handle({ request, response })).rejects.toThrow('Email required');
|
||||||
request = createPostFormRequest({ email: [ 'email', 'email2' ]});
|
request = createPostFormRequest({ email: [ 'email', 'email2' ]});
|
||||||
await expect(handler.handle({ request, response, provider })).rejects.toThrow('Email required');
|
await expect(handler.handle({ request, response })).rejects.toThrow('Email required');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not send a mail if a ForgotPassword record could not be generated.', async(): Promise<void> => {
|
it('does not send a mail if a ForgotPassword record could not be generated.', async(): Promise<void> => {
|
||||||
(accountStore.generateForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('error');
|
(accountStore.generateForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('error');
|
||||||
await expect(handler.handle({ request, response, provider })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves
|
||||||
|
.toEqual({ type: 'response', details: { email }});
|
||||||
expect(emailSender.handleSafe).toHaveBeenCalledTimes(0);
|
expect(emailSender.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
expect(messageRenderHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(messageRenderHandler.handleSafe).toHaveBeenLastCalledWith(renderParams);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends a mail if a ForgotPassword record could be generated.', async(): Promise<void> => {
|
it('sends a mail if a ForgotPassword record could be generated.', async(): Promise<void> => {
|
||||||
await expect(handler.handle({ request, response, provider })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves
|
||||||
|
.toEqual({ type: 'response', details: { email }});
|
||||||
expect(emailSender.handleSafe).toHaveBeenCalledTimes(1);
|
expect(emailSender.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(emailSender.handleSafe).toHaveBeenLastCalledWith({
|
expect(emailSender.handleSafe).toHaveBeenLastCalledWith({
|
||||||
recipient: email,
|
recipient: email,
|
||||||
@ -79,7 +69,5 @@ describe('A ForgotPasswordHandler', (): void => {
|
|||||||
text: `To reset your password, go to this link: http://test.com/base/idp/resetpassword/${recordId}`,
|
text: `To reset your password, go to this link: http://test.com/base/idp/resetpassword/${recordId}`,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
expect(messageRenderHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(messageRenderHandler.handleSafe).toHaveBeenLastCalledWith(renderParams);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { LoginHandler } from '../../../../../../src/identity/interaction/email-password/handler/LoginHandler';
|
import { LoginHandler } from '../../../../../../src/identity/interaction/email-password/handler/LoginHandler';
|
||||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||||
import type { InteractionHttpHandlerInput } from '../../../../../../src/identity/interaction/InteractionHttpHandler';
|
import type { HttpHandlerInput } from '../../../../../../src/server/HttpHandler';
|
||||||
import type { InteractionCompleter } from '../../../../../../src/identity/interaction/util/InteractionCompleter';
|
|
||||||
import { createPostFormRequest } from './Util';
|
import { createPostFormRequest } from './Util';
|
||||||
|
|
||||||
describe('A LoginHandler', (): void => {
|
describe('A LoginHandler', (): void => {
|
||||||
const webId = 'http://alice.test.com/card#me';
|
const webId = 'http://alice.test.com/card#me';
|
||||||
const email = 'alice@test.email';
|
const email = 'alice@test.email';
|
||||||
let input: InteractionHttpHandlerInput;
|
let input: HttpHandlerInput;
|
||||||
let storageAdapter: AccountStore;
|
let storageAdapter: AccountStore;
|
||||||
let interactionCompleter: InteractionCompleter;
|
|
||||||
let handler: LoginHandler;
|
let handler: LoginHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -19,11 +17,7 @@ describe('A LoginHandler', (): void => {
|
|||||||
authenticate: jest.fn().mockResolvedValue(webId),
|
authenticate: jest.fn().mockResolvedValue(webId),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
interactionCompleter = {
|
handler = new LoginHandler(storageAdapter);
|
||||||
handleSafe: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
handler = new LoginHandler({ accountStore: storageAdapter, interactionCompleter });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors on invalid emails.', async(): Promise<void> => {
|
it('errors on invalid emails.', async(): Promise<void> => {
|
||||||
@ -56,13 +50,13 @@ describe('A LoginHandler', (): void => {
|
|||||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }}));
|
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the OidcInteractionCompleter when done.', async(): Promise<void> => {
|
it('returns an InteractionCompleteResult when done.', async(): Promise<void> => {
|
||||||
input.request = createPostFormRequest({ email, password: 'password!' });
|
input.request = createPostFormRequest({ email, password: 'password!' });
|
||||||
await expect(handler.handle(input)).resolves.toBeUndefined();
|
await expect(handler.handle(input)).resolves.toEqual({
|
||||||
|
type: 'complete',
|
||||||
|
details: { webId, shouldRemember: false },
|
||||||
|
});
|
||||||
expect(storageAdapter.authenticate).toHaveBeenCalledTimes(1);
|
expect(storageAdapter.authenticate).toHaveBeenCalledTimes(1);
|
||||||
expect(storageAdapter.authenticate).toHaveBeenLastCalledWith(email, 'password!');
|
expect(storageAdapter.authenticate).toHaveBeenLastCalledWith(email, 'password!');
|
||||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(interactionCompleter.handleSafe)
|
|
||||||
.toHaveBeenLastCalledWith({ ...input, webId, shouldRemember: false });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,6 @@ import type { PodManager } from '../../../../../../src/pods/PodManager';
|
|||||||
import type { PodSettings } from '../../../../../../src/pods/settings/PodSettings';
|
import type { PodSettings } from '../../../../../../src/pods/settings/PodSettings';
|
||||||
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
||||||
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
|
||||||
import type { TemplateHandler } from '../../../../../../src/server/util/TemplateHandler';
|
|
||||||
import { createPostFormRequest } from './Util';
|
import { createPostFormRequest } from './Util';
|
||||||
|
|
||||||
describe('A RegistrationHandler', (): void => {
|
describe('A RegistrationHandler', (): void => {
|
||||||
@ -37,7 +36,6 @@ describe('A RegistrationHandler', (): void => {
|
|||||||
let ownershipValidator: OwnershipValidator;
|
let ownershipValidator: OwnershipValidator;
|
||||||
let accountStore: AccountStore;
|
let accountStore: AccountStore;
|
||||||
let podManager: PodManager;
|
let podManager: PodManager;
|
||||||
let responseHandler: TemplateHandler<NodeJS.Dict<any>>;
|
|
||||||
let handler: RegistrationHandler;
|
let handler: RegistrationHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -61,10 +59,6 @@ describe('A RegistrationHandler', (): void => {
|
|||||||
createPod: jest.fn(),
|
createPod: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
responseHandler = {
|
|
||||||
handleSafe: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
handler = new RegistrationHandler({
|
handler = new RegistrationHandler({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
webIdSuffix,
|
webIdSuffix,
|
||||||
@ -72,7 +66,6 @@ describe('A RegistrationHandler', (): void => {
|
|||||||
accountStore,
|
accountStore,
|
||||||
ownershipValidator,
|
ownershipValidator,
|
||||||
podManager,
|
podManager,
|
||||||
responseHandler,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -151,7 +144,17 @@ describe('A RegistrationHandler', (): void => {
|
|||||||
describe('handling data', (): void => {
|
describe('handling data', (): void => {
|
||||||
it('can register a user.', async(): Promise<void> => {
|
it('can register a user.', async(): Promise<void> => {
|
||||||
request = createPostFormRequest({ email, webId, password, confirmPassword, register });
|
request = createPostFormRequest({ email, webId, password, confirmPassword, register });
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toEqual({
|
||||||
|
details: {
|
||||||
|
email,
|
||||||
|
webId,
|
||||||
|
oidcIssuer: baseUrl,
|
||||||
|
createWebId: false,
|
||||||
|
register: true,
|
||||||
|
createPod: false,
|
||||||
|
},
|
||||||
|
type: 'response',
|
||||||
|
});
|
||||||
|
|
||||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||||
@ -168,7 +171,18 @@ describe('A RegistrationHandler', (): void => {
|
|||||||
it('can create a pod.', async(): Promise<void> => {
|
it('can create a pod.', async(): Promise<void> => {
|
||||||
const params = { email, webId, podName, createPod };
|
const params = { email, webId, podName, createPod };
|
||||||
request = createPostFormRequest(params);
|
request = createPostFormRequest(params);
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toEqual({
|
||||||
|
details: {
|
||||||
|
email,
|
||||||
|
webId,
|
||||||
|
oidcIssuer: baseUrl,
|
||||||
|
podBaseUrl: `${baseUrl}${podName}/`,
|
||||||
|
createWebId: false,
|
||||||
|
register: false,
|
||||||
|
createPod: true,
|
||||||
|
},
|
||||||
|
type: 'response',
|
||||||
|
});
|
||||||
|
|
||||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||||
@ -186,7 +200,18 @@ describe('A RegistrationHandler', (): void => {
|
|||||||
const params = { email, webId, password, confirmPassword, podName, register, createPod };
|
const params = { email, webId, password, confirmPassword, podName, register, createPod };
|
||||||
podSettings.oidcIssuer = baseUrl;
|
podSettings.oidcIssuer = baseUrl;
|
||||||
request = createPostFormRequest(params);
|
request = createPostFormRequest(params);
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toEqual({
|
||||||
|
details: {
|
||||||
|
email,
|
||||||
|
webId,
|
||||||
|
oidcIssuer: baseUrl,
|
||||||
|
podBaseUrl: `${baseUrl}${podName}/`,
|
||||||
|
createWebId: false,
|
||||||
|
register: true,
|
||||||
|
createPod: true,
|
||||||
|
},
|
||||||
|
type: 'response',
|
||||||
|
});
|
||||||
|
|
||||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||||
@ -225,12 +250,23 @@ describe('A RegistrationHandler', (): void => {
|
|||||||
|
|
||||||
it('can create a WebID with an account and pod.', async(): Promise<void> => {
|
it('can create a WebID with an account and pod.', async(): Promise<void> => {
|
||||||
const params = { email, password, confirmPassword, podName, createWebId, register, createPod };
|
const params = { email, password, confirmPassword, podName, createWebId, register, createPod };
|
||||||
podSettings.oidcIssuer = baseUrl;
|
|
||||||
request = createPostFormRequest(params);
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
|
|
||||||
const generatedWebID = urljoin(baseUrl, podName, webIdSuffix);
|
const generatedWebID = urljoin(baseUrl, podName, webIdSuffix);
|
||||||
podSettings.webId = generatedWebID;
|
podSettings.webId = generatedWebID;
|
||||||
|
podSettings.oidcIssuer = baseUrl;
|
||||||
|
|
||||||
|
request = createPostFormRequest(params);
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toEqual({
|
||||||
|
details: {
|
||||||
|
email,
|
||||||
|
webId: generatedWebID,
|
||||||
|
oidcIssuer: baseUrl,
|
||||||
|
podBaseUrl: `${baseUrl}${podName}/`,
|
||||||
|
createWebId: true,
|
||||||
|
register: true,
|
||||||
|
createPod: true,
|
||||||
|
},
|
||||||
|
type: 'response',
|
||||||
|
});
|
||||||
|
|
||||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||||
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
||||||
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
|
||||||
import type { TemplateHandler } from '../../../../../../src/server/util/TemplateHandler';
|
|
||||||
import { createPostFormRequest } from './Util';
|
import { createPostFormRequest } from './Util';
|
||||||
|
|
||||||
describe('A ResetPasswordHandler', (): void => {
|
describe('A ResetPasswordHandler', (): void => {
|
||||||
@ -14,7 +13,6 @@ describe('A ResetPasswordHandler', (): void => {
|
|||||||
const url = `/resetURL/${recordId}`;
|
const url = `/resetURL/${recordId}`;
|
||||||
const email = 'alice@test.email';
|
const email = 'alice@test.email';
|
||||||
let accountStore: AccountStore;
|
let accountStore: AccountStore;
|
||||||
let messageRenderHandler: TemplateHandler<{ message: string }>;
|
|
||||||
let handler: ResetPasswordHandler;
|
let handler: ResetPasswordHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -24,14 +22,7 @@ describe('A ResetPasswordHandler', (): void => {
|
|||||||
changePassword: jest.fn(),
|
changePassword: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
messageRenderHandler = {
|
handler = new ResetPasswordHandler(accountStore);
|
||||||
handleSafe: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
handler = new ResetPasswordHandler({
|
|
||||||
accountStore,
|
|
||||||
messageRenderHandler,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors for non-string recordIds.', async(): Promise<void> => {
|
it('errors for non-string recordIds.', async(): Promise<void> => {
|
||||||
@ -57,16 +48,16 @@ describe('A ResetPasswordHandler', (): void => {
|
|||||||
|
|
||||||
it('renders a message on success.', async(): Promise<void> => {
|
it('renders a message on success.', async(): Promise<void> => {
|
||||||
request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url);
|
request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url);
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toEqual({
|
||||||
|
details: { message: 'Your password was successfully reset.' },
|
||||||
|
type: 'response',
|
||||||
|
});
|
||||||
expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1);
|
expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1);
|
||||||
expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId);
|
expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId);
|
||||||
expect(accountStore.deleteForgotPasswordRecord).toHaveBeenCalledTimes(1);
|
expect(accountStore.deleteForgotPasswordRecord).toHaveBeenCalledTimes(1);
|
||||||
expect(accountStore.deleteForgotPasswordRecord).toHaveBeenLastCalledWith(recordId);
|
expect(accountStore.deleteForgotPasswordRecord).toHaveBeenLastCalledWith(recordId);
|
||||||
expect(accountStore.changePassword).toHaveBeenCalledTimes(1);
|
expect(accountStore.changePassword).toHaveBeenCalledTimes(1);
|
||||||
expect(accountStore.changePassword).toHaveBeenLastCalledWith(email, 'password!');
|
expect(accountStore.changePassword).toHaveBeenLastCalledWith(email, 'password!');
|
||||||
expect(messageRenderHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(messageRenderHandler.handleSafe)
|
|
||||||
.toHaveBeenLastCalledWith({ response, contents: { message: 'Your password was successfully reset.' }});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a default error for non-native errors.', async(): Promise<void> => {
|
it('has a default error for non-native errors.', async(): Promise<void> => {
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
import type { Provider } from 'oidc-provider';
|
|
||||||
import { IdpInteractionError } from '../../../../../src/identity/interaction/util/IdpInteractionError';
|
|
||||||
import type { IdpRenderHandler } from '../../../../../src/identity/interaction/util/IdpRenderHandler';
|
|
||||||
import {
|
|
||||||
IdpRouteController,
|
|
||||||
} from '../../../../../src/identity/interaction/util/IdpRouteController';
|
|
||||||
import type { HttpHandler } from '../../../../../src/server/HttpHandler';
|
|
||||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
|
||||||
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
|
||||||
|
|
||||||
describe('An IdpRouteController', (): void => {
|
|
||||||
let request: HttpRequest;
|
|
||||||
const response: HttpResponse = {} as any;
|
|
||||||
const provider: Provider = {} as any;
|
|
||||||
let renderHandler: IdpRenderHandler;
|
|
||||||
let postHandler: HttpHandler;
|
|
||||||
let controller: IdpRouteController;
|
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
|
||||||
request = {
|
|
||||||
randomData: 'data!',
|
|
||||||
method: 'GET',
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
renderHandler = {
|
|
||||||
handleSafe: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
postHandler = {
|
|
||||||
handleSafe: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
controller = new IdpRouteController('pathName', renderHandler, postHandler);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders the renderHandler for GET requests.', async(): Promise<void> => {
|
|
||||||
await expect(controller.handle({ request, response, provider })).resolves.toBeUndefined();
|
|
||||||
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
|
|
||||||
response,
|
|
||||||
contents: { errorMessage: '', prefilled: {}},
|
|
||||||
});
|
|
||||||
expect(postHandler.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls the postHandler for POST requests.', async(): Promise<void> => {
|
|
||||||
request.method = 'POST';
|
|
||||||
await expect(controller.handle({ request, response, provider })).resolves.toBeUndefined();
|
|
||||||
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
expect(postHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(postHandler.handleSafe).toHaveBeenLastCalledWith({ request, response, provider });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders an error if the POST request failed.', async(): Promise<void> => {
|
|
||||||
request.method = 'POST';
|
|
||||||
const error = new IdpInteractionError(400, 'bad request!', { more: 'data!' });
|
|
||||||
(postHandler.handleSafe as jest.Mock).mockRejectedValueOnce(error);
|
|
||||||
await expect(controller.handle({ request, response, provider })).resolves.toBeUndefined();
|
|
||||||
expect(postHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(postHandler.handleSafe).toHaveBeenLastCalledWith({ request, response, provider });
|
|
||||||
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
|
|
||||||
response,
|
|
||||||
contents: { errorMessage: 'bad request!', prefilled: { more: 'data!' }},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a default error message if none is provided.', async(): Promise<void> => {
|
|
||||||
request.method = 'POST';
|
|
||||||
(postHandler.handleSafe as jest.Mock).mockRejectedValueOnce('apple!');
|
|
||||||
await expect(controller.handle({ request, response, provider })).resolves.toBeUndefined();
|
|
||||||
expect(postHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(postHandler.handleSafe).toHaveBeenLastCalledWith({ request, response, provider });
|
|
||||||
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
|
|
||||||
response,
|
|
||||||
contents: { errorMessage: 'Unknown error: apple!', prefilled: {}},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing for other methods.', async(): Promise<void> => {
|
|
||||||
request.method = 'DELETE';
|
|
||||||
await expect(controller.handle({ request, response, provider })).resolves.toBeUndefined();
|
|
||||||
expect(postHandler.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,52 +0,0 @@
|
|||||||
import type { MockResponse } from 'node-mocks-http';
|
|
||||||
import { createResponse } from 'node-mocks-http';
|
|
||||||
import type { Provider } from 'oidc-provider';
|
|
||||||
import type { RedirectMap } from '../../../../../src/identity/interaction/util/InitialInteractionHandler';
|
|
||||||
import { InitialInteractionHandler } from '../../../../../src/identity/interaction/util/InitialInteractionHandler';
|
|
||||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
|
||||||
|
|
||||||
describe('An InitialInteractionHandler', (): void => {
|
|
||||||
const baseUrl = 'http://test.com/';
|
|
||||||
const request: HttpRequest = {} as any;
|
|
||||||
let response: MockResponse<any>;
|
|
||||||
let provider: jest.Mocked<Provider>;
|
|
||||||
// `Interaction` type is not exposed
|
|
||||||
let details: any;
|
|
||||||
let map: RedirectMap;
|
|
||||||
let handler: InitialInteractionHandler;
|
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
|
||||||
response = createResponse();
|
|
||||||
|
|
||||||
map = {
|
|
||||||
default: '/idp/login',
|
|
||||||
test: '/idp/test',
|
|
||||||
};
|
|
||||||
|
|
||||||
details = { prompt: { name: 'test' }};
|
|
||||||
provider = {
|
|
||||||
interactionDetails: jest.fn().mockResolvedValue(details),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
handler = new InitialInteractionHandler(baseUrl, map);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses the named handler if it is found.', async(): Promise<void> => {
|
|
||||||
await expect(handler.handle({ request, response, provider })).resolves.toBeUndefined();
|
|
||||||
expect(provider.interactionDetails).toHaveBeenCalledTimes(1);
|
|
||||||
expect(provider.interactionDetails).toHaveBeenLastCalledWith(request, response);
|
|
||||||
expect(response._isEndCalled()).toBe(true);
|
|
||||||
expect(response.getHeader('location')).toBe('http://test.com/idp/test');
|
|
||||||
expect(response.statusCode).toBe(302);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses the default handler if there is no match.', async(): Promise<void> => {
|
|
||||||
details.prompt.name = 'unknown';
|
|
||||||
await expect(handler.handle({ request, response, provider })).resolves.toBeUndefined();
|
|
||||||
expect(provider.interactionDetails).toHaveBeenCalledTimes(1);
|
|
||||||
expect(provider.interactionDetails).toHaveBeenLastCalledWith(request, response);
|
|
||||||
expect(response._isEndCalled()).toBe(true);
|
|
||||||
expect(response.getHeader('location')).toBe('http://test.com/idp/login');
|
|
||||||
expect(response.statusCode).toBe(302);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,4 +1,5 @@
|
|||||||
import type { Provider } from 'oidc-provider';
|
import type { Provider } from 'oidc-provider';
|
||||||
|
import type { ProviderFactory } from '../../../../../src/identity/configuration/ProviderFactory';
|
||||||
import { InteractionCompleter } from '../../../../../src/identity/interaction/util/InteractionCompleter';
|
import { InteractionCompleter } from '../../../../../src/identity/interaction/util/InteractionCompleter';
|
||||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
||||||
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
||||||
@ -11,16 +12,22 @@ describe('An InteractionCompleter', (): void => {
|
|||||||
const response: HttpResponse = {} as any;
|
const response: HttpResponse = {} as any;
|
||||||
const webId = 'http://alice.test.com/#me';
|
const webId = 'http://alice.test.com/#me';
|
||||||
let provider: Provider;
|
let provider: Provider;
|
||||||
const completer = new InteractionCompleter();
|
let completer: InteractionCompleter;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
provider = {
|
provider = {
|
||||||
interactionFinished: jest.fn(),
|
interactionFinished: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
|
const factory: ProviderFactory = {
|
||||||
|
getProvider: jest.fn().mockResolvedValue(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
completer = new InteractionCompleter(factory);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends the correct data to the provider.', async(): Promise<void> => {
|
it('sends the correct data to the provider.', async(): Promise<void> => {
|
||||||
await expect(completer.handle({ request, response, provider, webId, shouldRemember: true }))
|
await expect(completer.handle({ request, response, webId, shouldRemember: true }))
|
||||||
.resolves.toBeUndefined();
|
.resolves.toBeUndefined();
|
||||||
expect(provider.interactionFinished).toHaveBeenCalledTimes(1);
|
expect(provider.interactionFinished).toHaveBeenCalledTimes(1);
|
||||||
expect(provider.interactionFinished).toHaveBeenLastCalledWith(request, response, {
|
expect(provider.interactionFinished).toHaveBeenLastCalledWith(request, response, {
|
||||||
@ -36,7 +43,7 @@ describe('An InteractionCompleter', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('rejects offline access if shouldRemember is false.', async(): Promise<void> => {
|
it('rejects offline access if shouldRemember is false.', async(): Promise<void> => {
|
||||||
await expect(completer.handle({ request, response, provider, webId, shouldRemember: false }))
|
await expect(completer.handle({ request, response, webId, shouldRemember: false }))
|
||||||
.resolves.toBeUndefined();
|
.resolves.toBeUndefined();
|
||||||
expect(provider.interactionFinished).toHaveBeenCalledTimes(1);
|
expect(provider.interactionFinished).toHaveBeenCalledTimes(1);
|
||||||
expect(provider.interactionFinished).toHaveBeenLastCalledWith(request, response, {
|
expect(provider.interactionFinished).toHaveBeenLastCalledWith(request, response, {
|
||||||
|
@ -5,6 +5,7 @@ import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngi
|
|||||||
|
|
||||||
describe('A TemplateHandler', (): void => {
|
describe('A TemplateHandler', (): void => {
|
||||||
const contents = { contents: 'contents' };
|
const contents = { contents: 'contents' };
|
||||||
|
const templateFile = '/templates/main.html.ejs';
|
||||||
let templateEngine: jest.Mocked<TemplateEngine>;
|
let templateEngine: jest.Mocked<TemplateEngine>;
|
||||||
let response: HttpResponse;
|
let response: HttpResponse;
|
||||||
|
|
||||||
@ -17,10 +18,10 @@ describe('A TemplateHandler', (): void => {
|
|||||||
|
|
||||||
it('renders the template in the response.', async(): Promise<void> => {
|
it('renders the template in the response.', async(): Promise<void> => {
|
||||||
const handler = new TemplateHandler(templateEngine);
|
const handler = new TemplateHandler(templateEngine);
|
||||||
await handler.handle({ response, contents });
|
await handler.handle({ response, contents, templateFile });
|
||||||
|
|
||||||
expect(templateEngine.render).toHaveBeenCalledTimes(1);
|
expect(templateEngine.render).toHaveBeenCalledTimes(1);
|
||||||
expect(templateEngine.render).toHaveBeenCalledWith(contents);
|
expect(templateEngine.render).toHaveBeenCalledWith(contents, { templateFile });
|
||||||
|
|
||||||
expect(response.getHeaders()).toHaveProperty('content-type', 'text/html');
|
expect(response.getHeaders()).toHaveProperty('content-type', 'text/html');
|
||||||
expect((response as any)._isEndCalled()).toBe(true);
|
expect((response as any)._isEndCalled()).toBe(true);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user