toeverything/AFFiNE · error · AuthenticationRequired

authentication_required

authentication_required

Error message

You must sign in first to access this resource.

What it means

Thrown by acceptInvitation when inviteId does not match any email invitation (no workspaceUser record) so it is treated as an invitation-by-link, and there is no authenticated user in the GraphQL context. Link invitations require an existing account because the server must attach the user to the workspace.

Solutions

  1. Redirect the user to the sign-in page with the invite link as the return URL, then call acceptInvitation again after login.
  2. If calling the API directly, send the session cookie or Authorization token for an existing account.
  3. Register an account first if the user does not have one, then retry the link.
Defensive patterns

Strategy: validation

Validate before calling

// Only fire accept for link invites once a session exists
const me = await gql.request(CURRENT_USER_QUERY);
if (!me.currentUser) {
  // preserve the invite URL across login
  router.push(`/sign-in?redirect=${encodeURIComponent(location.pathname)}`);
} else {
  await gql.request(ACCEPT_INVITATION, { inviteId });
}

Try / catch

try {
  await acceptInvitation(inviteId);
} catch (e) {
  if (getErrorCode(e) === 'authentication_required') {
    redirectToSignInWithReturnUrl(currentUrl);
  }
}

Prevention

When it happens

Trigger: An anonymous visitor follows an invite link (e.g. /invite/<inviteId>) and the client fires the acceptInvitation mutation before a sign-in session exists.

Common situations: Invite link opened in an incognito window or after session expiry; frontend routing calls acceptInvitation before the auth provider finished restoring the session; API consumers calling the mutation without an Authorization header.

Related errors


AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18). Data as JSON: /api/errors/12920541ba62e9a9. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/workspaces/resolvers/member.ts:587

    @Args('inviteId') inviteId: string,
    @Args('workspaceId', { deprecationReason: 'never used', nullable: true })
    _workspaceId: string,
    @Args('sendAcceptMail', {
      nullable: true,
      deprecationReason: 'never used',
    })
    _sendAcceptMail: boolean
  ) {
    const role = await this.models.workspaceUser.getById(inviteId);
    // invitation by email
    if (role) {
      if (user.id !== role.userId) {
        throw new InvitationAccountMismatch();
      }

      await this.acceptInvitationByEmail(role);
    } else {
      // invitation by link
      const invitation = await this.cache.get<{
        workspaceId: string;
        inviterUserId: string;
      }>(`workspace:inviteLinkId:${inviteId}`);

      if (!invitation) {
        throw new InvalidInvitation();
      }

      const role = await this.models.workspaceUser.get(
        invitation.workspaceId,
        user.id
      );

      if (role) {
        // if status is pending, should accept the invitation directly
        if (role.status === WorkspaceMemberStatus.Pending) {
          await this.acceptInvitationByEmail(role);

View on GitHub (pinned to 2af30773ae)