NodeJS REST authentication using Passport and OAut

2019-03-17 16:37发布

I'm working on REST api using NodeJS. For authentication I decided to use Passport. I want truly RESTful api. So it means I have to use tokens instead of sessions.

I want to let users login using username and password, or using social networks like Facebook, Google and Twitter.

I make my own OAuth2.0 server for issuing Access and Refresh tokens using oauth2orize module. So now I can register new user and then issue them tokens. I followed this tutorial:

http://aleksandrov.ws/2013/09/12/restful-api-with-nodejs-plus-mongodb/

Verifying user for route:

// api ------------------------------------------------------------------------------------
    app.get('/api/userInfo',
        passport.authenticate('bearer', { session: false }),
        function(req, res) {
            // req.authInfo is set using the `info` argument supplied by
            // `BearerStrategy`.  It is typically used to indicate scope of the token,
            // and used in access control checks.  For illustrative purposes, this
            // example simply returns the scope in the response.
            res.json({ user_id: req.user.userId, name: req.user.username, scope: req.authInfo.scope })
        }
    );

All this works quite well. Unfortunately I don't know how to implement social authentication.

I was reading this tutorial:

http://scotch.io/tutorials/javascript/easy-node-authentication-facebook 

But in this tutorial they are not making a truly RESTful api. I already implemented user schema according this tutorial where tokens for local user are stored in separated models.

// define the schema for our user model
var userSchema = mongoose.Schema({
    local: {
        username: {
            type: String,
            unique: true,
            required: true
        },
        hashedPassword: {
            type: String,
            required: true
        },
        created: {
            type: Date,
            default: Date.now
        }
    },
    facebook: {
        id: String,
        token: String,
        email: String,
        name: String
    },
    twitter: {
        id: String,
        token: String,
        displayName: String,
        username: String
    },
    google: {
        id: String,
        token: String,
        email: String,
        name: String
    }
});

But now, how can I verify user?

passport.authenticate('bearer', { session: false }),

this is verifying only bearer token against to my db, but how can I verify social tokens? Am I missing something?

4条回答
爱情/是我丢掉的垃圾
2楼-- · 2019-03-17 16:54

if your using a bearer token all you have to do is pass the unique Identifier to the user of the API. This would most likely be done in one singular call maybe in the form of a login. Then every call to the API require that token which verifies the token exists in the database and updates its Time To Live value. You do not need individual tokens in your schema for each login you need an individual schema for tokens which has a Time To Live Value (TTL)

查看更多
beautiful°
3楼-- · 2019-03-17 17:00

Iv'e created the following Schema's:

var userSchema = mongoose.Schema({
    local: {
        username: String,
        password: String
    },
    facebook: {
        id: String,
        token: String,
        email: String,
        name: String
    },
    google: {
        id: String,
        token: String,
        email: String,
        name: String
    },
    token: {
        type: Schema.Types.ObjectId,
        ref: 'Token',
        default: null
    }
});

var tokenSchema = mongoose.Schema({
    value: String,
    user: {
        type: Schema.Types.ObjectId,
        ref: 'User'
    },
    expireAt: {
        type: Date,
        expires: 60,
        default: Date.now
    }
});

When logging in to my web app, I use a PassportJS Social plugin like facebook/google. Example:

//auth.js(router)
router.get('/facebook', passport.authenticate('facebook', {scope: ['email']}));

router.get('/facebook/callback', 
  passport.authenticate('facebook', { successRedirect: '/profile',
                                      failureRedirect: '/' }));

Now when I want to navigate my web application I use the session authentication that was made available to me via the Facebook plugin. When a user wants to request an API token they need to be logged in so I can Associate that token with the user.

So now a user has a token associated with them that they can use for Token authentication for my API.

My API routes dont care or look at sessions, all they care about is the token. Iv'e done this by creating an express router, and using passport's bearer strategy as a middleware for all routes heading towards my API.

//api.js (router)
//router middleware to use TOKEN authentication on every API request
router.use(passport.authenticate('bearer', { session: false }));

router.get('/testAPI', function(req, res){
        res.json({ SecretData: 'abc123' });
    });

So now I use only token authentication for my API's(Doesn't ever look at session data), and Session authentication to easily navigate my webapp. An example of me using sessions to navigate my app is shown below:

//secure.js(router) - Access private but NON-API routes.
//router middleware, uses session authentication for every request
router.use(function(req, res, next){
        if(req.isAuthenticated()){
            return next();
        }
        res.redirect('/auth');
    });
//example router
router.get('/profile', function(req, res){
        res.send("Private Profile data");
    });

Hopefully this will help you!

查看更多
4楼-- · 2019-03-17 17:03

I am using Facebook login for my own RESTful API for my Notepads app here. I started the application as one that will be used as a web page but still the communication after the login will be through the API.

Then I decided to create a mobile version of the same app that will use the API. I decided to make it that way: The mobile app logs in through Facebook and sends the facebook user id and FB access token to the API, the API calls Facebooks's API to verify these params and if successful registers a new user(or logs in an existing one) in my app's DB, creates a custom token for this user and returns it to the mobile app. From here the mobile app sends this custom token to authenticate the app with the API.

Here's some code:

The auth in the API (uses the fbgraph npm module):

var graph = require('fbgraph'),
Promise = require('bluebird')
...
Promise.promisify(graph.get);
...
var postAuthHandler = function (req, res) {
    var fbUserId = req.body.fbId,
    fbAccessToken = req.body.fbAccessToken,
    accessToken = req.body.accessToken;
    ...
    graph.setAppSecret(config.facebook.app.secret);
    graph.setAccessToken(fbAccessToken);

    var graphUser;
    var p = graph.getAsync('me?fields=id,name,picture')
        .then(function (fbGraphUser) {
            //when the given fb id and token mismatch:
            if (!fbGraphUser || fbGraphUser.id !== fbUserId) {
                console.error("Invalid user from fbAccessToken!");
                res.status(HttpStatus.FORBIDDEN).json({});
                return p.cancel();
            }

            graphUser = fbGraphUser;

            return User.fb(fbUserId);
        })
        .then(function (user) {
            if (user) {
                //user found by his FB access token
                res.status(HttpStatus.OK).json({accessToken: user.accessToken});
                //stop the promises chain here
                return p.cancel();
            }
            ...create the user, generate a custom token and return it as above...

https://github.com/iliyan-trifonov/notepads-nodejs-angularjs-mongodb-bootstrap/blob/6617a5cb418ba8acd6351ef9a9f69228f1047154/src/routes/users.js#L46 .

The User model:

var userSchema = new mongoose.Schema({
    facebookId: { type: String, required: true, unique: true },
    accessToken: { type: String, required: true, unique: true },
    name: { type: String, required: true },
    photo: { type: String, required: true },
    categories: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Category' }],
    notepads: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Notepad' }]
});

https://github.com/iliyan-trifonov/notepads-nodejs-angularjs-mongodb-bootstrap/blob/master/src/models/user.js#L9 .

The Facebook auth in the mobile app:

               auth: function(fbId, fbAccessToken) {
                return $http({
                    url: apiBase + '/users/auth',
                    data: {
                        fbId: fbId,
                        fbAccessToken: fbAccessToken
                    },
                    method: 'POST',
                    cache: false
                });
            },
            ...

https://github.com/iliyan-trifonov/notepads-ionic/blob/master/www/js/services.js#L33 .

The mobile app sends the token with the request:

  notepads: {
            list: function() {
                return $http({
                    url: apiBase + '/notepads?insidecats=1' + '&token=' + User.get().accessToken/*gets the token from the local storage*/,
                    method: 'GET',
                    cache: false
                });
            },

It's an Ionic/Angular/Cordova app. The Facebook login from the mobile app starts the Facebook app installed on your phone or opens a popup to login in Facebook. Then a callback returns the Facebook user's id and access token to my mobile app.

The fbgraph npm module: https://github.com/criso/fbgraph

查看更多
小情绪 Triste *
5楼-- · 2019-03-17 17:08

The social media strategies for passport depend on the session. They will not function without one.

I'm running int o the same issue I wanted my REST server to be stateless.

In my opinion there are 2 options.

  1. Add sessions to your rest server
  2. Add a new auth server which is responsible for creating and handing out token.

PS: You should tag this as passport so that pass port devs might see it.

查看更多
登录 后发表回答