The following approach does not work:
angular.module('myApp.myModule').factory('MyResource', function($resource, $cookies) {
var token = $cookies.get('token');
var user = $cookies.get('username');
console.log ("Token: "+token+" User: "+user);
return $resource(
'http://my-rest-api/whatever/:id',
{
headers: {
'token': token,
'user': user
}
}
)});
Console shows the correct data, but they were not sent..
That's the part somewhere in the related Controller (excerpt):
var getEntryOne = MyResource.get({ id: 1 }, function() {
console.log("Result: "+getEntryOne);
});
I get the "Message: Token invalid", I see the request-http-headers in firebug, they were not setted.
You are setting headers for get request then it should be there in get option of $resource
$resource('http://my-rest-api/whatever/:id',{},{
get:{
method:"GET",
headers:{
'token': token,
'user': user
}
},
});
If you wanted to add this header information to each request, then you could have http inteceptor which will be add header information on each request.
app.service('MyResourceInterceptor', ['$cookies', function($cookies) {
var token = $cookies.get('token'),
user = $cookies.get('username'),
service = this;
service.request = function(config) {
config.headers['token'] = token;
config.headers['user'] = user;
return config;
};
}]);
app.config([ '$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('MyResourceInterceptor');
}]);
Related
I'm facing issue on refreshing expired JWT token based on 401 (unauthorized) header response. What i want is when user get 401 (header) response, than a new (refresh) JWT should generated by calling specific service (api).
I'm sending XSRF-TOKEN & access_token (JWT) in header response and these are working fine. I even also can get refresh (expired) token by calling api manually. But can't get it worked with 401 (header) response.
I've a factory that take care of this promise and intercepts header requests. My (factory) code looks like this.
angular.module('myApp').factory('httpRequestInterceptor', httpRequestInterceptor);
function httpRequestInterceptor($cookies, $rootScope, $q, $location, $injector) {
var replays = [];
var refreshTokenPromise;
var factory = {
request: request,
responseError: responseError
};
return factory;
//////////
function requestTodoWhenDone() {
var token = store.get('token');
return $http({
method: 'POST',
url: ApiEndpoint.url,
params: {
grant_type: 'refresh',
id_token: $cookies.get('access_token')
}
})
.success(function(response) {
// Set the refreshed token.
$cookies.put('access_token', response.data.access_token);
})
.then(function(){
// Attempt to retry the request if request config is passed.
if( !angular.isUndefined(requestTodoWhenDone) && requestTodoWhenDone.length > 0 ) {
// Set the new token for the authorization header.
requestTodoWhenDone.headers = {
'Authorization': 'Bearer ' + $cookies.get('access_token')
};
// Run the request again.
return $http(requestTodoWhenDone);
}
});
}
//////////
// Add authorization token to headers
function request(config) {
config.headers = config.headers || {};
if ($cookies.get('access_token')) {
config.headers.Authorization = 'Bearer ' + $cookies.get('access_token');
}
return config;
}
// Intercept 401s and redirect you to login
function responseError(response, requestTodoWhenDone) {
if (response.status === 401 && $cookies.get('access_token')) {
return checkAuthorization(response);
}
return $q.reject(response);
/////////
function checkAuthorization(res) {
return $q(function(resolve, reject) {
var replay = {
success: function(){
$injector.get('$http')(res.config).then(resolve, reject);
},
cancel: function(){
reject(res);
}
};
replays.push(replay);
console.log(replays);
if (!refreshTokenPromise) {
refreshTokenPromise = $injector.get('requestTodoWhenDone') // REFRESH TOKEN HERE
.refreshToken()
.then(clearRefreshTokenPromise)
.then(replayRequests)
.catch(cancelRequestsAndRedirect);
}
});
////////////
function clearRefreshTokenPromise(auth) {
refreshTokenPromise = null;
return auth;
}
function replayRequests(auth) {
replays.forEach(function(replay) {
replay.success();
});
replays.length = 0;
return auth;
}
function cancelRequestsAndRedirect() {
refreshTokenPromise = null;
replays.forEach(function(replay) {
replay.cancel();
});
replays.length = 0;
$cookies.remove('token');
var $state = $injector.get('$state');
// SET YOUR LOGIN PAGE
$location.path('/login');
}
}
}
}
Based on above code I'm getting following error in console when token expires (401 response).
Console Error
Error: "[$injector:unpr] Unknown provider: requestTodoWhenDoneProvider <- requestTodoWhenDone
Any help on this would be highly appreciable.
Thanks.
Ok i ended up with different way that solves the issue. But i still can't be able to redirect user to login page when my token inactive time is also expires (this happens after jwt expires).
Here is the code.
authInterceptor.service.js
angular.module('someApp').factory('AuthorizationTokenService', AuthorizationTokenService);
AuthorizationTokenService.$inject = ['$q', '$injector', '$cookies'];
function AuthorizationTokenService($q, $injector, $cookies) {
// Local storage for token
var tokenVM = {
accessToken: null
};
// Subscribed listeners which will get notified when new Access Token is available
var subscribers = [];
// Promise for getting new Access Token from backend
var deferedRefreshAccessToken = null;
var service = {
getLocalAccessToken: getLocalAccessToken,
refreshAccessToken: refreshAccessToken,
isAccessTokenExpired: isAccessTokenExpired,
subscribe: subscribe
};
return service;
////////////////////////////////////
// Get the new Access Token from backend
function refreshAccessToken() {
// If already waiting for the Promise, return it.
if( deferedRefreshAccessToken ) {
return deferedRefreshAccessToken.promise
} else {
deferedRefreshAccessToken = $q.defer();
// Get $http service with $injector to avoid circular dependency
var http = $injector.get('$http');
http({
method: 'POST',
url: 'api_url',
params: {
grant_type: 'refresh',
id_token: $cookies.get('access_token')
}
})
.then(function mySucces(response) {
var data = response.data;
if( data ){
// Save new Access Token
$cookies.put('access_token', data.access_token);
if( $cookies.get('access_token') ) {
// Resolve Promise
deferedRefreshAccessToken.resolve(data.access_token);
// Notify all subscribers
notifySubscribersNewAccessToken(data.access_token);
deferedRefreshAccessToken = null;
}
}
}, function myError(error) {
deferedRefreshAccessToken.reject(error);
deferedRefreshAccessToken = null;
});
return deferedRefreshAccessToken.promise;
}
}
function getLocalAccessToken() {
// get accesstoken from storage - $cookies
if ( $cookies.get('access_token') ) {
var access_token = $cookies.get('access_token')
return access_token;
}
}
function isAccessTokenExpired() {
// Check if expiresAt is older then current Date
}
function saveToken(accessToken) {
// get accesstoken from storage - $cookies
var access_token = $cookies.put('access_token');
console.log('access_token ' + access_token);
return access_token;
}
// This function will call all listeners (callbacks) and notify them that new access token is available
// This is used to notify the web socket that new access token is available
function notifySubscribersNewAccessToken(accessToken) {
angular.forEach(subscribers, function(subscriber) {
subscriber(accessToken);
});
}
// Subscribe to this service. Be notifyed when access token is renewed
function subscribe(callback) {
subscribers.push(callback);
}
}
Than in config (app.js) I've following code which intercepts appropriate header(s) and refresh (request) api on 401 response.
Here is the config code
config.$inject = ['$stateProvider', '$urlRouterProvider', '$httpProvider'];
function config($stateProvider, $urlRouterProvider, $httpProvider) {
// Push httpRequestInterceptor
// $httpProvider.interceptors.push('httpRequestInterceptor');
//Intercept all http requests
$httpProvider.interceptors.push(['$injector', '$q', "AuthorizationTokenService", "$cookies", function ($injector, $q, AuthorizationTokenService, $cookies) {
var cachedRequest = null;
return {
request: function (config) {
//If request if for API attach Authorization header with Access Token
if (config.url.indexOf("api") != -1) {
// var accessToken = AuthorizationTokenService.getLocalAccessToken();
console.log('cookie ' + $cookies.get('access_token'));
config.headers.Authorization = 'Bearer ' + $cookies.get('access_token');
}
return config;
},
responseError: function (response) {
switch (response.status) {
// Detect if reponse error is 401 (Unauthorized)
case 401:
// Cache this request
var deferred = $q.defer();
if(!cachedRequest) {
// Cache request for renewing Access Token and wait for Promise
cachedRequest = AuthorizationTokenService.refreshAccessToken();
}
// When Promise is resolved, new Access Token is returend
cachedRequest.then(function(accessToken) {
cachedRequest = null;
if (accessToken) {
// Resend this request when Access Token is renewed
$injector.get("$http")(response.config).then(function(resp) {
// Resolve this request (successfully this time)
deferred.resolve(resp);
},function(resp) {
deferred.reject();
console.log('success: refresh token has expired');
});
} else {
// If any error occurs reject the Promise
console.log('error: refresh token has expired');
deferred.reject();
}
}, function(response) {
// If any error occurs reject the Promise
cachedRequest = null;
deferred.reject();
return;
});
return deferred.promise;
}
// If any error occurs reject the Promise
return $q.reject(response);
}
};
}]);
}
The code is working fine on 401 (response) case which happens when JWT expires. But its not redirecting me to login page (In this case I've added console in promise request in config instead of redirection code)
Please help on this, thanks...
I'm able to make the authentication using JWT and angular-jwt, but now how can i redirect the user to the home page including the token in the header request? I'm using Spring Boot for the server side and i'd like to know how to serve this request.
this is the front-end's responsibility, you should redirect the user from angular, you can use the $location service.
as to the token you can store it in the $rootScope or in a client side cookie, here is an example of a jwt auth service i have wrote before it uses the $rootScope to store the client state.
note that this is not a production code.
app.service('AuthService', function ($rootScope, $http, $location) {
var service = this;
service.authenticate = function (credentials) {
$http({
'url': authUrl,
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'data': credentials
}).then(function (response) {
if (response.data.token && response.data.token.length > 0) {
$rootScope.authenticated = true;
$rootScope.jwtToken = response.data.token;
$location.path('/home');
} else {
$rootScope.logout();
}
}, function () {
$rootScope.logout();
});
$rootScope.logout = service.logout;
};
service.isAuthenticated = function () {
return $rootScope.authenticated;
};
service.logout = function () {
$rootScope.authenticated = false;
$rootScope.jwtToken = null;
};
});
I'm using the code from this blog to make an authentication module for my angular app.
I have a test page that includes a login form, a submit button and a "profile" button that will query a restricted route.
<div class="container-fluid" ng-controller="AuthCtrl as auth">
<input type="text" ng-model="auth.user.username">
<input type="text" ng-model="auth.user.password">
<button ng-click="auth.login(auth.user)">LOGIN</button>
<button ng-click="auth.profile()">PROFILE</button>
</div>
The restricted route I'm querying after login is defined as:
const authenticate = expressJwt({
secret: SECRET
});
app.get('/me', authenticate, function(req, res) {
res.status(200).json(req.user);
});
The login works fine. I receive a token that I put in sessionStorage.
When I click on the profile button (request to /me) I get an unauthorized error.
If I refresh the page and click again on profile I do get the excpected behaviour. (/me returns user data with no error)
If I delete the token manually after that, I still have access to /me until I refresh the page.
This is my service:
function loginService($http) {
this.login = function(user) {
return $http.post('/auth', user).then(
function(response) {
return response.data;
},
function(response) {
return response;
});
};
this.profile = function() {
return $http.get('/me').then(
function(response) {
return response.data;
},
function(response) {
return response;
});
};
}
angular
.module('app')
.service('loginService', loginService);
And this is my controller with the httpProvider:
function AuthCtrl($window, $http, loginService) {
this.user = {username: "", password: ""};
this.login = function(user) {
loginService.login(user).then(function(data) {
$window.sessionStorage.token = data.token;
});
};
this.profile = function() {
loginService.profile().then(function(data) {
console.log(data);
});
};
}
function config($httpProvider, $windowProvider) {
var window = $windowProvider.$get();
if(window.sessionStorage.token) {
var token = window.sessionStorage.token;
$httpProvider.defaults.headers.common.Authorization = 'Bearer ' + token;
}
};
angular
.module('app')
.config(config)
.controller('AuthCtrl', AuthCtrl);
Could the problem come from storing the token in sessionStorage or from the http provider?
Eventually I will implement the secure cookie method but I'd like to get this one solved before proceding further.
I finally figured out it was coming for the $httpProvider code.
function config($httpProvider, $windowProvider) {
var window = $windowProvider.$get();
if(window.sessionStorage.token) {
var token = window.sessionStorage.token;
$httpProvider.defaults.headers.common.Authorization = 'Bearer ' + token;
}
};
The provider options wer set only once during app load.
Reloading the page would rerun the config code and enter the if(window.sessionStorage.token) condition as expected.
To make this config dynamic, I had to create an interceptor (factory) like so:
function config($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
};
function authInterceptor($rootScope, $q, $window) {
return {
request: function (config) {
config.headers = config.headers || {};
if ($window.sessionStorage.token) {
config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token;
}
return config;
},
responseError: function (rejection) {
if (rejection.status === 401) {
console.log("not authorised");
}
return $q.reject(rejection);
}
};
};
angular
.module('app')
.config(config)
.controller('AuthCtrl', AuthCtrl)
.factory('authInterceptor', authInterceptor);
I`m trying to make a request to an API server with $resource.
I want to make a post but angular turns post method into options and give an error like
OPTIONS http: / /l ocalhost/API.DWS/api/v1/user/login
XMLHttpRequest cannot load http:/ / localhost/API.DWS/api/v1/user/login. Response for preflight has invalid HTTP status code 405
var objectMethods = {
get: { method: 'GET' },
update: { method: 'PUT' },
create: { method: 'POST' },
remove: { method: 'DELETE' },
patch: { method: 'PATCH' }
};
var apiUrl = "http://localhost/API.DWS";
angular.module('nurby.version.services', [])
.config(function ($httpProvider) {
})
.factory('LoginService', ['$resource', '$http', function ($resource, $http) {
return $resource(apiUrl + "/api/v1/user/login", {},objectMethods);
}])
.controller('LogInController', ['$scope', '$rootScope', '$location','LoginService', '$http', function ($scope, $rootScope, $location, LoginService, $http) {
$scope.login = function (model) {
var loginObject = { Username: model.username, Password: model.password };
$http.defaults.useXDomain = true;
$http.defaults.headers['Content-Type'] = 'application/json';
$http.defaults.headers['Access-Control-Allow-Origin'] = '*';
LoginService.create({}, loginObject, function (data) {
if (data) {
toastr.success("itworks");
}
else {
toastr.error("not working")
}
})
}
}]);
you can define service.js and use it like below:
var APP_NAME = 'app';
angular.module(APP_NAME).service('WebService', ["$http", function ($http) {
this.login = function (parameters,callbackFunc)
{
$http({
url: 'api/login',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: $.param(parameters)
}).success(function (data) {
callbackFunc(data);
}).error(function (data) {
callbackFunc([]);
});
};
and use it in your controller like below:
LoginController = ['$scope', '$http', '$location', 'WebService','$window', function ($scope, $http, $location,$WebService,$window) {
$scope.login = function(admin){
var data = {email:admin.email,password:admin.password};
$WebService.login(data,function(result){
if(result.success){
$window.location.replace("index");
}
else{
$scope.loginError = result.fail;
}
});
}
}];
The problem here is that you are specifying a complete URL beginning "http://localhost/API.DWS" and you haven't loaded the web page from the same domain (maybe you used a different port?).
This means the browser sees your request as a Cross-Domain request. It therefore sends an OPTIONS request first to ask the server whether it will permit you to send the POST. You could configure your server to respond correctly to these requests, or change your code so the web page and the api are on the same domain.
How to configure your server will depend on which server you are running. Search for CORS and your web server and you should find useful information.
Inside my controller this worked for me
var resource = $resource(
"your_api_url",
{
callback: "JSON_CALLBACK"
},
{
getData: {
method: "JSONP",
isArray: false
}
}
);
function loadRemoteData() {
$scope.isLoading = true;
resource.getData().$promise.then(
function( friends ) {
$scope.isLoading = false;
},
function( error ) {
// If something goes wrong with a JSONP request in AngularJS,
// the status code is always reported as a "0". As such, it's
// a bit of black-box, programmatically speaking.
alert( "Something went wrong!" );
}
);
}
$scope.searchResources = function() {
$scope.isLoading = true;
resource.getData().$promise.then(
function( friends ) {
$scope.isLoading = false;
},
function( error ) {
// If something goes wrong with a JSONP request in AngularJS,
// the status code is always reported as a "0". As such, it's
// a bit of black-box, programmatically speaking.
alert( "Something went wrong!" );
}
);
};
$http.post('http://localhost:7001/v1/sessions', {
data: {
username: $scope.user.username,
password: $scope.user.password,
type: 'sessions'
}
})
.then(function(response) {
if(response.data.data.token) {
$http.defaults.headers.common.Authorization = response.data.data.token;
$state.go('app.dashboard');
} else {
$scope.authError = response;
}
}, function(x) {
$scope.authError = 'Server Error';
});
I can confirm that the if condition gets called and a response.data.data.token is present.
It goes to the app.dashboard state but is intercepted by my ui-router:
$stateProvider.state('app', {
abstract: true,
url: '/app',
templateUrl: 'tpl/app.html',
resolve: {
current_user: ['$http', function($http) {
return $http.get('http://localhost:7001/v1/users/4/entities');
}]
}
})
That call, however, does not have anything set in the header. I thought that $http.defaults would set a default value in the header. What am I doing incorrectly?
You must set the default headers in the config method and not in your service.
Example:
myApp.config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.headers.common['Content-Type'] = 'application/json; charset=utf-8';
}]);
Only in config you can configure the httpProvider. If you try to do that inside your service, it won't affect the $httpProvider service at all.
EDIT:
You must make use Interceptors in this scenario.
For purposes of global error handling, authentication, or any kind of
synchronous or asynchronous pre-processing of request or
postprocessing of responses, it is desirable to be able to intercept
requests before they are handed to the server and responses before
they are handed over to the application code that initiated these
requests.
Refer Angular Docs Interceptor section
Just some sample code:
app.service('APIInterceptor', function($rootScope, UserService) {
var service = this;
service.request = function(config) {
// check if the token is available. Once the token is available get it here from the UserService.
var access_token = UserService.getToken() || "unauthorized";
if (access_token) {
config.headers.authorization = access_token;
}
return config;
};
service.responseError = function(response) {
return response;
};
})
In your config
$httpProvider.interceptors.push('APIInterceptor');
I would prefered you to one service to use sharable data.
Code
app.service(dataService, function(){
this.data = {}
this.getData = function(){
return data;
};
this.setTokenData = function(token){
data.token = token;
}
});
Now your code would be while setting token you could use dataService
if(response.data.data.token) {
dataService.setTokenData(response.data.data.token);
$http.defaults.headers.common.Authorization = dataService.data.token; //dataService.getData().token;
$state.go('app.dashboard');
} else {
$scope.authError = response;
}
Then from service resolve you could use
$stateProvider.state('app', {
abstract: true,
url: '/app',
templateUrl: 'tpl/app.html',
resolve: {
current_user: ['$http', 'dataService', function($http, dataService) {
$http.defaults.headers.common.Authorization = dataService.getData().token;
return $http.get('http://localhost:7001/v1/users/4/entities');
}]
}
})