Documentation
¶
Overview ¶
Package oauthtest is a stand-in identity provider, so a provider sign-in can be driven in a test or a demonstration without registering an application with anybody.
It is not a mock. It implements the authorization-code flow properly — an authorization endpoint that hands back a single-use code, a token endpoint that verifies the PKCE challenge before exchanging it, and a userinfo endpoint behind the access token — so the code path exercised is the real one.
What makes it worth rig owning rather than every project writing is Server.Wear. It takes rig's own oauth.Google, oauth.Microsoft or oauth.GitHub and points the URLs here, leaving Name, Scopes, Parse and Extra exactly as production has them. So the double cannot drift from the thing it stands for, and a project never has to know how a given provider spells a profile — which is the mistake that does not fail loudly: a camelCase key parses to an empty profile and arrives as oauth.ReasonInternal two layers away.
The one thing it does that a real provider cannot is let you choose what it says about you. In a test that is Server.Expect; in a browser it is the consent page, which is where the interesting check is reachable by hand — sign in as an address that already has an identity and watch it link, turn "verified" off and watch it refuse to.
A consumer takes on one obligation. oauth.Provider.Name is written into rig_identity_oauth.provider, which is a Postgres enum in every project's own migrations — so wearing Google needs nothing, and Server.Custom needs a migration adding that label.
This package imports testing, the way net/http/httptest's callers do. Import it from a test, a seed or a development-only branch, and never from a path a deployment takes.
Index ¶
- Constants
- func Client() *http.Client
- func Handoff(tb testing.TB, res *http.Response) authwire.Handoff
- type Jar
- type Server
- func (s *Server) Custom(name string) oauth.Provider
- func (s *Server) Expect(p oauth.Profile) string
- func (s *Server) ExpectRefusal() string
- func (s *Server) Middleware(next http.Handler) http.Handler
- func (s *Server) Mount(mux oauth.Router)
- func (s *Server) Transport() http.RoundTripper
- func (s *Server) URL() string
- func (s *Server) Wear(p oauth.Provider) oauth.Provider
Constants ¶
const BasePath = "/idp"
BasePath is where this provider's own endpoints sit. It is under a path of its own so the stand-in can be mounted on an application's mux beside the routes it is standing in for, which is what the examples do: one origin, one listener, and a browser that never leaves the host it started on.
const CodeTTL = 2 * time.Minute
CodeTTL is how long an authorization code lasts. Generous for a redirect, short enough that a code left in a log is useless.
Variables ¶
This section is empty.
Functions ¶
func Client ¶
Client is a browser that stops at every redirect and keeps every cookie.
Stopping matters more than it sounds: every ending on the browser paths is a 303, and a client that followed them would test the page it landed on rather than the redirect that is the actual answer. The jar is Jar, for the reason given there.
It does not carry Server.Transport, deliberately. That one exists to redirect calls the *server* makes to a provider; putting it on the browser would send the application's own requests to the stand-in as well.
func Handoff ¶
Handoff reads the tokens a browser sign-in left in its cookie.
The value is base64url of authwire.Handoff's JSON, which is a contract with the front end rather than an implementation detail — and decoding it by hand is the last thing between a suite and asserting on a provider sign-in.
Types ¶
type Jar ¶
type Jar struct {
// contains filtered or unexported fields
}
Jar is a cookie jar that ignores every rule about hosts and security.
The state cookie rig writes is Secure and, unless oauth.Config.Insecure says otherwise, __Host- prefixed. net/http/cookiejar is right to drop both over the plain HTTP an httptest server speaks — and the result is that every provider sign-in in the suite fails for a reason no deployment has. Carrying the cookies by hand is what every project ends up doing; this is that, once.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the stand-in.
func New ¶
New builds a server that the caller will serve. origins are the redirect URIs it will accept, and the first is also where this provider is reachable.
A real provider has that list too, typed into a console one line at a time, and it is the constraint a tenant-per-subdomain deployment runs into: wildcards are rarely allowed, so every host that can start a sign-in has to be registered. This stand-in is honest about it and refuses an origin it was not given.
func Start ¶
Start builds a server with a listener of its own, closed when the test ends.
This is the form for a test that only needs a provider. An application serving the stand-in itself — a demonstration, or a suite driving a browser across several hosts — wants New and Server.Mount instead, so that everything stays on one origin.
extraOrigins are redirect URIs to accept besides this server's own. The application under test is always one of them, so pass its origin.
func (*Server) Custom ¶
Custom is a provider of this deployment's own, named whatever you like.
It is what an in-house identity server looks like to rig, and what an example mounts so that the enum obligation is visible: name is written into rig_identity_oauth.provider, so this needs a migration adding that label and Server.Wear over a built-in one does not. That is the point worth taking from it — adding Okta is a migration, not a line of configuration.
The profile is served in OpenID Connect's spelling, which is what an in-house server would most likely answer with and what oauth.Google reads.
func (*Server) Expect ¶
Expect stakes a profile and returns the authorization code that will produce it.
This is the form for a test that drives the callback directly: start a sign-in to collect the state, then deliver this code to it. Every call returns a code of its own, so a suite can run in parallel without holding a lock of its own — which is the reason it returns the code rather than setting a field.
A code from here carries no PKCE challenge, because no authorization request was made to carry one, so the token endpoint accepts whatever verifier rig sends. The browser path through the consent page does check it, and TestThePKCEVerifierIsChecked covers that; a test that wants to prove PKCE wants that path rather than this one.
func (*Server) ExpectRefusal ¶
ExpectRefusal returns an authorization code the token endpoint will refuse.
This is the branch that only appears in failure and that no project can reach on its own: a provider that authenticated somebody and then would not trade the code — a wrong client secret, most often, which refuses every sign-in identically and says nothing anywhere about why. rig answers it with oauth.ReasonExchange.
Like Server.Expect, every call returns a code of its own.
func (*Server) Middleware ¶
Middleware puts Server.Transport where rig's OAuth handler will find it.
This is the non-obvious half, and the one that costs an afternoon. The token exchange and the userinfo read are made by the *server*, not by the browser, and golang.org/x/oauth2 takes its HTTP client from the context — which for these two routes is the request's. So one wrapper around the application's handler covers the exchange, the profile read and GitHub's second call, and without it a sign-in fails as "GitHub refused the authorization code": the provider blamed for what is a name resolution.
Wrap the application under test, not this server.
func (*Server) Mount ¶
Mount registers the endpoints a provider has, plus the one GitHub's Extra reaches — see Server.Transport.
func (*Server) Transport ¶
func (s *Server) Transport() http.RoundTripper
Transport sends a request meant for somebody else here instead.
Two of the three URLs on an oauth.Provider are data and Server.Wear repoints them. The third is not: oauth.GitHub's Extra hardcodes api.github.com, because its user endpoint returns null for anybody who kept their address private and only the emails endpoint says whether one is verified. So the only way to reach a stand-in for that call is to point the HTTP client somewhere else.
A request already addressed to this server is left alone, so the token exchange and the userinfo read still go where Server.Wear sent them.
func (*Server) Wear ¶
Wear returns one of rig's own providers with its three URLs pointed here.
Name, Scopes, Parse and Extra are untouched, which is the point: the sign-in under test runs production's code for reading that provider's answer, and the profile is served in that provider's own spelling. Pass credentials or do not — the stand-in does not check them, because a wrong secret is the provider's refusal to model and oauth.ReasonExchange already has a test.
The provider must be one this package knows how to answer as. An unknown name is served OpenID Connect's spelling, which is Google's; see Server.Custom.