Skip to content
Cheryl M edited this page Aug 21, 2024 · 9 revisions

Authentication

Technology Used

Strategies

  • Username/password - local strategy
    • Signup
    • resend verification email
    • verify email
    • login
    • refresh access token
    • revoke access token (admin only)
    • logout
    • reset password
  • Discord
  • Github
  • LinkedIn
  1. User logins with one of the above methods
  2. Server verifies and sets access and refresh tokens (http cookies)

Notes on username/password signup and reset process

  • 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)

  1. Allow admins to manually activate the account if user contact us on discord
  2. 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)

Sign up and reset password flow

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
}

auth-1

Refresh token

auth-2

References

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

OAuth

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

todo/uncertainties

  • 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.

Authorization

Technologies Used

Things to Note

  • 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 all ForbiddenError generated by CASL, no need try/catch for this error

    example:

    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 into throwUnlessCan 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 the services , 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__,
    });

References

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

Further Security Considerations

  • we should make sure the jwt token is short lived
  • implement extra token validation for endpoints with strong security requirements (e.g. payment)

auth-3