mirror of
https://github.com/duplicati/duplicati.git
synced 2025-11-28 19:40:25 +08:00
This adds a nonce to the refresh token such that each request to obtain a refresh token must now also provide a matching nonce. When using non-persisted logins, the request to the server is the same, but the "remember me" flag toggles a shorter duration for the refresh token. The FE can then store the nonce in either local storage for persisted logins or in session storage for non-persisted logins. The default is currently to always issue refresh tokens with a nonce, but this can be toggled with the JWT configuration. The ngax client does not have the non-persisted login so it stores the nonce in local storage, using a name that is compatible with ngclient so the user can swap between them without needing to re-login. The server util was updated to also store the nonce. This fixes #6451
225 lines
8.6 KiB
JavaScript
225 lines
8.6 KiB
JavaScript
backupApp.service('AppService', function ($http, $cookies, $q, $cookies, DialogService, appConfig) {
|
|
this.apiurl = '../api/v1';
|
|
this.access_token = null;
|
|
this.access_token_promise = null;
|
|
|
|
const self = this;
|
|
|
|
function loginRequired() {
|
|
DialogService.dismissAll();
|
|
DialogService.accept('Not logged in', function () {
|
|
window.location = appConfig.login_url;
|
|
});
|
|
}
|
|
|
|
|
|
var setupConfig = function (method, options, data, targeturl) {
|
|
options = options || {};
|
|
options.method = options.method || method;
|
|
options.responseType = options.responseType || 'json';
|
|
options.headers = options.headers || {};
|
|
if (options.headers['Content-Type'] == null)
|
|
options.headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
if (self.access_token != null)
|
|
options.headers['Authorization'] = `Bearer ${self.access_token}`;
|
|
|
|
// Disable cache in IE
|
|
if (method == "GET" || method == "HEAD") {
|
|
options.headers['Cache-Control'] = 'no-cache';
|
|
options.headers['Pragma'] = 'no-cache';
|
|
}
|
|
|
|
if (($cookies.get('ui-locale') || '').trim().length > 0)
|
|
options.headers['X-UI-Language'] = $cookies.get('ui-locale');
|
|
|
|
return options;
|
|
};
|
|
|
|
// Input is a function that returns a promise (e.g. $http.get)
|
|
// The function is called and the promise is returned, but if the response is 401,
|
|
// the current token is removed and a new is obtained
|
|
// If the new token is obtained, the input function is called again, retrying the operation
|
|
// If the new token is not obtained, the user is redirected to the login page
|
|
var installResponseHook = function (promiseAction) {
|
|
var deferred = $q.defer();
|
|
|
|
promiseAction().then(
|
|
response => { deferred.resolve(response) },
|
|
|
|
response => {
|
|
if (response.status == 401) {
|
|
|
|
// If we have a token, we should remove it and obtain a new one
|
|
if (self.access_token != null) {
|
|
self.access_token = null;
|
|
self.access_token_promise = null;
|
|
}
|
|
|
|
// If we are already getting a token, this call will return a shared promise
|
|
self.getAccessToken().then(
|
|
() => {
|
|
// Got a new token, retry the operation
|
|
promiseAction().then(
|
|
response2 => deferred.resolve(response2),
|
|
response2 => {
|
|
deferred.reject(response2);
|
|
}
|
|
);
|
|
},
|
|
() => {
|
|
// Fail, but report the original failed response, not the refresh response
|
|
deferred.reject(response);
|
|
}
|
|
);
|
|
|
|
} else {
|
|
// Non-authentication error
|
|
deferred.reject(response);
|
|
}
|
|
}
|
|
);
|
|
|
|
return deferred.promise;
|
|
};
|
|
|
|
this.clearAccessToken = function () {
|
|
self.access_token = null;
|
|
self.access_token_promise = null;
|
|
}
|
|
|
|
// Returns a promise that resolves to the access token
|
|
this.getAccessToken = function () {
|
|
if (self.access_token != null) {
|
|
var deferred = $q.defer();
|
|
deferred.resolve(this.access_token);
|
|
return deferred.promise;
|
|
|
|
} else if (self.access_token_promise != null) {
|
|
return self.access_token_promise;
|
|
} else {
|
|
var deferred = $q.defer();
|
|
self.access_token_promise = deferred.promise;
|
|
|
|
const storedNonce = localStorage.getItem('v1:persist:duplicati:refreshNonce');
|
|
const body = storedNonce ? { Nonce: storedNonce } : undefined;
|
|
|
|
$http.post(self.apiurl + '/auth/refresh', body)
|
|
.then(function (response) {
|
|
self.access_token = response.data.AccessToken;
|
|
self.access_token_promise = null;
|
|
if (response.data.RefreshNonce)
|
|
localStorage.setItem('v1:persist:duplicati:refreshNonce', response.data.RefreshNonce);
|
|
deferred.resolve(self.access_token);
|
|
}, function (response) {
|
|
// Failed to get a new token, clear the old one
|
|
self.access_token = null;
|
|
self.access_token_promise = null;
|
|
|
|
// Auth error, refresh token invalid
|
|
if (response.status == 401)
|
|
loginRequired();
|
|
|
|
deferred.reject(response);
|
|
});
|
|
|
|
return deferred.promise;
|
|
}
|
|
}
|
|
|
|
this.get = function (url, options) {
|
|
let rurl = url;
|
|
if (!url.startsWith('http')) {
|
|
rurl = this.apiurl + url;
|
|
}
|
|
|
|
return this.getAccessToken().then(() => installResponseHook(() => $http.get(rurl, setupConfig('GET', options, null, rurl))));
|
|
};
|
|
|
|
this.patch = function (url, data, options) {
|
|
var rurl = this.apiurl + url;
|
|
options = options || {};
|
|
return this.getAccessToken().then(() => installResponseHook(() => $http.patch(rurl, data, setupConfig('PATCH', options, data, rurl))));
|
|
};
|
|
|
|
this.post = function (url, data, options) {
|
|
var rurl = this.apiurl + url;
|
|
options = options || {};
|
|
return this.getAccessToken().then(() => installResponseHook(() => $http.post(rurl, data, setupConfig('POST', options, data, rurl))));
|
|
};
|
|
|
|
this.postJson = function (url, data, options) {
|
|
var rurl = this.apiurl + url;
|
|
options = options || {};
|
|
options.headers = options.headers || {};
|
|
options.headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
return this.getAccessToken().then(() => installResponseHook(() => $http.post(rurl, data, setupConfig('POST', options, data, rurl))));
|
|
};
|
|
|
|
this.put = function (url, data, options) {
|
|
var rurl = this.apiurl + url;
|
|
options = options || {};
|
|
return this.getAccessToken().then(() => installResponseHook(() => $http.put(rurl, data, setupConfig('PUT', options, data, rurl))));
|
|
};
|
|
|
|
this.delete = function (url, options) {
|
|
var rurl = this.apiurl + url;
|
|
options = options || {};
|
|
return this.getAccessToken().then(() => installResponseHook(() => $http.delete(rurl, setupConfig('DELETE', options, null, rurl))));
|
|
};
|
|
|
|
this.get_export_url = function (backupid, passphrase, exportPasswords) {
|
|
var deferred = $q.defer();
|
|
this.post("/auth/issuetoken/export").then(
|
|
resp => {
|
|
var rurl = this.apiurl + '/backup/' + backupid + '/export';
|
|
rurl += '?export-passwords=' + encodeURIComponent(exportPasswords);
|
|
rurl += '&token=' + encodeURIComponent(resp.data.Token);
|
|
if ((passphrase || '').trim().length > 0)
|
|
rurl += '&passphrase=' + encodeURIComponent(passphrase);
|
|
|
|
deferred.resolve(rurl);
|
|
},
|
|
resp => deferred.reject(resp)
|
|
);
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
|
|
this.get_bugreport_url = function (reportid) {
|
|
var deferred = $q.defer();
|
|
this.post("/auth/issuetoken/bugreport").then(
|
|
resp => {
|
|
var rurl = this.apiurl + '/bugreport/' + reportid;
|
|
rurl += '?token=' + encodeURIComponent(resp.data.Token);
|
|
deferred.resolve(rurl);
|
|
},
|
|
resp => deferred.reject(resp)
|
|
);
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
this.responseErrorMessage = function (resp) {
|
|
if (resp == null) {
|
|
return '';
|
|
}
|
|
if (typeof resp === 'string' || resp instanceof String) {
|
|
return resp;
|
|
}
|
|
var message = resp.statusText;
|
|
if (resp.data != null) {
|
|
// Different ways to communicate error message (this should be refactored in the server at some point)
|
|
if (resp.data.Message != null) {
|
|
message = resp.data.Message;
|
|
} else if (resp.data.Error != null) {
|
|
message = resp.data.Error;
|
|
} else if (resp.data.message != null) {
|
|
message = resp.data.message;
|
|
} else if (resp.data.reason != null) {
|
|
message = resp.data.reason;
|
|
}
|
|
}
|
|
return message;
|
|
};
|
|
});
|