nodeSolidServer/oidc-auth-manager

Authorization flow does not allow switching accounts

Closed this issue · 42 comments

Steps to reproduce:

  1. Log in as user X on pod A (OIDC mode)
  2. (OPTIONAL) Log in on pod B as whatever user and log out again
  3. Go to a protected resource on pod B so you are asked to log in
  4. Enter pod A as identity provider

Expected result:

  • Pod A asks for your username and password (or should at least give you the option to continue or not)

Actual result:

  • Pod A logs you in as user X

Is there a Logout step that's involved?

Possibly a log out on pod B, added above.

Ok, so, I suspect I know what the problem may be. But let's talk about Logout in general, and what the expected/desired behavior is, for several scenarios. (We should probably add this discussion to the spec, or at least an app dev guide, afterwards).

Our actors are:

Alice, with webid https://alice.podA.com/#me,
Bob, with webid https://bob.podB.com/#me,
and Cindy, with server https://cindy.com.

And all resources in these scenarios are ACL-protected (non public), and users start out non-authenticated unless specified otherwise.

(RS - Resource Server, IdP - Identity Provider, RP - Relying Party).

Scenario 1 - RS/IdP same origin

Alice makes a direct non-AJAX request to an image resource on her own pod

So, Alice: GET https://alice.podA.com/icon.gif

Non-AJAX meaning, let's say she follows a link or types in the URL of an image directly into her browser. And the reason we specify "image resource" is because it's not currently wrapped in Mashlib, it should be returned by the pod directly to the browser.

Her browser makes the GET call, hits a 401, gets redirected to Login, she logs in, gets redirected back to image, sees the image.

Post-Login state: a) A cookie based session to *.podA.com in alice's browser. (Nothing in localStorage). So, she is "logged in to podA".

To Logout (since it's just a raw image, and there's no 'Logout' button anywhere), Alice can visit https://podA.com/logout directly (or clear her cookies or something).

Visiting the /logout endpoint, her pod clears her WebID from the session (so, she still has a session cookie, it's just empty). (If she used her browser to clear her cookie, it'll be deleted, but as soon as she makes another HTTP request to podA.com, the browser will generate another empty session cookie anyways.)

Post-Logout state: a) No state (well, an empty session cookie to podA.com).

Scenario 2 - RS/IdP/RP same origin

Alice visits her "home page" app, clicks Login

Alice: GET https://alice.podA.com/

This loads an HTML page + a JS app that uses a Solid Auth client, and includes a client-powered Login button.

She clicks Login, gets an auth client popup, selects idp, gets redirected to login form (in popup), logs in, popup closes, she is back to her homepage app, now Logged In.

Consent step: Skipped (resource is on same pod as IDP).

Post-Login state: a) A cookie based session to *.podA.com ("Logged in to PodA"), b) A client registration + user session token in localStorage for origin podA.com ("Logged in to Homepage App, with IDP as PodA").

To Logout, she clicks the Logout button. Following actions occur:

  1. Client: Clears her session token in localStorage
  2. Client: Makes an AJAX call to the IDP's /logout endpoint (determined from the server config during registration)
  3. IDP (PodA): Clears her WebID from session cookie (in /logout handler)

Post-Logout state: a) An empty session cookie, b) No user session credentials in localStorage (Note: the auth client currently stores the app registration for that page in local storage. It's a separate discussion on whether this is desireable, or if that should be cleared too.)

Scenario 3 - IdP one origin, RS separate origin

Alice makes a direct non-AJAX request to an image resource on Bob's pod

Alice: GET https://bob.podB.com/icon.gif

Browser makes a request, hits a 401, gets redirected to PodB's 'Select Provider' page, alice enters podA.com as her IDP, gets redirected to podA.com/login, then back to bob.podB.com/icon.gif.

Consent step: Currently, Solid skips the consent step. Classic decentralized login (OAuth2, Facebook Connect, etc) consent steps are generally encountered either a) The first time Alice's pod encounters bob's podB.com (maybe when Alice added Bob to her contacts), or b) Each time Alice logs into podB using her podA webid (this is more annoying to the user).

Post-login state:

a. PodA (IDP): Session cookie with alice's webid. "Logged in to PodA".
b. Bob's PodB (RP): Session cookie with alice's webid (the pod also has her ID token / credential, valid only for itself). "Logged in to PodB".

To Logout (again, since this is a raw image), Alice will have to visit bob.podA.com/logout directly (type it into the browser).

Post-logout state:

a. PodB (RP): Empty session cookie. Logged out of PodB.
b. PodA (IDP): Still logged in to PodA, so, session cookie with Alice's webid.

In this scenario, currently, Alice would have to separately visit her own pod's podA.com/logout endpoint to also log out of her pod.

OpenID Connect specs make provision for also optionally logging out of "upstream" IDPs (meaning, PodB would remember PodA's /logout endpoint, and would call it after it logged out Alice from itself).
But: one, we don't have it implemented (though we could). And two, the UI and user expectation is unclear. Does Alice expect to log out both out of Bob's pod and her own pod?

Scenario 4 - IdP one origin, RP+RS on another origin

Alice visits an App hosted on Bob's pod, clicks Login

Here, Alice visits some SPA type app (like Mashlib/Tabulator) which is hosted on Bob's pod podB.com, and she is not logged in anywhere.

Alice: GET https://bob.podB.com/meetings/one.ttl.

The app gets served up (wrapping the RDF resource). She clicks the app's Login button (powered by the auth client). The following things happen:

  1. The RP App (bob's Mashlib) looks to see if it already has a user session for Alice in localStorage, sees that it doesn't, and opens a Login popup.
  2. Alice selects her IDP (podA.com), gets redirected (inside the popup), logs in to her pod, gets redirected back to bob's pod, the window closes.
  3. The RP App (mashlib) stores Alice's user session token in localStorage for origin bob.podB.com.

Post-Login state:

a. IDP (PodA): Session cookie with alice's webid. "Logged in to PodA"
b. RP (Mashlib): Client reg + user session in localStorage for origin bob.podB.com. "Logged in to App"

Notice that the actual Resource Server, Pod B, doesn't know about Alice yet! However, the first time the App makes a REST request to Bob's pod (using a credential token in the header), Alice will get a session cookie with origin *.pobB.com, so she will also be "Logged in to Pod B". (Since the App is hosted on same origin as the pod).

To Logout, Alice clicks the App's Logout button. Following happens:

  1. (Client) App clears Alice's session token from localStorage for origin bob.podB.com. "Logged out of App".
  2. (Client) App also sends a REST request to Alice's IdP's end session endpoint, so, alice.podA.com/logout.
  3. (IdP/PodA) Clears Alice's webid from her session cookie. So, now "Logged out of IdP / PodA".
  4. (RS/PodB) The App does not know about Bob's pod, however, so, "Still logged in to Bob's Pod" (until the session expires).

Open Question 1: How should the App know to notify Pod B of logout? Alice's pod A could keep track of "downstream client apps/RPs" (again, the spec provides for it), but there's no good way for it to know about downstream Resource Servers -- it doesn't know that the App communicated with Bob's pod at all.

Perhaps the problem is that PodB created a session cookie for Alice, even though she has an external WebID (not hosted on Bob's pod)? (Notice that, given the current semantics and security model, this is still legitimate because the Origin: of the App (that made an AJAX request to Bob's pod) was still the same as the pod, so the cookie would have been created). So we could change it so that cookies are given not only for local-origin calls, but also for local WebIDs. (Though that has other usability implications).

Open Question 2: Does Alice expect to Logout from both the App and her own PodA, when she hits the logout button?
That's what the current behavior is.

Scenario 5 - IdP+RS one origin, RP second origin

Alice logs into an App on Bob's pod, App fetches a resource on her own PodA

What if Alice logs in to the same app as in Scenario 4, a Mashlib based app on Bob's pod, but only fetches resources from her own podA? (This is also the same scenario as if the RP App was hosted on a non-pod origin, e.g. AWS or Github.io).

Post-login state: a) Logged in to App (app has token), b) Logged in to IdP (PodA)

Now the App can make REST requests to Alice's own pod, but only using the app's token, not her own session cookie, since we disallow external origins to use session cookies.

If she clicks the Logout button, again, the App a) clears token from localStorage "Logged out of App", and b) Makes a request to podA.com/logout, which clears her session cookie to her PodA, so "Logged out of PodA".

Ok, no problem, all logged out.

Scenario 6 - IdP one origin, RP second origin, switching WebIDs

Alice visits App on Bob's pod, clicks Login, then needs to switch WebIDs (so, Logout then Login again)

OK, so, now we come to the scenario that started this conversation. If Alice is visiting Bob's pod, logs in, then wants to switch her WebID (by logging out & logging back in with something different), what should happen?

So, part of that depends on which WebIDs she is switching between. That means we have two sub-scenarios.

Scenario 6A: App on Bob's pod, switching from alice.podA.com/#me to OTHERalice.podA.com#me (new WebID hosted by SAME IdP as before).

Alice logs to RP App hosted on Bob's pod as her usual alice.podA.com/#me. Then wants to switch to a different WebID of hers. So, she clicks Logout.

Post-Logout state: Logged out from App, Logged out from PodA, still logged into PodB if App made any requests to PodB.

To switch/relogin: She clicks Login again. Gets a popup, selects PodA as provider, has to log in to her own PodA again, since she logged out of it.

Post-relogin state: PodA has session cookie for new WebID, App has localStorage credential for new WebID, OTHERalice.etc.., PodB has session cookie for old WebID.

(Potential) Problem: NOW, the App makes requests to Bob's PodB, they have 2 credentials: a) an Authorization: Bearer ... header with the token for new WebID, and b) A browser Cookie for domain *.podB.com with the OLD WebID.

Open Question 3: In the Solid Server auth handlers, do cookie session WebIDs take precedence over token credential WebIDs? (If so, that needs to be switched around.)

Scenario 6B: App on Bob's pod, switching from alice.podA.com/#me to something.podB.com/#me (the new WebID is on Pod B).

Logs in as alice.podA.com/#me, same post-login state as before. Clicks Logout. Same situation as before - logged out of PodA, logged out of App, but still logged into PodB as old webid via cookie.

Problem: When she goes to re-login, clicks the Login button, gets Select Provider popup, now selects PodB, and gets redirected to PodB's /authorize endpoint. However, since she still has the PodB session cookie, the /authorize endpoint (correctly) skips the login form, and returns her to the original App, logged in as the old WebID.

Solution: Same as the solution to Question 2; probably need to disallow session cookies for externally-hosted WebIDs.

Scenario 7 - IdP one origin, RP second origin, RS third origin (or more)

Alice visits an App on Bob's pod, clicks Login, App fetches a resource from Cindy's pod

Hoooo doggie, this is why we got into this decentralization shindig in the first place, right? Decentralized Twitter, log in once and fetch messages from countless other domains!

Alice visits bob.podB.com/cimba2-twitter-clone/ app. Logs in as alice.podA.com/#me.

And then the App starts fetching resources from Pod C, cindy.com! (And likely other origins, as well).

Post-Login (and post-fetch) state:

a. Logged into PodA (via session cookie to her home pod)
b. Logged into App (via ID token credential from PodA), stored in localStorage.
c. (for completeness) Cached Proof of Possession tokens for each intended resource server (cindy.com, etc), in memory (in a JS object), will go away once user navigates away from page. Not logged in, though!

Notice that there's no session cookies for cindy.com or any other external resource servers (since, if they're running Solid servers, they disallow cookies for external domains).

So how does Logout work? Same as in Scenario 4.
Alice clicks Logout, App auth client a) clears localStorage of Alice's session ID token from PodA, b) sends /logout request to Alice's home pod.

So now she's logged out of everywhere.

Open Question 4: (Need to make sure) Do we cache PoP tokens in the auth client memory (not in local storage)? And if so, do we clear that cache on logout?

Scenario 8 - SSO ("Single Sign-off")

Alice is logged in to multiple apps, in different tabs, as alice.podA.com/#me

What, there's more? Awww yess, we haven't even talked about multi-tab (or multi-window) Single Sign-On / Sign-Off.

The idea being, if Alice is logged in as herself, in multiple tabs (on different apps, multiple origins), what should happen on Logout? Let's go through the combinations.

8a - Logged into podA, App1, clicks Logout on App1

This is the same as our Scenario 2, so - should she expect to be logged out of both podA and App1? Yes. (So, that's fine, that's the current expected behavior).

8b - Logged into podA, App1, clicks Logout ON HER POD

Ah, but what if she's still logged into App1 (did not click the Logout button), but she visits (say, manually) her pod's /logout endpoint?

The following things currently happen:

  1. Her Pod clears her webid from session cookie. (No longer logged into Pod A).
  2. She's still logged in to App1 (since her token is in localStorage at App1's origin). Which means that App1 will still be able to make REST requests until the token expires. Still plausibly fine/expected.
  3. If App1 is hosted on PodA, and makes a REST request to PodA, the pod will (re)start her session via cookie, with the same WebID. Slightly counter-intuitive -- though she logged out of the pod, her app "logged her back in".

8c - Logged into podA, App1 and App2 (different tabs)

So, let's say Alice is using two different apps in two different tabs, same WebID. If she clicks the Logout button on one app, should she also be logged out of the other app? (In addition to the current behavior of being logged out of the pod).

This is what's known as "Single Sign-Off" -- you Logout once, and you're logged out of all RP client apps that are using that WebID. In the OIDC specs (and similar methods), this is done by keeping a WebSocket backchannel open, and the IdP (Alice's pod) notifies all the apps when any of them log out.

We haven't implemented that part of the spec yet. Should we?

Similarly (as with 8b), if Alice logs out of her PodA, should all the other tabs & apps that are using that WebID also be notified (and be logged out)?

So, I'm not a security expert, but here are my two cents.

It seems that cookies act as the mechanism to authenticate non-AJAX requests, and bearer tokens for AJAX requests. The existence of two mechanisms introduces questions such as precedence.

Let's try to only have one at the same time. What I propose is that:

  • we use the Bearer token to contact a server for the first time necessary (i.e., after a 401)
  • this results in a session cookie
  • from then on, the session cookie is the only thing that counts
  • logout means removing the cookie, not just emptying it; that way, the client can easily interpret the cookie (if it's [not] there, we are [not] logged in).

Additionally, we need much more introspection in solid-auth-client. I want to be able to ask it, at any moment, which server has acted as IDP and with which servers I'm currently logged in.

@dmitrizagidulin and @RubenVerborgh ,

All of your scenarios ultimately boil down to the fact that an OIDC-pod works best when the following holds true:

  1. User has the notion of a canonical WebID
  2. User is happy for an application to surreptitiously handle identity credentials tokenization and authentication -- because that's the nature of the protocol in use i.e., OIDC (with or without the WebID addition).

You may or may not know that we have an application call the OpenLink Structured Data Bot (OSDB) that is built using node.js and operates as a solid client. Anyway, I've asked the development lead of this particular project to contribute a response to this thread.

@kidehen No! :) That's not what it boils down to. What it "boils down to" is "Hey logging out is actually complicated in a decentralized environment. So we need to outline the scenarios, and make sure we've thought it through carefully." And all authn methods will need to deal with these scenarios (including the ones that you're promoting). Because part of the complication (as @RubenVerborgh points out) is that servers generate session cookies in addition to any other authn method. So WebID-TLS sessions will also generate those same cookies, and have the same difficulties in logging out.

  1. User has the notion of a canonical WebID

No. Switching WebIDs is definitely supported, that's the intention. There's a couple of issues (mostly having to do with cookies / auth precedence) in Scenario 6, but those will be fixed.

  1. User is happy for an application to surreptitiously handle identity credentials

Ok, what kind of fear-mongering language is that? :) Yes, software components do handle credentials in this setup, as they do in all authentication methods. That's what they're for.

@dmitrizagidulin ,

I'll rephrase, at the current time, OIDC-pod deployments work as I've described. If not, then why is it that I can only toggle WebIDs by manually cleaning out prior session data via my browser's inspector?

I am hoping OIDC-pod deployment can work better, but I am describing the current state of affairs.

Should my characterization be inaccurate, then you must be able to prove that via an existing instance of a pod, right?

As for:

No. Switching WebIDs is definitely supported, that's the intention. There's a couple of issues (mostly having to do with cookies / auth precedence) in Scenario 6, but those will be fixed.

I am assuming there is an issue regarding the fix then? And once resolved my current assessment will be corrected etc..

@kidehen Wait, let's consider the context of this discussion.

This here is an issue describing a bug, namely that WebID switching is not working. My comment with the various scenarios was just an attempt to understand why it's not working, and to talk about what the fixes should be. Your summarize that discussion, somewhat dismissively, as "what it boils down to is that you can't switch WebIDs". Well yes.. that's what this issue is about. It's a bug discussion.

If you're implying that the protocol doesn't allow switching WebIDs, that's not the case. There's a couple implementation issues, though, that need to be fixed.

Hello all, I'm the development lead on OpenLink's OSDB project (https://osdb.openlinksw.com). @kidehen asked me to comment on this thread.

If I understand correctly, the central issues which triggered this thread are:

  • "If Alice is visiting Bob's pod, logs in, then wants to switch her WebID (by logging out & logging back in with something different), what should happen?".
  • "logging out is actually complicated in a decentralized environment"

My exposure to Solid has been limited to using it as an OIDC IdP for OSDB. I've had some difficulties with it in this regard, so I wanted to relay my impressions of what's caused the difficulties, albeit from the point of view of Solid as a black box with no knowledge of the code base. Please correct me if I'm wrong.

Short version

When using OSDB as an RP with Solid as the OP, it appears:

  • Solid cookies are not session cookies, but persistent cookies. I think they should be session cookies.
  • Solid doesn't support RP-initiated logout
  • Solid doesn't provide an indication in the browser that a user is logged in, whereas Google displays a 'badge'; so a user knows they're logged into the Google IdP even when they open a new page/tab. The 'badge' makes it clear the user has to manually logout of Google.

Long version

FWIW, below is a description of how OSDB handles login/logout across both WebID and OIDC.

OSDB essentially handles two type of login 1) WebID 2) OIDC (including WebID-OIDC). Both types of login result in the end-user receiving a session cookie, osdb.sid, which is an opaque key mapping to a session object in the OSDB server. The server-side session object holds, amongst other things, the user's NetID. The NetID may be either:
a) WebID, if the user logged in using WebID-TLS(+delegation), or
b) the users profile URL (aka a NetID) if logged in through a 3rd party OIDC IdP, e.g. via Google, the NetID might be https://plus.google.com/+CarlBlakeley

OSDB ACLs should grant access based on both WebIDs or NetIDs.

In order to change their current OSDB login, a user obviously has to logout then login again. Logging out clears the osdb.sid session cookie and the server-side session object. This is all that needs be done for WebID-TLS logins (with or without delegation). However for OIDC logins there's more to consider.

OIDC Login/Logout Handling in OSDB

OSDB acts as the RP. Authorization flow is used. As part of the authentication process, OIDC provides an access token which can be used to access the end-users profile and selected data (depending on the scopes requested), and also to access protected APIs belonging to the IdP service. The user's profile URL from the authenticating IdP acts as a NetID as already explained.

All IdP (OP) registrations are held on the OSDB server, rather than in browser local storage (as is the case with OIDC implicit flow). The OSDB server maintains a server-side equivalent of browser local storage, oidc-rp-storage. oidc-rp-storage consists of multiple 'hives', one per authenticated end-user. Each 'hive' contains essentially the same data as that maintained by Solid in browser local storage, though I've made modifications.

When an OSDB end-user logs in through OIDC (or WebID-OIDC), a hive is created. The hive holds both the RP registration (under key 'rpConfig') and the user's session data (under key 'session'). The session data includes a clientId, an idToken and an accessToken. When the user logs out, the session data is destroyed, but the RP registration is retained. In order to link an end-user to their hive, they receive a second cookie, osdb.storage_id. This is a persistent cookie, rather than a session cookie. osdb_storage_id is also saved to browser local storage so it can be recreated if the persistent cookie is lost or destroyed for some reason.

As a side effect of logging into OSDB using OIDC, the end-user is also logged into the OP and typically receives a session cookie from the OP. This is the case with both Google and Solid. When the user logs out of the OSDB (RP), OSDB takes no action to log them out of the OP, so the OP session cookie remains. The end-user is left to log themself out of the OP if required. In the case of Google, the browser makes it apparent that the user is still logged in by displaying a user 'badge'. I'm not sure this is the case with Solid.

It also appeared to me that Solid was creating a persistent cookie rather than a session cookie. The only way I was able to change logins with Solid was by manually deleting the cookie after logging out of my first login - the act of logging out didn't destroy the cookie. From the code at the time, I think property cookie.maxAge needs removing to make connect.sid a session cookie, rather than a persistent cookie:
https://github.com/OpenLinkSoftware/node-solid-server/blob/08bea82dd9f010278f34d6d7a030efd507a5df02/lib/create-app.js#L234

The question of whether the RP should, when an end-user logs out of OSDB, take steps to also log them out of the OP is unresolved at present. The OIDC session management spec (http://openid.net/specs/openid-connect-session-1_0.html) includes features for handling logout, including RP-initiated logout. The nodejs oidc-rp library used by OSDB (https://www.npmjs.com/package/@trust/oidc-rp) claims to support RP-initiated logout and from looking at the code, appears to do so. But I'm not sure that Solid, as an OP, supports this feature. Google doesn't, so the end-user is responsible for manually logging themself out of the OP, after logging out of OSDB; or in Solid's case, manually destroying the 'sticky' session cookie.

@dmitrizagidulin ,

Re #20 (comment), I am not trying to be dismissive. Instead, I am trying to be succinct regarding what the current issue is.

Please understand, I simply want to be able to achieve the following:

  1. Identify the problem based on how it manifests
  2. Align problem with an issue that seeks to fix said problem.

Thus, at this point in time, is it accurate to assume the following:

[1] OIDC-mode pods require users to manually login and out in situations where multiple WebIDs are involved?

[2] This issue can be fixed, but there are some issues regarding clarity about the nature and scope of a logout.

I am sure we can get to a point of clarity here :)

Thanks @cblakeley, you bring up some great discussion points. Let's take em slightly out of order.

Solid doesn't support RP-initiated logout

It should, definitely file a bug if that's not working. Like you mentioned the RP client supports RP-initiated logout, as far as I know the current auth client invokes it on logout, and on the OP/server side, node-solid-server processes it when the client hits the /logout endpoint -- see clearUserSession() code here.

Solid doesn't provide an indication in the browser that a user is logged in, whereas Google displays a 'badge'; so a user knows they're logged into the Google IdP even when they open a new page/tab. The 'badge' makes it clear the user has to manually logout of Google.

That's a great point. I think Tim's Mashlib (and solid-ui lib specifically) are meant to serve as a set of reusable components, for just this sort of purpose. But I think we need two more action items here: 1) File an issue on mashlib or solid-ui to display a user's logged in status more clearly. and 2) If possible, implement and provide a lightweight badge (like Google and Facebook do) for app developers to be able to drop in, that would denote user session status.

@cblakeley

Solid cookies are not session cookies, but persistent cookies. I think they should be session cookies.

This one's tough (in terms of being a UI/UX decision, it's easy to switch to a session cookie implementation wise).

As a user, doesn't it drive you crazy when your login cookies aren't persisted? I know that on iOS, on mobile Safari, it drives me nuts that I have to re-login everywhere constantly despite having clicked 'Remember me' checkbox, because that browser forces session cookies and cookie expiration.

Like.. I don't know if Solid end users would like session cookies (instead of persistent cookies) very much.

Can you say more about why you'd prefer session cookies? (We could probably make it a config option, for server operators, to switch between persistent cookies vs session cookies, but I'm having trouble picturing the advantage of the latter.)

the act of logging out didn't destroy the cookie

True, the current Solid/Express behavior is to just always set a cookie on every request. So, logging out doesn't delete the cookie, but it clears the user's WebID from that cookie's session store.
Which means, logging out should have ended your user session without deleting the cookie (and as far as I tested yesterday, that part works). If it doesn't, let's file an issue. (@RubenVerborgh - I haven't forgotten about your recent comment regarding cookies, will reply to it in a bit).

@cblakeley

The server-side session object holds, amongst other things, the user's NetID.

That's really cool! I've wanted to add that functionality for a while - to give the option for users to log in with, say, their Google account (in addition to TLS, username+pw, etc). And to integrate this notion of external NetIDs into the ACL system. So, that's really cool that your project has that working! (It's definitely on the medium-term roadmap, personally.)

The OSDB server maintains a server-side equivalent of browser local storage, oidc-rp-storage. oidc-rp-storage consists of multiple 'hives', one per authenticated end-user. ... The hive holds both the RP registration (under key 'rpConfig') and the user's session data (under key 'session').

Nice. Solid-server currently does something similar, using the multi-rp-client library. I'm actually in the process of folding that functionality into the oidc-rp client itself, so that everybody doesn't have to roll their own (like what you did with oidc-rp-storage and I did with multi-rp-client).

@dmitrizagidulin
I've no preference for session cookies per se. My inability to switch my Solid login, as an OSDB user with Solid as the OP, seemed to be down to Solid creating a persistent cookie which persisted after I logged out of OSDB, before trying to login again with my 2nd Solid user ID. The fact that the Solid cookie persisted may have been down to RP-initiated logout not working for some reason. If you could confirm that RP-initiated logout works against a Solid IdP then I could re-check the logout code in the oidc-rp library and the RP-initiated logout from OSDB.

@kidehen

I am sure we can get to a point of clarity here :)

Excellent, I too am certain we can sort this out :)

I am assuming there is an issue regarding the fix [for switching WebIDs] then?

Yes, this one! This is the tracking issue, so far.

Thus, at this point in time, is it accurate to assume the following:
[1] OIDC-mode pods require users to manually login and out in situations where multiple WebIDs are involved?

Depends on what you mean by 'manually'. The intended design, currently, is that something has to sequentially call an auth client's logout() to clear the old WebID (possibly upstream and downstream), and then login() for the new WebID. The implementation problem, though, that's causing that to fail sometimes, is described in Scenario 6 above, and has to do with the cookie-setting mechanism on Solid-server. So that needs to be fixed.

Now, what that something is (that's calling logout & login), has so far been left to the app developer. We certainly don't expect the user to drop down to browser console and type in those functions, nor to have to clear localStorage or cookies - that's a bug. I think the general idea has been for app developers to provide at very least a Login/Logout button, or even a WebID-switching widget.

It would be definitely helpful (as @cblakeley pointed out) if we provided such a widget or badge, for developers to drop in. (Not sure if the current state of the art in reusable JS+HTML components is currently, but if Google can provide badges, we can too.)

[2] This issue can be fixed, but there are some issues regarding clarity about the nature and scope of a logout.

That's right, the nature and scope needs to be discussed a bit more (as in, what do we actually want to happen?). And this discussion needs to happen for decentralized applications in general, with most authentication methods, not just with WebID-OIDC.

(The reason we're running into this discussion now, and not previously, is that before with WebID-TLS, we couldn't log out at all, without restarting the browser. So we didn't get as far as considering the various scenarios.)

@dmitrizagidulin,

Depends on what you mean by 'manually'. The intended design, currently, is that something has to sequentially call an auth client's logout() to clear the old WebID (possibly upstream and downstream), and then login() for the new WebID. The implementation problem, though, that's causing that to fail sometimes, is described in Scenario 6 above, and has to do with the cookie-setting mechanism on Solid-server. So that needs to be fixed.

I am referring to a user having to resort to their browser's inspector for cleaning up session information after performing what they believe to be a logout-action.

Problems related to the above only arise when multiple WebIDs and the need to toggle them becomes part of the user-experience cocktail.

Now, what that something is (that's calling logout & login), has so far been left to the app developer. We certainly don't expect the user to drop down to browser console and type in those functions, nor to have to clear localStorage or cookies - that's a bug. I think the general idea has been for app developers to provide at very least a Login/Logout button, or even a WebID-switching widget.

Aha! This has never been clearly stated until now, as far as I can recall.
We added a login badge to our solid-server implementation to assist with this problem of not knowing one's currently logged in identity.

It would be definitely helpful (as @cblakeley pointed out) if we provided such a widget or badge, for developers to drop in. (Not sure if the current state of the art in reusable JS+HTML components is currently, but if Google can provide badges, we can too.)

Yes, so I assume this is a new feature request for a missing component?

@dmitrizagidulin ,

(The reason we're running into this discussion now, and not previously, is that before with WebID-TLS, we couldn't log out at all, without restarting the browser. So we didn't get as far as considering the various scenarios.)

TLS-sessions are currently pegged to Browser instances, so to open a new one you restart the Browser.

WebID-TLS+Delegation operates within the confines of an existing TLS-session while also moving protected resource access to WebACLs (Authorization) processing.

I use several WebIDs in the course of my daily activities. I never restart my browser. All of my TLS-Mode and OIDC-mode pod testing happens without a single browser restart. My only inconvenience is that I have to use my inspector to scrub prior session information en route to maintaining this state of affairs with my OIDC-mode pods.

Anyway, as per prior comment, we have reached a beachhead. I am just making this comment to shed additional light on WebID-TLS and the notion of logout, in its particular context.

The metaphor that works for me re. WebID-TLS+Delegation is as follows:

Trying to get to a pub on the super information highway

  1. I have a car identified by its registration number -- conveyed via X.509 cert bearing the imprint of the WebID that identifies my Software Agent (e.g. Browser or Curl etc.)

  2. I and/or others individuals may drive my car to destination, each of us possess a Driver's License -- conveyed via our individual WebIDs

  3. RDF statements describe how a specific Driver is related to the Car -- i.e., convey semantics of the relationship type

  4. Tollbooths on the super information highway are concerned about my Car Registration

  5. Pub is concerned about my credentials discerned from my WebID when serving me a drink -- they aren't concerned about how I got there.

Changing the car in this scenario is what requires some reset of the toolbooths regarding which car is being tracked -- that's the Brower restart requirement re. TLS.

I don't change my car, and all permitted drivers are registered in the WebID-profile doc of the car. For instance, I've given @RubenVerborgh access to my valet key, and he could get up to some mischief if he chooses, but for only as long as I let him :)

Now this metaphor is exactly perfect in regard to the realworld since toolbooths don't really reset per car, but I think you can get the point I am making re. WebID-TLS+Delegation.

Note, without Delegation, WebID-TLS behaves as you've characterized in your comments thus far :)

@kidehen even with TLS Delegation, you still have much of the same complexity of scenarios, when you log out, because solid-server sets session cookies immediately after you authenticate via TLS.

@dmitrizagidulin,

You may have the same scenario complexity, but you will not have the same UI/UX headaches for end-users. Moreover, end-users will be able to control access to resources they share on the Read-Webs they are using.

You do know that we can end this debate by simply comparing our setups. Why do we have to keep on arguing when I have several demonstrable instances of my claims? Can't you knock up a pod that can be used for comparison? At the very least, please do that :)

  1. https://github.com/solid/node-solid-server/wiki/Running-Test-Implementations-of-Solid-compatible-servers -- Tabulated list of Solid Pods for live testing, please add yours

  2. https://osdb.openlinksw.com -- an example of what's possible re. various Authentication and Authorization protocols re. Read-Write Web

  3. https://id.myopenlink.net/DAV/home/KingsleyUyiIdehen/ -- a protected data space

  4. https://id.myopenlink.net/DAV/home/KingsleyUyiIdehen/Public/ -- a public segment of a protected data space

  5. http://id.myopenlink.net/ods/webid_demo.html -- simple WebID-TLS authentication tool that also supports WebID-TLS+Delegation

  6. https://linkeddata.uriburner.com/sparql -- a query service endpoint that supports multiple Authentication protocols while implementing WebACL for acls etc..

All I want us to do is get solid to this point i.e., none of this stuff requires:

  1. Browser restarts during my working day
  2. Cleaning up prior session related information from my browser's local storage, using its inspector.

We should be able to do this :)

@kidehen

You do know that we can end this debate by simply comparing our setups.

This is not a debate :) This is a bug/issue, with clear steps to reproduce in the description. Which is what we need as Solid developers. Oh, also a technical discussion of what the UX of logging out should be.

Like.. what am I supposed to do with all those links you just posted? How are those relevant?

Can't you knock up a pod that can be used for comparison?

What? :) Why? This is not a streetcar racing club. What would me spinning up a node-solid-server instance accomplish? Why not use already existing servers like solid.community?

@RubenVerborgh

It seems that cookies act as the mechanism to authenticate non-AJAX requests, and bearer tokens for AJAX requests. The existence of two mechanisms introduces questions such as precedence.

Yeah, agreed completely.

  • we use the Bearer token to contact a server for the first time necessary (i.e., after a 401)
  • this results in a session cookie
  • from then on, the session cookie is the only thing that counts

I was picturing going in the opposite direction. Only because if the app is on a different origin than the pod, it won't be able to use a cookie at all.

What if we say - cookies are only used as a convenience mechanism to not have to re-enter credentials at the /authorize endpoint?

Pondering...

@dmitrizagidulin,

We are getting to know each other, so please be patient. My links demonstrate realization of the destination i.e., what is possible and typically refuting misconceptions associated with WebID-TLS+Delegation (which fixes the problems associated with basic WebID-TLS).

I'll leave it at that for now, we had a beachhead at: #20 (comment). Let's not lose that.

Do we have a new issue for the missing component outlined in that segment of thread?

@kidehen Ahh, ok, makes sense.

The missing component issue is: solid/solid#141

@cblakeley

If you could confirm that RP-initiated logout works against a Solid IdP

This is implemented, and was working when I tested it a couple days ago. (Something could've broken, but from everything I know, should be working)

I find it interesting and amusing that we're not the only ones having a detailed discussion on the semantics of logout (and what we think the desired behavior should be).

Mike Jones, one of the authors of the OIDC spec, recently led a discussion at the 2018 OAuth Security Workshop titled "What does Logout mean?" (Session Notes - Presentation Slides)

They go into even more details and discussed more different kinds of scenarios than in our discussion above - very cool.

@dmitrizagidulin /cc @kidehen

We now have RP-initiated logout working between https://osdb.openlinksw.com and https://solid.openlinksw.com:8444.

I'm using an OpenLink fork of the @trust/oidc-rp package which contains various changes including support for authorization flow and multiple RP registrations.

On the RP side, RP-initiated logout was broken. The master repo https://github.com/anvilresearch/oidc-rp contains the same bug. RelyingParty#logout was performing a redundant GET and returning nothing. I've modified the method to return the OP end_session_endpoint.

In the diffs below (oidc-rp.diff and osdb.diff), OSDB's OidcLogin#logout is equivalent to solid-auth-client/src/api.js#logout. OidcAuth#logout is equivalent to solid-auth-client/src/webid-oidc.js#logout. OidcLogin#logout now returns the end_session_endpoint URL to the OSDB/Express route handler which redirects the client to the OP end_session_endpoint. This redirection wasn't being done prior to this fix.

On the OP side, Solid wasn't recognizing the logout request query parameter post_logout_redirect_uri. (Fixed by solid.diff.)

According to http://openid.net/specs/openid-connect-session-1_0.html#RPLogout, post_logout_redirect_uri must be registered with the OP using the post_logout_redirect_uris registration parameter. AFAICT Solid doesn't yet support this?

Please could you fix the oidc-rp and oidc-auth-manager packages, then we can pull the changes. Thanks.

oidc-rp.diff

commit aef6022621fcb006b5d2bcfc58167aa7681fcab7
Author: Carl Blakeley <cblakeley@openlinksw.com>
Date:   Mon May 21 15:01:51 2018 +0100

    RelyingParty: Fix for RP-initiated logout.

diff --git a/src/RelyingParty.js b/src/RelyingParty.js
index 3a74a68..f6a90bb 100644
--- a/src/RelyingParty.js
+++ b/src/RelyingParty.js
@@ -318,15 +318,7 @@ class RelyingParty extends JSONDocument {
     }
 
     this.clearSession()
-
-    if (!configuration.end_session_endpoint)
-      return Promise.resolve(undefined)
-
-    let uri = configuration.end_session_endpoint
-    let method = 'get'
-
-    return fetch(uri, {method})
-      .then(onHttpError('Error logging out'))
+    return Promise.resolve(configuration.end_session_endpoint)
 
     // TODO: Validate `frontchannel_logout_uri` if necessary
     /**

osdb.diff

diff --git a/osdb/lib/OidcLogin.js b/osdb/lib/OidcLogin.js
index 13e5c4c..76b1d60 100644
--- a/osdb/lib/OidcLogin.js
+++ b/osdb/lib/OidcLogin.js
@@ -30,20 +30,22 @@ class OidcLogin {
 
   /**
    * @param storage: AsyncStorage
-   * @returns Promise<void>
+   * @returns Promise<end_session_endpoint>
    */
   static async logout(storage = rpDefaultStorage()) {
     logger.verbose('OidcLogin#logout');
     const session = await getSession(storage)
     if (!session) {
-      return
+      return;
     }
+    await clearSession(storage);
     try {
-      await OidcAuth.logout(storage);
+      let end_session_endpoint = await OidcAuth.logout(storage);
+      return end_session_endpoint;
     } catch (err) {
       logger.error('OidcLogin#logout:', err);
+      return;
     }
-    return clearSession(storage)
   }
 
   /**
diff --git a/osdb/lib/route_handlers/index.js b/osdb/lib/route_handlers/index.js
index d176954..f3f3d80 100644
--- a/osdb/lib/route_handlers/index.js
+++ b/osdb/lib/route_handlers/index.js
@@ -3,6 +3,7 @@
 const util = require('util');
 const uuid = require('uuid');
 const auth_types = require('../auth_types');
+const routes = require('../routes');
 const { OidcLogin } = require('../OidcLogin');
 const { OidcAuth } = require('../OidcAuth');
 const { OidcOobRegistration } = require('../OidcOobRegistration');
@@ -338,8 +339,15 @@ exports.logout = function(req, res) {
       switch (logout_context)
       {
         case "ui":
-          OidcLogin.logout(rpStorage(storage_ns)).then(() => {
-            res.redirect('/');
+          OidcLogin.logout(rpStorage(storage_ns)).then(end_session_endpoint => {
+            if (end_session_endpoint)
+            {
+              // Trigger RP-initiated logout at the OP
+              let osdb_post_idp_logout_uri = `${global.osdb.config.server_url}${routes.routes_dict().home}`;
+              res.redirect(`${end_session_endpoint}?post_logout_redirect_uri=${encodeURIComponent(osdb_post_idp_logout_uri)}`);
+            }
+            else
+              res.redirect(routes.routes_dict().home);
           })

solid.diff

--- /Users/carl/dev/oplsrc/git_repos/git_solid_server/node-solid-server/node_modules/oidc-auth-manager/src/handlers/logout-request.js	2017-08-10 16:56:29.000000000 +0100
+++ /Users/carl/dev/oplsrc/git_repos/git_solid_server_fixes/node-solid-server/node_modules/oidc-auth-manager/src/handlers/logout-request.js	2018-05-21 12:56:52.000000000 +0100
@@ -37,7 +37,7 @@
 
   static parseReturnUrl (req) {
     let query = req.query || {}
-    return query.returnToUrl
+    return query.post_logout_redirect_uri ? query.post_logout_redirect_uri :  query.returnToUrl
   }
 
   static logout (request) {

@cblakeley - thanks! Couple of things:

  1. Since these are all open source libraries, can you open issues on those repos and make PRs? Diffs posted here aren't very useful.
  2. I'm not quite sure I understand your oidc-rp change. The purpose of rp.logout() is to perform the logout (that's what it's currently doing with the fetch request), not to return anything. Your change makes it so that the logout function doesn't do anything... So I'm not sure how much it's a fix rather than a disabling of existing functionality. (Feel free to open an issue on https://github.com/solid/oidc-rp and we can discuss it there.)
  3. re post_logout_redirect_uri -- that's correct, it hasn't been implemented on the solid-server side. Feel free to open an issue on the oidc-auth-manager repo (or the solid-server repo is fine, since we'll be merging oidc-auth-manager into the main server repo shortly).

@dmitrizagidulin Re: [2] - http://openid.net/specs/openid-connect-session-1_0.html#RPLogout says "the RP, after having logged the End-User out of the RP, redirects the End-User's User Agent to the OP's logout endpoint URL". My understanding of that is that it's the user-agent, not the RP (as was the case prior to my oidc-rp change), that has to call the OP's logout endpoint. The RP then has the responsibility of redirecting the user-agent to the logout endpoint. The way I chose to do this was to modify logout() so that the endpoint URI is passed back up the call chain so that the RP's logout handler knows where to redirect to, and can also attach the post_logout_redirect_uri query param to the request.

@cblakeley ahhh I see what you're going after.

In any case (whether the RP should redirect the browser to the logout endpoint, or call the endpoint itself), the rp.logout() function should not be modified to do nothing and just return the end session endpoint.

So, part of the reason oidc-rp implementation decided to call the /logout endpoint directly (as opposed to 302 redirect the browser), has to do with wanting to prevent the intrusiveness of the redirect (to not take the user away from the current app). The popup login window allows login without redirection of the main window, and doing a direct call to /logout allows the same for logout.

Maybe it would make sense to parametrize the logout call, and let the app developer decide whether to redirect the window or log out in the background? (I suppose this would make even more sense once the front-end and back-end session logout specs are implemented.)

And part of this has to do with the larger discussion about the desired behavior of the 8+ logout scenarios, above.

@dmitrizagidulin: Maybe I'm misunderstanding something, but it's the user-agent, not the RP, that is logged into the OP. So I'd expect it to be the user-agent, not the RP, that logs out of the OP. Isn't having the RP attempt to logout of the OP on the user-agents's behalf a recipe for the problems seen with Solid and 'sticky' cookies. If the user-agent logs out of the OP directly, the OP has the chance to clear any session cookies in the user-agent? (I'm thinking here in the general sense of any RP, not specifically about Solid).

it's the user-agent, not the RP, that is logged into the OP.

So, this is complicated (as evidenced by all those scenarios). Part of the question is "what's the difference between the RP and the user-agent", and part of it is "what does 'logged into' mean".

The oidc-rp library is meant to be a baseline for both browser js apps and completely server-side and headless apps. On the headless server-side (or in a desktop app), the RP is essentially the user agent. In the browser, the distinction between the user agent and an RP which is JS code running in the user agent is blurred. (Meaning, the JS code can make REST calls which affect the user-agent state such as cookies and the contents of local storage).

As far as Solid and 'sticky' cookies - the cookies are not the problem. The problem is we don't have a clear design on what we want to happen, and didn't think through all the various combinations. (And that we have essentially 3 potential sessions to juggle -- the user's session with the OP, the user's separate session with the RP, and a potential 3rd session with some other RS.)

The way rp-initiated logout works currently is - the app calls logout(), which makes a REST call to the OP's /logout endpoint, and the OP clears the session associated with the cookie. This is important -- it doesn't delete the cookie, it clears the session associated with that cookie. So that the session no longer has a logged-in user.

Incidentally, part of the reason it was done that way (clearing the session instead of deleting the cookie) is that most http frameworks, including Express.js automatically set a session cookie with every request. So it doesn't matter if you delete it, the very next request, there'll be a new one.

Now, let's be clear. I'm not saying that the current oidc-rp logout() implementation is ideal (it's bending the spec slightly by making a REST call to /logout instead of redirecting the browser -- a UX tradeoff). What I am saying is - a) it's complicated, and b) I don't agree with the change of making logout() just return a url endpoint (instead of actually logging out).

@dmitrizagidulin /cc @kidehen
"On the server-side (or in a desktop app), the RP is essentially the user agent."

I don't see this as the case. When a server is acting as an RP with authorization flow, the RP and the user agent are clearly separate (as in the diagram http://openid.net/specs/openid-connect-basic-1_0.html#Overview). So in this scenario (authorization flow as opposed to implicit flow) I'm not convinced that having the RP logout on behalf of the user-agent is correct - the testing to date shows that this wasn't working and that the user-agent wasn't being logged out of the OP.

"The way rp-initiated logout works currently is ... it clears the session associated with that cookie. So that the session no longer has a logged-in user."

OpenLink's testing showed this wasn't the case. The user still appeared to have a Solid session. If a user logged out of Solid, then logged back in again - on the second login the user wasn't prompted for their credentials. They were logged back in using their first login, making it impossible to switch their Solid identity - unless they deleted the session cookie manually.

"The problem is we don't have a clear design on what we want to happen"

When the RP is a server-side app and a user logs out of the RP, my view, as far as OSDB is concerned, is that there should be two choices:

  1. the RP directs the user-agent to the OP, where the OP either logs out the user automatically or gives them the option of logging out (depending on the OP's logout implementation)
  2. the RP leaves the user-agent's session with the OP untouched - it's then up to the end-user to manually end their session with the OP.

My change to oidc-rp attempts to fix option 1). Option 2) at least requires that the user be aware that they still have an open session with the OP. This wasn't/isn't readily apparent - hence our discussion on the need for a 'badge' in the UI.

"I don't agree with the change of making logout() just return a url endpoint (instead of actually logging out)."

Are our different viewpoints a reflection of the different requirements of implicit flow and authorization flow? Your suggestion "Maybe it would make sense to parametrize the logout call, and let the app developer decide whether to redirect the window or log out in the background?" would accommodate both flow models and keep oidc-rp agnostic.

Are our different viewpoints a reflection of the different requirements of implicit flow and authorization flow?

Hmm possibly. (I agree that we should be able to acommodate both).
Let me get back to you on this...

@cblakeley

"the RP is essentially the user agent" / I don't see this as the case

You're right. I was picturing something else, not the authorization flow context.

Ok, so, I think I understand where you're coming from here.

I think the way to address this, which matches the existing RelyingParty api and leaves oidc-rp general purpose, would be to replace the logout() method with something like:

  /**
   * @param options {object}
   * 
   * @param [options.id_token_hint] {string}
   * @param [options.post_logout_redirect_uri] {string}
   * @param [options.state] {string}
   *
   * @param session {Session|Storage}
   * 
   * @returns {string} Logout uri (based on the OP's `end_session_endpoint`)
   */
  logoutRequest(options, session) {
    // ...
  }

Which would return the composed end session URI, with the proper query fragments etc, which the calling code can use to perform the logout action (to 302 redirect a user's browser in case of an Express app, or do a window.location redirect if used in a web browser client, etc).

Would that work?

Oh, also, I opened a new issue on node-solid-server - #675: Implement 'post_logout_redirect_uri' support

Hmm, though something needs to call clearSession(). Which wouldn't make sense to do in a url-composing method like logoutRequest(). Maybe leave clearing the session to calling code?

Oh, also! (On the subject of switching accounts, the original topic of this issue)
I just realized that the Request Parameters section of the client implementor's guide has an authorization request parameter prompt=select_account. With the description The Authorization Server SHOULD prompt the End-User to select a user account. This enables an End-User who has multiple accounts at the Authorization Server to select amongst the multiple accounts that they might have current sessions for. If it cannot obtain an account selection choice made by the End-User, it MUST return an error, typically account_selection_required.

Implementing support for that would address a (small) part of the 'switching accounts' issue being discussed, but still, kind of cool.

I think the new logoutRequest() method you propose would work.

something needs to call clearSession(). Maybe leave clearing the session to calling code?

Could RelyingParty.js#logout call this.clearSession() as it does now, then return the result of logoutRequest()? Provided the end result is the same for the callers, I guess the implementation detail is a matter of personal taste.

Implementing logoutRequest() and issue #675: Implement 'post_logout_redirect_uri' support should be the equivalent of the changes I made.

@cblakeley , @kidehen , could you please indicate whether you think this has been fixed by the other fixes we have finished?

@kjetilk,

This has been fixed. Associated PRs have been reviewed and merged.

/cc @RubenVerborgh @justinwb @cblakeley @smalinin

Great, thanks a lot!