How To
Authentication

Authentication

Reflect can control access to rooms by calling out to a separate server.

First, pass some auth token to the Reflect constructor's auth parameter. Typically this token will be generated by your server at page load time.

index.tsx
const r = new Reflect({
  auth: "some-auth-token",
  // ...
});

Next, provide an authHandler in the server entrypoint that validates the auth token:

/reflect/index.ts
import { mutators } from "../src/mutators";
 
export default function makeOptions() {
  return {
    authHandler: async (auth: string, roomID: string, env: Env) {
      const response = await fetch(
          `${env.authServerURL}?auth=${auth}&roomID=${roomID}`);
      return (await response.json()) as AuthData;
    },
  };
}

See Environment Variables for more information on the env parameter.

If the user is allowed in the room, return an object matching {userID: string} from the authHandler. Otherwise, throw an error.

Implementing AuthHandler

The most common implementation is to call an endpoint on your server, like in the example above.

But you can also use JWTs or similar. In that case, the auth string would be a JWT which would be verified in the AuthHandler:

async function jwtAuthHandler(
  auth: string,
  roomID: string,
  env: Env,
): Promise<AuthData> {
  const decoded = await jwt.verify(auth, env.pubkey);
  return decoded as AuthData;
}

Fine-Grained Authorization

Returning {userID: string} from the AuthHandler allows a user into a room.

It is also possible to control whether each individual mutation is allowed. To do so, return additional data as part of the AuthData object:

type MyAuthData = AuthData & { access: "read" | "write" };
 
async function detailedAuthHandler(
  auth: string,
  roomID: string,
  env: Env,
): Promise<MyAuthData> {
  const response = await fetch(
    `${env.authServerURL}?auth=${auth}&roomID=${roomID}`,
  );
  return (await response.json()) as MyAuthData;
}

This data will be visible to mutators via the WriteTransaction.auth property. You can use it to decide whether to allow a mutation:

const mutators = {
  editNodeContent: async (tx: WriteTransaction,
      {nodeID, content}: {nodeID: string, content: string}) {
    const node = await getNode(tx, nodeID);
    const auth = tx.auth as MyAuthData;
    if (auth === undefined || // auth is always undefined on client
        auth.access === 'write') {
      // Allow the mutation
    } else {
      console.error("Access denied");
    }
  }
}

Instead of simply skipping an unauthorized mutation, you can also perform different logic, for example making some similar change which is allowed.

Because Reflect is server-authoritative, whatever the server does will be synced to all clients, overriding any optimistic results. It's not possible for a client to "hack" their way into making a mutation that the server doesn't allow.

Invalidation

Authentications may need to be invalidated, for example because the credentials associated with the JWT have changed, or the AuthData associated with the user needs to be updated.

Invalidation can be done via the REST API, which provides endpoints for invalidating connections by user or room.

When a connection is invalidated, the Reflect client will automatically attempt to reconnect, allowing the authHandler to validate and initialize a new session.