MenuSigning in

Signing in

The OAuth scope an app asks for, what the permission sets grant, and the mistakes that fail silently.

Sign-in is ordinary atproto OAuth against the user’s own PDS. What’s specific to groups is the scope.

The scope

A group app asks for:

const AUD = encodeURIComponent("did:web:host.opensocial.fyi#opensocial");
const rpc = (method: string) => `rpc:${method}?aud=${AUD}`;

const scope = [
  "atproto",
  `include:fyi.opensocial.basePermissions?aud=${AUD}`,
  "include:fyi.opensocial.calendarPermissions",
  // Every host method you call, one entry each:
  rpc("fyi.opensocial.requestJoin"),
  rpc("fyi.opensocial.getJoinRequest"),
  rpc("fyi.opensocial.cancelJoinRequest"),
  rpc("fyi.opensocial.getGroupAuth"),
  // Only if you list the user's groups (see Finding groups):
  "space:fyi.opensocial.members?authority=*&action=read_self",
].join(" ");
  • include:fyi.opensocial.basePermissions: the group permission set. It covers reading a group’s meta space, reading the members space and writing the user’s own acceptance there, and using the user’s own invites space.
  • A modality permission set for each kind of space you use, such as include:fyi.opensocial.calendarPermissions for reading an events space and writing RSVPs.
  • One rpc: entry for every host method you call. Each takes the same ?aud=.
  • read_self on fyi.opensocial.members, only if you call listSpaces to find the user’s groups.

listGroups needs no scope: it takes no authentication.

What the permission sets grant

Set Grants
fyi.opensocial.basePermissions Read fyi.opensocial.meta spaces. Read fyi.opensocial.members spaces and write only fyi.opensocial.acceptance there. Read and write fyi.opensocial.invite in fyi.opensocial.invites spaces, and create the user’s own invites space.
fyi.opensocial.basePermissions (host methods) Service auth for createGroup, provisionGroup, createSpace, assignRoles, ejectMember, listGroups, getGroupAuth, createInvite, requestJoin, listInvites, revokeInvite.
fyi.opensocial.calendarPermissions Read fyi.opensocial.events spaces and write community.lexicon.calendar.rsvp there.

Check what a session actually got with session.getTokenInfo(). Its scope lists the expanded permissions.

The namespace rule

A browser client during development

A browser app on your own machine uses a loopback client: no hosted metadata, and the scope goes inside the client ID. Serve the app from 127.0.0.1, not localhost.

import { BrowserOAuthClient } from "@atproto/oauth-client-browser";

const redirectUri = "http://127.0.0.1:5191/";
const client = new BrowserOAuthClient({
  clientMetadata: {
    client_id: `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`,
    redirect_uris: [redirectUri],
    scope,
    response_types: ["code"],
    grant_types: ["authorization_code", "refresh_token"],
    token_endpoint_auth_method: "none",
    application_type: "native",
    dpop_bound_access_tokens: true,
  },
  handleResolver: "https://bsky.social", // on a local dev network: the dev PDS
  // Local dev network only:
  // plcDirectoryUrl: "http://localhost:2582",
  // allowHttp: true,
});

const result = await client.init(); // after the redirect back: { session }
await client.signIn("alex.bsky.social", { scope });

In production, use a hosted client: serve client-metadata.json from your app’s origin and use its URL as client_id.

Things that go wrong

  • Missing scope. The user’s PDS refuses with 403 ScopeMissingError and names the exact scope it wanted, for example Missing required scope "rpc:fyi.opensocial.requestJoin?aud=…". Add that entry.
  • Only ask for permission sets that resolve. Sign-in fails for users on other PDSes if a set’s publisher has no _lexicon DNS record. community.lexicon.calendar.basePermissions is one such set; use fyi.opensocial.calendarPermissions.
  • A hosted client’s metadata must declare the full scope. If your client-metadata.json declares less than you request, every login fails. Generate both from one function.
  • New scopes need a new sign-in. Existing sessions keep the scope they were granted. After you add a method or a space type, users have to sign in again.