i'm trying to redirect a user to 403 page when not authorized. I added roles 'admin', 'default-group' to CM9Cwq7HXD6yHjKRp and it's working like a charm on template level. But not working as expected on router.
My route groups are separated into 2 main group
// Public routes
var publicFlowRouter;
publicFlowRouter = FlowRouter.group({});
// Private routes
var privateFlowRouter;
privateFlowRouter = FlowRouter.group({
triggersEnter: [
function() {
var route;
if (!(Meteor.loggingIn() || Meteor.userId())) {
route = FlowRouter.current();
if (route.route.name !== 'home') {
Session.set('redirectAfterLogin', route.path);
}
return FlowRouter.go('home');
}
}
]
});
There isn't any problem for these routes but the problem starts with adminPrivateFlowRouter;
// Private routes extended for admin
var adminPrivateFlowRouter;
adminPrivateFlowRouter = privateFlowRouter.group({
triggersEnter: [
function() {
// If user is not authenticated redirect to homepage
console.log(Meteor.userId());
console.log(Roles.userIsInRole(Meteor.userId(), 'admin', 'default-group'));
if (Roles.userIsInRole(Meteor.userId(), 'admin', 'default-group')) {
console.log('Authenticated user');
} else {
console.log('403 Access Denied');
//return FlowRouter.go('home');
}
}
]
});
is not working solid. When i refresh the samepage console says sometimes
CM9Cwq7HXD6yHjKRp
false
403 Access Denied
CM9Cwq7HXD6yHjKRp
true
Authenticated user
I couldn't find where the problem is, thanks
See How to make FlowRouter wait for users collection on the client
You can solve your problem manually initializing FlowRouter with FlowRouter.wait() and FlowRouter.initialize() when Roles subscription is ready.
Related
I am trying to have protected routes like these: 1). loggedin user group routes 2.) admin routes, 3.) student routes, 4.) public routes. The LoggedInUser works as expected but the 2 other routes - schooladmin and students does not work as needed.
After logging in as an admin or as a student, according to the expectation the respective users should be able to go to the allowed urls but whenever, as an example, if a schooladmin admin goes to http://localhost/students it automatically redirects back to dashboard, and likewise for student. What am I to do right?
This route group allows only logged in users.
var LoggedInUser = FlowRouter.group({
name: 'currentUser', triggersEnter: [function (context, redirect) {
if (Meteor.loggingIn() || Meteor.userId()) {
FlowRouter.watchPathChange();
let currentRoute = FlowRouter.current();
if (!currentRoute.path) {
FlowRouter.go('/dashboard');
} else {
FlowRouter.go(currentRoute.path);
}
} else {
redirect('/');
}
}]
});
This is the route group for school admins
var schooladmin = LoggedInUser.group({
name: 'schooladmins', triggersEnter: [function (context, redirect) {
FlowRouter.watchPathChange();
let currentRoute = FlowRouter.current();
if (Roles.userIsInRole(Meteor.userId(), ['super-admin', 'admin'])) {
console.log(currentRoute.path);
FlowRouter.go(currentRoute.path);
} else {
redirect('dashboard');
}
}]
});
This is the route for students
var students = LoggedInUser.group({
name: 'students', triggersEnter:[function (context, redirect) {
FlowRouter.watchPathChange();
let currentRoute = FlowRouter.current();
if (Roles.userIsInRole(Meteor.userId(), ['manage-team', 'student-page'])) {
FlowRouter.go(currentRoute.path);
} else {
redirect('dashboard');
}
}]
});
Sample routes the groups are attached to
This sample route is for school admins only to access
schooladmin.route('/students', {
name: 'students', action(){
BlazeLayout.render('formrender', {formrend: 'student'});
}
});
this route is for student to access
students.route('/student/dashboard', {
name: 'students-dashboard', action(){
BlazeLayout.render('studentlayout', {studentrender: 'studentdashboard'});
}
});
The Roles package actually depends on a subscription, which means that if the subscription is not ready the Roles.userIsInRole method will always return false. And then your route fails, because FlowRouter always runs no matter what. This happens in very specific cases, but it happens and your users will notice.
Fortunately there is now a fix. We can have full control over when FlowRouter initializes.
There are 2 ways to achieve this.
Just above FlowRouter Declaration use Accounts.onLogin(function(user){}); method to check role and then redirect. (check user._id for Roles)
Click here for second solution https://medium.com/#satyavh/using-flow-router-for-authentication-ba7bb2644f42
I'm using accounts-ui and accounts-google in Meteor v1.4.1. I can't get the user.services object to appear scoped in the client code. In particular, I need google's profile picture.
I've configured the server-side code to authenticate with Google like so:
import { Meteor } from 'meteor/meteor';
import { ServiceConfiguration } from 'meteor/service-configuration';
const services = Meteor.settings.private.oauth;
for (let service of Object.keys(services)) {
ServiceConfiguration.configurations.upsert({
service
}, {
$set: {
clientId: services[service].app_id,
secret: services[service].secret,
loginStyle: "popup"
}
});
}
...and the client side code to configure permissions like so:
Accounts.ui.config({
requestPermissions: {
google: ['email', 'profile']
},
forceApprovalPrompt: {
google: true
},
passwordSignupFields: 'EMAIL_ONLY'
});
When users click the 'Sign-In with Google' button, a pop-up appears and they can authenticate. No prompt appears, however, despite forceApprovalPrompt being set to true for google.
The big issue is that when I execute this,
const user = Meteor.user();
console.log(user.services);
anywhere in client code, I do not see the expected user services information. I check my database and it is definitely there for the taking:
$ mongo localhost:27017
> db.users.find({})
> ... "services" : { "google" : { "accessToken" : ... } } ...
I'm curious what I'm missing? Should I explicitly define a publish function in order for user services data to exist in the client?
The services property is intentionally hidden on the client side for security reasons. There are a couple of approaches here :
Suggestions
My preferred one would be to expose a meteor method to bring you the
public keys and avatars you might need in the few places you'd need
them.
On a successful login, you could record the data you need somewhere in the user object, but outside of the services property.
As you said, you could make a new publication which explicitly specifies which fields to retrieve and which ones to hide. You have to be careful what you publish, though.
Code Examples
Meteor methods:
// server
Meteor.methods({
getProfilePicture() {
const services = Meteor.user().services;
// replace with actual profile picture property
return services.google && services.google.profilePicture;
}
});
// client
Meteor.call('getProfilePicture', (err, profilePicture) => {
console.log('profile picture url', profilePicture);
});
Update on successful user creation (you might want to have a login hook as well to reflect any avatar/picture changes in google):
// Configure what happens with profile data on user creation
Accounts.onCreateUser((options, user) => {
if (!('profile' in options)) { options.profile = {}; }
if (!('providers' in options.profile)) { options.profile.providers = {}; }
// Define additional specific profile options here
if (user.services.google) {
options.profile.providers.google = {
picture: user.services.google.picture
}
}
user.profile = options.profile;
return user;
});
Publish only select data...
// Server
Meteor.publish('userData', function () {
if (this.userId) {
return Meteor.users.find({ _id: this.userId }, {
fields: { other: 1, things: 1 }
});
} else {
this.ready();
}
});
// Client
Meteor.subscribe('userData');
I created a collection for adminuser's enter the system.I mean I dont want to use account packet for admin side but I dont know How to make Route setting after admin to be login.I made something but it doesnt work correct,
login.html
Template.login.events({
'click #entre': function (e, template) {
var Username = template.$('#username').val();
var Password = template.$('#password').val();
var getinfo= admin.findOne({});
if (Username == " " || Password == "") {
swal("Error", "All fields must be Completed.", "error");
} else if (getinfo.username== Username && getinfo.password== Password) {
swal("Success", "Welcome admin.", "success");
Session.set("hi", true);
} else {
swal("error", "Login Informations wrong.", "error");
}
}
});
router.js
Router.onBeforeAction(function () {
if (!Session.get("hi")) {
this.render('login');
} else {
this.render('dashboard');
}
});
Router.route('userList', function () {
this.render('userList');
});
Router.route('addnewuser', function () {
this.render('addnewuser');
});
Note:I want to make that when admin to be login,it can reach to all pages userlist,addnewuser etc.
If I understood it properly you want to give admin rights to certain routes of yours. There are several ways to achieve this using iron:router, either by building a Controller or using Filters. I would create an Iron Router Controller to tackle that and attach it to any route that needs that kind of checks. Both Controllers and Filters are actually reusable bits of code which is what we are looking for.
Make sure you add alanning:roles to your packages list (if you haven't already) and add some admin roles to at least one of your Meteor.users() like it's shown here
Building the actual Controllers is easy.
lib/router.js
AdminController = RouteController.extend({
onBeforeAction: function () {
var loggedInUser = Meteor.userId();
if (!!loggedInUser) {
if (!Roles.userIsInRole(loggedInUser, 'admin')) {
// Basic redirect to the homepage
Router.go('homepage');
this.stop();
}
} else {
// Log them in when they are not
Router.go('login');
this.stop();
}
this.next();
}
});
Router.route('/admin', {
name: 'admin',
controller: AdminController,
waitOn: function () {
// return subscriptions here
}
});
Continue to add that to any other routes you like.
I've been searching about how to implement authentication/authorization in SPA's with AngularJS and ASP.NET Web API and I have one doubt. First we can implement authentication and authorization on server side with ASP.NET Identity. Then we create an Angular service to use this to authenticate a user and after that requests to Web API actions that use the Authorize attribute will be allowed.
There's still one problem. The logged in user will probably won't be allowed to access some pages of the app. Although using the app itself it won't be allowed, the HTML for the SPA is still available. If the user goes to http://website.com/app/views/notAllowedPage.html it will render in the browser. It's really not useful I know, but still I think to be a security failure, since the user shouldn't be allowed to get this HTML from the server.
Is there a way to secure this HTML or it is simply not possible?
We discussed the same problem in our developers group. Our conclusion was not to see this as a security thread.
What you want to protect is the data that is displayed, not the static "layout" of a HTML page. As long as the WebAPI services that deliver the data are secured and only allow authorized users to retrieve the data, we are safe.
Would that suit your needs as well?
We currently use this same setup.
Since we are using Angular, we don't do much with MVC itself or the Razor engine. The only things we are really doing with Razor is rendering a layout and the basic page (usually Index()).
So my recommendation is to do the same--instead of having a page website.com/app/views/notAllowedPage.html, have the user navigate to website.com/app/NotAllowed/, and secure the NotAllowedController with an Authorize attribute.
Create a service for securing your Angular htmlpage
As per the these services a guest user can't access the secure pages
Angular service
angular.module('application').factory('authorizationService', function ($resource, $q, $rootScope, $location,dataFactory) {
return {
permissionCheck: function (roleCollection) {
var deferred = $q.defer();
var parentPointer = this;
var user = dataFactory.getUsername();
var permission = dataFactory.getUserRole()
var isPermissionLoaded=dataFactory.getIsPermissionLoaded();
if (isPermissionLoaded) {
this.getPermission(permission, roleCollection, deferred);
}
else {
$location.path("/login");
}
return deferred.promise;
},
getPermission: function (permission, roleCollection, deferred) {
var ifPermissionPassed = false;
angular.forEach(roleCollection, function (i,role) {
switch (role) {
case roles.ROLE_USER:
angular.forEach(permission, function (perms) {
if (perms=="ROLE_USER") {
ifPermissionPassed = true;
}
});
break;
case roles.ROLE_ADMIN:
angular.forEach(permission, function (perms) {
if (perms=="ROLE_ADMIN") {
ifPermissionPassed = true;
}
});
break;
default:
ifPermissionPassed = false;
}
});
if (ifPermissionPassed==false) {
$location.path("/login");
$rootScope.$on('$locationChangeSuccess', function (next, current) {
deferred.resolve();
});
} else {
deferred.resolve();
}
},
isUserAuthorised: function () {
var isPermissionPassed = false;
var permission = dataFactory.getUserRole()
var isPermissionLoaded=dataFactory.getIsPermissionLoaded();
if (isPermissionLoaded) {
angular.forEach(permission, function (perms) {
if (perms=="ROLE_USER") {
ifPermissionPassed = true;
}
});
}
return isPermissionPassed;
}
};
});
and this code for your app.js where u mention your url and their controller
var application = angular.module('application', ['ngRoute']);
var roles = {
ROLE_USER: 0, //these are the roles which I want to secure from guest user
ROLE_ADMIN: 1
};
var routeForUnauthorizedAccess = '/login'; //if any unauthories user access the page the user will redirect on the login page
$routeProvider.when('/', {
templateUrl: 'view/home.html',
controller: 'homepage'
}).when('/login', {
templateUrl: 'view/loginpage.html',
controller: 'login'
}).when('/signup', {
templateUrl: 'view/signup.html',
controller: 'signup'
}).when('/dashboard', {
templateUrl: 'view/dashboard.html',
controller: 'dashboard',
resolve: {
permission: function(authorizationService, $route) {
return authorizationService.permissionCheck([roles.ROLE_USER, roles.ROLE_ADMIN]);
},
}
// the last dashboard page is access by only admin and the user who logged IN
Having created a profile page for my app, I would like to display a list of social services that the user is on. It struck me that the easiest way would be to use Meteor's built in accounts system for this.
Is there a good way to add external services to an existing account?
Also, will the user then be able to log in with either (e.g.) Facebook and his password from my app?
Another question that naturally follows: Is there a good way to add an application specific password to an account that was created with an external service?
Here's an alternate method. In this solution, I'm overriding a core function and adding some custom behavior. My goal is to associate the service data with the currently logged in user, then allow the core function to do its thing like normal.
orig_updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
Accounts.updateOrCreateUserFromExternalService = function(serviceName, serviceData, options) {
var loggedInUser = Meteor.user();
if(loggedInUser && typeof(loggedInUser.services[serviceName]) === "undefined") {
var setAttr = {};
setAttr["services." + serviceName] = serviceData;
Meteor.users.update(loggedInUser._id, {$set: setAttr});
}
return orig_updateOrCreateUserFromExternalService.apply(this, arguments);
}
Pros:
Avoids creation of unnecessary accounts
Code is short and easy to understand
Code is easy to remove if this functionality is added to Meteor core
Cons:
Requires the user to be logged in. If a user logs in with twitter initially, logs out, and then logs in with facebook, then two seperate accounts will be created.
Users who share a computer may get their accounts merged unintentionally.
Relies on knowledge of how updateOrCreateUserFromExternalService works. This isn't terrible - because it's part of Meteor's public api it probably won't change drastically (not often anyway). But it's still risky.
Here is how I add credentials to existing user account: .../meteor-how-to-login-with-github-account.html
Yes, a user account can be associated with multiple services and have a password-based login at the same time. In the Meteor docs, you can see the structure of such a user account:
{
_id: "bbca5d6a-2156-41c4-89da-0329e8c99a4f", // Meteor.userId()
username: "cool_kid_13", // unique name
emails: [
// each email address can only belong to one user.
{ address: "cool#example.com", verified: true },
{ address: "another#different.com", verified: false }
],
createdAt: 1349761684042,
profile: {
// The profile is writable by the user by default.
name: "Joe Schmoe"
},
services: {
facebook: {
id: "709050", // facebook id
accessToken: "AAACCgdX7G2...AbV9AZDZD"
},
resume: {
loginTokens: [
{ token: "97e8c205-c7e4-47c9-9bea-8e2ccc0694cd",
when: 1349761684048 }
]
}
}
}
For adding a username/password login to an existing account, you can use Accounts.sendResetPasswordEmail on the server side. This also ensures the change happens authenticated and authorized.
Of course you can also just update the user record on the server side with a new password yourself, but this might create a security hole in your app. I would also advise against implementing your own crypto protocol for this if possible, as it is hard.
If you want to add other services than email, you could for example
call a server method that saves a random, long token in the current user's MongoDB document and returns it to the client.
re-login the user with another service using Accounts.loginWith[OtherService]. This logs the user out and in again, using a new account on the other service.
call a second server method with the returned token from the first method as parameter. This second method searches for the user account with the given token and merges its data into the current (new) account.
Check out the example and answer in this posting. It pretty much gives you the code to integrate multiple external and internal accounts. With minor tweaks, you can add the password fields for each account as you desire.
How to use Meteor.loginWithGoogle with mrt:accounts-ui-bootstrap-dropdown
Code:
isProdEnv = function () {
if (process.env.ROOT_URL == "http://localhost:3000") {
return false;
} else {
return true;
}
}
Accounts.loginServiceConfiguration.remove({
service: 'google'
});
Accounts.loginServiceConfiguration.remove({
service: 'facebook'
});
Accounts.loginServiceConfiguration.remove({
service: 'twitter'
});
Accounts.loginServiceConfiguration.remove({
service: 'github'
});
if (isProdEnv()) {
Accounts.loginServiceConfiguration.insert({
service: 'github',
clientId: '00000',
secret: '00000'
});
Accounts.loginServiceConfiguration.insert({
service: 'twitter',
consumerKey: '00000',
secret: '00000'
});
Accounts.loginServiceConfiguration.insert({
service: 'google',
appId: '00000',
secret: '00000'
});
Accounts.loginServiceConfiguration.insert({
service: 'facebook',
appId: '00000',
secret: '00000'
});
} else {
// dev environment
Accounts.loginServiceConfiguration.insert({
service: 'github',
clientId: '11111',
secret: '11111'
});
Accounts.loginServiceConfiguration.insert({
service: 'twitter',
consumerKey: '11111',
secret: '11111'
});
Accounts.loginServiceConfiguration.insert({
service: 'google',
clientId: '11111',
secret: '11111'
});
Accounts.loginServiceConfiguration.insert({
service: 'facebook',
appId: '11111',
secret: '11111'
});
}
Accounts.onCreateUser(function (options, user) {
if (user.services) {
if (options.profile) {
user.profile = options.profile
}
var service = _.keys(user.services)[0];
var email = user.services[service].email;
if (!email) {
if (user.emails) {
email = user.emails.address;
}
}
if (!email) {
email = options.email;
}
if (!email) {
// if email is not set, there is no way to link it with other accounts
return user;
}
// see if any existing user has this email address, otherwise create new
var existingUser = Meteor.users.findOne({'emails.address': email});
if (!existingUser) {
// check for email also in other services
var existingGitHubUser = Meteor.users.findOne({'services.github.email': email});
var existingGoogleUser = Meteor.users.findOne({'services.google.email': email});
var existingTwitterUser = Meteor.users.findOne({'services.twitter.email': email});
var existingFacebookUser = Meteor.users.findOne({'services.facebook.email': email});
var doesntExist = !existingGitHubUser && !existingGoogleUser && !existingTwitterUser && !existingFacebookUser;
if (doesntExist) {
// return the user as it came, because there he doesn't exist in the DB yet
return user;
} else {
existingUser = existingGitHubUser || existingGoogleUser || existingTwitterUser || existingFacebookUser;
if (existingUser) {
if (user.emails) {
// user is signing in by email, we need to set it to the existing user
existingUser.emails = user.emails;
}
}
}
}
// precaution, these will exist from accounts-password if used
if (!existingUser.services) {
existingUser.services = { resume: { loginTokens: [] }};
}
// copy accross new service info
existingUser.services[service] = user.services[service];
existingUser.services.resume.loginTokens.push(
user.services.resume.loginTokens[0]
);
// even worse hackery
Meteor.users.remove({_id: existingUser._id}); // remove existing record
return existingUser; // record is re-inserted
}
});