Coder Social home page Coder Social logo

passport-magic-login's Introduction

passport-magic-login

Passwordless authentication with magic links for Passport.js 🔑

  • User signup and login without passwords
  • Supports magic links sent via email, SMS or any other method you prefer
  • User interface agnostic: all you need is an input and a confirmation screen
  • Handles secure token generation, expiration and confirmation

Originally implemented by Tobias Lins for Splitbee and eventually extracted for Feedback Fish:

Screenshot 2021-01-09 at 16 55 23 Screenshot 2021-01-09 at 16 55 28 Screenshot 2021-01-09 at 16 56 24

Usage

To use magic link authentication, you have to:

  1. Setup the Passport strategy and Express routes on your server
  2. POST a request with the users email or phone number from the client once they have entered it into the login input

Installation

npm install passport-magic-login

Frontend usage

This is what the usage from the frontend looks like once you've set it all up. It only requires a single request:

// POST a request with the users email or phone number to the server
fetch(`/auth/magiclogin`, {
  method: `POST`,
  body: JSON.stringify({
    // `destination` is required.
    destination: email,
    // However, you can POST anything in your payload and it will show up in your verify() method
    name: name,
  }),
  headers: { 'Content-Type': 'application/json' }
})
  .then(res => res.json())
  .then(json => {
    if (json.success) {
      // The request successfully completed and the email to the user with the
      // magic login link was sent!
      // You can now prompt the user to click on the link in their email
      // We recommend you display json.code in the UI (!) so the user can verify
      // that they're clicking on the link for their _current_ login attempt
      document.body.innerText = json.code
    }
  })

Backend setup

To make this work so easily, you first need to setup passport-magic-login:

import MagicLoginStrategy from "passport-magic-login"

// IMPORTANT: ALL OPTIONS ARE REQUIRED!
const magicLogin = new MagicLoginStrategy({
  // Used to encrypt the authentication token. Needs to be long, unique and (duh) secret.
  secret: process.env.MAGIC_LINK_SECRET,

  // The authentication callback URL
  callbackUrl: "/auth/magiclogin/callback",

  // Called with th e generated magic link so you can send it to the user
  // "destination" is what you POST-ed from the client
  // "href" is your confirmUrl with the confirmation token,
  // for example "/auth/magiclogin/confirm?token=<longtoken>"
  sendMagicLink: async (destination, href) => {
    await sendEmail({
      to: destination,
      body: `Click this link to finish logging in: https://yourcompany.com${href}`
    })
  },

  // Once the user clicks on the magic link and verifies their login attempt,
  // you have to match their email to a user record in the database.
  // If it doesn't exist yet they are trying to sign up so you have to create a new one.
  // "payload" contains { "destination": "email" }
  // In standard passport fashion, call callback with the error as the first argument (if there was one)
  // and the user data as the second argument!
  verify: (payload, callback) => {
    // Get or create a user with the provided email from the database
    getOrCreateUserWithEmail(payload.destination)
      .then(user => {
        callback(null, user)
      })
      .catch(err => {
        callback(err)
      })
  }
  
  
  // Optional: options passed to the jwt.sign call (https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback)
  jwtOptions: {
    expiresIn: "2 days",
  }
})

// Add the passport-magic-login strategy to Passport
passport.use(magicLogin)

Once you've got that, you'll then need to add a couple of routes to your Express server:

// This is where we POST to from the frontend
app.post("/auth/magiclogin", magicLogin.send);

// The standard passport callback setup
app.get(magicLogin.callbackUrl, passport.authenticate("magiclogin"));

That's it, you're ready to authenticate! 🎉

License

Licensed under the MIT license. See LICENSE for more information!

passport-magic-login's People

Contributors

andyrichardson avatar enberg avatar eric-burel avatar jackca avatar maraisr avatar marcusvmsa avatar mxstbr avatar ramrami avatar rfbowen avatar tmkn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

passport-magic-login's Issues

export types

any chance of exporting the VerifyCallback and Options types? typescript isn't inferring the super class at all when used as part of a nestjs implementation...

import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import Strategy from "passport-magic-login";

@Injectable()
export class MagicLoginStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({...} as Options); // <-- would be nice to cast to the Option type here instead of digging through the type declarations
  }
}

TypeError: MagicLoginStrategy is not a constructor

Hello,

While literally copy pasting your code, I get a error message that the MagicLoginStrategy is not a constructor. Why is this happening?

const magicLogin = new MagicLoginStrategy({
      ....
})

// Add the passport-magic-login strategy to Passport
// passport.use(magicLogin)

Thanks!

Make the code part in send customizable / overrideable

The current 5 char generated code can be brute forced, if it were possible to make it a lot longer / overrideable it would be possible to use magicLogin in a way that doesn't require the clicking of the link on the same device.

Something like this:

send = (req: Request, res: Response,code?: string): void => {
    const payload = req.method === 'GET' ? req.query : req.body;
    if (
      req.method === 'POST' &&
      !req.headers['content-type']?.match('application/json')
    )
 {
      res
        .status(400)
        .send('Content-Type must be application/json when using POST method.');
      return;
    } if (!payload.destination) {
      res.status(400).send('Please specify the destination.');
      return;
    }

    if (!code){
      code = Math.floor(Math.random() * 90000) + 10000 + '';
    }

[Security Risk] Dangerous flow when JWT is incorrect or missing

Problem description

If JWT token in an authentication link is missing, is incorrect or its signature is incorrect then false is passed to the verify method. This flow seems not to be expected as even the library documentation on the passport page here https://www.passportjs.org/packages/passport-magic-login/ does not seem to handle this situation properly assuming payload.destination always exists.

Possible security implication

When false is passed then payload.destination is equal to undefined. If this is not handled and payload.destination is then passed to an ORM or ODM while looking for the user in a database this can lead to the situation in which undefined is passed directly to the where clause of the database query. In this case most ORMs like TypeORM will assume that the query should not have a where clause at all and will return first user from the database. This leads to login without access to user's email and bypassing whole application security.

Proof of concept

When application is using following verify method

verify: (payload, callback) => {
    const user = userRepository.findOne({
        where: { email: payload.destination },
    });
    
    if(user)
        callback(null, user)
    else 
        callback("user not found")
}

and the callback parameter is empty or contains malformed JWT token, the application will login user as a first user in the database.

Proposed mitigation

  1. Throw an error and break the application flow if JWT token provided with the link is incorrect or empty
  2. Mark in the documentation that payload parameter in the verify method should be checked if equal to false

Property 'callbackUrl' does not exist on type 'MagicLoginStrategy'.

The current readme reads;

"Once you've got that, you'll then need to add a couple of routes to your Express server:"

// This is where we POST to from the frontend
app.post("/auth/magiclogin", magicLogin.send);

// The standard passport callback setup
app.get(magicLogin.callbackUrl, passport.authenticate("magiclogin"));

However this is a broken example as we get the error;
Property 'callbackUrl' does not exist on type 'MagicLoginStrategy'.ts(2339)
image

callbackUrl exists on the options interface but this is private on the MagicLoginStrategy class:

import { Request, Response } from 'express';
import { SignOptions } from 'jsonwebtoken';
import { StrategyCreatedStatic } from 'passport';
declare type VerifyCallback = (payload: any, verifyCallback: (err?: Error | null, user?: Object, info?: any) => void, req: Request) => void;
interface Options {
    secret: string;
    callbackUrl: string;
    jwtOptions?: SignOptions;
    sendMagicLink: (destination: string, href: string, verificationCode: string, req: Request) => Promise<void>;
    verify: VerifyCallback;
    /** @deprecated */
    confirmUrl?: string;
}
declare class MagicLoginStrategy {
    private _options;
    name: string;
    constructor(_options: Options);
    authenticate(this: StrategyCreatedStatic & MagicLoginStrategy, req: Request): void;
    send: (req: Request, res: Response) => void;
    /** @deprecated */
    confirm: (req: Request, res: Response) => void;
}
export default MagicLoginStrategy;

So either:

  • The docs are wrong and there's a new approach to this

Or

  • the class needs a little update

I'm voting duff docs as a little poke in the file history shows me that options has always been private 🤷‍♂️

I managed to get this working by just not using it..

router.get('/magiclogin/callback', 
  passport.authenticate("magiclogin"),
  (req: Request, res: Response, next: NextFunction) => {
    res.status(200).json({ user: req.user });
    next();
  }
);

It'd probably also be worth mentioning serializeUser and deserializeUser functions in the docs, initially I thought I could exclude them by some magic, fortunately I've used passport before, newer users may come unstuck.

passport.serializeUser(function(user, cb) {
  process.nextTick(function() {
    cb(null, user);
  });
});

passport.deserializeUser<IUser>(async function(user, cb) {
  const dbUser = await User.findOne({ email: user.email }).exec()
  process.nextTick(function() {
    console.log('deserialized user', dbUser)
    return cb(null, dbUser);
  });
});

Send magic link without API call?

Thank you for your great work on this strategy!

Is there an existing way to send or generate a magic link using just JavaScript, without needing to do it via a POST/GET call (which the current send() method is specifically designed to do)?

It would be great to be able to send or generate magic links from within a custom API endpoint, for example:

  • magicLogin.send('[email protected]') to send an email notification to a user telling them that a document is available that included a magic link that takes them straight to it; or
  • magicLogin.getMagicLink('[email protected]') to generate a magic link URL to be used elsewhere

What happen, when i open the link on phone?

Hi @mxstbr
thank you very much for this passport strategy to support magic links. I would like to use it in my webapp. But what would happen if the user opens the link on phone instead of desktop? Would the user still be logged in on the desktop because he sent the request from there?

Does the frontend need to verify the email address for security purposes?

Hi!

Thanks for your great library! And sorry if this is a beginners questions and I am missing something...

But I was comparing this solution to Firebase's implementation of "email link authentication". By their documentation they require the frontend to store the entered email in localStorage so they can verify it against the authenticated user token for security purposes. If the user opens the email on another device they force the user to input their original email address. This is "To prevent a sign-in link from being used to sign in as an unintended user or on an unintended device" according to their documentation.

Is this a security concern that should be addressed when using your library (or is it already included perhaps and I'm missing something)?

Thanks again for a great passport library 🏆

error TS2351: This expression is not constructable.

I get the following error when trying to use it with typescript:

error TS2351: This expression is not constructable.
  Type 'typeof import("/Users/guiguiguigui93/Progetti/aibattle/node_modules/passport-magic-login/dist/index")' has no construct signatures.

my code

const magicLogin = new MagicLoginStrategy({
    secret: process.env.MAGIC_LINK_SECRET,
    callbackUrl: "/auth/magiclogin/callback",
    sendMagicLink: async (destination, href) => {
        console.log("sendMagicLink", destination, href);
    },
    verify: (payload, callback) => {
        console.log("verify", payload);
    },
    jwtOptions: {
        expiresIn: "2 days",
    }
})

Unable to user req as NextApiRequest

Hello i'm try to use passport-magic-login in a nextJS application.

in my call back i have this

export default async function handler(req: NextApiRequest, res: NextApiResponse) { passport.authenticate('magiclogin',async (err:any, payload:any) => { // ... doing auth stuff res.redirect('/test') })(req,res) }

But it throw an 500 error with next is not a function.

Can clicking the link be skipped when creating a new account?

When someone needs to log back in to an existing account, it makes sense to ask them to click the magic link to prove they own an email.

However, when someone is creating a new account for the first time using a new email, it would be nice to have the option to log them in directly without requiring that extra step of actually clicking the magic link. Of course, the email address wouldn't be verified, but I think in some cases it can be worth it just for lowering the friction of creating an account?

Is there a way this can be accomplished currently, or could it maybe be added as an option?

fastify support

Please change
res.json({ success: true, code: code });

to
res.send({ success: true, code: code });

to make it work on both express and fastify router

Pass more data at signup

First of all, thank you this package is awesome 😍

I would have one question though: Is it possible to pass more data at signup than just the email address (like a username, language, etc.) or is it an anti-pattern with this way of authenticated a user?

sendLink href has added undefined

The href being generated by the sendMagicLink function seems be adding 'undefined' before the question mark and token=.
Here is my strategy:

const magicLogin = new MagicLoginStrategy({
  secret: process.env.MAGIC_LINK_SECRET,
  callbackURL: '/auth/magiclogin/callback',
  sendMagicLink: async (destination, href) => {
    try {
      console.log(href)
      await sendEmail(destination.email, destination.givenName, href);
    } catch (error) {
      console.error(error);
    }
  },
  verify: async (payload, callback) => {
    try {
      const dbUser = await prisma.user.upsert({
        where: {
          email: payload.destination,
        },
        update: {
          updatedAt: new Date(Date.now()),
        },
        create: {
          data: {
            email: payload.destination,
            emailVerified: new Date(Date.now()),
          },
        },
        include: {
          employee: {
            select: {
              userId: true,
              isAdmin: true,
            },
          },
        },
      });
      return callback(null, dbUser);
    } catch (error) {
      return callback(error);
    }
  },
});
passport.use(magicLogin);

router.post('/sendlink', magicLogin.send);
router.get('/magiclogin/callback', passport.authenticate('magiclogin'));

The href that is passed to my sendMail function looks like this:

undefined?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXN0aW5hdGlvbiI6eyJlbWFpbCI6ImVyaWMuYnJhbnNvbkBnbWFpbC5jb20iLCJnaXZlbk5hbWUiOiJFcmljIn0sImNvZGUiOiI5OTE4MSIsImlhdCI6MTY3NjA0MzMwOCwiZXhwIjoxNjc2MDQ2OTA4fQ.9vAUmm1pAPIX-tJq8qE3HK-mRY-ZMH7qeJByIqO2RWo

I can remove the undefined but I don't know how it got there in the first place.

This is all in windows 10 in Node 18.12.1

Automatically route with express

Amazing project! A feature request :)

The current documentation reads "Once you've got that, you'll then need to add a couple of routes to your Express server:"

// Add the passport-magic-login strategy to Passport
passport.use(magicLogin)

// This is where we POST to from the frontend
app.post("/auth/magiclogin", magicLogin.send);

// This is what the user visits to confirm the login attempt and redirects them to the callbackUrl
app.get(magicLogin.confirmUrl, magicLogin.confirm);

// This is the standard Passport callbackUrl thing
app.get(magicLogin.callbackUrl, passport.authenticate("magiclogin"));

But we can instead make it a bit simpler to the user like this:

app.use(magicLogin(passport));

Then inside magicLogin, we register the passport.use() and the different routes. This way the user doesn't need to do all of this scaffolding by themselves and the only work needed is:

import MagicLoginStrategy from "passport-magic-login"

const magicLogin = new MagicLoginStrategy({
  // all the config
})

app.use(magicLogin(passport));

This is IMHO a much cleaner API.

No feature request is complete without the cons, so the main issue I see of this is the lack of transparency on what is going on internally. But this is probably okay since the configuration key names are self-explanatory + there's documentation pointing out at this.

token.ts expiry

Hello, is it possible to specify an expiration period for tokens? I'm seeing expiresIn: '60min' in your token.ts. I'd love to make that a bit longer (for a specific use case)

What's the best strategy to have sendMagicLink fail?

I'd like to use sendMagicLink only to pre-registered users.
At the moment, I'm throwing an error if the submitted email isn't in the database.

This prints something on my server console because of this line:

console.error(error);

Is that expected? I understand that sendMagicLink might fail for unexpected reasons, but I would have hoped that somehow there was a more elegant way so that no "intended" error is printed in the console.

Edge Runtime compatibility

Any timeframe for this becoming compatible with Edge runtime?
I am trying to use and I get the following Typescript error:

TS2345: Argument of type '(req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>) => void' is not assignable to parameter of type 'Nextable<RequestHandler<NextRequest, NextFetchEvent>> | RouteMatch'.   Type '(req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>) => void' is not assignable to type 'Nextable<RequestHandler<NextRequest, NextFetchEvent>>'.     Types of parameters 'req' and 'args_0' are incompatible.       Type 'NextRequest' is missing the following properties from type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>': get, header, accepts, acceptsCharsets, and 83 more.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.