guys
on my exisiting api i already have user authhication using Bearer security. Using http header api_key and later tokens.
My problem seems to be i have diffefrent end point that are only need to be consumed based on roles.
For example to post a new user :
POST user should only be authenticated to user with admin role.
I have looked at the swagger spec here but nothing i could find on thier docuemation and google as well.
Please could give me some brain stroming idea ? below is my access verifaction code in nodejs and express.
swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) {
// Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain
app.use(middleware.swaggerMetadata());
app.use(middleware.swaggerSecurity({
Bearer: function(req,def,apiKey,next){
apiKey= apiKey.slice(7)
debug("token check",def,apiKey)
var ok=checkToken(apiKey)
if(ok) {
req.user=ok
debug('Token is ok')
return next()
}
debug("Invalid token",apiKey)
var err=Error("Invalid token")
err.statusCode=403
next(err)
}
}));
As of this writing, the solution is still homebrewing. Swagger does not, save through oAuth scopes or using a "hacky" api-key security definition (https://stackoverflow.com/a/40222161/3736937), have a built in RBAC mechanism.
Fortunately, we can create some pretty basic middleware to handle the problem because swagger does allow us to add x-swagger-* members to the swagger definition.
So here's what I did:
Add x-swagger-roles to each endpoint that requires RBAC (Role-based Access Control)
paths:
"/":
x-swagger-router-controller: getStatus
get:
operationId: getStatus
x-swagger-roles:
- admin
tags:
- "users"
summary: "Returns message: 'working'"
description: "default endpoint for testing"
responses:
$ref: "#/definitions/AnyResponse"
Place middleware before swagger-node-runner is registered with the application. In our case we're using expressjs, so the connect middleware is used.
var findOne = function (haystack, arr) {
return arr.some(function (v) {
return haystack.indexOf(v) >= 0;
});
};
app.use(function(req, res, next) {
var operation = runner.getOperation(req);
if(operation && operation.definition) {
var definition = operation.definition;
var requiredRoles = definition['x-swagger-roles'];
// if the endpoint has no required roles then go to the next route
if(!requiredRoles) return next();
// get the users roles
var userRoles = req.session.roles; // this may differ for you
// if any roles match then go to the next route
if(findOne(userRoles, requiredRoles)) return next();
// if no roles match then assert that this endpoint is forbidden
else return res.sendStatus(403);
}
next();
})
// it's important to register the middleware after the role check
runner.expressMiddleware().register(app);
Notes:
This code has not been tested in production, and should be reviewed by a security professional.
x-swagger-roles will not appear in your swagger-ui without altering it, which is beyond the scope of this answer.
Related
I am currently working on a authentication service for a node.js microservices application using typescript, WebStorm, passport and jwt. While trying to add the route to "/api/login", I am noticing that the intellisense does not seem to pick up the user object of req.user or the authorization object of req.header.authorization. For example, the following method is not working because it can not find the user object:
private generateToken(req: Request, res: Response, next: NextFunction){
req.token = jwt.sign({
id: req.user.id,
firstname: req.user.firstname,
lastname: req.user.lastname,
roles: req.user.roles
}, process.env.AUTH_KEY, {
expiresIn: "7d"
});
return next();
}
I am using the Request object from express:
import { NextFunction, Request, Response, Router } from "express";
Would I need to use a different Request object?
Also, if I need to force authentication to certain api routes but lock other routes down, how should this be done using passport-jwt? I know there is an express-unless package that I can use for express-jwt.
Not sure why this question downvoted, maybe because it should be two separate questions.
You can extend type declarations for Express.
Extending the express type definitions
Add a file library-ext.d.ts into your source directory with this.
declare module 'express' {
export interface Request {
user?: any;
}
}
For req.header.authorization try: req.headers['authorization']. Notice the 's'.
Relating to general authentication
It depends whether your registered users can also use the guest routes. If you never need identity on the guest routes then just register the passport authentication middleware only on the authenticated routes, or split the routes into separate routers. That's fairly simple to do, just search stack overflow or look in the docs for it.
The more complicated case is when you need both authenticated and non-authenticated users to access a route - think either a guest or authenticated customer adding something to a cart. Unfortunately passport-jwt rejects with a 401 when a token is not in the authorzation header, so the easiest way I found, rather than forking the project or rolling my own strategy, was to use middleware to add a known value to represent an otherwise anonymous request. Then just make sure that middleware is before the passport authentication in the affected routes. Here's a snippet to get you going:
CoreCtrl
class CoreCtrl {
simulateAnonymous(req, res, next) {
if (!req.headers.authorization) {
req.headers.authorization = 'Bearer guest-token';
}
return next();
}
}
Then somewhere in your Express Setup
setupRouters() {
// the public and admin routers are bound to the application
const coreCtrl = new CoreCtrl(this.serverOpts);
const anonymousCtrl = coreCtrl.simulateAnonymous.bind(coreCtrl);
this.routers.admin.use(anonymousCtrl);
this.routers.admin.use(passport.authenticate('UserBearer', { session: false }));
this.routers.public.use(anonymousCtrl);
this.routers.public.use(passport.authenticate('CustomerBearer', { session: false }));
}
Note that I had separate routers for public and admin set up here, that's not necessary but just to illustrate how to do it.
Then in the bearer strategy, you would have some code similar to this.
/**
* Run the strategy
*
* #param token {String} The JWT Token
* #param done {Callback} Callback function
*/
exec(token:string, done):Promise<any> {
// this is the workaround to support not passing a token for guest users.
if (token === 'guest-token') {
return done(null, {
userId: 'guest',
roles: ['guest']
});
}
// otherwise decode the token and find the user.
}
Finally, in some later Middleware you can check if the 'guest' role has access to the protected resource. I'd recommend acl module to manage role-based ACL list.
Property 'isAuthenticated' does not exist on type 'Request'.
Google lands here for that search, and the solution is to use Request/Response types in Express. My code was defaulting to the Fetch API's Request/Response definitions.
import { Express, NextFunction, Request, Response } from 'express'
The similar question was asked by someone else (here) but got no proper answer. Since this is basic and important for me (and maybe for someone else as well), I'm trying to ask here. I'm using Node.js+Express+EJS on the server side. I struggled to make the token authentication succeeded by using jsonwebtoken at the server and jQuery's ajax-jsonp at the web browser. Now after the token is granted and stored in the sessionStorage at the browser side, I can initiate another ajax request with the token included in the request header, to get the user's profile and display it somewhere in the 'current' page. But what I want is to display a new web page to show the user's profile instead of showing it in the 'current' page (the main/index page of the website). The question is:
How to initiate such an HTTP GET request, including the token in the HTTP header; and display the response as a new web page?
How the Node.js handle this? if I use res.render then where to put the js logic to verify the token and access the DB and generate the page contents?
Or, should we say the token mechanism is more suitable for API authentication than for normal web page authentication (where the web browser provides limited API)?
I think the answer to this question is important if we want to use the token mechanism as a general authentication since in the website scenario the contents are mostly organized as web pages at the server and the APIs at the client are provided by the browser.
By pure guess, there might be an alternative way, which the ajax success callback to create a new page from the current page with the response from the server, but I have no idea of how to realize that as well.
By calling bellow code successfully returned the HTML contents in customer_profile.ejs, but the client side ajax (obviously) rejected it.
exports.customer_profile = function (req, res) {
var token = req.headers.token;
var public_key = fs.readFileSync(path.resolve() + '/cert/public_key.pem');
var decoded = jwt.verify(token, public_key);
var sql = 'SELECT * FROM customer WHERE username = "' + decoded.sub + '"';
util.conn.query(sql, function (err, rows) {
if (!err) {
for (var i = 0; i < rows.length; i++) {
res.render('customer_profile', {customer_profile: rows[i]});
break;
}
}
});
};
I am trying to find a solution to this as well. Please note, I am using Firebase for some functionality, but I will try to document the logic as best as I can.
So far what I was able to figure out is the following:
Attach a custom header to the HTTP request client-side
// landing.js - main page script snippet
function loadPage(path) {
// Get current user's ID Token
firebase.auth().currentUser.getIdToken()
.then(token => {
// Make a fetch request to 'path'
return fetch(`${window.location.origin}/${document.documentElement.lang}/${path}`, {
method: 'GET',
headers: {'X-Firebase-ID-Token': token} // Adds unverified token to a custom header
});
})
.then(response => {
// As noted below, this part I haven't solved yet.
// TODO: Open response as new webpage instead of displaying as data in existing one
return response.text();
})
.then(text => {
console.log(text);
})
.catch(error => {
console.log(error);
});
}
Verify the token according to your logic by retrieving the corresponding header value server-side
// app.js - main Express application server-side file
// First of all, I set up middleware on my application (and all other setup).
// getLocale - language negotiation.
// getContext - auth token verification if it is available and appends it to Request object for convenience
app.use('/:lang([a-z]{2})?', middleware.getLocale, middleware.getContext, routes);
// Receives all requests on optional 2 character route, runs middleware then passes to router "routes"
// middleware/index.js - list of all custom middleware functions (only getContext shown for clarity)
getContext: function(req, res, next) {
const idToken = req.header('X-Firebase-ID-Token'); // Retrieves token from header
if(!idToken) {
return next(); // Passes to next middleware if no token, terminates further execution
}
admin.auth().verifyIdToken(idToken, true) // If token provided, verify authenticity (Firebase is kind enough to do it for you)
.then(token => {
req.decoded_token = token; // Append token to Request object for convenience in further middleware
return next(); // Pass on further
})
.catch(error => {
console.log('Request not authorized', 401, error)
return next(); // Log error to server console, pass to next middleware (not interested in failing the request here as app can still work without token)
});
}
Render and send back the data
// routes/index.js - main router for my application mounted on top of /:lang([a-z]{2})? - therefore routes are now relative to it
// here is the logic for displaying or not displaying the page to the user
router.get('/console', middleware.getTranslation('console'), (req, res) => {
if(req.decoded_token) { // if token was verified successfully and is appended to req
res.render('console', responseObject); // render the console.ejs with responseObject as the data source (assume for now that it contains desired DB data)
} else {
res.status(401).send('Not authorized'); // else send 401 to user
}
});
As you can see I was able to modularize the code and make it neat and clear bu use of custom middleware. It is right now a working API returning data from the server with the use of authentication and restricted access
What I have not solved yet:
As mentioned above, the solution uses fetch API and result of the request is data from server (html) and not a new page (i.e when following an anchor link). Meaning the only way with this code now is to use DOM manipulation and setting response as innerHTML to the page. MDN suggests that you can set 'Location' header which would display a new URL in the browser (the one you desire to indicate). This means that you practically achieved what both, you and I wanted, but I still can't wrap my head around how to show it the same way browser does when you follow a link if you know what I mean.
Anyways, please let me know what you think of this and whether or not you were able to solve it from the part that I haven't yet
We have implemented a SAML SSO ,we have used passport-saml for the same. The login works perfectly using "http-post" as authnRequestBinding.But I am unable to find any such method for logout . it appears logout defaults to http redirection and this does not work correctly.
First of all, you need to define the logoutUrl and logoutCallback in the config for the passport-saml. The logoutUrl is the url where your server will send the logoutRequest. So it is an URL got from the identity provider you are using. The logoutCallback is as the name says, the callbackUrl which will be called by the browser (through redirect) after logout is done. Here's how you add those configurations:
module.exports = {
development: {
app: {
name: 'Passport SAML strategy example',
port: process.env.PORT || 8443
},
passport: {
strategy: 'saml',
saml: {
logoutUrl: 'idp-logout-url-here',
logoutCallback: 'your-logout-callback-url-here'
}
}
}
};
Then you need to have the SamlStrategy somewhere in your code, where you will use the config defined above. Of course the config will have other variables as well, I just put the logout related variables there for now.
Finally, you need to have your own logout route defined in your node application, which will initiate the logout process when called:
app.get('/logout', function(req, res) {
if (req.user == null) {
return res.redirect('/');
}
return SamlStrategy.logout(req, function(err, uri) {
return res.redirect(uri);
});
});
As you can see from above, it will call the logout function defined in the SamlStrategy. So there is a logout function defined in the passport-saml strategy. As in the above, you need to give it a callback function, which will then redirect the response to the uri. That uri will be the logoutCallback url you defined earlier.
If you're wondering what is the SamlStrategy there, it is actually the strategy of the passport-saml. I can show you how to get it working. In a separate file, called 'saml-strategy.js' for example, put this:
const SamlStrategy = require('passport-saml').Strategy;
var config = require('./config.js')['development'];
module.exports = new SamlStrategy(
{
otherImportantvariable1: config.passport.saml.OtherImportantvariable1,
logoutUrl: config.passport.saml.logoutUrl,
logoutCallback: config.passport.saml.logoutCallback
}
function (profile, done) {
user = Object.assign({}, profile);
return done(null, user);
}
);
Insert all your important config variables same way as the logout related variables are defined above. Include the config created in the first step.
Then you can just require the SamlStrategy to the same file where you have your routes:
const SamlStrategy = require('../config/saml-strategy');
Please ask if anything is unclear!
Logout fix for ADFS is as follows,
1) Session index attribute should be added as part of passport-saml logout request. you can get that from passport profile object.
function (profile, done) {
console.log('Profile: %j', profile);
return done(null,
{
id: profile.nameID,
sessionIndex: profile.sessionIndex
});
If there is no sessionIndex returned from ADFS. Then, NameID rule in relying part trusts should be as follows,
Add NameID as "Claim rule name", choose "Active Directory" as Attribute store, choose "SAM-Account-Name" as LDAP Attribute and "Name ID" as "Outgoing claim type", finish the wizard and confirm the claim rules window. (Reference: spring saml ADFS)
2) Debug using the ADFS logs (Event viewer) and check whether your error is similar to the one below,
The SAML Single Logout request does not correspond to the logged-in
session participant. Requestor: app.yyy.com
Request name identifier:
Format: urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified,
NameQualifier: SPNameQualifier: , SPProvidedId: Logged-in session
participants: Count: 1, [Issuer: app.yyy.com, NameID: (Format: ,
NameQualifier: SPNameQualifier: , SPProvidedId: )]
This request failed.
If yes, It means that nameIDFormat should be left empty for logging out the session. Previously I was using nameIDFormat that was specified for authentication request and it did not work.
HTTP-POST binding does not make any difference without this config.
Hope this helps !
Say that I am building a simple web app with a REST back-end where users have their own page with user information.
What I would like to achieve is, if an unauthenticated user makes a request to
www.mywebapp.com/api/user/john
they would be provided with limited information (only age and email for example). But if the users logs in, and makes the same request, the server will also respond with more information (like personal preferences and such).
I am thinking that maybe the middleware that validates the users token, passes on permission on the request (req.role = guest or req.role = user). Then in the user/:name endpoint it would check the role and respond with different content.
The other option would be to make a new route endpoint for authenticated users, and then check which one to call on the client side.
What is best practice here?
This is how I ended up doing:
let router = express.Router();
router.get('/profile', authenticate, hasRole("Admin", true), controller.showProfileAdmin);
router.get('/profile', hasRole("User", true), controller.showProfileUser);
// will not call next('route') if Guest requirements is not met
router.get('/profile', hasRole("Guest"), controller.showProfile);
// hasRole() returns a middleware function that checks if user meets role requirement.
// If nextRoute is true, the function calls next('route'). If nextRoute is false
// or undefined, the function responds with a 403
function hasRole(roleRequired, nextRoute) {
return (req, res, next) => {
// Just checking so that the user has authority for this role.
if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) {
return next();
//else client is not authorized
} else {
// If nextRoute is true, continue to the next route.
if(nextRoute){
return next('route');
//Else respond with a forbidden status.
} else {
res.sendStatus(403);
}
}
}
}
I'm making an API that has different levels of access, the 'client' may only read. But 'admin' must have write access. The different roles are check every time as a policy in Sails.js and sets the rights in the req.session.
I just need to give the 'client' no access to create, update and delete actions, therefore i created an controller that has those CRUD actions and checks if the user has the right role. All of the actions that has restricted access are redirected trough routes.js to this controller.
Now my problem is that when i'm deleting an entry like: Category.destroy(req.param('id')); Gives me undefined and has no done method. Unlike mentioned by the docs I managed to fix the problem by creating this:
var deleted = Category.destroy(req.param('id'), function(err, status) {
if (status == 1){
res.json({message: 'Category is deleted'});
} else {
res.json({message: 'Oops, something went wrong'});
}
});
But there has to be another way to apply authentication to those basic actions. Because now i've to write all actions.
Is there something wrong with the code for the delete function that i wrote? And is it possible to apply policies and redirect to default model actions, as if there was no authentication at all?
You can define policies at the Models or Controllers level. Here's an example from the /config/policies.js.
module.exports.policies = {
// Default policy (allow public access)
'*': true,
'events': 'eventsPolicy', // Policy for a Model
someController: { // Policy for a Controller
// Apply the "authenticated" policy to all actions
'*': 'authenticated',
// For someAction, apply 'somePolicy' instead
someAction: 'somePolicy'
}
};
Under the api/policies is where you can define the access level.
module.exports = function (req, res, next) {
if (req.session.user) {
var action = req.param('action');
if (action == "create") {
req.body.userId = req.session.user.id;
req.body.username = req.session.user.username;
}
next();
} else {
res.send("You're not authenticated.", 403);
}
};
Hope this helps.
Just modified all the policies en renamed the controllers, as stated in de CLI: 'sails generate model example' gives an notification about the controller being named as singular. So I didn't need to redirect all model actions to the plural controller (examples). Now all the basic CRUD actions are working as it should.
The sails.js video tutorial helped me a lot: http://www.youtube.com/watch?feature=player_embedded&v=GK-tFvpIR7c
My guess (not a Sails user myself) would be you either pass a callback, or you'll get an object back which has a done() method:
Category.destroy(id, function(...) {...}); // method 1
Category.destroy(id).done(function(...) {...}); // method 2