New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pod management authentication #972
Comments
Or could just be "sign up" / "log in" buttons. Could also become a multi-page thing (on the client, not on the server). Then people would first create an account (or log in to one) and from there they could manage their pods. Seems like a good idea actually. |
The problem is that the registration class also becomes more complex because of all different situations it has to take into account. What would potentially be easiest for the server side, is if account creation and all the other components were separated from each other. So first a user makes an account, and then on their account page they can link a WebID/create a pod/register with the IDP as separate actions. The disadvantage is that multiple steps are required before you get a working pod, although you could always make a fancy HTML page that hides the fact that multiple steps are taken in the backend. It might also complicate setup a bit. This would also make it easier to support other login methods besides email/password since then we could have 2 separate handlers there for creating an account, without having to worry about all the other stuff that also has to happen during the registration step. |
My suggestion on how to implement this: All of this works from the assumption of still using our internal key/value system for this. Having storage that can be easily queried (such as a SPARQL store) would make some of this easier, but would require external dependencies or some custom solution with lots of caching involved (e.g., using N3.js stores to cache a turtle file). InterfacesThe idea is to have a main account object that links to the relevant login method(s), pods and WebIDs. For each one of those there would be a separate object containing the relevant settings. When creating an account, we would generate a unique identifier that never changes and can be used to link these together. interface AccountSettings {
id: string; // The unique identifier
logins: string[]; // Unique identifiers for each login object, more about this below
pods: string[]; // Base URLs of all pods linked to this account
webIds: string[]; // All WebIDs linked to this account
}
interface LoginSettings {
accountId: string;
id: string;
type: string; // Identifies the type of login
}
// Example Login interface
interface EmailPasswordLoginSettings extends LoginSettings {
type: 'email/password';
email: string;
password: string;
}
// This interface already exists but will need to be updated a bit
interface PodSettings {
accountId: string;
// Everything relevant to the pod, we already have something like this for dynamic pods
}
interface WebIdSettings {
accountId: string;
webId: string;
useIdp: boolean; // Determines if this WebID can be identified by the IDP
} All of these could potentially be extended in the future when needed. When logging in we can have a waterfall handler that determines based on the Pod ownershipThe reason everything is stored in separate objects instead of 1 big object is because sometimes we other entry points into the account. Specifically for the case where we want pod owners to always have control access to resources in their pod. In that case we want to find the pod name(s) based on the WebID. This can be done in this case by first finding the WebID object. There we get the accountId from, which we use to get the Account object containing the Pod URLs. CookiesWe could require users to log in with their WebID to access the account pages, but this would not really be user friendly (and requires an account to have a WebID). The alternative is to use cookies. We can store cookies in an expiring storage linking the cookie value to the account identifier, which can then be used to find all the relevant account data. This can potentially be done in a CredentialsExtractor. We can create a new CredentialGroup specifically for users identified by cookies so these don't get used for acl. These credentials can then be passed along to the relevant handler. Currently this is not supported yet by the interfaces used. One possible solution would be to add the credentials to the Operation object. Or they could be added as metadata to the body. Writing the cookies back is also going to require some changes. Response headers are set through the response metadata, but none of the objects IDP handlers can return support this. One solution is to create a new interface of something an IDP handler can return and let the base IDP class interpret this correctly. The other solution is to adapt one of the interfaces to return a Representation instead of a simple JSON object. Cookie header metadata could then be added there. |
Looks good!
This makes me wonder if we don't want to write that code already in a query-based way (for future-proof reasons), but then have an underlying translation layer…
…but yeah, that might be expensive. |
I have a working version of this locally that still requires a lot of cleanup before it can be pushed. I'll already give an overview of what the API currently looks like for potential feedback if needed. JSON API
I wanted to fully separate WebID and pod creation. So creating a pod would just create an empty pod, and then you could insert a WebID in there with a different call, but that requires several larger changes in the pod generation so this is future work. Besides the above, there are still the old APIs to consent during an OIDC authentication, and the forgot/reset password APIs. Login procedureLogging in using OIDC now takes the following API steps:
Steps 1 and 4 (if the remember option is chosen) both set a cookie meaning that in future requests only step 6 would remain. These are the API calls, the user would see 4 HTML pages:
In practice, we can make pages 1 and 3 so they redirect automatically if there is only 1 option, HTML APIEven though everything is separated now, we could still have an CookiesAfter logging in, future API requests need to have the cookie attached to identify the user. If this is annoying for API calls, we could also change it so the cookie value can be sent along in the JSON body, or use the Authentication header instead. But then we would have to extract the value from the cookie as the users interact with the API through the HTML pages. Solid OIDC can also be used to identify the user. If there is an incoming request without cookie, we will check the credentials. If the request has WebID credentials we will find the associated account (if there is one) and use that to identify the user. So once you have a WebID linked to an account, that is a way to do future actions. EDIT: This should probably be removed again as this would allow someone to use client credentials to create more client credentials once we add support for that. |
All good!
So these are authentication-dependent resources. Are they aliases for unique resources such as
Would definitely make those globally unique.
Let Cookie be one of the methods indeed. |
Yes, if those would exist. The current suggestion doesn't actually include such unique links.
As in, make it so they are accessed through Will have to do some rethinking of how routing works in the IDP components as they currently are all static paths. |
Yes, I think we need that; having (only) auth-dependent resources to expose this functionality, will make it difficult for admin accounts etc. |
Looking into having the URLs as follows. Using
Could also have a And once client credentials are added also |
Shall we do Yes to all of the above. |
Warning: very long. Having a look to continue work on this. When I stopped working on this I already had working APIs, but a lot of cleanup is still required and I will also have to see if there are any internal hacks currently just so the API would work. Below is an extensive overview of all the API requests and responses that are needed to create an account and all the other things it can do. These all actually work as I have them in a postman collection test run. The idea is that we would still have a frontend that hides these multiple requests from the user. We could even still have the one simple registration page that we currently have, besides perhaps a more extensive combination of pages that allows more flexibility. Get indexThe starting point for the IDP API. The one URL that you need to "know".
Output{
"controls": {
"main": {
"index": "http://localhost:3000/.account/",
"login": "http://localhost:3000/.account/login/",
"account": "http://localhost:3000/.account/account/",
"register": "http://localhost:3000/.account/register/"
},
"account": {}
},
"apiVersion": "0.4"
} Going to remove the Create account
Input{} Output{
"account": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/",
"accountId": "e0aa64d5-261a-4616-aff3-488538632440",
"controls": {
"main": { ... },
"account": {}
}
} Additionally, this response also returns a cookie header making the user logged in as this account. This is necessary since we just created an account without an actual way to login, as this will be added in a next step. This newly created account is temporary though, and will be removed after some time (currently 24h) if no login method gets added. The Get account controlsThis is doing a request to the initial index URL again, because now the request is logged in (due to the cookie) so will show the controls specifically for this account. These could not be added in the previous response due to architectural reasons since the component adding those controls simply checks if the request is authenticated (due to cookie) which was not the case there. This is a bit annoying but I'm not sure if there is a clean fix (and it is just one extra HTTP call in a larger set).
Output{
"controls": {
"main": { ... },
"account": {
"account": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/",
"password": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/login/password/",
"pod": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/pod/",
"webId": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/webid/",
"credentials": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/credentials/",
"logout": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/logout/"
}
}
} All the fields here are configured through Components.js, so if there were other login methods these would also have entries here. Perhaps something to be said to split off the login controls to a separate subgroup so they are grouped together if there are multiple. Add password login method
Input{
"email": "test@example.com",
"password": "secret",
"confirmPassword": "secret"
} The Output{
"password": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/login/password/test%40example.com/",
"controls": {
"main": { ... },
"account": { ... }
}
} The returned URL in the Log outSomething to be said that perhaps this should just happen automatically every time a new login method gets added/changed. This invalidates the cookie.
Find password login URLStarting again from the main controls in the very first request, there was a URL to find the available login methods:
Output{
"logins": {
"password": "http://localhost:3000/.account/login/password/"
},
"controls": {
"main": { ... },
"account": {}
}
} This lists all the available login methods and their specific URL to access them. Log in using email/password
Input{
"email": "test@example.com",
"password": "secret"
} OutputThe same as when creating an account above. Note that most of the APIs described here that require a POST request also accept GET requests which will describe how the API should be used. For example, doing a GET to the login/password URL here yields the following: {
"fields": {
"email": { "required": true, "type": "string" },
"password": { "required": true, "type": "string" }
},
"controls": {
"main":{ ... },
"account":{}
}
} Create PodThis, and all the other steps below, require the request to have a cookie indicating they are logged in. These are all URLs that can be found in the account controls.
Input{
"name": "test"
} The only required field. It also takes an optional Output{
"baseUrl": "http://localhost:3000/test/",
"webId": "http://localhost:3000/test/profile/card#me",
"controls": { ... }
} The URL of the pod and the WebID that was used for its default authorization resources. The WebID is also relevant in case this one was generated in the pod so the user knows what it is. As an aside: I wanted to split up WebID creation and pod creation even more than this, but it felt like that would make this very big PR even much bigger and complicated so this is the compromise (for now). Register WebIDThe above step did not register the WebID to the account. It just created the relevant resources. This step makes it so a WebID can be used for Solid OIDC authentication when you log in with this account.
Input{
"webId": "http://localhost:3000/test/profile/card#me"
} Output{
"url": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/webid/5c1b70d3ffaa840394dda86889ed1569cf897ef3d6041fb4c9513f82144cbb7f/",
"oidcIssuer": "http://localhost:3000/",
"controls": { ... }
} Similarly, a resource URL for this "connection". The OIDC issuer value is relevant to tell the user they need to add the oidcIssuer triple to their WebID. When generating a pod with a WebID we cheat and already add this triple just to make life easier for newer users. We could instead also do a PATCH in this call but then we would "uglify" the WebID. In case the WebID is an external the first response will be an error and tell the user to add a specific identification triple to their WebID to prove they are the owner. This is not something you can do if the WebID was generated in the previous step, since you can't authenticate yet as that WebID, so in case the WebID is contained within a pod associated with this account, we do not require such an identification triple and immediately make the connection. Create credentials tokenThis is still possible. (This is the token that can be used to have authenticated requests later without needing to log in with the browser).
Input{
"name": "token",
"webId": "http://localhost:3000/test/profile/card#me"
} Output{
"id": "token_b04bea98-6cf1-4633-bcf9-3930bf507265",
"secret": "e5033adda0efbe1985304e5161fbc7ab5b78a225132fbca77456669f3601a86f111a2e8a83e96a0df640af61ee988c4c571be58e55b0253cf4d781e1f6e387dd",
"url": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/token_b04bea98-6cf1-4633-bcf9-3930bf507265/",
"controls": { ... }
} Looking at this I realize that the generated URL should be in the See account detailsThis will return all the information about the current account and will contain everything we did above, including their resource URLs if any have to be modified.
Output{
"id": "e0aa64d5-261a-4616-aff3-488538632440",
"logins": {
"password": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/login/password/test%40example.com/"
},
"pods": {
"http://localhost:3000/test/": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/pod/test/"
},
"webIds": {
"http://localhost:3000/test/profile/card#me": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/webid/5c1b70d3ffaa840394dda86889ed1569cf897ef3d6041fb4c9513f82144cbb7f/"
},
"credentials": {
"token_b04bea98-6cf1-4633-bcf9-3930bf507265": "http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/token_b04bea98-6cf1-4633-bcf9-3930bf507265/"
},
"controls": { ... },
} Update passwordBy doing a POST to the resource URL of the password login you can modify it, specificlly in this case by changing the password. Other login methods would have their own handlers to do something else.
Input{
"oldPassword": "secret",
"password": "secret2",
"confirmPassword": "secret2"
} Same comment about the Register combined accountThere is also a single API call that does everything our current registration API does. This just in case there were some users highly dependent on that and would not be happy about having to completely redo that. But this could be temporary and come with a warning that we'll remove it in the future. I just noticed that it still requires a user to be logged in though (which can be done by creating an empty account), so it's not a complete replacement of that API. Or this might be a bug which would also make sense. This combines the create login, create pod, and link WebID steps (and create account if the above is a bug). The idea is also that if some of the steps fail, the request the partial succesful steps are already returned. This could for example be the case if an external WebID is used and an identification triple needs to be added. As can be seen I remember less about this API specifically since it's not really part of the whole new API idea and just there to help migration potentially.
Input{
"login": {
"email": "register@example.com",
"password": "secret",
"confirmPassword": "secret"
},
"pod": {
"name": "register"
}
} Output{
"login": {
"password": "http://localhost:3000/.account/account/bb8f7b5b-c071-4fef-b303-2e04e40ed551/login/password/register%40example.com/"
},
"pod": {
"baseUrl": "http://localhost:3000/register/",
"webId": "http://localhost:3000/register/profile/card#me"
},
"controls": { ... }
} OIDC requestsThese are not part of my postman test suite as this is more complicated to set up, but the login procedure mentioned in the above comment is still correct:
Most of the above is doing a GET request on the relevant account resource to get all the relevant options, and then sending it to the correct OIDC API, which should be part of the controls when doing an OIDC request. There are some issues here because the |
Done in #1677 |
Currently, #938 makes it so there can only be 1 pod per WebID. This is an issue and something we don't want. A nice solution would be if internally there was 1 account with a email/password/WebID combination that can have multiple pods. The idea is then that during registration at most 1 pod can be made, but afterwards other pods can be created by logging in and adding a new pod.
There are multiple ways to handle this, but all of them are a bit extensive so it made sense to make a separate issue for this first.
Update the registration page
There would then be a checkbox on the registration page "I already have an account". If checked the confirm password field would disappear. Server-side the password/email combination would then be used to find the correct account and add the newly created pod to it. Some more checks would be needed to make sure there is no conflicting data. E.g., trying to create a WebID for an already existing account.
I originally started writing this, but I think we've reached our limit on how much should put on the registration page. Adding more just makes it too complex. I feel like this should be split over multiple pages/handlers.
Separate
createPod
APIThis would be a separate page from
register
that would do the behavior described in the previous block. So a simple page that takes some parts from the registration page (email, password, pod name). Internally we then check the email/password combination and create the pod.One issue here is discoverability of this page.
Create account login page that sets cookie
The 2 solutions above are simpler, but mostly feel like temporary solutions until we have some form of account page. The last option would be to be a bit more robust there. There would be an
/idp/account
page that shows a login screen if not logged in. After logging in we would set a cookie to remember the user being logged in and/idp/account
would show their account/pod information. To do this we could have aCredentialsExtractor
that reads the cookie header and adds the corresponding email address to a new credential group (IdpUser
or something). #968 already adds permission readers to the IDP so could be copied from there. On that page we would then pretty much have the same as the above option, but it could be easier extended for more account options.We could even have the PermissionReader set the credentials to the user's WebID so they could access secure data without using the authn client (if they're accessing data on the same server where they registered), but perhaps that would bring us too much into client application territory.
I'm most fan of this option, but it is also the one that would require the most work, which would mean postponing #938 for a while, or merging it but then only allowing 1 pod per WebID for a while.
Authentication notes
This option requires an IDP handler to only accept a request based on parsed credentials. Currently these handlers only receive an
Operation
object so they have no knowledge of those. This will probably have to change since the credential data is required to store the pod metadata. So adding the credentials to the input parameters, or adding some extra metadata to store them.The text was updated successfully, but these errors were encountered: