I am trying to achieve role based authentication for a particular admin view using ngRoute. There is an example of authenticating with routers in the AngularFire docs, but it does not take into account a user who has an admin role.
I am using Firebase to store admin by user id:
admin {
<some-uid>: true,
<another-uid>: true
}
From AngularFire docs:
// for ngRoute
app.run(["$rootScope", "$location", function($rootScope, $location) {
$rootScope.$on("$routeChangeError", function(event, next, previous, error) {
// We can catch the error thrown when the $requireSignIn promise is rejected
// and redirect the user back to the home page
if (error === "AUTH_REQUIRED") {
$location.path("/home");
}
});
}]);
app.config(["$routeProvider", function($routeProvider) {
$routeProvider.when("/home", {
// the rest is the same for ui-router and ngRoute...
controller: "HomeCtrl",
templateUrl: "views/home.html",
resolve: {
// controller will not be loaded until $waitForSignIn resolves
// Auth refers to our $firebaseAuth wrapper in the example above
"currentAuth": ["Auth", function(Auth) {
// $waitForSignIn returns a promise so the resolve waits for it to complete
return Auth.$waitForSignIn();
}]
}
}).when("/account", {
// the rest is the same for ui-router and ngRoute...
controller: "AccountCtrl",
templateUrl: "views/account.html",
resolve: {
// controller will not be loaded until $requireSignIn resolves
// Auth refers to our $firebaseAuth wrapper in the example above
"currentAuth": ["Auth", function(Auth) {
// $requireSignIn returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.$requireSignIn();
}]
}
});
}]);
app.controller("HomeCtrl", ["currentAuth", function(currentAuth) {
// currentAuth (provided by resolve) will contain the
// authenticated user or null if not signed in
}]);
app.controller("AccountCtrl", ["currentAuth", function(currentAuth) {
// currentAuth (provided by resolve) will contain the
// authenticated user or null if not signed in
}]);
Related
I am using SolidJS and building a SPA (no server rendering). For authentication, I use the #aws-amplify/core and #aws-amplify/auth packages. At the application root I call the Hub.listen function:
Hub.listen('auth', ({ payload }) => console.log(payload));
In the SignUp component I call Auth.federatedSignIn:
const SignUp = () => {
return (
<button onClick={() => {
Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google });
}}>
Sign up
</button>
);
}
I have configured the Amplify as such:
Amplify.configure({
Auth: {
region: import.meta.env.VITE_AWS_REGION,
userPoolId: import.meta.env.VITE_AWS_POOL_ID,
userPoolWebClientId: import.meta.env.VITE_AWS_POOL_CLIENT_ID,
oauth: {
domain: import.meta.env.VITE_AUTH_URL,
responseType: 'code',
redirectSignIn: location.origin + '/account/external',
redirectSignOut: location.origin + '/my',
},
},
});
When I click on the button I am redirected to the import.meta.env.VITE_AUTH_URL (simply outside of my app), choose an account, and then return back to the /account/external page. At that time I expect a consoled payload object in Web tools, but there is nothing. I get it when I call Auth.signOut(), so I assume that I configured Amplify correctly and Hub is subscribed to the auth channel.
My thoughts were that Hub cannot catch any events because after returning the application basically renders again in a new context and Hub simply isn't able to catch anything (events aren't sent from AWS?). I tried to declare the urlOpener function under the oauth property in the config and Google's sign page opened in a new tab, but even then I couldn't get any events in the preserved old page (from which I called Auth.federatedSignIn).
Questions:
How should I organize the code to get the signIn and signUp events?
Can I pass some data into the Auth.federatedSignIn to get it back in the Hub.listen, so I will be able to join the CognitoUser with the data that existed at the time of starting Sign in/Sign up (I want to add a new login type to existed user)?
Here is an example regarding the first question. Just check that your listener is set before you call the Auth.federatedSignIn() method.
export default class SignInService {
constructor(private landingFacade: LandingFacade) {
this.setupAuthListeners(); // Should be called at the top level.
}
private setupAuthListeners() {
Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
this.landingFacade.signInSuccess();
break;
case 'signIn_failure':
console.log('Sign in failure', data);
break;
case 'configured':
console.log('the Auth module is configured', data);
}
});
}
public async signIn(): Promise<void> {
await Auth.federatedSignIn();
}
}
For the second one: I'll use a local state and set/query the object you need.
My understanding is that I need to undertake the following steps:
Make the users' roles read-only
Use security rules on the data which access the roles to control access
Check for the role in the router
There are various examples on the official documentation how to deal with the security rules, but I couldn't figure out how to check for the role in the router. Let's assume I have an admin-only area, if someone who is not an admin tries to access that page I want that user to be redirected.
I'm currently following the official example using UI-Router, so this is my code:
app.config(["$stateProvider", function ($stateProvider) {
$stateProvider
.state("home", {
// the rest is the same for ui-router and ngRoute...
controller: "HomeCtrl",
templateUrl: "views/home.html",
resolve: {
// controller will not be loaded until $waitForSignIn resolves
// Auth refers to our $firebaseAuth wrapper in the factory below
"currentAuth": ["Auth", function(Auth) {
// $waitForSignIn returns a promise so the resolve waits for it to complete
return Auth.$waitForSignIn();
}]
}
})
.state("account", {
// the rest is the same for ui-router and ngRoute...
controller: "AccountCtrl",
templateUrl: "views/account.html",
resolve: {
// controller will not be loaded until $requireSignIn resolves
// Auth refers to our $firebaseAuth wrapper in the factory below
"currentAuth": ["Auth", function(Auth) {
// $requireSignIn returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.$requireSignIn();
}]
}
});
}]);
I'm guessing I'll have to check in the resolve for a user role, but how would I access the data from the database there?
Update:
I tried André's solution, but "waitForAuth" (console.log("test1") never triggers. "waitForSignIn" does though, but then nothing happens - there is no error message.
.state('superadmin-login', {
url: '/superadmin',
templateUrl: 'views/superadmin-login.html',
'waitForAuth': ['Auth', function (Auth) {
console.log('test1');
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.refAuth().$waitForSignIn();
}],
})
.state('superadmin', {
url: '/center-of-the-universe',
templateUrl: 'views/superadmin.html',
resolve: {
// YOUR RESOLVES GO HERE
// controller will not be loaded until $requireAuth resolves
// Auth refers to our $firebaseAuth wrapper in the example above
'currentAuth': ['Auth', function (Auth) {
console.log('test2');
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.refAuth().$requireSignIn();
}],
//Here i check if a user has admin rights, note that i pass currentAuth and waitForAuth to this function to make sure those are resolves before this function
hasAdminAccess: function (currentAuth, waitForAuth, Rights) {
console.log('test');
return Rights.hasAdminAccess(currentAuth);
}
}
})
Here's how i did it.
First i made a factory to check if the user has the correct rights:
angular.module('rights.services', [])
.factory('Rights', function ($q) {
var ref = firebase.database().ref();
return {
hasAdminAccess: function (user) {
var deferred = $q.defer();
ref.child("Rights").child("Admin").child(user.uid).once('value').then(function (snapshot) {
if (snapshot.val()) {
deferred.resolve(true);
}
else{
deferred.reject("NO_ADMIN_ACCESS");
}
});
return deferred.promise;
}
};
});
And secondly i use this factory inside the resolve:
.state('logged', {
url: '',
abstract: true,
templateUrl: helper.basepath('app.html'),
resolve: {
// YOUR RESOLVES GO HERE
// controller will not be loaded until $requireAuth resolves
// Auth refers to our $firebaseAuth wrapper in the example above
"currentAuth": ["Auth", function (Auth) {
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.refAuth().$requireSignIn();
}],
"waitForAuth": ["Auth", function (Auth) {
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.refAuth().$waitForSignIn();
}],
//Here i check if a user has admin rights, note that i pass currentAuth and waitForAuth to this function to make sure those are resolves before this function
hasAdminAccess: function (currentAuth, waitForAuth, Rights) {
return Rights.hasLightAccess(currentAuth);
}
})
})
Keep in mind the way you save user roles in firebase can be different from how i do it in this example. This is (part of) how it looks in firebase:
{"moderators":
{
"0123eeca-ee0e-4ff1-9d13-43b8914999a9" : true,
"3ce9a153-eea8-498f-afad-ea2a92d79950" : true,
"571fa880-102d-4372-be8d-328ed9e7c9de" : true
}
},
{"Admins":
{
"d3d4effe-318a-43e1-a7b6-d7faf3f360eb" : true
}
}
And the security rules for these nodes:
"Admins": {
"$uid": {
//No write rule so admins can only be added inside the firebase console
".read": "auth != null && auth.uid ==$uid"
}
},
"Moderators" : {
//Admins are able to see who the moderators are and add/delete them
".read" : "(auth != null) && (root.child('Admins').hasChild(auth.uid))",
".write" : "(auth != null) && (root.child('Admins').hasChild(auth.uid))",
"$uid": {
".read": "auth != null && auth.uid ==$uid"
}
}
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 have two states in my application -- /auth and /masters. the latter is the state where i wld like to direct the user only once he or she has been authenticated.
So, i understand that we can use '$urlRouterProvider.otherwise' to configure a default state in the application to /auth. So the foll is my code:
angular.module('ngClassifieds', ['ngMaterial', 'ui.router', 'firebase'])
.config(function($mdThemingProvider, $stateProvider, $urlRouterProvider) {
$mdThemingProvider
.theme('default')
.primaryPalette('blue-grey')
.accentPalette('orange');
$urlRouterProvider.otherwise('/auth');
$stateProvider
.state('auth', {
url: '/auth',
templateUrl: 'components/auth/auth.tpl.html',
controller: 'authCtrl'
})
$stateProvider
.state('masters', {
url: '/masters',
templateUrl: 'components/classifieds.tpl.html',
controller: 'classifiedsCtrl'
});
});
Now, if i enter, for example, anything other than /masters, i am directed to /auth; however, if i enter /masters, i am not directed to /auth.
i was made to understand that i need to look for AUTH_REQUIRED error in Firebase (https://www.firebase.com/docs/web/libraries/angular/guide/user-auth.html) in order to achieve the desired result. However, i feel i'm punching above my weight in trying to incorporate the functionality. So i'd appreciate if you can provide me some guidane. This is how i have tried to refactor the above code, but it's a mess:
angular.module('ngClassifieds', ['ngMaterial', 'ui.router', 'firebase'])
.run(["$rootScope", "$state", function($rootScope, $state) {
$rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) {
// We can catch the error thrown when the $requireAuth promise is rejected
// and redirect the user back to the home page
if (error === "AUTH_REQUIRED") {
$state.go("auth");
}
});
}]);
.config(function($mdThemingProvider, $stateProvider, $urlRouterProvider) {
$mdThemingProvider
.theme('default')
.primaryPalette('blue-grey')
.accentPalette('orange');
$urlRouterProvider.otherwise('/auth');
$stateProvider
.state('auth', {
url: '/auth',
templateUrl: 'components/auth/auth.tpl.html',
controller: 'authCtrl',
resolve: {
// controller will not be loaded until $waitForAuth resolves
// Auth refers to our $firebaseAuth wrapper in the example above
"currentAuth": ["Auth", function(Auth) {
// $waitForAuth returns a promise so the resolve waits for it to complete
return Auth.$waitForAuth();
}]
}
})
$stateProvider
.state('masters', {
url: '/masters',
templateUrl: 'components/classifieds.tpl.html',
controller: 'classifiedsCtrl',
resolve: {
// controller will not be loaded until $requireAuth resolves
// Auth refers to our $firebaseAuth wrapper in the example above
"currentAuth": ["Auth", function(Auth) {
// $requireAuth returns a promise so the resolve waits for it to complete
// If the promise is rejected, it will throw a $stateChangeError (see above)
return Auth.$requireAuth();
}]
}
});
});
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.