-
Notifications
You must be signed in to change notification settings - Fork 5
Auth
- Passport
- JWT (httpCookies)
- Username/password - local strategy
- Signup
- resend verification email
- verify email
- login
- refresh access token
- revoke access token (admin only)
- logout
- reset password
- Discord
- Github
- User logins with one of the above methods
- Server verifies and sets access and refresh tokens (http cookies)
- we should avoid returning statuses which indicate whether the account exists in our database
- we have set up a cron job to automatically clean up verification tokens every 2 weeks
- emails are sent by MaliJet
Note
edge case 1: user trying to sign up again with an unactivated email (from previous signup)
send another email (replace old token), also update the password
Note
edge case 2: user trying to sign up with an existing email (activated)
do not return error message, but send an email saying someone trying to create an account with that email, if they forget the password please use the reset password feature
Note
edge case 3: user never receive emails for activation / reset password (could be email server issue or any technical issues)
- Allow admins to manually activate the account if user contact us on discord
- Allow admins to set a temporary password, and user should be able to change their password when they are logged in (not forget password link)
A jwt token signed with a random string from the built-incrypto library + userId will be emailed to the user, the token will have an expiry of 1 hour, and will be single use
Token Format:
{
token: 'MFR_uJHRnHdK6sqq1IoXE6vwVFtxo2mFUzSYfI4Vc9nv6JKT6T5mKk-QLsM9kvKAheOaF1ZoToGJy8lT0rnaaA',
userId: '7e1a8d44-fabe-4196-87ee-2a7ba4b85a29',
signOptions: { expiresIn: '1h' },
iat: 1702046224,
exp: 1702651024
}
NestJS Recipes Passport
Net Ninja OAuth (Passport.js)
NestJS Authentication: JWTs, Sessions, logins, and more! | NestJS PassportJS Tutorial
Guide to Verification Emails – Best Designs and Examples
Refresh Tokens Explained
when user login using an OAuth provider, we check if the user already has an account, if not create it with the email registered with the provider, set a password they would be able to reset to login with that email/password later on, otherwise they won't since they won't know the first password. If the email is in our database, we "merge" the accounts, i.e add a new oauth profile to an existing user account.
provider/login
- start here, either create a new user or just return the user
provider/redirect
- redirects here, user details attached to req
by passport in the validate
method of the strategy, we use that to generate access and refresh token just like using username/password, everything will just work like credential login from here
- oauth signup flow is still to be determined by the design team
- we need to setup something to "merge account", there will be people signing up with different oauth providers and the own email, they won't be flagged as exist as they are different emails.
-
Add Prisma models/entities to
src/ability/prisma-generated-types.ts
-
Permissions (abilities) are defined in
src/ability/ability.factory/ability.factory.ts
where it uses roles in req -
Exception Filter
to catch allForbiddenError
generated by CASL, no need try/catch for this errorexample:
ForbiddenError.from(ability) .setMessage("Forbidden - Insufficient privilege (CASL)") .throwUnlessCan(Action.Create, "Voyage");
-
Guard -
abilities.guard.ts
Use roles in req.user (created by the access token strategy at.strategy.ts validate function), compare route permission requirement set by the decorator with the defined CASL abitlies in ability.factory.ts -
Decorator
@CheckAbilities
( file:abilities.decorator.ts )
Sets metadata for the guard to use, in the following form RequiredRole[] (array)export interface RequiredRule { action: Action; subject: PrismaSubjects; }
-
If there’s no
@CheckAbilities
, the route is accessible by all logged in users -
This is a rule with condition
can([Action.Manage], "VoyageTeam", { id: { in: user.voyageTeams.map((vt) => vt.teamId) }, });
For conditions, we’ll have to pass an actual object like VoyageTeam object from "@prisma/client" instead of ‘VoyageTeam` (string) if we want the condition to work. Note that if we are not using a basic select, we’ll need to make sure the condition exist, e.g.
canReadAndSubmitForms(req.user, { ...form, formTypeId: form.formType.id, });
If “VoyageTeam” (string) is passed as the rule will work the same as
can([Action.Manage]. "voyageTeam"
without the condition. Which means this works for both cases with and without condition, depends on what is passed intothrowUnlessCan
or similar functions.
This also mean if we need to use a condition like check if the data is users own team data, we’ll have to do it in theservices
, as the guard only does check without conditions
example:const mockUser = { voyageTeams: [{ teamId: 2, memberId: 1 }], userId: "userId", email: "email", roles: ["voyager"], }; const ability = abilityFactory.defineAbility(mockUser); ForbiddenError.from(ability).throwUnlessCan(Action.Read, { ...voyageTeam, __caslSubjectType__: "VoyageTeam", });
In this example, voyageTeam is of type VoyageTeam, append caslSubjectType: "VoyageTeam", to the object since that’s how CASL knows the Subject type based on the CASL Factory definition we defined in ability.factory.ts
return build({ detectSubjectType: (object) => object.__caslSubjectType__, });
ZEN prisma, CASL setup example How to Manage User Access in NestJS | Authorization with CASL - old but still worth watching, refer to above for latest example
- we should make sure the jwt token is short lived
- implement extra token validation for endpoints with strong security requirements (e.g. payment)