Documentation
¶
Overview ¶
Package authtest runs AuthKit in a host's Go tests: a real Client on a scratch PostgreSQL schema, an Outbox that captures every email and SMS, an identity provider to sign in with (IdP), and helpers for the usual setup (a verified user, a signed-in session, a role, an authenticator app, a device key, a replica, a stale session).
auth, outbox := authtest.New(t, authtest.WithConfig(func(c *authkit.Config) {
c.Roles = myapp.Roles()
}))
alice := authtest.NewUser(t, auth)
authtest.GrantRole(t, auth, iam.RootGroup(), iam.UserSubject(alice.ID), myapp.Admin)
tokens := authtest.SignIn(t, auth, alice)
// call the host's handlers with "Bearer "+tokens.AccessToken, or drive
// auth.Handler() and read codes and links from outbox.
New needs AUTHKIT_TEST_DATABASE_URL, a database where the test may create schemas. Without it the test is skipped, or fails when AUTHKIT_TEST_REQUIRE_DB=1. AUTHKIT_TEST_KEEP_DB=1 keeps each schema.
The package is covered by AuthKit's compatibility contract like the rest of the module (docs/stability.md).
Index ¶
- Constants
- func ClientSecretSHA256(secret string) string
- func GrantRole(t testing.TB, auth *authkit.Client, group iam.GroupRef, subject iam.Subject, ...)
- func Identity(t testing.TB, auth *authkit.Client, credential string) hauth.Identity
- func PKCEChallenge(verifier string) string
- func Replica(t testing.TB, auth *authkit.Client, opts ...Option) *authkit.Client
- func RevokeDeviceKey(t testing.TB, auth *authkit.Client, k DeviceKey)
- func RevokeRole(t testing.TB, auth *authkit.Client, group iam.GroupRef, subject iam.Subject, ...)
- func SignIn(t testing.TB, auth *authkit.Client, u User) iam.TokenSet
- func StaleSession(t testing.TB, auth *authkit.Client, accessToken string) string
- func TOTPCode(t testing.TB, secret string, at time.Time) string
- type Assertion
- type AuthorizationServer
- func (as *AuthorizationServer) Approve(t testing.TB, accessToken, id string) string
- func (as *AuthorizationServer) Authorize(t testing.TB, u User, f CodeFlow) OAuthTokens
- func (as *AuthorizationServer) AuthorizeAs(t testing.TB, signedIn iam.TokenSet, f CodeFlow) OAuthTokens
- func (as *AuthorizationServer) BeginAuthorization(t testing.TB, f CodeFlow, verifier, state string) string
- func (as *AuthorizationServer) ClientCredentials(t testing.TB, clientID, clientSecret, resource string, scopes []string, ...) OAuthTokens
- func (as *AuthorizationServer) Consent(t testing.TB, signedIn iam.TokenSet, f CodeFlow) *url.URL
- func (as *AuthorizationServer) Exchange(t testing.TB, x TokenExchange) OAuthTokens
- func (as *AuthorizationServer) HTTPClient() *http.Client
- func (as *AuthorizationServer) JWTBearer(t testing.TB, r JWTBearerRequest) OAuthTokens
- func (as *AuthorizationServer) JWTBearerToken(t testing.TB, r JWTBearerRequest) (int, []byte)
- func (as *AuthorizationServer) Refresh(t testing.TB, clientID, clientSecret string, tokens OAuthTokens) OAuthTokens
- func (as *AuthorizationServer) RequestClientCredentials(t testing.TB, r ClientCredentialsRequest) OAuthTokens
- func (as *AuthorizationServer) Revoke(t testing.TB, clientID, clientSecret, token string) int
- func (as *AuthorizationServer) Token(t testing.TB, req TokenRequest) (int, []byte)
- func (as *AuthorizationServer) TokenEndpoint() string
- type Capability
- type ClientCredentialsRequest
- type CodeFlow
- type DPoPKey
- type DeviceKey
- type GrantAuthorizer
- type IdP
- type JWTBearerRequest
- type Message
- type OAuthTokens
- type Option
- type Outbox
- type TOTP
- type TokenExchange
- type TokenRequest
- type User
Constants ¶
const ( Issuer = "https://example.com" Audience = "authtest" )
Issuer and Audience are the token issuer and audience New configures.
const Password = "Authtest-password-1"
Password is the password NewUser gives every account.
Variables ¶
This section is empty.
Functions ¶
func ClientSecretSHA256 ¶ added in v1.5.0
ClientSecretSHA256 is the OAuthClientConfig.SecretSHA256 of secret.
func GrantRole ¶ added in v0.147.0
func GrantRole(t testing.TB, auth *authkit.Client, group iam.GroupRef, subject iam.Subject, role iam.Role)
GrantRole gives subject role in group with system authority. A role that requires MFA needs the account's second factor first (EnrollTOTP).
func Identity ¶ added in v1.7.0
Identity is the identity a request bearing credential (an access token or API key) acts as, verified as auth's gates verify it (verify.Required): AuthKit's operations accept it, as they accept the identity behind a gate. The test fails when auth refuses the credential.
func PKCEChallenge ¶ added in v1.5.0
PKCEChallenge is verifier's RFC 7636 S256 code challenge.
func Replica ¶ added in v0.147.0
Replica builds another Client on auth's database and schema, as another replica of the deployment runs: the Config and Deps auth was built with (its Outbox included, when New built it), then opts. auth may be any Client authkit.New built, a host's own included. A different Token.Issuer makes a sibling deployment sharing the account store; different HTTPConfig serves the same accounts another way. HTTP is copied, so opts may set its fields; replace, never mutate, the maps and slices opts change: the replica shares auth's.
func RevokeDeviceKey ¶ added in v1.10.0
RevokeDeviceKey revokes k, as its machine signing out does: every capability it signed stops working.
func RevokeRole ¶ added in v0.147.0
func RevokeRole(t testing.TB, auth *authkit.Client, group iam.GroupRef, subject iam.Subject, role iam.Role)
RevokeRole takes role in group from subject with system authority.
func SignIn ¶ added in v0.147.0
SignIn signs u in with its password through auth's HTTP surface and follows the AuthResult to a session: a second factor is answered with u.TOTP (or the account's remembered app, TOTPOf); an enrollment the deployment requires adds an authenticator app with the enrollment token, which SignIn remembers for the account's later sign-ins. It returns the session's tokens.
func StaleSession ¶ added in v0.147.0
StaleSession moves the sign-in of the session behind accessToken a day into the past, as if its user signed in long ago, and returns a new access token for that session: routes that need a recent sign-in then ask it for a step-up. auth may be any Client authkit.New built.
Types ¶
type Assertion ¶ added in v1.10.0
type Assertion struct {
// Issuer (iss) is the client_id.
Issuer string
// Subject (sub) names the workload; "" is the key's Thumbprint.
Subject string
// Audience (aud) is the token endpoint (AuthorizationServer.
// TokenEndpoint).
Audience string
// Lifetime sets exp from now: 0 is one minute; a negative one makes an
// expired assertion.
Lifetime time.Duration
// ID (jti) is "" for a random one.
ID string
// Capability is the device-key capability it carries
// (DeviceKey.Capability).
Capability string
// Claims are extra claims.
Claims map[string]any
}
Assertion is an RFC 7523 JWT-bearer assertion's claims, as a workload signs them (DPoPKey.Assertion).
type AuthorizationServer ¶ added in v1.5.0
type AuthorizationServer struct {
// Client is the deployment's AuthKit Client: its users, roles and
// sessions.
Client *authkit.Client
Outbox *Outbox
// URL is the issuer: the HTTPS server's URL, without a trailing slash.
URL string
// contains filtered or unexported fields
}
AuthorizationServer is an AuthKit authorization server (Config. AuthorizationServer) on an HTTPS test server, for testing a resource server or an OAuth client against real sign-ins and real tokens. URL is the issuer: its metadata (iam.OpenIDConfigurationPath), JWKS and OAuth endpoints are served beneath it, as in production.
func NewAuthorizationServer ¶ added in v1.5.0
func NewAuthorizationServer(t testing.TB, opts ...Option) *AuthorizationServer
NewAuthorizationServer serves New's Client over HTTPS (an httptest TLS server) with Token.Issuer set to the server's URL, so verifiers fetch its metadata and keys as they would from a deployment. Declare its clients and resource servers with WithConfig (Config.AuthorizationServer); opts apply after the issuer is set. It needs AUTHKIT_TEST_DATABASE_URL like New, and is closed at cleanup.
func (*AuthorizationServer) Approve ¶ added in v1.5.0
func (as *AuthorizationServer) Approve(t testing.TB, accessToken, id string) string
Approve approves the pending request id with the sign-in accessToken belongs to, as the SPA does, and returns the client redirect.
func (*AuthorizationServer) Authorize ¶ added in v1.5.0
func (as *AuthorizationServer) Authorize(t testing.TB, u User, f CodeFlow) OAuthTokens
Authorize signs u in and runs the authorization code flow for it as a browser and the SPA would: the authorization request with PKCE, the SPA's approval for that sign-in, then the code's redemption at the token endpoint. It fails the test on any refusal.
func (*AuthorizationServer) AuthorizeAs ¶ added in v1.5.0
func (as *AuthorizationServer) AuthorizeAs(t testing.TB, signedIn iam.TokenSet, f CodeFlow) OAuthTokens
AuthorizeAs is Authorize for a sign-in the test already holds.
func (*AuthorizationServer) BeginAuthorization ¶ added in v1.5.0
func (as *AuthorizationServer) BeginAuthorization(t testing.TB, f CodeFlow, verifier, state string) string
BeginAuthorization sends an authorization request with verifier's S256 challenge and returns the pending request's id the server sent the browser to the SPA with.
func (*AuthorizationServer) ClientCredentials ¶ added in v1.5.0
func (as *AuthorizationServer) ClientCredentials(t testing.TB, clientID, clientSecret, resource string, scopes []string, key *DPoPKey) OAuthTokens
ClientCredentials gets a confidential client's own access token for resource; key, when set, binds it.
func (*AuthorizationServer) Consent ¶ added in v1.6.0
Consent runs f's authorization request and the SPA's approval for signedIn, and returns the client redirect: its code, or the error a refusal (the grant authorizer's: access_denied) sends back.
func (*AuthorizationServer) Exchange ¶ added in v1.5.0
func (as *AuthorizationServer) Exchange(t testing.TB, x TokenExchange) OAuthTokens
Exchange runs a token exchange and fails the test on a refusal. A public client without a key gets a fresh one.
func (*AuthorizationServer) HTTPClient ¶ added in v1.5.0
func (as *AuthorizationServer) HTTPClient() *http.Client
HTTPClient trusts the server's certificate and never follows a redirect, so a test reads each Location itself.
func (*AuthorizationServer) JWTBearer ¶ added in v1.10.0
func (as *AuthorizationServer) JWTBearer(t testing.TB, r JWTBearerRequest) OAuthTokens
JWTBearer runs a jwt-bearer grant and fails the test on a refusal.
func (*AuthorizationServer) JWTBearerToken ¶ added in v1.10.0
func (as *AuthorizationServer) JWTBearerToken(t testing.TB, r JWTBearerRequest) (int, []byte)
JWTBearerToken runs a jwt-bearer grant and returns the status and body, refusals included.
func (*AuthorizationServer) Refresh ¶ added in v1.5.0
func (as *AuthorizationServer) Refresh(t testing.TB, clientID, clientSecret string, tokens OAuthTokens) OAuthTokens
Refresh redeems tokens' refresh token as clientID (with clientSecret for a confidential client), proving tokens' DPoP key, and returns the rotated tokens.
func (*AuthorizationServer) RequestClientCredentials ¶ added in v1.6.0
func (as *AuthorizationServer) RequestClientCredentials(t testing.TB, r ClientCredentialsRequest) OAuthTokens
RequestClientCredentials is ClientCredentials with authorization_details, failing the test on a refusal.
func (*AuthorizationServer) Revoke ¶ added in v1.5.0
func (as *AuthorizationServer) Revoke(t testing.TB, clientID, clientSecret, token string) int
Revoke posts token to the revocation endpoint (RFC 7009) as clientID and returns the status.
func (*AuthorizationServer) Token ¶ added in v1.5.0
func (as *AuthorizationServer) Token(t testing.TB, req TokenRequest) (int, []byte)
Token posts req to the token endpoint and returns the status and body.
func (*AuthorizationServer) TokenEndpoint ¶ added in v1.10.0
func (as *AuthorizationServer) TokenEndpoint() string
TokenEndpoint is the token endpoint's URL: a jwt-bearer assertion's aud.
type Capability ¶ added in v1.10.0
type Capability struct {
// Audience is the resource server's identifier.
Audience string
// Workload is the key the capability is bound to (cnf.jkt).
Workload *DPoPKey
// AuthorizationDetails are the operations, an RFC 9396 JSON array.
AuthorizationDetails string
// Lifetime sets exp from now: 0 is one hour; a negative one makes an
// expired capability.
Lifetime time.Duration
// ID (jti) is "" for a random one.
ID string
// Claims are other claims, such as the host's run id.
Claims map[string]any
}
Capability is what a device key lets a workload do for its user (DeviceKey.Capability).
type ClientCredentialsRequest ¶ added in v1.6.0
type ClientCredentialsRequest struct {
ClientID string
ClientSecret string
Resource string
Scopes []string
AuthorizationDetails string
DPoP *DPoPKey
}
ClientCredentialsRequest is a confidential client's request for its own access token; DPoP, when set, binds it.
type CodeFlow ¶ added in v1.5.0
type CodeFlow struct {
ClientID string
ClientSecret string
RedirectURI string
Resource string
Scopes []string
Nonce string
AuthorizationDetails string
DPoP *DPoPKey
// Prompt ("login") and MaxAge (seconds; 0 is now) ask for a fresh
// sign-in: approving with an older one answers step_up_required.
Prompt string
MaxAge *int
}
CodeFlow is one authorization code request. ClientSecret is set for a confidential client; Resource, Scopes ("offline_access" for an offline grant) and AuthorizationDetails (an RFC 9396 JSON array) are what the client asks for. DPoP binds the tokens to a key, and the grant to it for a key-bound client: a public client always has one (Authorize makes it when nil).
type DPoPKey ¶ added in v1.5.0
type DPoPKey struct {
// contains filtered or unexported fields
}
DPoPKey is a client's RFC 9449 proof-of-possession key (ES256), as a browser keeps it: tokens bound to it name its Thumbprint, and every request using them carries a fresh Proof.
func NewDPoPKey ¶ added in v1.5.0
NewDPoPKey generates a P-256 key.
func (*DPoPKey) Assertion ¶ added in v1.10.0
Assertion signs a with the key, its public JWK in the header, as a workload asserts itself for the jwt-bearer grant.
func (*DPoPKey) Authorize ¶ added in v1.5.0
Authorize sets req's DPoP-bound Authorization and a fresh proof for its method and URL (without the query).
func (*DPoPKey) Proof ¶ added in v1.5.0
Proof is a single-use DPoP proof for method and target (no query): accessToken is the token it accompanies ("" at a token endpoint), nonce the server's DPoP-Nonce ("" for none).
func (*DPoPKey) Thumbprint ¶ added in v1.5.0
Thumbprint is the key's RFC 7638 thumbprint: a bound token's cnf.jkt.
type DeviceKey ¶ added in v0.147.0
type DeviceKey struct {
ID string
// UserID is the account's.
UserID string
Key ed25519.PrivateKey
// AccessToken is the enrollment's sign-in.
AccessToken string
}
DeviceKey is a device key enrolled on an account (see package devicekey).
func EnrollDeviceKey ¶ added in v0.147.0
EnrollDeviceKey enrolls a new Ed25519 device key on u with the devicekey client, reading the emailed code from outbox and answering a second factor with u.TOTP. The Client needs Config.DeviceKeys.Enabled.
func (DeviceKey) Capability ¶ added in v1.10.0
func (k DeviceKey) Capability(t testing.TB, c Capability) string
Capability signs c with k for k's user, as a CLI does for a run (devicekey.SignCapability).
type GrantAuthorizer ¶ added in v1.6.0
type GrantAuthorizer struct {
Decide func(iam.OAuthGrantRequest) (iam.OAuthGrantDecision, error)
// contains filtered or unexported fields
}
GrantAuthorizer is a recording grant authorizer: install it with WithDeps (d.OAuthGrants = g.Authorize). Decide answers each request; nil grants the defaults.
func (*GrantAuthorizer) Authorize ¶ added in v1.6.0
func (g *GrantAuthorizer) Authorize(_ context.Context, req iam.OAuthGrantRequest) (iam.OAuthGrantDecision, error)
Authorize is the iam.OAuthGrantAuthorizer.
func (*GrantAuthorizer) Last ¶ added in v1.6.0
func (g *GrantAuthorizer) Last(kind iam.OAuthGrantKind) (req iam.OAuthGrantRequest, ok bool)
Last is the latest request of kind; ok is false when there is none.
func (*GrantAuthorizer) Requests ¶ added in v1.6.0
func (g *GrantAuthorizer) Requests() []iam.OAuthGrantRequest
Requests are the requests decided so far, oldest first.
type IdP ¶ added in v1.1.0
type IdP struct {
// contains filtered or unexported fields
}
IdP is an OpenID Provider on a local TLS server, for tests of signing in with an identity provider: discovery, JWKS, and the token and userinfo endpoints. A sign-in start redirects the browser to it; SignIn answers the way it would.
func (*IdP) Provider ¶ added in v1.1.0
Provider is an OpenID Connect provider named name for this IdP, trusted to verify email, for Deps.Providers. opts come after the defaults.
func (*IdP) SignIn ¶ added in v1.1.0
SignIn is the query of the IdP's redirect back to AuthKit's callback once user signs in at authURL, the IdP URL a sign-in start redirected to: its state and a code. AuthKit redeems the code for an ID token naming user's Subject, Email (verified when EmailVerified), PreferredUsername and DisplayName.
type JWTBearerRequest ¶ added in v1.10.0
type JWTBearerRequest struct {
ClientID string
ClientSecret string
Key *DPoPKey
Capability string
Resource string
Scopes []string
Assertion string
}
JWTBearerRequest is an RFC 7523 JWT-bearer token request: Key signs the assertion carrying Capability and proves itself with DPoP. Assertion, when set, is sent as is (Key still proves itself; nil Key sends no proof); otherwise Key asserts itself for ClientID at the token endpoint.
type Message ¶ added in v0.147.0
type Message = testoutbox.Message
Message is one captured email or SMS: Channel ("email" or "sms"), Kind, To, Language and what it carries (Code, Link and the Link's Token, the verification Purpose, a ContactChange or DeviceKey notice).
type OAuthTokens ¶ added in v1.5.0
type OAuthTokens struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
Scope string `json:"scope"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
IssuedTokenType string `json:"issued_token_type"`
DPoP *DPoPKey `json:"-"`
// AuthorizationDetails is the grant's RFC 9396 array, when it has one.
AuthorizationDetails json.RawMessage `json:"authorization_details"`
}
OAuthTokens is the token endpoint's answer. DPoP is the key the tokens are bound to, nil for bearer tokens.
type Option ¶ added in v0.147.0
type Option func(*setup)
Option adjusts what New builds.
func WithConfig ¶ added in v0.147.0
WithConfig edits the Config New passes to authkit.New, after its defaults.
type Outbox ¶ added in v0.147.0
type Outbox = testoutbox.Outbox
Outbox captures every email and SMS AuthKit sends, in order, so a test can complete sign-up, verification, reset and sign-in flows. New wires one; for a Client built another way, pass Email() and SMS() as Deps.Email and Deps.SMS. Its methods:
- Last(t, kind, to) Message: the newest message of kind to that address or number ("" = anyone); the test fails when there is none.
- Messages(kind, to) []Message: all of them, oldest first ("" = any).
- Email() authkit.EmailSender, SMS() authkit.SMSSender: the senders.
- SetEmailHealth(err), SetSMSHealth(err): what the senders' CheckHealth returns (nil, the default, is healthy), so a test can take a channel down and bring it back.
For example, out.Last(t, iam.MessageVerification, email).Code.
func New ¶ added in v0.147.0
New migrates AuthKit into a fresh schema, builds a Client on it, and returns the Client with the Outbox wired as its email and SMS senders. The defaults differ from a zero Config only where a test needs them to:
- Token: Issuer and Audience.
- TwoFactor.TOTPSecretKey: random, so authenticator apps can enroll.
- HTTP: served (DirectPeerIP), with every rate limit lifted (RateLimits).
- SignIn: every limit off, since tests sign many accounts in from one address.
- Deps.KeySource: an RSA key generated once per test binary.
- Database.Schema and Database.RiverSchema: the scratch schema, unless set.
The Client is not started: call Start when a test needs River's work, such as Deps.OnEvent or the deletion hooks. Cleanup closes the Client and drops the scratch schema.
type TOTP ¶ added in v0.147.0
type TOTP struct {
Secret string
// contains filtered or unexported fields
}
TOTP is an authenticator app enrolled on an account.
func EnrollTOTP ¶ added in v0.147.0
EnrollTOTP signs u in and adds an authenticator app through auth's HTTP surface, as a user would. SignIn uses it from now on; keeping it as u.TOTP works too.
type TokenExchange ¶ added in v1.5.0
type TokenExchange struct {
ClientID string
ClientSecret string
SubjectToken string
Resource string
Scopes []string
AuthorizationDetails string
DPoP *DPoPKey
}
TokenExchange is an RFC 8693 request: SubjectToken, the user's AuthKit access token (SignIn's), for an access token to Resource.
type TokenRequest ¶ added in v1.5.0
TokenRequest is one raw token endpoint request: Params as clientID (with Basic authentication when ClientSecret is set), with a DPoP proof of DPoP when set.
type User ¶ added in v0.147.0
type User struct {
iam.User
Email string
Password string
// TOTP is the authenticator app SignIn answers a second-factor challenge
// with. Unset, SignIn uses the app EnrollTOTP or an earlier SignIn enrolled
// on the account (TOTPOf).
TOTP *TOTP
}
User is an account and what SignIn needs to sign it in. Email is the account's address as text ("" for none).