Documentation
¶
Overview ¶
Package authwire is what the authentication endpoints send and receive.
It exists because there are two ends of that conversation and only one of them used to have a name for it. The shapes lived unexported inside rig/auth's HTTP handlers, so a client — the generated Go SDK, a TypeScript one, anything somebody writes by hand — had to restate them, and a restatement drifts silently: a field renamed on the server is a field a client quietly stops sending, and nothing fails to compile on either side.
So the shapes live here, in a package with no dependencies worth speaking of, and both ends import it. The server fills them in; a client reads them.
Nothing here is generated, and nothing here is per-project. These endpoints are rig's own: the same routes with the same bodies in every application that turns authentication on. What differs from one project to the next is which of them are mounted and how long the credentials last, and that is in the document — see github.com/simonjanss/rig/pkg/ir.Auth.
Index ¶
- Constants
- type APIKeyView
- type AcceptAsMeRequest
- type AcceptRequest
- type AccountView
- type AuthLogEntryView
- type CreateKeyRequest
- type CreateKeyResponse
- type CreateTenantRequest
- type EmailCodeRequest
- type Handoff
- type ImpersonateRequest
- type InvitationPreview
- type InvitationToMeView
- type InvitationView
- type InviteRequest
- type List
- type Page
- type Pagination
- type ProvisionRequest
- type RefreshRequest
- type SessionView
- type SignInResponse
- type TenantView
- type TokenPair
- type VerifyEmailCodeRequest
- type VerifyEmailRequest
Constants ¶
const HandoffCookie = "rig_handoff"
HandoffCookie is where a browser sign-in leaves its tokens.
A provider sign-in that ends in a redirect cannot answer with a body, so the tokens travel in a short-lived cookie the landing page reads and deletes. The name is here rather than in the handler that writes it because both ends need it and only one of them is Go — see Handoff.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type APIKeyView ¶
type APIKeyView struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
KeyID string `json:"keyId"`
// Kind is Integration or Personal. A client cannot tell them apart without
// it, and they behave differently enough that an administration screen
// showing a list of keys has to say which is which.
Kind string `json:"kind"`
Scopes []string `json:"scopes"`
CIDRAllowList []string `json:"cidrAllowList,omitempty"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
RevokedAt *time.Time `json:"revokedAt,omitempty"`
}
APIKeyView is one key, without the secret — which nothing stored can produce again.
type AcceptAsMeRequest ¶
type AcceptAsMeRequest struct {
InvitationID uuid.UUID `json:"invitationId"`
Client string `json:"client"`
}
AcceptAsMeRequest is the body of POST <base>/me/invitations/accept, where the caller is already signed in and names the invitation by identifier.
type AcceptRequest ¶
AcceptRequest is the body of POST <base>/invitations/accept, where the token from the invitation mail is the credential.
type AccountView ¶
type AccountView struct {
// ID names the account — the person inside this tenant. It is not the
// identity: somebody in two tenants has one identity and two accounts, and
// this is the one that was just created.
ID uuid.UUID `json:"id"`
TenantID uuid.UUID `json:"tenantId"`
EmailAddress string `json:"emailAddress"`
DisplayName string `json:"displayName"`
// Kind and Role are the values the database stores, as strings, so that a
// client sends back the same words it was given.
Kind string `json:"kind"`
Role string `json:"role"`
// TimeZone is an IANA name, for example Europe/Stockholm. Empty means UTC.
TimeZone string `json:"timeZone,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
AccountView is what a provisioning call comes back with. It is deliberately not the whole row: an account's audit trail is administration, and this answers "it exists now".
type AuthLogEntryView ¶
type AuthLogEntryView struct {
ID uuid.UUID `json:"id"`
// At is when it happened, in UTC.
At time.Time `json:"at"`
// Event is what happened — one of the values of the rig_auth_event enum.
Event string `json:"event"`
// Outcome is whether it worked: Succeeded or Failed, and no third value.
Outcome string `json:"outcome"`
// AccountID is who it happened to, absent when the attempt never resolved
// to an account — which is what a wrong address looks like.
AccountID *uuid.UUID `json:"accountId,omitempty"`
// EmailAddress is the address as presented, lowercased. Present even when no
// account matched, which is the case worth reading.
EmailAddress string `json:"emailAddress,omitempty"`
IPAddress string `json:"ipAddress,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
// APIKeyID is the key involved, when one was.
APIKeyID *uuid.UUID `json:"apiKeyId,omitempty"`
// APIKeyRef is the public half of a key as presented, whether or not it
// resolved to a row.
APIKeyRef string `json:"apiKeyRef,omitempty"`
// SessionID is the session family involved, named the way [TokenPair] names
// it rather than after the root token it is stored as.
SessionID *uuid.UUID `json:"sessionId,omitempty"`
// Detail is whatever else was worth recording. Reuse detection puts the
// original and current address and user agent here, which is what turns
// "somebody replayed a token" into "somebody replayed it from Frankfurt".
Detail map[string]any `json:"detail,omitempty"`
}
AuthLogEntryView is one recorded authentication event.
There is no tenant member, because there is nothing it could say: the reader behind this shape answers within one tenant and cannot be asked to do otherwise. What is missing for a subtler reason is the events that resolved to *no* tenant — an attempt that named none, or one against an address with no account anywhere. Those are recorded, they are what a rate limit most needs, and no tenant has the standing to read them.
The keys are camelCase here and stay camelCase whatever a project sets `api.json_case` to. That setting shapes the keys rig *generates*; this module is hand-written and shared by every project, so its shape cannot vary with one project's preference.
type CreateKeyRequest ¶
type CreateKeyRequest struct {
Name string `json:"name"`
Scopes []string `json:"scopes"`
CIDRAllowList []string `json:"cidrAllowList"`
ExpiresAt *time.Time `json:"expiresAt"`
// Kind is Integration or Personal, and defaults to Integration.
//
// A personal key acts as its creator, so it cannot name a service account —
// the manager refuses that combination and so does a CHECK on the table. An
// integration key is the default because it is the one whose writes stay
// attributable after the person who set it up has left.
Kind string `json:"kind"`
// ServiceAccountID is who the key acts as. It defaults to the caller, which
// is the common case and the one that keeps the writes attributable.
ServiceAccountID *uuid.UUID `json:"serviceAccountId"`
}
CreateKeyRequest is the body of POST <base>/api-keys.
type CreateKeyResponse ¶
type CreateKeyResponse struct {
Key APIKeyView `json:"key"`
// Secret is shown exactly once. Nothing stored can produce it again, which
// is what makes storing only a hash safe.
Secret string `json:"secret"`
}
CreateKeyResponse is the only response that will ever contain the secret.
type CreateTenantRequest ¶
type CreateTenantRequest struct {
Name string `json:"name"`
// Client is web, mobile or machine, for the session the new tenant is
// entered with.
Client string `json:"client"`
}
CreateTenantRequest is the body of POST <base>/tenants.
type EmailCodeRequest ¶ added in v0.6.0
type EmailCodeRequest struct {
EmailAddress string `json:"emailAddress"`
}
EmailCodeRequest is the body of POST <base>/email-code.
The address and nothing else. There is no client here because nothing is issued yet — that belongs on the verify, which is where a session comes from.
type Handoff ¶ added in v0.5.1
type Handoff struct {
TokenPair
// IdentityToken and IdentityExpiresAt are as [SignInResponse] has them, and
// they are the only members always present: somebody who belongs to no
// tenant yet arrives with these and no pair.
IdentityToken string `json:"identityToken"`
IdentityExpiresAt time.Time `json:"identityExpiresAt"`
}
Handoff is what that cookie holds: a SignInResponse without the tenants.
The list is left out because a cookie is limited to about four kilobytes and a person in thirty tenants would silently exceed it — a sign-in that fails with nothing in any log. Handoff.IdentityToken fetches the same list from GET <base>/me/tenants, which is the call the picker makes anyway.
The value is base64url of this shape's JSON. It is a contract with the code that reads the cookie, which is why it is named here: a browser front end mirrors this and nothing on either side would fail to compile if the two drifted.
type ImpersonateRequest ¶
ImpersonateRequest is the body of POST <base>/impersonate.
type InvitationPreview ¶ added in v0.6.0
type InvitationPreview struct {
// ID is what POST <base>/me/invitations/accept takes, for the case this
// exists to serve second: somebody already signed in who has just followed
// the link. It is safe to hand out because that endpoint refuses an
// invitation that is not the caller's own.
ID uuid.UUID `json:"id"`
TenantID uuid.UUID `json:"tenantId"`
// TenantName is the point of the whole endpoint. A page that cannot say
// where somebody has been invited is a page that looks like phishing.
TenantName string `json:"tenantName"`
// EmailAddress is masked — "b***@school.example". Enough to recognise which
// of your addresses this is, or to notice you are signed in as somebody
// else, and not enough for a forwarded link to confirm an address to
// whoever it was forwarded to.
EmailAddress string `json:"emailAddress"`
// Role is what accepting grants, so that accepting is an informed decision
// rather than a button.
Role string `json:"role"`
// InvitedBy is the display name of whoever sent it, and empty when a key
// sent it or when that person has since been removed — both of which a page
// has to render rather than fail on.
InvitedBy string `json:"invitedBy,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
}
InvitationPreview is what an invitation's link says, answered to whoever holds it and before anybody has proved anything.
Deliberately not an InvitationToMeView. That one is answered to a caller who has proved they are the person invited; this one to a caller who has proved only that they hold a link. The difference is the address.
type InvitationToMeView ¶
type InvitationToMeView struct {
ID uuid.UUID `json:"id"`
// TenantName is the part that means anything to somebody who has not been
// there. The identifier is for the accept call.
TenantID uuid.UUID `json:"tenantId"`
TenantName string `json:"tenantName"`
Role string `json:"role"`
// InvitedBy is the display name of whoever sent it. It is here for the same
// reason it is on the preview: a name is what makes "you have been invited"
// something somebody recognises rather than something they distrust.
InvitedBy string `json:"invitedBy,omitempty"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
InvitationToMeView is one invitation waiting for the caller, seen by the person who was invited.
It carries no token. An invitation's token is the credential that redeems it and it was sent to an address; listing tokens would turn "I can see my invitations" into "I can accept them", which is a different claim about who reached the mailbox.
type InvitationView ¶
type InvitationView struct {
ID uuid.UUID `json:"id"`
EmailAddress string `json:"emailAddress"`
DisplayName string `json:"displayName"`
// Role is the role the invitation grants on acceptance, not one the invited
// person holds yet.
Role string `json:"role"`
// InvitedBy is the display name of whoever sent it, and empty when a key
// sent it or when that person has since been removed.
InvitedBy string `json:"invitedBy,omitempty"`
CreatedAt time.Time `json:"createdAt"`
// ExpiresAt is when the token stops working. A listed invitation past it is
// history, not something still waiting to be accepted.
ExpiresAt time.Time `json:"expiresAt"`
}
InvitationView is one invitation into the caller's tenant, seen by whoever administers it.
It carries no token, for the same reason InvitationToMeView does not: the token is the credential that redeems the invitation, and an administrator who can list invitations is not thereby somebody who can accept one on another person's behalf.
type InviteRequest ¶ added in v0.6.0
type InviteRequest struct {
EmailAddress string `json:"emailAddress"`
// DisplayName is what to call them in this tenant. Empty falls back at
// accept time to the name the person already has.
DisplayName string `json:"displayName"`
// Role is optional and defaults to Basic.
Role string `json:"role"`
}
InviteRequest is the body of POST <base>/invitations.
Who sent it is never in here. It comes from the caller's claims, because a body that could name the inviter is a body that could name somebody else.
type List ¶
type List[T any] struct { Data []T `json:"data"` }
List is the envelope every collection here comes back in.
One member, so that a list can grow a sibling — a count, a cursor — without the response changing shape from an array into an object, which is the change that breaks every caller at once.
type Page ¶
type Page[T any] struct { Data []T `json:"data"` Pagination Pagination `json:"pagination"` }
Page is the envelope for a collection here that is too big to answer whole.
Most of this package's lists are List instead, and that is not an oversight: a tenant's keys, invitations and tenants are a handful of rows, and paginating four endpoints that will never need it costs four handlers and four sets of tests. The authentication trail is millions of rows and cannot borrow that argument.
type Pagination ¶
type Pagination struct {
// Offset is how many rows were skipped before this page.
Offset int `json:"offset"`
// Limit is the most rows this page could have held.
Limit int `json:"limit"`
// Total is every row matching the query, ignoring pagination. It is what
// tells a caller whether there is another page.
Total int64 `json:"total"`
}
Pagination is where a returned page sits in the full result set.
The same three members, with the same names, the generated endpoints answer with. Two shapes for one idea would mean a client holding two decoders for what is visibly the same thing.
type ProvisionRequest ¶
type ProvisionRequest struct {
EmailAddress string `json:"emailAddress"`
DisplayName string `json:"displayName"`
// Kind and Role are optional: a person and Basic unless the caller says
// otherwise. They are strings on the wire so that a client sends the same
// words the database stores.
Kind string `json:"kind"`
Role string `json:"role"`
TimeZone string `json:"timeZone"`
}
ProvisionRequest is the body of POST <base>/accounts.
type RefreshRequest ¶
type RefreshRequest struct {
RefreshToken string `json:"refreshToken"`
}
RefreshRequest is the body of POST <base>/refresh.
The token travels in the body rather than the Authorization header. It is not the credential for anything else, and a header is how it ends up in an access log next to a hundred access tokens that expire in ten minutes.
type SessionView ¶
type SessionView struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
LastUsedAt time.Time `json:"lastUsedAt"`
ExpiresAt time.Time `json:"expiresAt"`
IPAddress string `json:"ipAddress,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
// AccountID is whose session it is. Always filled in, including in the
// caller's own list where it is the caller: a member that is present for one
// reading of an endpoint and absent for another is a member a client cannot
// rely on.
AccountID uuid.UUID `json:"accountId"`
Client string `json:"client"`
// Current marks the session making this request, so an interface can label
// it rather than inviting somebody to revoke the tab they are looking at
// without warning.
Current bool `json:"current"`
}
SessionView is one session, as somebody reviewing it sees it.
type SignInResponse ¶
type SignInResponse struct {
TokenPair
// IdentityToken proves who somebody is and names no tenant. It is what
// the tenant picker runs on, and it is issued even alongside a session,
// because switching tenant later is the same flow.
IdentityToken string `json:"identityToken"`
IdentityExpiresAt time.Time `json:"identityExpiresAt"`
// Tenants is every one this person belongs to, so a client can draw the
// picker without a second call. Empty means they belong to none yet, which is
// an ordinary state and no longer a refusal.
Tenants []TenantView `json:"tenants"`
}
SignInResponse is what a sign-in answers with.
The pair is embedded rather than nested, so accessToken and the rest stay where they have always been: a client written against the old shape keeps working, and the new fields are additions it can ignore.
The token fields are omitted entirely when there is no session — somebody who belongs to no tenant yet. An empty accessToken would look like a token and fail on first use; an absent one says what happened.
type TenantView ¶
type TenantView struct {
TenantID uuid.UUID `json:"tenantId"`
TenantName string `json:"tenantName"`
TenantSlug string `json:"tenantSlug"`
AccountID uuid.UUID `json:"accountId"`
Role string `json:"role"`
// Current marks the tenant this request was made in, so an interface can
// show where somebody is without comparing identifiers itself.
Current bool `json:"current"`
}
TenantView is one tenant somebody belongs to.
type TokenPair ¶
type TokenPair struct {
AccessToken string `json:"accessToken,omitempty"`
RefreshToken string `json:"refreshToken,omitempty"`
ExpiresAt time.Time `json:"expiresAt,omitzero"`
// RefreshExpiresAt is when the session itself ends. A client needs both:
// one says when to refresh, the other says when to stop trying.
RefreshExpiresAt time.Time `json:"refreshExpiresAt,omitzero"`
SessionID uuid.UUID `json:"sessionId,omitzero"`
}
TokenPair is what every endpoint that starts or continues a session returns.
type VerifyEmailCodeRequest ¶ added in v0.6.0
type VerifyEmailCodeRequest struct {
EmailAddress string `json:"emailAddress"`
// Code is the digits as typed. Spaces and hyphens are tolerated; leading
// zeros are part of the code and have to survive whatever a client does to
// it, which is why it is a string and never a number.
Code string `json:"code"`
// Remember asks for a long session.
Remember bool `json:"remember"`
// Client is what will hold the session: "web", "mobile" or "machine".
Client string `json:"client"`
}
VerifyEmailCodeRequest is the body of POST <base>/email-code/verify.
type VerifyEmailRequest ¶
type VerifyEmailRequest struct {
Token string `json:"token"`
}
VerifyEmailRequest is the body of POST <base>/email/verify.