Documentation
¶
Overview ¶
Package handoff hands a browser its tokens when a provider sign-in ends somewhere else.
A sign-in that finishes in a redirect cannot answer with a body. The browser followed the provider's callback to this API and has to end up on the front end, which in a deployment worth the name is served on another host: the API on api.example.com, the application on app.example.com. So the tokens travel in a short-lived cookie the landing page reads and deletes, and the redirect carries nothing but a destination.
Everything here is arithmetic over URLs and cookie attributes, with no provider machinery behind it: this package does not import github.com/simonjanss/rig/auth/oauth, so the rules can be read and tested on their own. What uses it is github.com/simonjanss/rig/auth/authhttp.Handler.SignInToBrowser, which github.com/simonjanss/rig/auth.New selects when a project names a web origin and writes no OnSignIn of its own.
The two things that are easy to get wrong and fail silently are both decided in New rather than left to a caller:
- The cookie's Domain. A browser drops a Set-Cookie whose Domain is not a registrable domain of the host that sent it, and says nothing about it. New derives it from the public suffix list and refuses at startup when the two hosts share none, so the deployment fails to boot rather than failing one sign-in at a time.
- Where a returnTo resolves. A relative path is resolved against the front end, not against this API, because that is the origin the person is looking at — and an absolute one is only honoured when the cookie will actually reach it.
Index ¶
- Constants
- type Config
- type Handoff
- func (h *Handoff) Cookie(in authwire.Handoff, dest *url.URL) (*http.Cookie, error)
- func (h *Handoff) Destination(returnTo string) *url.URL
- func (h *Handoff) Fail(w http.ResponseWriter, r *http.Request, returnTo, reason string)
- func (h *Handoff) Origin() string
- func (h *Handoff) Write(w http.ResponseWriter, r *http.Request, in authwire.Handoff, returnTo string) error
Constants ¶
const DefaultCallbackPath = "/auth/callback"
DefaultCallbackPath is where the front end is expected to receive a handoff.
const DefaultTTL = time.Minute
DefaultTTL bounds how long the handoff cookie lives.
It is a value in flight between two page loads that happen back to back, so the window only has to cover a redirect and a script. A minute is generous for that and short enough that a cookie left behind by a browser that never arrived is not a credential lying around.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Origin is where the front end is served, as scheme://host[:port] with no
// path — https://app.example.com. Required.
Origin string
// CallbackPath is the path on Origin that receives a handoff. Empty is
// [DefaultCallbackPath].
CallbackPath string
// APIHost is the host this API is served on, which is the other half of the
// cookie's Domain. Required, and a host rather than a URL: a scheme is
// accepted and ignored.
APIHost string
// TTL is how long the cookie lives. Zero is [DefaultTTL].
TTL time.Duration
}
Config says where the front end is and how to reach it from here.
type Handoff ¶
type Handoff struct {
// contains filtered or unexported fields
}
Handoff writes the end of a browser sign-in. Build one with New.
func New ¶
New checks the two hosts against each other and returns the writer.
It fails when the front end's origin is not an origin, when the callback path is not a path, or when a cookie written here could never be read there. That last one is the point of doing this at startup: the alternative is a deployment that boots, redirects, and loses every sign-in.
func (*Handoff) Cookie ¶
Cookie is the handoff itself, addressed to one destination.
Path is the destination's, so the cookie is sent to the page that reads it and to nothing else on the front end. It is deliberately not HttpOnly: a script on that page is the only thing that can act on these tokens, which is the same reason they are in a cookie at all rather than in a fragment.
func (*Handoff) Destination ¶
Destination is where this sign-in should send the browser.
Empty is the configured callback. A relative path is resolved against the front end's origin rather than this API's, which is the whole difference between the two endings: the person is looking at the front end, and a path they asked to return to is a route there.
An absolute URL is honoured only when the cookie written for it would actually be sent to that host; anything else — another origin, a scheme-less //host, a URL that does not parse — falls back to the callback. There is no error return because there is nothing useful a caller could do with one: a returnTo has already been checked against the allow-list by the time it gets here, and the callback is always a correct answer.
func (*Handoff) Fail ¶
Fail redirects to the same destination with no cookie and an error on the query string.
One front-end route handles both outcomes, and it tells them apart by whether there is a cookie to take. The reason is a short stable token rather than a sentence, because it is going into a URL somebody may screenshot and into a switch the application writes — the wording of a message is neither.
The rest of the query survives, so a sign-in that started at a particular page still comes back to it with its own parameters intact.