Conversation
|
|
|
||
| <div class="flex flex-col gap-y-4"> | ||
| <div class="flex flex-col gap-y-2" data-test-id="grant-team"> | ||
| <%= if match?([_, _ | _], @grantable_teams) do %> |
There was a problem hiding this comment.
This is a very obtuse way to express if length(@grantable_teams) > 1. Was credo complaining? I'd honestly consider make it stfu if it did.
| @@ -0,0 +1,157 @@ | |||
| defmodule PlausibleWeb.Live.OAuthAuthorize do | |||
There was a problem hiding this comment.
Unless I'm missing something, I don't see anything that would explicitly require making this a LiveView. A static view with a form would work just as well if not better (selection update goes via server and the interaction might be jarring on a more laggy connection). Is that meant for some other, more dynamic features getting added some time later?
There was a problem hiding this comment.
Hmm, I didn't think about potential lagginess. Regarding selecting scopes and stuff, it's probably going to happen, but I haven't yet thought about it.
It was a form initially. CIMD was fetched on form load. The server validated that the document checks out, self-referential and all that. (A sample document that's valid: https://gist.githubusercontent.com/apata/7387339c3c81540524bc670da0b05f00/raw/client-metadata.json)
- The user manually validated that they want to authorize based on the info from CIMD and the query params.
- They click "Authorize". POST happens for the form.
On POST, we need to save who the user was, which team they selected, the client id, client name, client redirect URI, resource, scopes, code challenge and the challenge method. Where do we get client name from? Hidden form input? Can we trust that value server-side?
In the form solution, to have this trust, we fetched CIMD again on POST. However, that created the situation where the CIMD might have changed between rendering and submitting the form: since we can't possibly allow the user to press authorize for one application and receive grant for another, there's a new class of errors to handle.
I thought LV solved this quite elegantly. Now I'm not sure though.
We could go back to a form if we figured out a way to avoid a second fetch. Sign the whole thing on GET (Phoenix.Token.sign(conn, @salt, %{user_id: user.id, ctx: ctx})), then verify with short TTL on POST (Phoenix.Token.verify(conn, @salt, token, max_age: @max_age_seconds))? Apparently, this is similar to how LV does it, but if we handroll it, we get more control over the exact contents, TTL and salt.
PS: In a separate PR, I plan to cache CIMDs as suggested in the OAuth 2.1 draft. With N app servers, do the GET page and POST form hit the same server (and thus cache)?
PPS: I also considered the fact that LV necessitates JS as positive. This flow is supposed to be completed by a human.
There was a problem hiding this comment.
Indeed, signing and verifying is a good point. However, going all in with LV just for that might be perhaps a slight overkill?
The mere necessitation of JS is not a strong argument IMHO. It can be worked around easily, especially with huge corpora of code doing this that LLMs can draw from (scrapers are probably significant percentage of the overall crap they hoover up 😅).
There was a problem hiding this comment.
Agreed that "has JS => more likely to be human" is not a strong argument in favor. UX lag is a stronger one against LV (though I wonder if all our live views have gone through this justification check).
Therefore I've refactored back to the form in 7a8f07f. The signing and verifying part wasn't too bad.
There was a problem hiding this comment.
One advantage is that it allows me to withdraw the PR with e2e tests for this. Will do so once we agree this is the way forward
| @@ -0,0 +1,116 @@ | |||
| defmodule PlausibleWeb.OAuth.AuthorizationRequest do | |||
| @moduledoc """ | |||
| Validates an incoming [authorization request](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#name-authorization-request) | |||
There was a problem hiding this comment.
| Validates an incoming [authorization request](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#name-authorization-request) | |
| Validates and turns an incoming [authorization request](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#name-authorization-request) |
| into the context the consent screen renders and the decision is taken against. | ||
|
|
||
| `build/1` is the only place the client's metadata document is fetched during a | ||
| consent round trip. The context it returns travels from the controller into |
There was a problem hiding this comment.
| consent round trip. The context it returns travels from the controller into | |
| consent round trip. The context it returns travels from the controller to |
|
|
||
| `build/1` is the only place the client's metadata document is fetched during a | ||
| consent round trip. The context it returns travels from the controller into | ||
| the LiveView that takes the decision, so nothing downstream re-reads a |
There was a problem hiding this comment.
| the LiveView that takes the decision, so nothing downstream re-reads a | |
| the LiveView that makes the decision, so nothing downstream re-reads a |
?
There was a problem hiding this comment.
Speaking of, @apata would you consider simplifying the robo-talk? Those comments/moduledocs are exhausting to read.
There was a problem hiding this comment.
Agreed, @aerosol! Generally I do, but this snuck through.
| alias Plausible.OAuth.CIMD | ||
| alias Plausible.OAuth.ProtectedResources | ||
|
|
||
| @type t() :: %{ |
There was a problem hiding this comment.
WDYT about using an embedded_schema here with a changeset that would cast and validate all that?
There was a problem hiding this comment.
Is the idea to bring in Ecto so we'd be able to use its tested validation logic? I hadn't even considered that. Now that I am thinking of it, I can see your point, but TBH I'm a bit wary of it.
Some of its features related to changesets are not ideal IMO.
Just as an example that I stumbled upon already in previous PRs: when validating field F 1) length and 2) matching regex, it ends up running both validators, even if the length validation fails. That means there's an open door for regex denial of service (ReDoS) attacks, though "common sense" and glance at code would not reveal it.
There was a problem hiding this comment.
Potentially risky checks can be wrapped in something like maybe_validate_format(changeset, ...), where the wrapper runs the validation only when changeset.valid? is true. It's not a must, though it could perhaps help reduce the boilerplate somewhat.
There was a problem hiding this comment.
I tried to refactor to embedded schema, but the module got bigger, not smaller, and IMO harder to understand ':) It's possible I didn't try hard enough.
By boilerplate, do you mean the type and the remap from string keys to atom keys?
Since 7a8f07f, this module has better defined responsibilities, so I think the boilerplate isn't that jarring.
| @moduledoc """ | ||
| The teams an authorization request may bind a grant to. | ||
|
|
||
| `current_team` is deliberately not consulted: on the authorize route the URL |
There was a problem hiding this comment.
current_team can't be set to anything the current user does not have access to already. https://github.com/plausible/analytics/blob/dynamic-funnel-poc/lib/plausible_web/plugs/auth_plug.ex#L30-L33 Besides, I don't see any relevance of current_team in this context - so maybe it's best to remove this paragraph to avoid confusion?
There was a problem hiding this comment.
You're right, will check this moduledoc, it's a bit loose. Let's discuss the actual implementation here: https://github.com/plausible/analytics/pull/6712/changes/BASE..78454561932ab7d06c190acd814bb1ea716168ff#paneldiscussion_r4153052032
There was a problem hiding this comment.
I've removed this module and cleaned up the moduledocs of authorization request and authorization response. They were indeed confusing, hopefully it's a bit more clear now.
| def resolve(assigns, identifier) do | ||
| teams = list(assigns) | ||
|
|
||
| Enum.find(teams, &(&1.identifier == identifier)) || List.first(teams) |
There was a problem hiding this comment.
| Enum.find(teams, &(&1.identifier == identifier)) || List.first(teams) | |
| Enum.find(teams, List.first(teams), &(&1.identifier == identifier)) |
| @@ -0,0 +1,21 @@ | |||
| defmodule PlausibleWeb.Plugs.IgnoreTeamParam do | |||
There was a problem hiding this comment.
Again, __team can't cause current_team override to an arbitrary team just like that. Are we sure this is necessary?
There was a problem hiding this comment.
Thanks! I'm aware it can't switch to random team, but it can switch the user's current team. That switch affects the other Plausible tabs they have open: https://3.basecamp.com/5308029/buckets/36789884/card_tables/cards/10217223382
The screen where I'm turning this parameter off is not supposed to be linked to from Plausible, right? It's only for outside applications (they build this link with the appropriate query params based on our advertised OAuth metadata).
Picking the right team to authorize is the user's responsibility. I want the default value to be their currently active team.
Is there a legitimate reason an outside application needs to be able to switch the user's current team programmatically?
If it switched only for this tab, then maybe I'd be OK with it, but at the moment, the switch has a wider impact and I'm not OK with it.
Theoretically, we could look at the unintentional switching as a wider bug and I can remove this extra plug from this PR.
There was a problem hiding this comment.
Yeah, how team switching works is not great but is also limited by the fact we had to leave URL scheme intact. AFAIK the only way to make it resilient to multi-tab usage scenario, would be including team in every URL (either as a parameter or part of the path), which is not an option.
What's the benefit of selectively switching it off here though? This particular view completely ignores current team while it can still be switched from another tab.
There was a problem hiding this comment.
Removed the plug in 7a8f07f
The fix was too narrow and led to worse UX for legitimate cases: we do want to read current team as the default selected one.
Changes
Wires up
GET /login/oauth/authorize. Needs feature flagmcpon user to work.Basic visuals for starters:

Tests
Changelog
Documentation
Dark mode