Currently I'm trying to cache a user's information. I give you guys a scenario of what is happening.
a user is trying to log in with an account A. Account A's name appears on the navbar. After that user log out and tries to log in with an account B, on the navbar itself the name is still belongs to account A's name.
The code
service.js
.factory('Auth', function($http, $q, AuthToken) {
// create auth factory object
var authFactory = {};
// log a user in
authFactory.login = function(username, password) {
// return the promise object and its data
return $http.post('/api/login', {
username: username,
password: password
})
.success(function(data) {
AuthToken.setToken(data.token);
return data;
});
};
// log a user out by clearing the token
authFactory.logout = function() {
// clear the token
AuthToken.setToken();
};
// check if a user is logged in
// checks if there is a local token
authFactory.isLoggedIn = function() {
if (AuthToken.getToken())
return true;
else
return false;
};
// get the logged in user
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me', {cache: true});
else
return $q.reject({ message: 'User has no token.' });
};
// return auth factory object
return authFactory;
})
// ===================================================
// factory for handling tokens
// inject $window to store token client-side
// ===================================================
.factory('AuthToken', function($window) {
var authTokenFactory = {};
// get the token out of local storage
authTokenFactory.getToken = function() {
return $window.localStorage.getItem('token');
};
// function to set token or clear token
// if a token is passed, set the token
// if there is no token, clear it from local storage
authTokenFactory.setToken = function(token) {
if (token)
$window.localStorage.setItem('token', token);
else
$window.localStorage.removeItem('token');
};
return authTokenFactory;
})
// ===================================================
// application configuration to integrate token into requests
// ===================================================
.factory('AuthInterceptor', function($q, $location, AuthToken) {
var interceptorFactory = {};
// this will happen on all HTTP requests
interceptorFactory.request = function(config) {
// grab the token
var token = AuthToken.getToken();
// if the token exists, add it to the header as x-access-token
if (token)
config.headers['x-access-token'] = token;
return config;
};
// happens on response errors
interceptorFactory.responseError = function(response) {
// if our server returns a 403 forbidden response
if (response.status == 403)
$location.path('/login');
// return the errors from the server as a promise
return $q.reject(response);
};
return interceptorFactory;
});
controller.js
angular.module('mainCtrl', [])
.controller('MainController', function($rootScope, $location, Auth) {
var vm = this;
// get info if a person is logged in
vm.loggedIn = Auth.isLoggedIn();
// check to see if a user is logged in on every request
$rootScope.$on('$routeChangeStart', function() {
vm.loggedIn = Auth.isLoggedIn();
// get user information on page load
Auth.getUser()
.then(function(data) {
vm.user = data.data;
});
});
// function to handle login form
vm.doLogin = function() {
vm.processing = true;
// clear the error
vm.error = '';
Auth.login(vm.loginData.username, vm.loginData.password)
.success(function(data) {
vm.processing = false;
// get user information on page load
Auth.getUser()
.then(function(data) {
vm.user = data.data;
});
// if a user successfully logs in, redirect to users page
if (data.success)
$location.path('/');
else
vm.error = data.message;
});
};
// function to handle logging out
vm.doLogout = function() {
Auth.logout();
$location.path('/logout');
};
});
index.html
<ul class="nav navbar-nav navbar-right">
<li ng-if="!main.loggedIn">Login</li>
<li ng-if="main.loggedIn">Hello {{ main.user.username }}</li>
<li ng-if="main.loggedIn">Logout</li>
<li><button class="btn btn-primary">Write</button></li>
</ul>
So basically my assumption of the problem lies in the service.js where i added cache: true. Do i need to add some logic to it?
There are two different caches that may be involved in your app. First it's the angular cache which you have set below {cache: true}
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me', {cache: true});
else
return $q.reject({ message: 'User has no token.' });
};
This cache is only there for the duration of the app, once you leave or reload the page, it's gone!
The other cache which is the browser cache is a little more complex to deal with. Note this has no relationship with the Angular cache, so if this is your problem simply turning off {cache: false} wont help. To prevent cache you will need to send a list of different caching headers in your restful API and it may not always work.
The easiest way to prevent cache is to add a version to your url which doesnt actually affect your results but tricks your browser into thinking that it's a different url. This is referred to as Cache Busting.
The easiest way to cache bust is to add a Math.Random() to append to the url. The chances of Math.Random to be the same is probably in the billions.
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me?rand=' + Math.random(), {cache: true});
else
return $q.reject({ message: 'User has no token.' });
};
However, if you want a better way to do it specific for your app, you could append the username to your url. This way it will cache for the same users which means you are actually taking advantage of the caching mechanism and not getting tied down by it!
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me?user=' + <username>, {cache: true});
else
return $q.reject({ message: 'User has no token.' });
};
Related
I am using the Autho JWT for authenticating users for a frontend angular app. After succesful authentication it returns a token. The token returned contains payload data that contains username, user_id and email. An example of token is:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Im11dGhpb3JhIiwidXNlcl9pZCI6MiwiZXhwIjoxNDc1NTAxNDIxLCJlbWFpbCI6Im11dGhpb3JhQGdtYWlsLmNvbSJ9._jtLZ6FTlKZ_uDzqDgedIJ_4LC2LpiaVqgqfjT_4k_A
Since the token is base64 encoded i would love to know how to get the username from the token.
Here is a solution to get the user if the token is validated
/*global angular*/
angular.module('authService', [])
// ===================================================
// auth factory to login and get information
// inject $http for communicating with the API
// inject $q to return promise objects
// inject AuthToken to manage tokens
// ===================================================
.factory('Auth', function($http, $q, AuthToken) {
// create auth factory object
var authFactory = {};
// log a user in
authFactory.login = function(username, password) {
// return the promise object and its data
return $http.post('/api/authenticate', {
username: username,
password: password
})
.success(function(data) {
AuthToken.setToken(data.token);
return data;
});
};
// log a user out by clearing the token
authFactory.logout = function() {
// clear the token
AuthToken.setToken();
};
// check if a user is logged in
// checks if there is a local token
authFactory.isLoggedIn = function() {
if (AuthToken.getToken())
return true;
else
return false;
};
// get the logged in user
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me', { cache: true });
else
return $q.reject({ message: 'User has no token.' });
};
// return auth factory object
return authFactory;
})
// ===================================================
// factory for handling tokens
// inject $window to store token client-side
// ===================================================
.factory('AuthToken', function($window) {
var authTokenFactory = {};
// get the token out of local storage
authTokenFactory.getToken = function() {
return $window.localStorage.getItem('token');
};
// function to set token or clear token
// if a token is passed, set the token
// if there is no token, clear it from local storage
authTokenFactory.setToken = function(token) {
if (token)
$window.localStorage.setItem('token', token);
else
$window.localStorage.removeItem('token');
};
return authTokenFactory;
})
// ===================================================
// application configuration to integrate token into requests
// ===================================================
.factory('AuthInterceptor', function($q, $location, AuthToken) {
var interceptorFactory = {};
// this will happen on all HTTP requests
interceptorFactory.request = function(config) {
// grab the token
var token = AuthToken.getToken();
// if the token exists, add it to the header as x-access-token
if (token)
config.headers['x-access-token'] = token;
return config;
};
// happens on response errors
interceptorFactory.responseError = function(response) {
// if our server returns a 403 forbidden response
if (response.status == 403) {
AuthToken.setToken();
$location.path('/login');
}
// return the errors from the server as a promise
return $q.reject(response);
};
return interceptorFactory;
});
I've seen other posts about problems with $save(), but I couldn't relate it to my code. I have a profile controller with an updateProfile() method that ultimately attempts to save the new data to the database after it has been changed.
I have defined my profile controller as follows:
angular.module('angularfireSlackApp')
.controller('ProfileCtrl', function($state, md5, profile) {
var profileCtrl = this;
var currentUser = firebase.auth().currentUser;
profileCtrl.profile = profile;
// Retrieves the user's email from input field, hashes it, and saves the data to the database
profileCtrl.updateProfile = function() {
console.log(profileCtrl.profile); // Profile object exists and is populated as expected
profileCtrl.profile.emailHash = md5.createHash(currentUser.email);
profileCtrl.profile.$save();
}
});
My user service:
angular.module('angularfireSlackApp')
// Provides a means of retrieving User data or list of all Users
.factory('Users', function($firebaseArray, $firebaseObject) {
// Provides a means of retrieving User data or list of all Users
// Create a reference to users that can be used to retrieve an array of users
var usersRef = firebase.database().ref("users");
users = $firebaseArray(usersRef);
var Users = {
// Returns a firebase object of a specific user's profile
getProfile: function(uid) {
return $firebaseObject(usersRef.child(uid));
},
// Returns the display name of a specific user
getDisplayName: function(uid) {
return users.$getRecord(uid).displayName;
},
// Returns the Gravatar url that corresponds to the user
getGravatar: function(uid) {
return 'www.gravatar.com/avatar/' + users.$getRecord(uid).emailHash;
},
// Returns a firebase array of users
all: users
};
return Users;
});
Profile state from main app:
.state('profile', {
url: '/profile',
controller: 'ProfileCtrl as profileCtrl',
templateUrl: 'users/profile.html',
resolve: {
auth: function($state) {
firebase.auth().onAuthStateChanged(function(user) {
if (user == null) {
$state.go('home');
console.log("In auth but user NOT valid");
} else {
console.log("In auth and user valid");
}
});
},
profile: function(Users) {
firebase.auth().onAuthStateChanged(function(user) {
if (user != null) {
console.log("In profile and user valid");
return Users.getProfile(user.uid).$loaded();
} else {
console.log("In profile but user NOT valid");
}
});
}
}
});
console.log:
For some reason I'm getting an error that profileCtrl.profile.$save() is not a function. I know that the profileCtrl.profile object is legitimate and that I'm using $save() appropriately, but I just can't figure out what else could be the problem.
My gut feeling is that I'm missing something simple, but I'm brand new to AngularJS and Firebase so I wouldn't be surprised.
In the resolve of your "profile" state you are not returning anything.
You should fix it to:
auth: function($state, $firebaseAuth) {
return $firebaseAuth().$onAuthStateChanged().then(function(user) {
if (!user) {
$state.go('home');
console.log("In auth but user NOT valid");
} else {
console.log("In auth and user valid");
}
}); // classic situation to use $requiresAuth()
},
profile: function(Users, $firebaseAuth) {
return $firebaseAuth().$requireSignIn();
}
Moreover, your service shouldn't keep the reference to the $firebaseArray but create it for each controller that wants to use it.
The down-side is you'll have to make some changes, but the up-side is a more predictable, maintainable code:
var Users = {
// Returns a firebase object of a specific user's profile
getProfile: function(uid) {
return $firebaseObject(usersRef.child(uid));
},
// Returns the display name of a specific user
getDisplayName: function(uid) {
// This is actually an issue to get the name synchronously, but I see no reason why not using the Firebase SDK and fetch a-sync
//return users.$getRecord(uid).displayName;
return usersRef.child(uid)
.child('displayName')
.once('value')
.then(function (snap) {
return snap.val();
});
},
// Returns the Gravatar url that corresponds to the user
getGravatar: function(uid) {
// Again fetch a-synchronously
//return 'www.gravatar.com/avatar/' + users.$getRecord(uid).emailHash;
return usersRef.child(uid)
.child('emailHash')
.once('value')
.then(function (snap) {
return 'www.gravatar.com/avatar/' + snap.val();
});
},
// Returns a firebase array of users
all: function () {
return $firebaseArray(usersRef);
}
};
Also refer to the new AngularFire docs: API reference
I have login tied to Facebook authentication, and this is all handled by Firebase.
However I need to make an API call to Facebook 'me/friends/'
Since I am already logged in, how would I use OAuth object to make a call without making another request.
I am using following wrapper for Angular for connection to Facebook.
https://github.com/ccoenraets/OpenFB
You don't need a wrapper. $firebaseAuth() + $http() = easy Graph API requests.
The Graph API is pretty easy to use and will work easily with Firebase.
Make sure you have the Facebook Friends permission enabled or you won't get any data back.
You can use $firebaseAuth() to login and get the Facebook access_token. That token can be used against the Graph API to get data via HTTP requests. Angular has a good $http library for making these calls.
Don't mind the way I structure the code, I prefer to use the Angular styleguide.
angular.module('app', ['firebase'])
.constant('FirebaseUrl', 'https://<my-firebase-app>.firebaseio.com/')
.constant('FacebookAppId', '<app-id>')
.service('RootRef', ['FirebaseUrl', Firebase])
.factory('Auth', Auth)
.factory('Friends', Friends)
.controller('MainCtrl', MainCtrl);
function Friends($http, RootRef, $q, FacebookAppId) {
function getFriends() {
// get the currently logged in user (may be null)
var user = RootRef.getAuth();
var deferred = $q.defer();
var token = null;
var endpoint = "https://graph.facebook.com/me/friends?access_token="
// if there is no logged in user, call reject
// with the error and return the promise
if (!user) {
deferred.reject('error');
return deferred.promise;
} else {
// There is a user, get the token
token = user.facebook.accessToken;
// append the token onto the endpoint
endpoint = endpoint + token;
}
// Make the http call
$http.get(endpoint)
.then(function(data) {
deferred.resolve(data);
})
.catch(function(error) {
deferred.reject(error);
});
// return the promise
return deferred.promise;
}
return {
get: getFriends
};
}
Friends.$inject = ['$http', 'RootRef', '$q', 'FacebookAppId'];
function Auth($firebaseAuth, RootRef) {
return $firebaseAuth(RootRef);
}
Auth.$inject = ['FirebaseAuth', 'RootRef'];
function MainCtrl($scope, Friends) {
$scope.login = function login() {
Auth.$authWithOAuthPopup('facebook').then(function(authData) {
console.log(authData, 'logged in!');
});
};
$scope.getFriends = function getFriends() {
Friends.get()
.then(function(result) {
console.log(result.data);
});
};
}
MainCtrl.$inject = ['$scope', 'Friends'];
I am really trying to get a hold on how all of this works. I feel like this should work. I have an Auth factory and when the jwt is expired it calls its method 'delegate' which gets a new token with the refresh token. For some reason I get the error '_this.delegate is not a function'. (I am doing delegate in both cases for testing purposes)
webapp.factory('Auth', function($http, API_URL, $window, $location, jwtHelper ) {
var _this = this;
var delegate = function(){
$http.post(API_URL+'/admin/delegate', {refresh_token: $window.sessionStorage.refreshToken } ).success(function(result) {
$window.sessionStorage.authToken = result.token;
$window.sessionStorage.refreshToken = result.refresh_token;
console.log('delegate-result: '+JSON.stringify(result));
$location.path('/about');
//LocalService.set('authToken', JSON.stringify(result));
});
};
return {
//returns true if there is an auth token
isAuthenticated: function() {
var storedJwt = $window.sessionStorage.authToken;
console.log('stored JWT: '+storedJwt);
var storedPayload = jwtHelper.decodeToken(storedJwt);
console.log('payload: '+JSON.stringify(storedPayload));
if(jwtHelper.isTokenExpired(storedJwt)){
console.log('is expired expired: '+jwtHelper.getTokenExpirationDate(storedJwt));
_this.delegate();
} else {
console.log('is not expired expires: '+jwtHelper.getTokenExpirationDate(storedJwt));
//For testing
_this.delegate();
}
return $window.sessionStorage.authToken;
//LocalService.get('authToken');
},
delegate: delegate,
//login function, should be moved to login controller
login: function(email, password) {
var login = $http.post(API_URL+'/authenticate', {email: email, password: password } );
login.success(function(result) {
console.log('login-result: '+JSON.stringify(result));
$window.sessionStorage.authToken = result.token;
$window.sessionStorage.refreshToken = result.refresh_token;
$location.path('/about');
//LocalService.set('authToken', JSON.stringify(result));
});
return login;
},
All you need to do is remove _this from _this.delegate().
The function delegate() is in scope where you are using it.
There is nothing wrong with using var in a factory. A service is different
In my SPA I catch every 401 response from REST requests. From there I don't redirect to login page immediatly, but first I check to the backend if the problem is that the token has expired or not. If not (the user is not known) I redirect to login, but if it was an expired problem, I generate a new token then I run again the request that previously failed into a 401. Here is the code for my interceptor:
var $http, loginService;
return function (promise) {
return promise.then(function (response) {
return response;
}, function (response) {
if (response.status === 401) {
$http = $http || $injector.get('$http');
loginService = loginService || $injector.get('loginService');
var defer = $q.defer();
var promiseToken = defer.promise;
var configPreviousRequest = response.config;
console.log(configPreviousRequest);
var url = configurationService.serverUrl + "mobile" + configurationService.apiVersion + "/verify";
var request = $http.post(url, {'code': loginService.getVmmToken()});
// Get the token. If success, we try to login again
return request.then(
function (responseVerify) {
loginService.setVmmsToken(responseVerify.data);
loginService.setAuthentificationToken();
configPreviousRequest.headers.vmmsToken = responseVerify.data;
return $http(configPreviousRequest);
},
function () {
$location.path('/login');
});
}
return $q.reject(response);
});
};
But here is the result in Chrome Network tool. All methods are not in the correct number (called too many times for /verify and /blocks)
So I logged (with console.log(configPreviousRequest);) to see what happens. Here are logs:
We clearly observe that for one 401 error, I intercept it many times.
And I have no clue why :)
Has someone any idea?
Thanks