Skip to content

Linking a person to an upstream service a resource brokers #160

Description

@dickhardt

Summary

AAuth authorizes an agent to perform operations. It has no ceremony for
linking a person to a third-party service that a resource brokers access to —
one that grants the agent nothing.

This issue proposes that shape for discussion. We intend to implement it first
and learn from it before proposing any draft text
— filing it here so the
design is public and reviewable while we do.

The gap

Some resources are proxies: they front an upstream API (Google Calendar, HubSpot,
Slack) that the person must have connected before the resource can serve any
request at all. Today that connection can only be established as a side effect of
an authorization:

  • The agent names operations at the authorization endpoint.
  • The resource decides a connection is required and attaches an interaction to
    the resource token.
  • The person server flows the person through it on the way to consent.

That works, but it means upstream access can never be acquired before it is
needed
. Every new service, and every new upstream account, first surfaces as an
interruption in the middle of unrelated work. An agent that knows it will need
Gmail, Calendar and Contacts has no way to say so up front.

Proposal

1. A connection object in resource metadata

Its presence in /.well-known/aauth-resource.json declares that this resource
fronts an upstream the person must link, and describes what can be requested.

"connection": {
  "endpoint":      "https://calendar-googleapis-com.example/connections",
  "upstream_name": "Google",
  "scopes": [
    { "scope": ".../auth/calendar.readonly", "default": true,
      "description": "Read your calendars and events" },
    { "scope": ".../auth/calendar.events",
      "description": "Create and change events" }
  ],
  "account_description": "Google email account"
}

Notes on two choices that were not obvious:

  • Raw upstream scopes, not an abstracted profile vocabulary. An earlier draft
    named breadths (read / write / full). It lost information — an agent
    understands .../auth/calendar.readonly better than it understands "read"
    and required per-resource mapping that scope-set comparison with the upstream's
    own implication rules already does.
  • account_description present or absent is the whole account model. Present
    means the agent must name an account when linking, because the identifier is
    something the person knows and the upstream's authorize URL can pre-select
    (Google's login_hint, Slack's team). Absent means it must not, because the
    value is chosen inside the provider's UI and is not something a person can type
    (a HubSpot portal id, a Threads profile). Everything else — how many accounts
    exist, whether one is required on a given call — is observable at runtime and
    does not belong in static metadata.

The description fields are read by the agent, not rendered to a person, so they
stay in one language. See §4.

2. A connections collection

POST   {connection.endpoint}            link an upstream account
GET    {connection.endpoint}            list this person's connections
DELETE {connection.endpoint}/{account}  disconnect one

All authenticated with a person token.

POST body: { scopes?, account? }. Omitting scopes requests the declared
defaults; a requested set must be a subset of what is declared. account is
present exactly when account_description is.

The response is a resource token carrying an interaction and no scope
claim
(§3). Already linked at those scopes or broader returns a status, not an
error — retry-safety is what makes "link everything up front" a safe instruction
for an agent.

GET returns, per connection: the account, a human label where the account key
is not itself human, the scopes held, when it was connected, and whether the
grant is still believed live. Two details that matter in practice: return the
implication-expanded scope set, so a client does not need the upstream's
hierarchy rules to reason correctly; and be explicit that this is what the
resource believes it holds, since a person can revoke at the upstream without
telling anyone.

3. The resource token needs no new claim

A link is signalled by the interaction the token already carries. Adding a claim
describing the connection was tried and removed: the person server does not
render it, its logic needs only to know that an interaction is required, it
never sends the value back to the resource, and the agent already knows what it
asked for. The resource holds the detail on the pending record the interaction
code keys.

That leaves one new invariant:

A scope claim is present exactly when an auth token will be issued.

A link ceremony issues nothing, so its token carries no scope. This conflicts
with scope being unconditionally REQUIRED on a resource token, which is the one
normative change the proposal needs.

The person server's handling becomes three independent tests:

r3 reference present  → render consent from the document
else scope present    → render consent from the scope descriptions
else                  → no consent card

interaction present   → send the person to the resource's interaction endpoint

scope present         → issue an auth token
else                  → terminate the ceremony without one

An authorization and a link compose — a token may carry both, which is
exactly the case that exists today when a resource decides a connection is needed
before it can honour an authorization.

4. Two audiences for text

Worth stating explicitly because it settled several questions above: text in this
protocol has two audiences, and they want opposite treatment.

  • A person reads it — consent cards, interaction explainers. Must be an
    identifier the renderer localises, or language-tagged.
  • An agent reads it and renders to the person in their own language — tool
    descriptions, operation summaries, scope descriptions. Plain, single-language
    prose is correct; translating it is waste.

A corollary that removed a metadata field: naming what an upstream calls an
account (workspace, organisation, portal) looks like a localisation lever
and is really a localisation trap. A vendor's term of art does not survive
translation, and the authoritative word is whatever that vendor uses in that
locale — which a fixed enum cannot mirror. A generic noun that is right
everywhere beats a specific one that is wrong in most places.

5. Interaction endpoints belong in metadata

Not strictly part of this ceremony, but it came out of designing it and applies
to every interaction AAuth already has.

Today an interaction is carried as a URL plus a code. If the endpoint is
published in resource metadata and only the code travels, the redirect target
stops being a per-request value chosen by the token issuer and becomes a stable,
published property that a registry can validate once and a reconcile job can
watch for drift. It costs nothing to fetch: the resource token already names the
well-known document that publishes its signing key, so any party verifying the
token has that document in hand.

Status

Not a draft PR. We are building this to find out whether it is the right shape,
and would rather discover the problems in an implementation than in spec text.
Comments on the shape — particularly the scope-implies-issuance invariant in
§3, and whether the connections collection is the right home for this — are very
welcome.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions