Skip to content

Instantly share code, notes, and snippets.

@Fang-
Last active October 28, 2024 15:29
Show Gist options
  • Save Fang-/41ed84b2a6dd96ca67c6a5dbda1fd35d to your computer and use it in GitHub Desktop.
Save Fang-/41ed84b2a6dd96ca67c6a5dbda1fd35d to your computer and use it in GitHub Desktop.
Mirage (Eauth)

mirage (eauth)

Urbit's identity layer pervades everything on Mars. Unfortunately, despite talking to Mars a lot, Earth is not quite so fortunate. A notable example are requests made to Urbit's HTTP server, Eyre. Presently, when viewing for example a blog post hosted by someone else's urbit, there is no way for a user to leave a comment signed by their own identity right then and there: they need to go through their own urbit instead, maybe even installing a matching blog app.

Here, we propose a mechanism by which HTTP clients may authenticate themselves as a specific urbit on HTTP endpoints served by any other urbit.

authentication flow

Imagine a host ship, ~hoster, serving an interactive web interface, and a visitor, who owns an urbit ship, ~sampel. The visitor wants to use their Urbit identity to interact with ~hoster's web interface.

  1. Upon loading a web page on ~hoster, the visitor may be redirected to a login screen. It contains a @p input field. The visitor writes ~sampel and presses the "login" button.
  2. On ~hoster, Eyre receives the login request. This prompts it to generate a nonce and make a request of ~sampel through remote scry: asking for ~sampel's public-facing URL, sampel-url.
  3. ~sampel knows what hostname and port it was last authenticated through, and responds with that.
  4. ~hoster receives ~sampel's URL, and can now construct the response to the login request; a redirect to ~sampel at: [sampel-url]?server=~hoster&nonce=[nonce].
  5. Visitor's client follows the redirect.
  6. ~sampel receives the HTTP request and serves a confirmation page. It displays the local identity (~sampel) and the login target (~hoster), perhaps explains the dangers, and asks the visitor for confirmation.
    • Of course, crucially, this page is only accessible using a valid local authentication cookie.
  7. Visitor confirms the remote login.
  8. ~sampel generates a secret and sends it to ~hoster via Ames, alongside the nonce, signalling that the secret proves the visitor's identity.
  9. ~hoster responds to the Ames message with its own public-facing URL, hoster-url, to which the visitor must return.
  10. ~sampel receives ~hoster's URL, and can now serve a redirect to [hoster-url]?nonce=[nonce]&secret=[secret].
  11. Visitor's client follows the redirect.
  12. ~hoster receives the incoming request, confirms that the login nonce and secret match and were proven over Ames, and grants a fresh session cookie that is associated with the ~sampel identity. Eyre may have stored an initial redirect target when the login flow started. The response that grants the cookie may include a redirect to that target, putting the visitor seamlessly back into the flow that prompted the login in the first place.
sequenceDiagram
  autonumber
  actor visitor
  note right of visitor: visitor starts log in to ~hoster as ~sampel
  visitor->>~hoster: POST https://hoster.com/~/login, ship=~sampel
  activate ~hoster
  ~hoster->>~sampel: scry: /e/x/eauth/url
  ~sampel->>~hoster: data: `'https://sampel.net/~/eauth'
  ~hoster->>visitor: 303 https://sampel.net/~/eauth?server=~hoster&nonce=abc
  deactivate ~hoster
  visitor->>~sampel: GET https://sampel.net/~/eauth?server=~hoster&nonce=abc
  ~sampel->>visitor: 200
  note right of visitor: visitor approves log in
  visitor->>~sampel: POST https://sampel.net/~/eauth?server=~hoster&nonce=abc, approve=true
  activate ~sampel
  ~sampel->>~hoster: [%eauth-open nonce=abc secret=xyz]
  ~hoster->>~sampel: [%eauth-okay nonce=abc url='https://hoster.com/~/eauth']
  ~sampel->>visitor: 303 https://hoster.com/~/eauth?nonce=abc&secret=xyz
  deactivate ~sampel
  visitor->>~hoster: GET https://hoster.com/~/eauth?nonce=abc&secret=xyz
  activate ~hoster
  ~hoster->>visitor: 200 + cookie + maybe 303
  deactivate ~hoster
Loading

(Note here that steps 1 through 6 are all optional, it is perfectly reasonable for an EAuth login to be initiated from the client side as well.)

authenticated requests

When the requester provides a valid session cookie, Eyre may now associate a non-local, non-fake1 identity with that request. It can use this for internal permission checks, and can set the src.bowl to contain that identity if a request gets handled by userspace.

Likewise, Eyre channels created & used in this way behave as normal, except that the pokes and watches they send will have the foreign identity in the src.bowl when they get processed by the agent.

login management

Because the sessions do not live on the visitor's own urbit, they will never have absolute control over their lifecycles. But a well-behaved host will still help facilitate session management on the visitor's behalf.

When a new session is created, and no session for that @p existed on the host previously, the host should send a notification to that @p over ames, telling it that there are now active sessions for it on the host. When a session gets removed (due to expiry or logout), and it was the last remaining session for that @p, it should tell the @p over ames that there are no active sessions for it remaining on the host.

This lets the visitor's urbit track all the places across the network that it is logged in to, for display in Landscape or other system management apps. Hosts should respect requests by the visitor's urbit (again, over ames) to "log out all sessions".

login screen

The login screen for this should probably be different from the normal login screen, or at least the login page should have clearly different "modes", for whether to allow remote login or not. Presently, unauthenticated requests usually get redirected to the login page, but there is no guarantee that the app that served the redirect actually accepts requests from non-local identities. As such, when redirecting to a login page, the place that generated the redirect should be able to indicate whether remote login should be an option or not.

notable omissions

As with the open eyre proposal1, we intentionally continue giving 403 responses for non-locally-authenticated requests to the /~/scry endpoint.

risks

security

Previously envisioned approaches and older iterations of this proposal were vulnerable to man-in-the-middle impersonation attacks. In the iteration presented here, because both the ~hoster and ~sampel ships confirm their public URLs over Ames, a third party cannot cause redirects to illicit locations.

It is important that this kind of negotiation goes both ways over Ames. The initial request can always come from an unauthenticated HTTP request. "I want to log in as ~sampel", by itself, provides no guarantee whatsoever that that request is legitimate... but it does kick off the login flow. So not only does the client ship need to approve the remote login over Ames, the server ship needs to provide a final eauth URL to the client over Ames.

In a world where this URL is provided through a URL query parameter instead, a malicious ~hosten might impersonate ~hoster, and gain access into ~hoster with ~sampel's credentials:

sequenceDiagram
  autonumber
  actor visitor
  visitor->>~hosten: POST https://hosten.com/~/login, ship=~sampel
  activate ~hosten
  ~hosten->>~hoster: POST https://hoster.com/~/login, ship=~sampel
  activate ~hoster
  ~hoster->>~sampel: [%eauth-gib nonce=xyz]
  ~sampel->>~hoster: [%eauth-url nonce=xyz url='sampel.net']
  ~hoster->>~hosten: 303 https://sampel.net/~/eauth?server=~hoster&nonce=xyz&return=hoster.com
  deactivate ~hoster
  note left of ~hosten: note the server and return address
  ~hosten->>visitor: 303 https://sampel.net/~/eauth?server=~hoster&nonce=xyz&return=hosten.com
  deactivate ~hosten
  visitor->>~sampel: GET https://sampel.net/~/eauth?server=~hoster&nonce=xyz&return=hosten.com
  note right of visitor: visitor approves log in, not noticing the malicious return url
  rect rgba(200,100,100,0.2)
  visitor->>~sampel: POST https://sampel.net/~/eauth?server=~hoster&nonce=xyz&return=hosten.com, approve=true
  activate ~sampel
  ~sampel->>~hoster: [%eauth-fin nonce=xyz secret=zzz]
  ~sampel->>visitor: 303 https://hosten.com/~/eauth?client=~sampel&nonce=xyz&secret=zzz
  deactivate ~sampel
  visitor->>~hosten: GET https://hosten.com/~/eauth?client=~sampel&nonce=xyz&secret=zzz
  note left of ~hosten: /!\ ~hosten has now learned the ~sampel->~hoster eauth secret /!\
  end
  activate ~hosten
  ~hosten->>~hoster: GET https://hoster.com/~/eauth?client=~sampel&nonce=xyz&secret=zzz
  ~hoster->>~hosten: 200 + cookie + maybe 303
  deactivate ~hosten
Loading

abuse prevention

Earlier versions of this proposal did not use remote scry, and instead had ~hoster send an Ames message to ~sampel in response to an unauthenticated HTTP request. This is obviously fertile ground for abuse, especially in an implementation where each attempt would get its own Ames flow.

The current design only formally initiates attempts from the client-side, and so only in response to authenticated HTTP requests. ~hoster may still be made to store some state through unauthenticated requests, but it's not permanent state, and can and should be cleaned up in response to memory pressure.

knowing an accessible hostname

Eyre knows which port it itself is serving on, and may be aware of any number of domain names that resolve to itself. It also knows up to a single SSL certificate. Under specific combinations of these, it is trivial for Eyre to know how it can be accessed over HTTP/S. But this is not always the case, and even if it was, there is no guarantee the urbit is not running behind some proxy server setup that is accessible elsewhere.

For constructing the base-url from (2), Eyre remembers simply the security, hostname and port whenever it gets logged in to by the local identity. Given that the user needs to log in to their urbit to approve the EAuth attempt anyway, this is not a crazy restriction. Of course, the user also may provide Eyre with a manually-configured URL to use instead.

For (9), we do something similar, but instead base it off the incoming HTTP request in (1). Of course, since that entire start to the flow is optional, we fall back to the logic from the above paragraph if (1) never occurred.

being fast enough

Between (1)-(4) and (6)-(7) the server needs to wait for receipt of an Ames message. In cases where this takes a long time to come through (or does not arrive at all) Eyre should be careful to manually serve a legible 504 response before the connection times out.

...How long it takes for the connection to time out is dependent on the client, and appears to vary greatly between browsers. A safe bet would be to time out explicitly after the amount of time it takes for the UX to be degraded beyond repair. That is, probably less than a minute.

Connection and Keep-Alive headers are prohibited in HTTP/2 and /3, so we may not want to rely on those to indicate we desire a more-predictable amount of time.

src.bowl semantics

It may or may not be important to know how "real" the src.bowl is. Whether it came from Ames, or through Eyre. If it ends up mattering, the userspace provenance proposal2 may provide a good way for agents to find out.

Appendix: new types

::  +authentication-state: state used in the login system
::
+$  authentication-state
  $:  ::  sessions: a mapping of session cookies to session information
      ::
      sessions=(map @uv session)
      ::  visitors: in-progress incoming eauth flows
      ::
      visitors=(map @uv visitor)
      ::  visiting: outgoing eauth sessions, completed or pending
      ::
      visiting=(map ship (map @uv portkey))
      ::  endpoint: hardcoded local eauth endpoint for %syn and %ack
      ::
      ::    user-configured or auth-o-detected, with last-updated timestamp.
      ::    both shaped like 'prot://host'
      ::
      endpoint=[user=(unit @t) auth=(unit @t) =time]
  ==

::  +visitor: completed or in-progress incoming eauth flow
::
::    duct: boon duct
::      and
::    sesh: login completed, session exists
::      or
::    pend: awaiting %tune for %keen sent at time, for initial eauth http req
::    ship: the @p attempting to log in
::    base: local protocol+hostname the attempt started on, if any
::    last: the url to redirect to after log-in
::    toke: authentication secret received over ames or offered by visitor
::
+$  visitor
  $:  duct=(unit duct)
  $@  sesh=@uv
  $:  pend=(unit [http=duct keen=time])
      ship=ship
      base=(unit @t)
      last=@t
      toke=(unit @uv)
  ==  ==

::  +portkey: completed or in-progress outgoing eauth flow
::
::    made: live since
::      or
::    duct: confirm request awaiting redirect
::    toke: secret to include in redirect, unless aborting
::
+$  portkey
  $@  made=@da          ::  live since
  $:  pend=(unit duct)  ::  or await redir
      toke=(unit @uv)   ::  with secret
  ==

::  +eauth-plea: client talking to host
::
+$  eauth-plea
  $:  %0
  $%  ::  %open: client decided on an attempt, wants to return to url
      ::  %shut: client wants the attempt or session closed
      ::
      [%open nonce=@uv token=(unit @uv)]
      [%shut nonce=@uv]
  ==  ==

::  +eauth-boon: host responding to client
::
+$  eauth-boon
  $:  %0
  $%  ::  %okay: attempt heard, client to finish auth through url
      ::  %shut: host has expired the session
      ::
      [%okay nonce=@uv url=@t]
      [%shut nonce=@uv]
  ==  ==
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment