Skip to content
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

Closed
joachimvh opened this issue Oct 4, 2021 · 12 comments
Closed

Pod management authentication #972

joachimvh opened this issue Oct 4, 2021 · 12 comments

Comments

@joachimvh
Copy link
Member

joachimvh commented Oct 4, 2021

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 API

This 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 a CredentialsExtractor 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.

@RubenVerborgh
Copy link
Member

Update the registration page

There would then be a checkbox on the registration page "I already have an account".

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.
And identities. And anything else.

Seems like a good idea actually.

@joachimvh
Copy link
Member Author

joachimvh commented Oct 7, 2021

Or could just be "sign up" / "log in" buttons. Could also become a multi-page thing (on the client, not on the server).

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.

@joachimvh
Copy link
Member Author

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).

Interfaces

The 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 type field which handler should check the rest of the object.

Pod ownership

The 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.

Cookies

We 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.

@RubenVerborgh
Copy link
Member

Looks good!

Having storage that can be easily queried (such as a SPARQL store) would make some of this easier

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 would require external dependencies or some custom solution with lots of caching involved (e.g., using N3.js stores to cache a turtle file).

…but yeah, that might be expensive.

@joachimvh
Copy link
Member Author

joachimvh commented Mar 16, 2022

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

/idp/login/: returns URLs pointing to all login endpoints we have. Only entry currently is /idp/login/password/

/idp/login/password/: accepts an email/password POST. Will generate a cookie and set the correct header if necessary. This cookie can then be used for future requests to identify as the account. A GET returns the link to the create API below.

/idp/login/password/create/: accepts an email/password/confirmPassword POST. If the user is logged in, this will attach the login method to the account. If the user is not logged in, an additional create boolean can be sent along. This will create a new account with this login method. This solution to create new accounts will be generalized so it is supported by all login methods. Probably also going to make it so you can only have 1 instance of a login method per account (so no 2 email/password combinations for 1 account).

/idp/account/: lists all the WebIDs/Pods/Login methods associated with the logged in account.

/idp/pod/: lists all pods associated with the logged in account. Additionally lists all the metadata per pod (which at this point is only the owner(s) of that pod). Not yet added but probably also the API to change Pod owners (potentially a PATCH that contains the pods that need to be changed).

/idp/pod/create/: accepts a POST with a name field. Creates a pod with the given name and links it to the logged in account. A webId field can be sent along to mark that WebID as the owner, if not WebID generated in the pod will be used. Additional settings can also be sent along, to fill in the pod templates. Possible values here depend on the templates used.

/idp/webid/: returns the list of WebIDs associated with the logged in account. In case of an OIDC authentication procedure, also accepts a POST containing the WebID the user wants to use in the client that is logging in.

/idp/webid/link/: associates the POSTed WebID with the logged in account. In case of an external WebID an ownership check will be done using the token as usual. In case the WebID is located in a pod already linked to the account, this will immediately succeed. Otherwise it would be impossible to link a WebID in a newly created pod to a new account.

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 procedure

Logging in using OIDC now takes the following API steps:

  1. GET /idp/login/ to find all login methods.
  2. Choose a login method from the list above. Currently only /idp/login/password/
  3. POST to the chosen login method with the necessary credentials.
  4. GET /idp/webid/ to find the WebIDs available for the logged in user.
  5. POST the chosen WebID to /idp/webid/.
  6. POST to /idp/consent/ to consent to the client being able to identify as that WebID.

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:

  1. A page listing the login methods.
  2. A page listing the specifics for the chosen login method.
  3. A page listing all the possible WebIDs.
  4. The consent page.

In practice, we can make pages 1 and 3 so they redirect automatically if there is only 1 option,
so the user experience would actually be the same as it was before in those cases.

HTML API

Even though everything is separated now, we could still have an /idp/register/ page that combines all of the APIs above to provide the same experience to the users as before. Besides that we should probably have a general account page that lists all the information and provides links to add pods/webids/login methods.

Cookies

After 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.

@RubenVerborgh
Copy link
Member

All good!

/idp/account/: lists all the WebIDs/Pods/Login methods associated with the logged in account.

/idp/pod/: lists all pods associated with the logged in account. Additionally lists all the metadata per pod (which at this point is only the owner(s) of that pod). Not yet added but probably also the API to change Pod owners (potentially a PATCH that contains the pods that need to be changed).

So these are authentication-dependent resources. Are they aliases for unique resources such as /idp/47/pod (which are only accessible with the right credentials)?

/idp/pod/create/
/idp/webid/link/

Would definitely make those globally unique.

After 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.

Let Cookie be one of the methods indeed.

@joachimvh
Copy link
Member Author

So these are authentication-dependent resources. Are they aliases for unique resources such as /idp/47/pod (which are only accessible with the right credentials)?

Yes, if those would exist. The current suggestion doesn't actually include such unique links.

/idp/pod/create/
/idp/webid/link/

Would definitely make those globally unique.

As in, make it so they are accessed through /idp/47/pod/create?

Will have to do some rethinking of how routing works in the IDP components as they currently are all static paths.

@RubenVerborgh
Copy link
Member

As in, make it so they are accessed through /idp/47/pod/create?

Yes, I think we need that; having (only) auth-dependent resources to expose this functionality, will make it difficult for admin accounts etc.

@joachimvh
Copy link
Member Author

joachimvh commented Apr 13, 2022

Looking into having the URLs as follows. Using account instead of idp since if we're changing the entire API might as well change that now.

/account/login/
/account/login/password/
/account/login/password/create/
/account/$ID/
/account/$ID/pod/
/account/$ID/pod/create/
/account/$ID/webid/
/account/$ID/webid/link/

Could also have a /account/$ID/login/password/create/ or something similar to differentiate between creating an account and adding a login to an already existing account.

And once client credentials are added also /account/$ID/credentials/. Perhaps also /account/$ID/credentials/$CREDENTIAL_ID so DELETE requests can be sent there directly, but that's another path complexity that will have to be tackled. Perhaps some for of RegexRoute.

@RubenVerborgh
Copy link
Member

Shall we do /.account/?

Yes to all of the above.

@joachimvh
Copy link
Member Author

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 index

The starting point for the IDP API. The one URL that you need to "know".

curl http://localhost:3000/.account/

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 "apiVersion": "0.4" from the responses below for brevity.

Create account

curl -X POST http://localhost:3000/.account/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 account field is the resource URL corresponding to the newly created account. Not sure why the accountId is also in there, might be architectural reasons where a component somewhere needs this value as well.

Get account controls

This 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).

curl http://localhost:3000/.account/

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

curl -X POST http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/login/password/

Input

{
    "email": "test@example.com",
    "password": "secret",
    "confirmPassword": "secret"
}

The confirmPassword is still here because our current architecture has it, but I'm thinking to just remove that field and do password confirmation completely on the frontend.

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 password field is the resource URL corresponding to this specific login method in this specific account. Would have to check my own code again, for example can remove it by doing a DELETE request to that URL (note that the server does prevent you from deleting all your login methods).

Log out

Something to be said that perhaps this should just happen automatically every time a new login method gets added/changed. This invalidates the cookie.

curl -X POST http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/logout/

Find password login URL

Starting again from the main controls in the very first request, there was a URL to find the available login methods:

curl http://localhost:3000/.account/login/

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

curl -X POST http://localhost:3000/.account/login/password/

Input

{
    "email": "test@example.com",
    "password": "secret"
}

Output

The 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 Pod

This, 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.

curl -X POST http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/pod/

Input

{
    "name": "test"
}

The only required field. It also takes an optional settings object which gets passed along to the PodManager to allow flexibility there and can contain a webId field, which will be used to set up the default ACL resources. If not present the WebID that is generated in the pod will be used, so all similar to how we currently do registration.

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 WebID

The 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.

curl -X POST http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/webid/

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 token

This is still possible. (This is the token that can be used to have authenticated requests later without needing to log in with the browser).

curl -X POST http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/credentials/

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 /credentials subpath of the account URL so will need to change that.

See account details

This 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.

curl http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/

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 password

By 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.

curl -X POST http://localhost:3000/.account/account/e0aa64d5-261a-4616-aff3-488538632440/login/password/test%40example.com/

Input

{
    "oldPassword": "secret",
    "password": "secret2",
    "confirmPassword": "secret2"
}

Same comment about the confirmPassword here.

Register combined account

There 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.

curl http://localhost:3000/.account/register/

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 requests

These 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:

  1. Choose a login method. Redirect in case the server is configured to only have 1.
  2. Login using the chosen method. Also skipped if you already have a cookie. At least it should, I need to test this.
  3. Choose a WebID from your list of WebIDs. Skipped if you only have 1. Error if you have 0.
  4. The consent screen as before.

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 oidc-provider has its own cookie system, but we can't really reuse it, so there will be 2 separate cookies and the trick is to make sure they work nicely together. Or at least don't interfere.

@joachimvh
Copy link
Member Author

Done in #1677

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants