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’smetaspace, reading themembersspace and writing the user’s ownacceptancethere, and using the user’s owninvitesspace.- A modality permission set for each kind of space you use, such as
include:fyi.opensocial.calendarPermissionsfor reading an events space and writing RSVPs. - One
rpc:entry for every host method you call. Each takes the same?aud=. read_selfonfyi.opensocial.members, only if you calllistSpacesto 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 ScopeMissingErrorand names the exact scope it wanted, for exampleMissing 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
_lexiconDNS record.community.lexicon.calendar.basePermissionsis one such set; usefyi.opensocial.calendarPermissions. - A hosted client’s metadata must declare the full scope. If your
client-metadata.jsondeclares 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.