I successfully managed to use interceptors (AngularJs ) in my Ionic application. Previous post .
While it is working perfectly well in the browser using "ionic serve".
There is no content loaded in the header title and content block ( "ion-content" ) using "ionic run android" (either emulating on genymotion or on my own phone). See screenshot below.
I'm pretty sure it comes from the interceptors i'm using, because before that, the app was working on any platforms. Also, as soon as i remove the interceptors it is working again. Here the code.
Note that i'm checking which url is called so i don't go into a circular dependency or checking useless url, only the calls to my api go through.
app.config(function($httpProvider){
$httpProvider.interceptors.push(['$location', '$injector', '$q', function($location, $injector, $q){
return {
'request' : function(config){
// intercept request
// carefull includes might not work while emulating
// use instead indexOf for that case
if(!config.url.includes('/oauth/v2/token') && config.url.includes('/api')){
// inject the service manually
var OauthService = $injector.get('OauthService');
var access_token = OauthService.token();
config.url = config.url+'?access_token='+access_token.key;
}
return config;
}
}
}]);
});
Any ideas what could give this error? (By the way console is showing no errors on browser).
UPDATE :
OauthService.js :
app.factory('OauthService', function($http, $localStorage) {
return {
token : function(){
// Store actual token
access_token = $localStorage.getObject('access_token');
// Store actual identity
identity_token = $localStorage.getObject('identity_token');
// IF no user logged
if(isObjectEmpty(identity_token)){
// IF access_token does NOT exist OR will expires soon
if( isObjectEmpty(access_token) || Date.now() > (access_token.expires_at - (600*1000)) ){
// Create an anonymous access_token
return $http
.get(domain+'/oauth/v2/token?client_id='+public_id+'&client_secret='+secret+'&grant_type=client_credentials')
.then(function (response) {
$localStorage.setObject('access_token', {
key: response.data.access_token,
type: 'anonymous',
expires_at: Date.now()+(response.data.expires_in*1000)
});
return response.data.access_token;
});
}
}
// IF user is logged
else{
// IF access_token does NOT exist OR will expires soon OR is anonymous
if( isObjectEmpty(access_token) || Date.now() > (access_token.expires_at - (600*1000)) || access_token.type == 'anonymous' ){
// Create an access_token with an identity
return $http
.get(domain+'/oauth/v2/token?client_id='+public_id+'&client_secret='+secret+'&api_key='+identity_token+'&grant_type=http://oauth2.dev/grants/api_key')
.then(function (response) {
$localStorage.setObject('access_token', {
key: response.data.access_token,
type: 'identity',
expires_at: Date.now()+(response.data.expires_in*1000)
});
return response.data.access_token;
});
}
}
return access_token.key;
}
};
})
Did you install cordova whitelist plugin ?
cordova plugin add cordova-plugin-whitelist
or if you want to save the reference to your config.xml file:
cordova plugin add cordova-plugin-whitelist --save
If you don't have that your device won't be able to access external resources.
You can find more info here.
UPDATE:
I've checked your previous answer.
The idea of the interceptor is to intercept calls to an external service insert some action in the pipeline.
I would change your interceptor:
$httpProvider.interceptors.push(['$location', '$injector', '$q', '$localStorage', function($location, $injector, $q, $localStorage){
return {
'request' : function(config) {
config.headers = config.headers || {};
access_token = $localStorage.getObject('access_token');
if (access_token) {
config.headers.Authorization = 'Bearer ' + access_token;
}
}
'response' : function(response){
if (response.status === 401) {
logger.debug("Response 401");
}
return response || $q.when(response);
}
'responseError' : function(rejection){
if (rejection.status === 401) {
var OauthService = $injector.get('OauthService');
var access_token = OauthService.token();
if (access_token === null)
{
return $q.reject(rejection);
}
// Append your access token to the previous request and re-submits.
rejection.config.headers['Authorization'] = 'Bearer ' + access_token;
return $injector.get('$http')(rejection.config);
}
// This is necessary to make a `responseError` interceptor a no-op.
return $q.reject(rejection);
}
}
}]);
If you look at the interceptor above it manages all the requests to an external resource (REST api) and appends a bearer token to the authorization header if needed.
The response does not do much as it is only there for logging purposes.
responseError is the place where you should intercept and check if your token as expired, fetch a new one and resubmit the request.
We check if the user is not authorized to the request:
if (rejection.status === 401) { ... }
If not we request a new access token. I guess your OauthService does that.
If we have a new access token:
var access_token = OauthService.token();
we can, again, append the access token to the request header:
rejection.config.headers['Authorization'] = 'Bearer ' + access_token;
and resubmit the previous request:
return $injector.get('$http')(rejection.config);
If you want to find out more about interceptors you can read these blogs.
Related
I'm fairly new to Angular and I'm trying to implement a mechanism for keeping active users logged in as long as they're active.
I have a token endpoint that issues a JWT token to a user
{
"access_token": "base64encodedandsignedstring",
"token_type": "bearer",
"expires_in": 299,
"refresh_token": "f87ae3bee04b4ca39af6f22a198274df",
"as:client_id": "mysite",
"userName": "me#email.com",
".issued": "Wed, 19 Apr 2017 20:15:58 GMT",
".expires": "Wed, 19 Apr 2017 20:20:58 GMT"
}
And another call that takes the refresh_token and uses it to generate a new access token. From the Api standpoint this should enable me to pass in the refresh_token and generate a new JWT with a new expires date.
I'm not 100% sure on how to wire up the Angular side to support this, my login function:
var _login = function (LoginData) {
var data = "grant_type=password&username=" + LoginData.UserName + "&password=" + LoginData.Password + "&client_id=4TierWeb";
var deferred = $q.defer();
$http.post(serviceBase + 'authToken', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).then(function (response) {
localStorageService.set('authorizationData', { token: response.data.access_token, userName: LoginData.userName, refreshToken: response.data.refresh_token, useRefreshTokens: true });
_authentication.isAuth = true;
_authentication.userName = LoginData.UserName;
deferred.resolve(response);
}, function (err, status) {
_logOut();
deferred.reject(err);
});
return deferred.promise;
};
My refresh function:
var _refreshToken = function () {
var deferred = $q.defer();
var authData = localStorageService.get('authorizationData');
if (authData) {
if (authData.useRefreshTokens) {
var data = "grant_type=refresh_token&refresh_token=" + authData.refreshToken + "&client_id=4TierWeb";
localStorageService.remove('authorizationData');
$http.post(serviceBase + 'authToken', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).then(function (response) {
localStorageService.set('authorizationData', { token: response.data.access_token, userName: response.data.userName, refreshToken: response.data.refresh_token, useRefreshTokens: true });
// response.headers.Authorization = 'Bearer ' + response.token;
deferred.resolve(response);
}, function (err, status) {
_logOut();
deferred.reject(err);
});
}
}
return deferred.promise;
};
And my interceptor:
app.factory('authInterceptorService', ['$q', '$location', 'localStorageService', function ($q, $location, localStorageService) {
var authInterceptorServiceFactory = {
request: function (config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
},
responseError: function (error) {
if (error.status === 401) {
$location.path('/login');
}
return $q.reject(error);
}
};
return authInterceptorServiceFactory;
}]);
My interceptor works great without the refresh mechanism in place as above, but when I add the refresh mechanism:
authService.RefreshToken();
config.headers.Authorization = 'Bearer ' + authData.token;
I'm able to pull down a new JWT but the next line doesn't seem to be working correctly anymore, I'm getting 401 on my landing page and there is no bearer token in the payload, what am I missing here?
Updated Interceptor:
app.factory('authInterceptorService',['$q', '$location', 'localStorageService', '$injector', function($q, $location, localStorageService, $injector) {
return {
request: function(config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
},
responseError: function(rejection) {
//var promise = $q.reject(rejection);
if (rejection.status === 401) {
var authService = $injector.get('authService');
// refresh the token
authService.refreshToken().then(function() {
// retry the request
var $http = $injector.get('$http');
return $http(rejection.config);
});
}
return $q.reject(rejection);
}
};
}
]);
You need to wait for the refresh_token request to complete obtaining a new access token and then use the response to issue a new request.
Like: authService.refreshToken().then(doRequest())
Lets suppose that you have 2 functions inside authService:
function getAccessToken() { ...get access token like in login()... } - returning Promise
function refreshToken() { ...existing logic... } - returning Promise
Let us say that you will use jwt_decode(jwt) to decode the JWT token.
I think you can go two ways with your implementation:
1st way: get the token and immediately subscribe in order to refresh when expired
function getAccessToken() {
...
return $http(...)
.then(function(response) {
// ...correct credentials logic...
if(authService.refreshTimeout) {
$window.clearTimeout(authService.refreshTimeout);
}
// decode JWT token
const access_token_jwt_data = jwt_decode(response.data.access_token);
// myOffset is an offset you choose so you can refresh the token before expiry
const expirationDate = new Date(access_token_jwt_data * 1000 - myOffset);
// refresh the token when expired
authService.refreshTimeout = $window.setTimeout(function() {
authService.refreshToken();
});
return response.data;
})
.catch(function(error) {
// ...invalid credentials logic...
return $q.reject(error);
});
}
NOTE: You can use window instead of $window. I don't think that you actually need a new digest cycle at that moment. A new digest will be launched when $http request completes successfully or not.
NOTE: This means that you need to take care also of the case when you reload the page. Thus re-enabling the refresh timeout. So you can reuse the logic within getAccessToken() for subscribing to expiry date but this time you get the token from the localStorage. This means that you can refactor this logic into a new function called something like function subscribeToTokenExpiry(accessToken). So you can call this function in your authService constructor if there is an access token in your localStorage.
2nd way: refresh the token in your HTTP interceptor after receiving an error code from server.
You can refresh your token if your interceptor receives an error that match a token expiry case. This depends strongly on your back-end implementation so you may receive HTTP 401 or 400 or anything else and some custom error message or code. So you need to check with your back-end. Also check if they are consistent in returning the HTTP statuses and error codes. Some implementation details might change over time and framework developers might advice users to not rely on that specific implementation because is only for internal use. In that case you can leave only the HTTP status and omit the code, as you will have better chances of having the same in the future. But ask your back-end or the ones that created the framework.
NOTE: regarding Spring OAuth2 back-end implementation, find the details at the end of this answer.
Getting back to your code, your interceptor should look like:
app.factory('authInterceptorService',
['$q', '$location', 'localStorageService', 'authService', '$injector',
function ($q, $location, localStorageService, authService, $injector) {
var authInterceptorServiceFactory = {
request: function (config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
},
responseError: function (response) {
let promise = $q.reject(response);
if (response.status === 401
&& response.data
&& response.data.error === 'invalid_token') {
// refresh the token
promise = authService.refreshToken().then(function () {
// retry the request
const $http = $injector.get('$http');
return $http(response.config);
});
}
return promise.catch(function () {
$location.path('/login');
return $q.reject(response);
});
}
};
return authInterceptorServiceFactory;
}]);
Spring Security OAuth2 back-end related:
I add this section for those curious about Spring Authorization Server implementation as Spring is a very popular framework in the Java world.
1) Expiry date
Regarding the expiry date, this is expressed in seconds. You will find the "exp" key inside your access_token and refresh_token after you JWT decode the string.
This is in seconds because you add the JwtAccessTokenConverter which uses DefaultAccessTokenConverter that does:
if (token.getExpiration() != null) {
response.put(EXP, token.getExpiration().getTime() / 1000);
}
JwtAccessTokenConverter is added when the Authorization Server is being configured:
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// ...
endpoints.accessTokenConverter(jwtAccessTokenConverter)
// ...
}
}
2) Access token expired response
You might need to handle one or both of HTTP 400 and HTTP 401 statuses and rely on { "error": "invalid_token" }. But this depends strongly on how the back-end was implemented using Spring.
See the explanations bellow:
Regarding, the resource server configuration (the one to which we send the request to in order to get the resource we want), the flow is as follows:
OAuth2AuthenticationProcessingFilter servlet filter to get access token from request
OAuth2AuthenticationManager to parse token string
DefaultTokenServices to obtain the access token object.
OAuth2AuthenticationProcessingFilter try catch will delegate the exceptions to OAuth2AuthenticationEntryPoint which creates the response for the exception.
DefaultTokenServices is a ResourceServerTokenServices implementation.
There are two possible such implementations, one is this DefaultTokenServices and the other is RemoteTokenServices.
If we use DefaultTokenServices then the token will be checked on the resource server. This means that the resource server has knowledge of the key that signed the token in order to check the token validity. This approach means distributing the key to all parties that want such behavior.
If we use RemoteTokenServices then the token will be checked against /oauth/check_token endpoint handled by CheckTokenEndpoint.
On expiry CheckTokenEndpoint will create an InvalidTokenException with HTTP 400, that will converted by OAuth2ExceptionJackson2Serializer into HTTP 400 with data { "error": "invalid_token", "error_description": "Token has expired" }.
On the other hand DefaultTokenServices will create also a InvalidTokenException exception but with other message and without overriding the HTTP status thus being HTTP 401 in the end. So this will become HTTP 401 with data { "error": "invalid_token", "error_description": "Access token expired: myTokenValue" }.
Again this, HTTP 400 or HTTP 401, happens because InvalidTokenException is thrown in both cases DefaultTokenServices throws without overriding getHttpErrorCode() which is 401 but CheckTokenEndpoint overrides it with 400.
Note: I added a Github Issue in order to check if this behavior, 400 vs 401, is correct.
I've used this interceptor at a couple of occasions without any problems.
You can set it up to refresh the token silently and only throw an error (and navigate to the login screen) if the refresh fails. Hope this helps
Is it secure to use a refresh token in an Angular application ? I'am not sure...
The OIDC implicit flow (which is the flow used for SPA or mobile apps), there is no refresh token involved.
I am using angularjs, In backend I check every api authenticated.
Every request should be checked the parameter access_token.
$provide.factory('MyHttpInterceptor', function($q, $location, $localStorage) {
return {
request : function(config) {
config.params = config.params || {};
if ($localStorage.access_token) {
config.params.access_token = $localStorage.access_token;
}
return config || $q.when(config);
},
};
});
// Add the interceptor to the $httpProvider.
$httpProvider.interceptors.push('MyHttpInterceptor');
I using this code. Its working good, but I saw in development tools(Network) the html, css, js files also added the parameters.
like.
http://localhost/webapp/views/template/left-menu.html?access_token=xxxxxxxxxxxxxxxxx
http://localhost/webapp/css/index.css?access_token=xxxxxxxxxxxxxxxxx
But I don't like to send access_token to all http request(html,css,js).
I like to send the access_token for what are have prefix api
http://localhost:9090/api/user/get?access_token=xxxxxxxxxxxxxxxxxxxxxx
//I think the solution is find the http url and grep the text api, if found means add the parameter. Don't konw this is good approach.
Please me the good approach.
I expect only backend api request only.
Also I don't expect every serive http request to add parameter.
Its possible to add common one place in config ?
You can check url:
$provide.factory('MyHttpInterceptor', function($q, $location, $localStorage) {
return {
request : function(config) {
var apiPattern = /\/api\//;
config.params = config.params || {};
if ($localStorage.access_token && apiPattern.test(config.url)) {
config.params.access_token = $localStorage.access_token;
}
return config || $q.when(config);
}
};
});
// Add the interceptor to the $httpProvider.
$httpProvider.interceptors.push('MyHttpInterceptor');
I have developed an Spring Web application using Angular JS. For the current project the client side authentication is using Cookies.
After learning the advantage JWTs,
I rewrote the application Authentication using JWTs(Json Web Token).
My concern is How to handle "Rember Me" Functionality using JWT in AngularJS with out using Cookies funtionality or Laravel support.
If anyone of you experts can share me suggestion or Sample code. It would be really helpful. I tried searching internet and failed to get an sample implementation to refer.
Thanks.
One of the option to store JWT at client side could be window.localStorage which stores data with no expiration date.
And after that, with each $http request(in Authentication header) you send this token to the server using Interceptor like following,
angular.module('myApp').factory('authInterceptor', ['$q', function ($q) {
return {
request: function (config) {
config.headers = config.headers || {};
if (config.headers.skipAuthorization === false) {
var token = localStorage.getItem('authenticationToken');
if (token != null) {
config.headers.Authorization = token;
}
}
return config;
},
response: function (response) {
if (response.headers("Authorization") != undefined || response.headers("Authorization") != '') {
localStorage.setItem('authenticationToken', response.headers("Authorization"));
}
return response;
},
responseError: function (rejection) {
if (rejection.status === "401") {
localStorage.removeItem('authenticationToken');
}
return $q.reject(rejection);
}
};
} ]);
angular.module('myApp').config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
} ]);
And with each request where you want this token to be sent to the server, set skipAuthorization:false in header as,
$http({
....
headers:{skipAuthorization:false}
}).then(....)
Im working with angularjs and Im trying to handle errors with interceptors.
I have run into the issue of how handling the different error session expires with login failed when server replies both with 401.
It seems that interceptors defined in the config will execute before any other interceptors (order definition matters):
var configInterceptor = function($httpProvider)
{
$httpProvider.interceptors.push('unauthorizedInterceptor');
};
angular.module('app', [])
.config(configInterceptor)
.controller....
The interceptors defined in the $resource will be considered only after they have gone through the configInterceptor.
var res = $resource(serviceUrl + '/users/login',{},{
post:{
method:'POST',
params: {},
withCredentials: true,
interceptor: {
responseError: function(){
console.log('login interceptor');
}
}
}
});
I would like to have a single point for controlling when the session has expired (pushing the user to the login page and sending an appropriate message) without the need to add the unauthorizedInterceptor to all $resources, one by one.
If the error is due to users trying to log in and failed, then the interceptor should treat it differently (message will be different).
Any way to resolve this properly? I tried also defining interceptors to only be applied to a specific module but they are triggered.
I would do something like that:
angular.module('app', []).factory('unauthorizedInterceptor', unauthorizedInterceptor);
function unauthorizedInterceptor($q, ngUserAuthService) {
return {
'responseError': function (response) {
if (response.status === 401) {
// find out if the session has expired or the user login has failed
if (sessionExpired()) {
doRedirectToSomewhere();
} else if (loginHasFailed()) {
response.loginFailed = true; // use this later
}
}
return $q.reject(response);
}
};
}
And then only add an interceptor to the $resource when you want to check for failed login:
var res = $resource(serviceUrl + '/users/login',{},{
post:{
method:'POST',
params: {},
withCredentials: true,
interceptor: {
responseError: function(response) {
console.log('login interceptor');
console.log('The login has failed: ' + response.loginFailed);
}
}
}
});
I am using the Django REST token authentication for my API.
I posted my credentials to obtain token endpoint. However when I try to set the header in a correct way it keeps responding with a http 401 error. I tried it using curl -X GET http://127.0.0.1:8000/events/ -H 'Authorization: Token 4d92d36768ca5d555b59cf68899eceab39c23704 ' and that does work! This is my code:
app.controller('HomeController', ['$scope','$http', function($scope,$http) {
$scope.username = '';
$scope.password = '';
$scope.submitLogin = function () {
var credentials = {
username : $scope.username,
password : $scope.password,
};
var req = $http.post('http://127.0.0.1:8000/api-token-auth/', credentials);
req.success(function(data, status, headers, config) {
$scope.token = data.token;
var str1 = 'Token ';
$scope.tokenheader = str1.concat($scope.token);
$http.defaults.headers.common.Authorization = $scope.tokenheader;
});
req.error(function(data, status, headers, config) {
alert( "failure message: " + JSON.stringify({data: data}));
});
};
$scope.getEvents = function () {
var req = {
method: 'GET',
url: 'http://127.0.0.1:8000/events/',
}
$http(req).then(
function() {
console.log('succes')
},
function(){
console.log('fail')
});
};
}]);
And the error message in chrome dev tools:
XMLHttpRequest cannot load http://127.0.0.1:8000/events/.
Response for preflight has invalid HTTP status code 401
How do I get rid of this 401 error?
Edit: I just found out the fault lies in the fact that I did not have CORS installed on my API. I was using a CORS plugin in chrome that worked for the authentication part of my api but not for my events url!
Did you check that the token is actually added to your request?
You can do this for example using the Chrome developers tools.
Personally I prefer to use the $httpprovider.interceptor as described in:
angularjs $httpProvider interceptor documentation
This ensures that the tokens are always present on any call.
If you are accessing more than one API, you should consider adding something like:
$httpProvider.interceptors.push(['$q', '$location', '$log', 'loginService', 'restHelperService',
function ($q, $location, $log, loginService, restHelperService) {
return {
request: function (config) {
// check if the request comes with an url
if (config.url) {
// check that the call is to the REST api, if yes add token
if (restHelperService.isRestCall(config.url)) {
// add auth header or revert to login
if (loginService.userIsLoggedIn()) {
config.headers = config.headers || {};
config.headers.Authorization = 'Token ' + loginService.getToken().token;
} else {
$location.path('/login');
}
}
}
return config;
},
responseError: function (response) {
if (response.status === 401 || response.status === 403) {
// clear auth token if the REST call failed with the current token
if (response.config && response.config.url && restHelperService.isRestCall(response.config.url)) {
$log.debug(" restCall failed due to bad credentials, resetting credentials");
loginService.resetCredentials();
$location.path('/login');
}
}
return $q.reject(response);
}
};
}]);
}])
This avoid issues that will arise when you start adding the token to API calls that don't expect them. Also the code ensures that a user will be automatically redirected to the login page if the credentials are not valid.
The example, I'm using two additional services. A loginService that manages the tokens and a restHelperService that manages the urls of the REST framework.
I would recommend doing the same as else it will be hard to access the credentials from outside your controller.
You need to add Token to the headers:
get($http, "/some_url", {headers: {"Authorization": "Token " + $your_token}}
....
....
);
Response code 401 means Unauthorized. If you are using Token based authentication then in case of fail it would be 403, Forbidden.
So my guess would be that it's username/password who is messing with it. In your curl example you are not using them.