mulesoft / osprey

Generate Node.JS API middleware from a RAML definition
Other
431 stars 65 forks source link

Using passort-jwt auth with osprey? #102

Open daldridge-cs opened 8 years ago

daldridge-cs commented 8 years ago

Prior to using osprey, I could use the passport-jwt plugin to require and validate a JSON Web Token for secure routes:

  // Initialize passport and register a JWT auth strategy
  app.use(passport.initialize());
  passport.use(new passportJwt.Strategy({
    jwtFromRequest: passportJwt.ExtractJwt.fromAuthHeader(),
    secretOrKey: config.secret,
  }, (jwtPayload, done) => {
    User.findById(jwtPayload.id, (err, user) => {
      if (err) {
        return done(err, false);
      } else if (user) {
        done(null, user);
      } else {
        done(null, false);
      }
    });
  }));

  // Configure that all routes under /api/secure require authentication
  app.all('/api/secure/*', passport.authenticate('jwt', { session: false }));

Now that I'm using osprey to perform request validation, I would like to instead declare the security in RAML ... :

#%RAML 0.8
title: osprey-test
version: 1.0.0
baseUri: /
securitySchemes:
  - jwt:
      type: x-jwt
      describedBy:
        headers:
          Authorization:
            description: Valid JSON Web Token
            example: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
        responses:
          401:
          403:

/api/secure:
  securedBy: [jwt]
  /someSecureChildRoute:
    get: ...

... and use osprey.security(...) to enforce it, but it's unclear to me how to covert my old code:

  // Don't do this anymore...
  // app.all('/api/secure/*', passport.authenticate('jwt', { session: false }));

  ramlParser.loadFile(ramlPath)
    .then((raml) => {
      app.use(osprey.server(raml));
      app.use(osprey.security(raml, {
        jwt: // ???? what goes here ???
      }));
      app.use(osprey.errorHandler());
      app.listen(config.port, () => {
        // ...
      });
    })
    .catch((err) => {
      // ...
    });

Anybody have an idea on how to do this?

ghost commented 8 years ago

Hi! I'm using RAML and Osprey for one university project of mine. We decided to implement JWT-based authentication so I've been experimenting a bit during the last few days and I've faced the same problem. This is an example project I've produced, I hope it's useful for anyone wanting to use JWTs within Osprey.

This is ./assets/api.raml containing some simple API and custom security schema:

#%RAML 0.8
title: StudyRooms
version: 0.0.1
baseUri: &domain localhost
protocols:
  - HTTP
mediaType: application/json
securitySchemes:
  - JWT: 
      description: Jason Web Schemas
      type: x-jwt
      describedBy:
        headers:
          Authorization:
            required: true
            type: string
        responses:
          401:
              description: |
                  Bad or expired token. This can happen if the user or Dropbox
                  revoked or expired an access token. To fix, you should re-
                  authenticate the user.
      settings:
        x-jwt-header:
          x-jwt-typ: JWT
          x-jwt-alg: HS512
        x-jwt-payload:
          x-jwt-iss: *domain
          x-jwt-aud: *domain

/index:
  get:
    responses:
      200:
        body:
          application/json:
            schema: | #!include schemas/public.json
              {
                "type" : "array",
                "items" : {
                  "type" : "object",
                  "properties": {
                      "public": {
                          "type": "string",
                          "required": true
                        }
                    }
                }
              }

/users:
  get:
    securedBy:
      - JWT
    displayName: Show all users
    responses:
      200:
        body:
          text/plain:
            example: Auth
  post:
    displayName: Create new user
    body:
      application/json:
        schema: |
          {
            "type": "object",
            "properties": {
              "username": {
                  "type": "string"
                },
              "password": {
                  "type": "string"
                }
            },
            "required": ["username", "password"]
          }
    responses:
      204:
        description: User successfully registered
      409:
        description: Username already in use
      400:
        description: Request is not well-formed
  /{username}:
    uriParameters:
      username:
        displayName: User Name
        type: string
    /auth:
      post:
        displayName: Authenticate username
        body:
          application/json:
            schema: |
              {
                "type": "object",
                "properties": {
                  "username": {
                      "type": "string"
                    },
                  "password": {
                      "type": "string"
                    }
                },
                "required": ["username", "password"]
              }
        responses:
          200:
            body:
              application/json:
                schema: |
                  {
                    "type": "object",
                    "properties": {
                        "token": {
                            "required": true,
                            "type": "string"
                          }
                      }
                  }
          401:
            description: Wrong Password
          409:
            description: Error
          400:
            description: Request is not well-formed

And this is ./app.js:

var express = require('express');
var parser = require('raml-parser');
var osprey = require('osprey');
var path = require('path');
var jsonwebtoken = require('jsonwebtoken');
var passport = require('passport');
var jwt = require('passport-jwt');
var JwtStrategy = jwt.Strategy;
var ExtractJwt = jwt.ExtractJwt;
var usersStorage = require('./storage/users');

var secret = 'sarebbe ottimale se questo segreto fosse un pelo più random';

var app = express();

var apiPath = path.join(__dirname, 'assets', 'api.raml');

var signOpts = {
    expiresIn: 15 * 60
};

function jwtSecurity(scheme, name) {

    var authOpts = {
        jwtFromRequest: ExtractJwt.fromAuthHeader(), // TODO whatch at scheme for extractor
        secretOrKey: secret,
        issuer: scheme.settings['x-jwt-payload']['x-jwt-iss'],
        audience: scheme.settings['x-jwt-payload']['x-jwt-aud'],
        algorithms: [
            scheme.settings['x-jwt-header']['x-jwt-alg']
        ]
    };

    signOpts.audience = authOpts.audience;
    signOpts.issuer = authOpts.issuer;
    signOpts.algorithm = authOpts.algorithms[0];

    passport.use(
        new JwtStrategy(authOpts,
            function verify(payload, done) {
                usersStorage.getUser(payload.sub, function (err, user) {
                    if (user) {
                        done(false, user);
                    } else {
                        done(false, false);
                    }
                });
            }
        )
    );

    return {
        handler: function (options, path) {
            return passport.authenticate('jwt', {session: false});
        }
    }
}

app.use(passport.initialize());

parser.loadFile(apiPath).then(function (raml) {
    app.use(osprey.server(raml));

    app.use(osprey.security(raml, {
        JWT: jwtSecurity
    }));

    var router = osprey.Router();

    router.get('/index', function (res, res, next) {
        res.json({
            public: 'Welcome! This is an example!'
        });
    });

    router.get('/users', function (req, res, next) {
        usersStorage.getAll(function (err, us) {
            res.json(us);
        });
    });

    router.post('/users', function (req, res, next) {
        usersStorage.addUser(req.body.username, req.body.password, function (err, user) {
            if (err) {
                res.statusCode = 409;
                res.end();
            } else {
                res.statusCode = 204;
                res.end();
            }
        });
    });

    router.post('/users/{username}/auth', function (req, res, next) {
        var password = req.body.password;
        var username = req.params.username;
        username = username.substr(1, username.length - 2);
        if (password) {
            usersStorage.getUser(username, function (err, u) {
                if (err) {
                    res.statusCode = 409;
                    res.end();
                } else {
                    if (u.password === password) {
                        var token = jsonwebtoken.sign({sub: username}, secret, signOpts);
                        res.json({token: token});
                    } else {
                        res.statusCode = 401;
                        res.end();
                    }
                }
            });
        } else {
            res.statusCode = 400;
            res.end();
        }
    });

    app.use(router);

    app.use(osprey.errorHandler());

    app.use(function (err, req, res, next) {
        res.statusCode = err.statusCode;
        res.json({
            message: err.message,
            status: err.statusCode
        });
    });

    app.listen(3000)
});

As you can see passport-jwt is gracefully integrated. You can also specify some parameters within RAML specification. Sadly this implementation only allow tokens to be stored into the Authorization header: but it's quite simple to extend this behaviour.

I really hope this could be useful to someone. I know it's poorly commented, if something is unclear just ask!

sallespro commented 8 years ago

ciao @gciatto , tks for the kick start with jwt. missing require('./storage/users').

ElridgeDMello commented 7 years ago

Unable to successfully setup osprey.security with a custom security scheme (also using passport). I read what @gciatto posted here and the README.md in this repo, but have not been able to create a custom security handler. Please see my sample app posted on issue #117