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(),
  };
}
Duplicate username and email are handled for you Parse answers 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;
}
NeedUse
Require a signed-in callervalidation: {requireUser: true}
Require a rolerequireRoles: ['Admin']
Require every listed rolerequireAllRoles: true
Log outParse.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
Roles can contain roles 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

SymptomCause
Typed fields on req.user are unavailableNot cast — req.user! as User
A user can edit another userSaved with useMasterKey instead of the caller's session token
Password reset email never arrivesNo email adapter, or publicServerURL unset
A role grants nothingThe role row does not exist, or the ACL cannot read it
A revoked role still worksThe 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 203Username 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.