I have a REST web service that uses OAuth 2 for authenticating and authorizing requests.
I have an endpoint, that when receiving the correct credentials, responds with an access token that will be used in consequent requests.
This avoids persisting a session and using cookies, which is what we wanted to accomplish when we started designing our web service.
Along with the access token a request token is also received from the response. This refresh token is used for getting a new access token when the original one has expired.
With short timespan and token refreshing, an user can't authenticate as another user just by stealing the access token (theorically it can, but for a short period of time, i.e. until it's refreshed).
I have created the following provider for AngularJS:
var restProvider = function () {
var _enabled = true;
var _authUrl = '';
var _clientId = '';
var _sessionLife;
// Returns whether the service is available or not
var _supported = function () {
return _enabled && _authUrl;
}
var _fetchTokens = function (http, deferred, url, credentials, storage) {
credentials.client_id = _clientId;
credentials.grant_type = 'password';
http.post(url, credentials, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: function (data, headers) {
var str = [];
for(var p in data)
str.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
return str.join('&');
}
}).then(function (successResponse) {
storage.save('access_token', successResponse.data.access_token, {
expiration: successResponse.data.expires_in;
});
storage.save('refresh_token', successResponse.data.refresh_token, {
expiration: _sessionLife;
});
storage.save('token_type', successResponse.data.token_type);
deferred.resolve(successResponse);
}, function (errorResponse) {
deferred.reject(errorResponse);
});
}
return {
//The following methods are for configuration only
//If no arguments are provided, returns the property's value
//If arguments are provided, returns "this" for chaining multiple methods
//val: Boolean
enabled: function (val) {
if(val === undefined || val === null) return _enabled;
_enabled = val;
return this;
},
//endpoint: String
authorizationEndpoint: function (endpoint) {
if(!endpoint) return _authUrl;
_authUrl = endpoint;
return this;
},
//clientId: String
client: function (clientId) {
if(!clientId) return _clientId;
_clientId = clientId;
return this;
},
//seconds: Number
sessionLife: function (seconds) {
if(!seconds) return _sessionLife;
_sessionLife = seconds;
return this;
},
//"storage" and "logger" are injected services of mine
$get: ['$http', '$q', 'storage', 'logger', function ($http, $q, storage, logger) {
return {
signIn: function (username, password) {
var deferred = $q.defer();
if(username && password && _supported())
_fetchTokens($http, deferred, _authUrl, { username: username, password: password }, storage);
else
deferred.reject();
return deferred.promise;
}
}
}
}
}
As you can see it's a very long code and it's even incomplete, this is only the authenticating function.
_fetchTokens
makes a POST request to my web service's OAuth endpoint and retrieves the tokens form the response. It then proceeds to persist them (For example, localStorage) so they can be used for later requests and continue to live on even if the application is closed.
signIn
is the public function that the client will see from outside. It simply creates the deferred object and calls _fetchTokens
.
My problem is that consequent requests should send the access token (on the Authorization header). If a request is sent with an expired token, a 401 Unauthorized is returned, so this service must proceed to try to refresh the token. After successfully refreshing the token, it shall retry the original request again but with the new access token.
This means that a simple line of code from the client:
service.get('api.example.com/products');
Should do all this behind the scenes.
With this approach I think there's going to be a lot of replicated code, so I was looking for help on how to better structure my code. Not only for better quality but also for better readability.
I suppose such a service is not that uncommon so I hope that many people has experienced this type of situations.