toeverything/AFFiNE · warning · ActionForbidden

action_forbidden

action_forbidden

Error message

You are not allowed to perform this action.

What it means

The deprecated clientToken resolver field issues a user session token for the resolved UserType. It enforces that the resolved user is the same as the authenticated currentUser; if user.id !== currentUser.id, ActionForbidden is thrown. This prevents one authenticated user from minting a session token on behalf of a different user. The field itself is marked deprecated ('use auth session exchange instead').

Source

Thrown at packages/backend/server/src/core/auth/resolver.ts:89

  @Query(() => UserType, {
    name: 'currentUser',
    description: 'Get current user',
    nullable: true,
  })
  currentUser(@CurrentUser() user?: CurrentUser): UserType | undefined {
    return user;
  }

  @ResolveField(() => ClientTokenType, {
    name: 'token',
    deprecationReason: 'use auth session exchange instead',
  })
  async clientToken(
    @CurrentUser() currentUser: CurrentUser,
    @Parent() user: UserType
  ): Promise<ClientTokenType> {
    if (user.id !== currentUser.id) {
      throw new ActionForbidden();
    }

    const userSession = await this.auth.createUserSession(user.id);

    return {
      sessionToken: userSession.sessionId,
      token: userSession.sessionId,
      refresh: '',
    };
  }

  @Public()
  @Mutation(() => Boolean)
  async changePassword(
    @Args('token') token: string,
    @Args('newPassword') newPassword: string,
    @Args('userId', { type: () => String, nullable: true }) userId?: string
  ) {

View on GitHub (pinned to 26c515e050)

Solutions

  1. Migrate to the auth session exchange flow (the deprecated replacement) and stop selecting the token field.
  2. Only request the token field on the current user (user.id === currentUser.id).
  3. Update the client to the latest version that no longer references this deprecated field.
  4. Audit the query to ensure it does not alias another user into the UserType parent.

Example fix

# before (deprecated, errors for non-self user)
query { otherUser { id token { token } } }
# after — use auth session exchange for the current user
mutation { authSessionExchange(...) { ... } }
Defensive patterns

Strategy: type-guard

Validate before calling

// only request the deprecated token field for the current user
function canRequestToken(parentUser, currentUser): boolean {
  return parentUser.id === currentUser.id;
}

Type guard

function isActionForbidden(err: unknown): boolean {
  return (
    !!err &&
    typeof err === 'object' &&
    (err as { code?: string }).code === 'action_forbidden'
  );
}

Try / catch

try {
  const { token } = await client.query(user.id === me.id ? TOKEN_FIELD : NO_TOKEN);
} catch (err) {
  if (isActionForbidden(err)) {
    // stop selecting the deprecated token field on other users
  }
  throw err;
}

Prevention

When it happens

Trigger: A GraphQL query selecting user { token } (or clientToken) on a UserType whose id differs from the JWT/cookie-authenticated currentUser id — e.g. querying another user's profile and asking for their token field.

Common situations: A client still on the deprecated token field tries to fetch a token while viewing another user's profile. Misconstructed query selects token on a non-self user node. Stale client code that hasn't migrated to the auth session exchange flow.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/86ed763ba78c4e23. Report an issue: GitHub.