Guide
Users & authentication
Signup, login, sessions, roles and password reset — the part you would otherwise spend a fortnight building.
What you get without writing it
Parse ships a _User class, hashed passwords, session tokens with
revocation, email verification, password reset, and a _Role system that the
ACL layer already understands. None of it needs building — it needs configuring.
Extending the user
Add your own fields by declaring a model against the built-in class.
import {
ParseClass, ParseField, CloudFunction, catchError, invalidateRoles,
getUserRoles, getUsersRoles, BeforePasswordResetRequest, roleKey,
BeforeLogin, AfterLogin,
} from 'parse-server-kit';
@ParseClass('_User')
export default class User extends Parse.User {
@ParseField({ type: 'String' })
declare displayName: string;
@ParseField({ type: 'String', enum: ['en', 'ar'] })
declare locale: string;
@ParseField({ type: 'Pointer', targetClass: 'IMG' })
declare avatar: any;
}
The class name is _User, with the underscore — that is Parse's own class,
not a new one. Extend Parse.User rather than BaseModel so you
keep setUsername, setPassword and the rest.
Signing up
@CloudFunction({
methods: ['POST'],
validation: { fields: {
username: { required: true },
password: { required: true },
email: { required: true },
} },
})
static async signUp(req: Parse.Cloud.FunctionRequest) {
const user = new Parse.User();
user.setUsername(req.params.username);
user.setPassword(req.params.password);
user.setEmail(req.params.email);
user.set('displayName', req.params.displayName);
const [err, created] = await catchError(user.signUp());
if (err) throw err;
// signUp logs them in - hand the token back to the client.
return {
objectId: created!.id,
sessionToken: created!.getSessionToken(),
};
}
202 for a taken username and 203 for a taken
email, before anything is written. You do not need to check first — catch the code and
translate it for your client.
Logging in
const [err, user] = await catchError(
Parse.User.logIn(req.params.username, req.params.password)
);
if (err) throw err; // 101 for wrong username or password
return { sessionToken: user!.getSessionToken(), objectId: user!.id };
Parse deliberately returns the same error for an unknown username and a wrong password, so the endpoint cannot be used to discover which accounts exist. Keep that property in whatever message you show.
Sessions
A session token identifies the caller on every later request. Clients send it as a header:
X-Parse-Application-Id: my-api
X-Parse-Session-Token: r:8f2c…
Inside a cloud function it arrives as req.user:
@CloudFunction({ methods: ['POST'], validation: { requireUser: true } })
static async updateProfile(req: Parse.Cloud.FunctionRequest) {
const user = req.user! as User; // cast for typed fields
user.displayName = req.params.displayName;
// Save AS that user, so the ACL applies to the write.
const [err] = await catchError(
user.save(null, { sessionToken: user.getSessionToken() })
);
if (err) throw err;
return user;
}
| Need | Use |
|---|---|
| Require a signed-in caller | validation: {requireUser: true} |
| Require a role | requireRoles: ['Admin'] |
| Require every listed role | requireAllRoles: true |
| Log out | Parse.User.logOut(), which revokes the token |
Roles
A role is a named set of users, and it is what an ACL and a CLP refer to.
// Create once, at setup or in a seed.
const acl = new Parse.ACL();
acl.setPublicReadAccess(true); // so ACLs can resolve it
const role = new Parse.Role('Editor', acl);
await role.save(null, { useMasterKey: true });
// Add and remove members
role.getUsers().add(user);
await role.save(null, { useMasterKey: true });
role.getUsers().remove(user);
await role.save(null, { useMasterKey: true });
invalidateRoles(user.id); // if the role cache is on
// Reading membership
const roles = await getUserRoles(user); // ['Editor', 'Member']
const many = await getUsersRoles(users); // Map
role.getRoles().add(otherRole) makes every member of the inner role a
member of the outer one. That is how "Admin implies Editor" is expressed without listing
people twice — and it is how per-tenant roles scale.
Password reset and email
await Parse.User.requestPasswordReset(email);
Parse sends the mail, hosts the reset page and expires the link. It needs an email adapter and a publicServerURL configured on the server; without them the call succeeds and no message arrives.
@BeforePasswordResetRequest()
static async onReset(req: any) {
// Audit it, rate-limit it, or refuse for a suspended account.
if (req.object.get('suspended')) {
throw new Parse.Error(101, 'Account unavailable');
}
}
Protecting user data
_User rows are readable by default in many setups. Two things to decide
deliberately:
@ParseClass('_User', {
clp: {
find: { requiresAuthentication: true },
get: { requiresAuthentication: true },
count: {}, // master key only
create: '*', // anyone may sign up
update: { requiresAuthentication: true },
delete: { [roleKey('Admin')]: true },
},
protectedFields: {
'*': ['email', 'phone'], // hidden from other users
[roleKey('Admin')]: [],
},
})
create: '*' has to stay open if you want public signup — that is the one
operation anonymous callers need. Everything else can be tightened.
Reacting to auth events
@BeforeLogin()
static async onBeforeLogin(req: any) {
if (req.object.get('suspended')) {
throw new Parse.Error(101, 'Account suspended');
}
}
@AfterLogin()
static async onAfterLogin(req: any) {
req.object.set('lastLoginAt', new Date());
await req.object.save(null, { useMasterKey: true });
}
@BeforeLogin is the right place to block an account — it runs after the password is verified but before a session exists.
Common mistakes
| Symptom | Cause |
|---|---|
Typed fields on req.user are unavailable | Not cast — req.user! as User |
| A user can edit another user | Saved with useMasterKey instead of the caller's session token |
| Password reset email never arrives | No email adapter, or publicServerURL unset |
| A role grants nothing | The role row does not exist, or the ACL cannot read it |
| A revoked role still works | The role cache is on — call invalidateRoles(userId) when you revoke |
| Everyone can list all users | _User CLP left at its default |
| Signup fails with 202 or 203 | Username or email already taken — expected, translate it |
For OAuth providers, anonymous users, session security options and email adapter setup, the Parse users guide is the reference.