I have a problem with my angular app- after a user signs in, if he hits the refresh button, the signin info is lost and the app redirects to the log in page. I found a SO answer for something similar here using $cookieStore but I don't think it can work for me as I'm not using cookies. Can anyone suggest a solution? Here's my authorization service-
var app = angular.module('myApp.services');
app.factory('SignIn', ['$resource', '$q', function($resource, $q) {
var signInUrl = 'https://example.com'
var API = $resource(signInUrl, {}, {
signIn: {
withCredentials: true,
url: signInUrl + '/session',
method: 'POST'
},
signOut: {
url: authApiUrl + '/session',
method: 'DELETE'
},
currentUser: {
url: signInUrl + '/users/#me',
method: 'GET'
}
});
var _currentUser = undefined;
return {
isAuthenticated: function() {
return !!_currentUser;
},
getUser: function(){
var d = $q.defer();
// If _currentUser is undefined then we should get current user
if (_currentUser === undefined) {
API.currentUser(function(userData) {
_currentUser = userData;
d.resolve(userData);
}, function(response) {
if (response.statusCode === 401) {
_currentUser = null;
d.resolve(_currentUser);
} else {
d.reject(response);
}
});
} else {
d.resolve(_currentUser);
}
return d.promise;
},
signIn: function(username, password){
var d = $q.defer();
API.signIn({email: username, password: password}, function(data, headers){
_currentUser = data;
d.resolve(_currentUser);
}, d.reject);
return d.promise;
},
signOut: function(){
var d = $q.defer();
API.signOut(function(){
_currentUser = null;
d.resolve();
}, d.reject);
return d.promise;
}
};
}]);
If you just need to keep track of the _currentUser data past a refresh then you could use sessionStorage within the browser. That extends all the way back to IE 8 and we really shouldn't be supporting any browsers before that anyway.
Usually these things are done with cookies though. When the client first makes a connection to the server (even before the first API call in some cases) a cookie is sent to the client so the server can maintain a session associated with that particular client. That's because the cookie is automatically sent back to the server with each request and the server can check its local session and say, "Oh, I'm talking to this user. Now I can use that additional piece of context to know if I can satisfy their API call or not."
You don't show any of your other API calls here but I'm guessing that you're sending something out of the _currentUser with each API call to identify the user instead? If so, that certainly works, and it avoids the need to synchronize cookies across multiple servers if you're clustering servers, but you're going to have to use something local like sessionStorage or localStorage that won't get dumped like your current in-memory copy of the data does when you refresh the page.
Related
We have experienced the following problem:
We had implemented Azure AD authenticated web API and we have been successfully had been making calls to it with ADAL 1.0.12 (we had our own wrapper around it for processing iframe for silent login).
Now we got rid all together of our wrapper and upgraded to the latest version and started to get the problem described here: token renewal operation timed out.
The Azure AD architecture behind the API is very simple: a registered web app with Read directory data and Sign in and read user profile permissions. We have another native app registered with permissions to the web app (Access to the Web APP NAME HERE). Both applications have oauth2AllowImplicitFlow set to true in their manifests.
Both apps have registered a wildcard redirect URIs for https://ourtenant.sharepoint.com/* (we are making the calls from SharePoint to the web API). In addition, the native client app has registered redirect URI to the unique URI (App ID URI) of the web API app.
Both apps have been granted admin consent before - they were working fine with the old version of ADAL.js
So, here is some code. Please, note that we had tried both without displayCall callback (with page refresh directly on the current page) and with page refresh (check the commented code in initLogin method). Also we had a switch for generating a popup or an iframe with a callback on successful login.
The problem is with authContext.acquireToken. Note that if we call OurNamespace.authContext.getCachedToken(OurNamespace.clientId) we get a stored token for the resource.
Note that also, we are calling properly handleWindowCallback after each page refresh / iframe / popup.
Happens regardless of the browser.
OurNamespace = {
serviceMainUrl: "https://localhost:44339",
webApiAppIdUri: "WEB-API-URI",
tenant: "TENANT",
clientId: "NATIVE-APP-GUID", // Native APP
adalEndPoints: {
get: null
},
adal: null,
authContext: null,
dummyAuthPage: null,
usePopup: true,
init: function () {
this.dummyAuthPage = "DummmyLogin.aspx";
OurNamespace.adalEndPoints.get = OurNamespace.serviceMainUrl + "/api/values";
OurNamespace.authContext = new AuthenticationContext({
tenant: OurNamespace.tenant + '.onmicrosoft.com',
clientId: OurNamespace.clientId,
webApiAppIdUri: OurNamespace.webApiAppIdUri,
endpoints: OurNamespace.adalEndPoints,
popUp: false,
postLogoutRedirectUri: window.location.origin,
cacheLocation: "localStorage",
displayCall: OurNamespace.displayCall,
redirectUri: _spPageContextInfo.siteAbsoluteUrl + "/Pages/" + OurNamespace.dummyAuthPage
});
var user = OurNamespace.authContext.getCachedUser(); // OurNamespace.authContext.getCachedToken(OurNamespace.clientId)
if (user) {
OurNamespace.azureAdAcquireToken();
} else {
OurNamespace.initLogin();
}
},
initLogin: function () {
//OurNamespace.authContext.config.displayCall = null;
//var isCallback = OurNamespace.authContext.isCallback(window.location.hash);
//OurNamespace.authContext.handleWindowCallback();
//if (isCallback && !OurNamespace.authContext.getLoginError()) {
// window.location.href = OurNamespace.authContext._getItem(OurNamespace.authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
//}
OurNamespace.authContext.login();
},
displayCall: function (url) {
var iframeId = "azure-ad-tenant-login",
popup = null;
if (OurNamespace.usePopup) {
popup = window.open(url, 'auth-popup', 'width=800,height=500');
} else {
var iframe = document.getElementById(iframeId);
if (!iframe) {
iframe = document.createElement("iframe");
iframe.setAttribute('id', iframeId);
document.body.appendChild(iframe);
}
iframe.style.visibility = 'hidden';
iframe.style.position = 'absolute';
iframe.src = url;
popup = iframe.contentDocument;
}
var intervalId = window.setInterval(function () {
try {
// refresh the contnetDocument for iframe
if (!OurNamespace.usePopup)
popup = iframe.contentDocument;
var isCallback = OurNamespace.authContext.isCallback(popup.location.hash);
OurNamespace.authContext.handleWindowCallback(popup.location.hash);
if (isCallback && !OurNamespace.authContext.getLoginError()) {
popup.location.href = OurNamespace.authContext._getItem(OurNamespace.authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
window.clearInterval(intervalId);
if (OurNamespace.usePopup) {
popup.close();
}
var user = OurNamespace.authContext.getCachedUser();
if (user) {
console.log(user);
} else {
console.error(OurNamespace.authContext.getLoginError());
}
}
} catch (e) { }
}, 400);
},
azureAdAcquireToken: function () {
OurNamespace.authContext.acquireToken(OurNamespace.adalEndPoints.get, function (error, token) {
if (error || !token) {
SP.UI.Status.addStatus("ERROR", ('acquireToken error occured: ' + error), true);
return;
} else {
OurNamespace.processWebApiRequest(token);
}
});
},
processWebApiRequest: function (token) {
// Alternatively, in MVC you can retrieve the logged in user in the web api with HttpContext.Current.User.Identity.Name
$.ajax({
type: "GET",
url: OurNamespace.adalEndPoints.get,
contentType: "application/json; charset=utf-8",
dataType: "json",
data: {},
headers: {
'Authorization': 'Bearer ' + token
},
success: function (results) {
var dataObject = JSON.parse(results);
SP.UI.Status.addStatus("Info", "ADAL GET data success: " + dataObject.webApiUser);
$(".grid-info").html("<h1 class=\"h1\">Current Web API authenticated user: " + dataObject.webApiUser + "</h1>");
},
error: function (xhr, errorThrown, textStatus) {
console.error(xhr);
SP.UI.Status.addStatus("ERROR", ('Service error occured: ' + textStatus), true);
}
});
}
}
I am testing using the 1.0.15 adal.js and get the access token successfully by using the authContext.acquireToken which actually call the this._renewToken(resource, callback) to acquire the access token by the hide iframe. Here is the full test sample code for your reference:
<html>
<head>
<script src="/js/1.0.15/adal.js"></script>
<script>
var config = {
tenant: 'adfei.onmicrosoft.com',
clientId: '7e3b0f81-cf5c-4346-b3df-82638848104f',
redirectUri: 'http://localhost/spa.html',
navigateToLoginRequestUrl:false,
};
var authContext=new AuthenticationContext(config);
var hash = window.location.hash;
if(hash.length!=0){
authContext.handleWindowCallback(hash);
var user=authContext.getCachedUser();
}
function login(){
authContext.login();
}
function acqureToken(){
authContext.acquireToken("https://graph.microsoft.com", function(error, token, message){
console.log(token)
})
}
</script>
</head>
<body>
<button onclick="login()">Login</button>
<button onclick="acqureToken()">AcquireToken</button>
</body>
</html>
Is it helpful? Or would you mind sharing a run-able code sample for this issue?
I'm not sure if what version of adal.js you are using. But there is a loadFrameTimeout setting for the config object that you can set in milliseconds. Check the top of your adal.js file and it should be there.
I have this $http request interceptor
app.config(function($httpProvider) {
$httpProvider.interceptors.push(function() {
return {
request: function(req) {
// Set the `Authorization` header for every outgoing HTTP request
req.headers['cdt_app_header'] = 'tamales';
return req;
}
};
});
});
Is there any way we can add a header or cookie to every $http request, but keep the header value secure / not visible with JavaScript?
We can add an obfuscation layer with this header to prevent easy access to our API endpoints, but I am wondering about a more truly secure solution.
Cookies are used for secure sessions, and these are more secure because they cannot be accessed with JavaScript. Say we have a user who can do this request with front-end code:
GET /api/users
we don't really want them to be able to make a simple request with cURL or a browser without an extra piece of information. The cookie we give them will give them the ability to use the browser address bar to make a GET request to /api/users, but if we add the requirement to have another cookie or header in place, then we can prevent them from accessing endpoints that are authorized for, in a format that we don't really want them to use.
In other words, we want to do our best to give them access, but only in the context of a front-end Angular app.
I can't add a comment because of my rep but what are you doing on the back-end to authorize users? If the cookie is signed and contains user permissions it shouldn't matter that the header is visible in the client as it will also be verified on the back-end API call.
in this sample i used HttpRestService to get RESTful API, read this article
at first we create a service to get our configs in this sample is getConfigs
we use getConfigs in the app.run when application is started, after get the configs we set them all in the header as sample.
after that we can get userProfile with new header and also secure by call it from our controller as you see.
in this sample you need to define apiUrl, it's your api host url, remember after logout you can remove the header, also you can define your configs dynamically to make more secure for your application.
HttpRestService.js github link
app.js
var app = angular.module("app", ["HttpRestApp"]);
app.service
app.service("service", ["$http", "$q", "RestService", function (http, q, restService) {
this.getConfigs = function () {
var deferred = q.defer();
http({
method: "GET",
async: true,
headers: {
"Content-Type": "application/json"
},
url: "you url to get configs"
}).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.resolve(error);
});
return deferred.promise;
}
var api = {
user: "User" //this mean UserController
}
//get user with new header
//this hint to your api with this model "public Get(int id){ return data; }"
//http://localhost:3000/api/users/123456
this.getUserProfile= function(params, then) {
restService.get(params, api.user, true).then(then);
}
}]);
app.run
app.run(["RestService", "service", function (restService, service) {
var header = {
"Content-Type": "application/json"
}
//get your configs and set all in the header
service.getConfigs().then(function (configs) {
header["systemId"] = configs.systemId;
});
var apiUrl = "http://localhost:3000/";
restService.setBaseUrl(apiUrl, header);
}]);
app.controller
app.controller("ctrl", ["$scope", "service", function ($scope, service) {
$scope.getUserProfile = function () {
//this is just sample
service.getUserProfile({ id: 123456 }, function (data) {
$scope.user = data;
});
}
$scope.getUserProfile();
}]);
I have a simple web app based on this project ( https://github.com/arthurkao/angular-drywall ), running with NodeJS and AngularJS as the front-end.
I'm trying to set up a simple page that displays a list of all connected users on a map (using Google Maps, Geolocation and PubNub).
Here's how I'm actually doing it:
angular.module('base').controller('TravelCtrl',
function($rootScope, $scope, NgMap, security, $geolocation, PubNub){
$rootScope.extusers = []; //remote users
$scope.initTravel = function() { //declare the init function
PubNub.init({
subscribe_key: $rootScope.security.keys.psk,
publish_key: $rootScope.security.keys.ppk,
uuid: $rootScope.security.currentUser.username,
ssl: true
});
PubNub.ngSubscribe({
channel: "travel",
state: {
position: {},
}
});
console.log("Loaded Travel");
$geolocation.getCurrentPosition({
timeout: 60000
}).then(function(position) { //when location is retreived
$scope.position = position;
PubNub.ngSubscribe({
channel: "travel",
state: {
position: {
lat: Math.floor($scope.position.coords.latitude*1000)/1000, //decrease accuracy
long: Math.floor($scope.position.coords.longitude*1000)/1000,
},
}
});
$rootScope.$on(PubNub.ngPrsEv("travel"), function(event, payload) {
$scope.$apply(function() {
$scope.extusers = PubNub.ngPresenceData("travel");
});
});
PubNub.ngHereNow({ channel: "travel" });
$scope.showInfo = function(evt, marker) { //show user window on map
$scope.extuser = marker;
$scope.showInfoWindow('infoWindow');
};
});
};
if ($rootScope.hasLoaded()) { //if username and keys are already loaded, then init module
$scope.initTravel();
} else { //else, wait for username and keys to be loaded
$rootScope.$on('info-loaded', function(event, args) {
$scope.initTravel();
});
}
}
);
Although it works, it seems like it's very buggy and only loads sometimes. Occasionally, I get this:
Result screenshot
I really don't know what I'm doing wrong, as I simply followed the tutorials on PubNub's AngularJS SDK.
I think this has to do with how I'm initialising the application.
angular.module('app').run(['$location', '$rootScope', 'security', function($location, $rootScope, security) {
// Get the current user when the application starts
// (in case they are still logged in from a previous session)
$rootScope.hasLoaded = function() {
return (security.keys && security.info && security.currentUser); //check if everything is loaded correctly
};
$rootScope.checkLoading = function() {
if ($rootScope.hasLoaded()) {
$rootScope.$broadcast('info-loaded'); //broadcast event to "TravelCtrl" in order to init the module
}
};
security.requestKeys().then($rootScope.checkLoading); //request secret keys
security.requestSiteInfo().then($rootScope.checkLoading); //then templating info (site title, copyright, etc.)
security.requestCurrentUser().then($rootScope.checkLoading); //and finally, current user (name, id, etc.)
$rootScope.security = security;
// add a listener to $routeChangeSuccess
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
$rootScope.title = current.$$route && current.$$route.title? current.$$route.title: 'Default title';
});
}]);
1- Request secret keys, site info and current user with JSON API.
2- Wait until everything's loaded then init the application with the appropriate keys (PubNub, Google Maps)
--
My question is:
How do you instantiate an AngularJS app after retrieving useful information via a RESTful API?
I'm pretty new to AngularJS, and I wouldn't be surprised if my approach is totally ridiculous, but I really need to get some advice on this.
Thanks in advance for your help,
Ulysse
You don't have to wait that the AJAX Query ended to initate the angular APPs.
you can use the $http promise ( details her )
In the controller :
// Simple GET request example:
$http({
method: 'GET',
url: '/someUrl'
}).then(function successCallback(response) {
// this callback will be called asynchronously
// when the response is available
// data is now accessible in the html
$scope.data = response ;
// you can call a function to add markers on your maps with the received data
addMarkerOnMap(response);
}, function errorCallback(response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
You can also add a watch on some variable to wait modification on them :
// you should have $scope.yourVarName declared.
$scope.$watch('yourVarName', function(newValue, oldValue) {
console.log(newValue);
});
Or watch a list/object
$scope.$watchCollection('[var1,var2]', function () {
},true);
I am creating an login page using Angular. After I process my login in the backend, I set the values in MyService from my LoginCtrl and then move to the next page using $window.location.href= 'main.jsp'; . But when I call the values which I set in LoginCtrl from HomeCtrl, the values are empty?
I know that Services are singletons and will maintain the same state throughout the app. But in this case, It jut resets. I think it is because of using $window.location.href. Please help me solve my problem.
This is my service ( MyService ):
app.service('MyService', function() {
var user = {
name: '',
permissions: ''
};
this.getUser = function() {
return user;
}
this.setUser = function(userr) {
this.user = userr;
}
});
This my LoginCtrl: ( I've just posted the http.post part)
$http({
method: 'POST',
url: 'login',
data: JSON.stringify($scope.user),
headers: {
'Content-Type': 'application/json'
}
}).success(function(data) {
if (!("failure" == data)) {
console.log(data);
var user = MyService.getUser();
user.name = data.name;
user.permissions = data.permissions;
console.log(user);
console.log(MyService.getUser());
$window.location.href = 'main.jsp';
// MyService.changeLocation('main.jsp', true);
} else {
$scope.information = "Invalid username/password!"
}
}).error(function(data) {
console.log(data);
});
And this is my HomeCtrl:
app.controller('HomeCtrl', function($scope, $http,MyService) {
console.log(MyService.getUser());
var user = MyService.getUser();
$scope.flashMessage="Hello " + user.name;
});
Here user.name is empty.
You are changing your web page. The angular application is not persisted across the website boundary; remove the alteration to the window.location.href.
In order to simulate page changing in Angular consider using the official router (shipped with Angular 1.4+), ngRoute or Angular UI Router. These solutions use the HTML History Api and fallback to hashbang URLs to emulate the sort of thing you're trying to achieve.
This ends up creating a single-page application, which is what Angular is designed for.
In LoginCtrl, while reaching the success callback, you are not setting the response value(data in your case) to user object in MyService service.
You are getting the user object from the Service by
var user = MyService.getUser();
But setting the values to that object will not set the user object in the Service.
You need to use MyService.getUser(user); to set values in your service and the same will be available in your HomeCtrl
$http({
method: 'POST',
url: 'login',
data: JSON.stringify($scope.user),
headers: {
'Content-Type': 'application/json'
}
}).success(function(data) {
if (!("failure" == data)) {
console.log(data);
var user= {};
user.name = data.name;
user.permissions = data.permissions;
MyService.getUser(user); //set the values for user
var obj= MyService.getUser(); //get the values for user
console.log(obj);
//should display user object
//with respective name and permissions should be available
console.log(MyService.getUser());
$window.location.href = 'main.jsp';
// MyService.changeLocation('main.jsp', true);
} else {
$scope.information = "Invalid username/password!"
}
}).error(function(data) {
console.log(data);
});
UPDATE:
The reason why your code doesnt seem to work is: you are using $window incorrectly to change the route. $window.location.href = 'main.html' is somehow changing the route outside angular's context and hence not running the HomeCtrl. To fix this, you need to do the following:
First, define routes for your angular application (preferabbly using ui-router)
app.config(function($stateProvider){
$stateProvider
.state('login',{
url:'/',
templateUrl:'login.html',
controller:'LoginCtrl'
})
.state('main',{
url:'/main',
templateUrl:'main.html',
controller:'HomeCtrl'
})
.state("otherwise", { url : '/'})
})
Use $location.url('/main'). Notice it is same as the url pattern we defined for state: main. Or better, you should use $state.go('home'); to redirect the user to desirable state
Here's a working plunkr
Hope this helps!
Maybe I'm going about this the wrong way, i'm not sure, but I have 2 services, one is a user service which gets a bunch of details about the user from the server, the other being one that relies on some user details from the user service and then makes some more calls to the server to get other information.
Anyway, because of the async stuff that goes on when the 2nd service makes the calls the information required from the user server has not yet been populated.
I know Angular services can depend on one another, but not in this context it would appear?
factory('User', ['$resource', function ($resource) {
return $resource(usersUrl, {}, {
//The data model is loaded via a GET request to the app
query: {method: 'GET', params: {}, isArray: false},
putupdate: {method: 'PUT', params:{}}
});
}])
.factory('UserData', function() {
var data = {}
data.userinfo = {};
if(data = {}){
}
return {
updateinfo: function(newdata) {
data.userinfo = newdata;
// alert(data.userinfo.user)
},
userinfo: data
}
})
.factory('PlansData', ['UserData', 'User', '$rootScope', function(userData, user, $rootScope) {
var data = {}
data.plansinfo = {};
//alert(userData.data.userinfo.user.email)
if(data = {}){
}
return {
updateinfo: function(newdata) {
alert(user.query())
data.plansinfo = newdata;
},
plansinfo: data
}
}])
So I have a user service and a caching userdata service, but if I ever try and call anything from UserData in the PlansData service I get undefined.
How do I get plansData to wait for UserData to have some data?
Thanks
Tom
I'm not sure what you're trying to accomplish, but this line of code:
if(data = {}){
}
In both your services is wiping out your data object. You're setting the whole data object to be {}