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.
const r = new Reflect({
auth: "some-auth-token",
// ...
});
Next, provide an authHandler
in the server entrypoint that validates the auth token:
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.