my Angular client calls a web api function which checks if the user is authorized by password and user name. Here is the relevant beginning of the method:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
var userManager = context.OwinContext.Get<DividendsManagerUserManager>();
if (allowedOrigin == null) allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
var user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "Der Benutzername oder das Passwort ist ungültig.");
return;
}
As you see, I use context.SetError in case the provided login is invalid. Doing so returns this message as seen in Fiddler:
HTTP/1.1 400 Bad Request
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Expires: -1
Server: Microsoft-IIS/10.0
Access-Control-Allow-Origin: *
X-Powered-By: ASP.NET
Date: Fri, 18 Nov 2016 12:49:10 GMT
Content-Length: 97
{"error":"invalid_grant","error_description":"Der Benutzername oder das Passwort ist ungültig."}
Looks good to me. But in Angular, the api call "succeeds", because the success function callback is executed:
var _login = function(loginData) {
var data = "grant_type=password&username=" + loginData.userName + "&password=" + loginData.password + "&client_id=" + authSettings.clientId;
var deferred = $q.defer();
$http.post(serviceBase + 'token', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).success(function(response) {
localStorageService.set('authorizationData', { token: response.access_token, userName: loginData.userName, refreshToken: response.refresh_token, useRefreshTokens: true });
fillAuthData();
deferred.resolve(response);
}).error(function(err, status) {
_logOut();
deferred.reject(err);
});
return deferred.promise;
};
Anyone knows what wents wrong?
Regards,
Torsten
P.S.: Within the success function I could check if the response contains an error. But that feels like a bad hack.
In a promise chain, rejections can be converted to success by returning values to the rejection handler. This makes it possible to fix problems and retry an operation. But it can also lead to bugs related to unwanted conversions.
promise.then(function successHandler(value) {
//return to chain value
return value;
}).catch(function rejectHandler(errorValue) {
//to avoid conversion
//throw to chain rejection
throw errorValue;
//OR return rejected promise
//return $q.reject(errorValue);
});
One of the most common causes of unwanted conversions is failing to return or throw anything:
//ERRONEOUS
function responseError(errorResponse) {
console.log(errorResponse.status);
}
In the above example, the rejection handler erroneously converts the rejection to a success. When a function omits either a return statement or a throw statement, the function returns undefined. This causes the rejection to be converted to a success that resolves with a value of undefined.
So when debugging erroneous convertions, check the rejection handlers. Then look for a badly behaving interceptor.
I just found the source of the error. I wrote an interceptor. My intention was just to set a property of a service I injected into the interceptor. Because of this, I just did not manipulate the rejection in any way and just passed it through like this:
function responseError(rejection) {
busyService.isBusy = true;
return rejection;
}
But this seems to make the $http.post a success.
If I do this the $http.post is an error (as wanted):
function responseError(rejection) {
busyService.isBusy = true;
var deferred = $q.defer();
deferred.reject(rejection);
return deferred.promise;
}
I still don't know why and will have to further investigate. If someone can further explain, this would be nice.
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'm trying to implement some error handling into my MCV AngularJS application, but came across this one issue that I'm not sure how to solve.
Structure
In my AngularJS service ticketService I have the following method:
this.downloadFile = function (fileId) {
return $http.get(baseUrl + "/Downloadfile/" + fileId, { responseType: "blob" });
}
And in my controller:
$scope.downloadFile = function (fileId) {
ticketService.downloadFile(fileId)
.then(function (response) {
// Handle correct request and response
}, function (err) {
// Handle error
notify({ message: "Something went wrong: " + err.data.Message, position: "center", duration: 10000 });
})
}
Here's what I return from the backend MVC Web API method:
var error = new HttpError("Failed to find file, bla bla bla.");
return Request.CreateResponse(HttpStatusCode.NotFound, error);
Problem
My issue is that since my responseType is set to be blob, my err object is the same response type. I would believe that it should be possible for my backend service to override this response type, and respond with an object that contains some Message.
From this response, I would've thought that I could get err.data.Message, but perhaps I misunderstood this scenario?
Thank you in advance.
public HttpResponseMessage Get(int id)
{
try
{
return Request.CreateResponse(HttpStatusCode.OK, new { Status =
"OK", Message = this._myContext.GetCustomer(id) });
}
catch(Exception e)
{
return Request.CreateResponse(HttpStatusCode.Conflict, new {
Status = "NO", Message = e.ToString() });
}
}
You can return any message like "Failed to find file, bla bla bla." in Message. Then you just need to check in ajax success method like data.Message,
The $http service uses the XHR API which is not capable of changing the responseType on the fly.
You can set the status message and use that:
public ActionResult Foo()
{
Response.StatusCode = 403;
Response.StatusDescription = "Some custom message";
return View(); // or Content(), Json(), etc
}
Then in AngularJS:
$scope.downloadFile = function (fileId) {
return ticketService.downloadFile(fileId)
.then(function (response) {
// Handle correct request and response
return response.data;
}).catch(function (response) {
// Handle error
console.log(response.status);
console.log(response.statusText);
throw response;
});
};
Alternative approaches are:
Use the Fetch API which has a more powerful and flexible feature set.
Use the FileReader API and JSONparse() method to convert the Blob to a JavaScript Object.
I have a controller which uses the following line to post data to server through a factory called SendDataFactory:
SendDataFactory.sendToWebService(dataToSend)
And my factory SendDataFactory looks like this:
angular
.module('az-app')
.factory('SendDataFactory', function ($http, $q) {
var SendData = {};
/**
* Sends data to server and
*/
SendData.sendToWebService = function (dataToSend) {
var url = "example.com/url-to-post";
var deferred = $q.defer();
$http.post(url, dataToSend)
//SUCCESS: this callback will be called asynchronously when the response is available
.then(function (response) {
console.log("Successful: response from submitting data to server was: " + response);
deferred.resolve({
data: data
});
},
//ERROR: called asynchronously if an error occurs or server returns response with an error status.
function (response) {
console.log("Error: response from submitting data to server was: " + response);
deferred.resolve({
data: data
});
}
);
return deferred.promise;
}
return SendData;
});
I have seen some examples in here and the internet with
$http.post().success...
but I want to use
$http.post().then...
since angular documentation says:
The $http legacy promise methods success and error have been deprecated. Use the standard then method instead. If $httpProvider.useLegacyPromiseExtensions is set to false then these methods will throw $http/legacy error.
What I need:
Now in my controller I need to check if the $http.post().then... was successful or not and then do something based on success or fail. How can I achieve this?
I think this is what you meant:
$http.post(url, dataToSend)
//SUCCESS: this callback will be called asynchronously when the response is available
.then(function (response) {
console.log("Successful: response from submitting data to server was: " + response);
deferred.resolve({
data: response //RETURNING RESPONSE SINCE `DATA` IS NOT DEFINED
});
},
//ERROR: called asynchronously if an error occurs or server returns response with an error status.
function (response) {
console.log("Error: response from submitting data to server was: " + response);
//USING THE PROMISE REJECT FUNC TO CATCH ERRORS
deferred.reject({
data: response //RETURNING RESPONSE SINCE `DATA` IS NOT DEFINED
});
}
);
return deferred.promise;
}
In your controller you now can use:
SendDataFactory.sendToWebService(dataToSend)
.then(function(data) { /* do what you want */ })
.catch(function(err) { /* do what you want with the `err` */ });
Reject the promise instead of resolving it when it is rejected by $http.
/**
* Sends data to server and
*/
SendData.sendToWebService = function (dataToSend) {
var url = "example.com/url-to-post";
var deferred = $q.defer();
$http.post(url, dataToSend)
//SUCCESS: this callback will be called asynchronously when the response is available
.then(function (response) {
console.log("Successful: response from submitting data to server was: " + response);
deferred.resolve(response.data); // Resolving using response.data, as data was not defined.
},
//ERROR: called asynchronously if an error occurs or server returns response with an error status.
function (response) {
console.log("Error: response from submitting data to server was: " + response);
deferred.reject(response.data); // Rejecting using response.data, as data was not defined.
}
);
return deferred.promise;
}
You can then call it from your controller the same way as you handle the callback in the service using then.
Since $http returns a promise, it can be simplified further though using promise chaining. This way there is no need to use an extra deferred object.
/**
* Sends data to server and
*/
SendData.sendToWebService = function (dataToSend) {
var url = "example.com/url-to-post";
return $http.post(url, dataToSend)
//SUCCESS: this callback will be called asynchronously when the response is available
.then(function (response) {
console.log("Successful: response from submitting data to server was: " + response);
return response.data; // Resolving using response.data, as data was not defined.
},
//ERROR: called asynchronously if an error occurs or server returns response with an error status.
function (response) {
console.log("Error: response from submitting data to server was: " + response);
return $q.reject(response.data); // Rejecting using response.data, as data was not defined.
}
);
}
The return of the post is falling through the error callback yet the json I'm expecting is being returned by the api call. It is visible when I console.log the error object. I'll post the error log after the code.
The payload (userCreds) is json. Using postman, the api call returns as expected
Here's the post call:
var deferred = $q.defer();
$http.post(url, userCreds)
.then(function(data, status) {
$log.info('validateLogin status: ' + status)
if(data){
requestor = data;
}
deferred.resolve(requestor);
}, function (data, status) {
var error = data || "Request failed";
$log.error('validateLogin error: ' + JSON.stringify(error));
deferred.reject(error);
});
return deferred.promise;
The error object has the correct response in it with a 302 status. I noticed the transformRequest and transformResponse were null. I don't recall having to define these in the past. I thought angular automatically dealt with strings and javascript objects during transformations.
{"data":{"$id":"1","innCodes":[],"userTypeId":0,"formId":0,"onqUserId":null,"fullName":"User Smith","firstName":"User","lastName":"Smith","phone":"214-555-4450","email":"user#email.com","userId":null,"password":null,"title":"Project Manager","fax":null,"mobile":null,"role":null},"status":302,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"url":"http://localhost:25396/api/user/ValidateCredentials/","data":{"userName":"userid1234","password":"pwd123456"},"headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json;charset=utf-8"}},"statusText":"Found"}
Ended up being the valid api response 302 (found) that was causing the quirky behavior. I was able to get the status code changed to 200 (OK) and now everything is working as expected. The 302 status was causing angular to reject the promise.
This line of code in angular v 1.2.26
return (isSuccess(response.status))
? resp
: $q.reject(resp);
I've read several of the other posts about this problem and none of the solutions seem to be working for me. I have the following code in my View:
this.model.set({
username: $('#user-username').val(),
role: $('#user-role').val(),
description: $('#user-description').val()
});
this.model.save({ user_id: this.model.get('user_id')}, {
success: function(user, response) {
console.log('success:', response);
$('.flash-message').text("Success").show();
},
error: function(user, response) {
console.log('error:', response);
$('.flash-message').text(response.error).show();
}
});
and this on my server controller (nodejs running express 3):
UserController.prototype.updateAction = function(req, res) {
if (req.route.method != "put") {
res.send({status: "error", error: "update must be put action and must include values"});
return false;
}
var query = {'user_id': req.params.id};
var user = req.body;
var userRepository = this.userRepository
// delete _id to avoid errors
delete user._id;
userRepository.update(query, user, {}, function(err, updated) {
if ((err) || (!updated)) {
res.send({"status": "error", "error": err});
return false;
}
// send updated user back
util.log('updated user ' + user.user_id);
res.setHeader('Content-Type', 'application/json');
res.status(200);
res.send(JSON.stringify({"status": "success", "updated": updated}));
});
}
On save, my model is saved correctly in the server and I have verified the server response with this. So, as far as I can tell the server is returning status 200, valid JSON, with a valid JSON response header. And yet my backbone model.save function always triggers the error callback. Can anyone please tell me why and how to resolve this?
I am able to get this to work if set the dataType to text like so:
this.model.save({ user_id: this.model.get('user_id')}, {
dataType: "text",
success: function(user, response) {
console.log('success:', response);
$('.flash-message').text("Success").show();
},
error: function(user, response) {
console.log('error:', response);
$('.flash-message').text(response.error).show();
}
});
but doing so does not allow me to get the response back from the server. Instead I get this in the response var:
success: {
"_id": "5133b02062e15ed1d2000001",
}
Backbone expects to get back the model that it sent in its PUT or POST request body.
Instead of:
res.send(JSON.stringify({"status": "success", "updated": updated}));
Try this in your server's response:
res.json(user);
There may be a possibility that your call may have got in state 200 connection established which backbone detects as error, Backbone throws success only when the call is 200OK.
What's your server code? You need to make sure you're sending json back to backbone like so:
//In your express POST route
user.save(function(err) {
if(err){
console.log(err);
return res.json(401);
} else {
console.log('user: ' +user.username + ' saved');
return res.json(200);
}
Then in your backbone view you can check for the response and do what you need:
//some function in your view
this.model.save(this.formValues, {
success: function(model, response, options) {
if (response == 200) {
console.log('success :' + response);
//Do stuff
} else {
console.log('error: '+response);
//etc.
Also note that as per the backbone model documentation:
"save accepts success and error callbacks in the options hash, which will be passed the arguments (model, response, options)"