I've built an API using C# that uses JWT tokens for authorization. On the frontend I store these tokens in local storage and get them, when creating a request. When creating GET or DELETE requests, everything works fine, and using console.log() I can see that fetch options have the Authorization header added. However when using POST or PATCH methods, the Authorization header is missing immediatly after adding it to the object. Here is my request method:
const send = async (apiOptions: ApiParams): Promise<FetchReturn> => {
const accessToken = GetAccessToken()
const options: ApiOptions = {
method: apiOptions.method,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
console.log(options)
if (apiOptions.data) {
options.headers = {
'Content-Type': 'application/json'
}
options.body = JSON.stringify(apiOptions.data)
}
const result = await fetch(`${getUrl()}/${apiOptions.path}`, options).then(res => res).catch(err => err)
if (!result.ok) {
if (IsExpired()) {
const refreshResult = await fetch(`${getUrl()}/api/user/refresh`, {method: 'POST', headers:{
'Content-Type': 'application/json'
}, body: JSON.stringify(GetRefreshRequest())}).then(res => res).catch(err => err)
if (refreshResult.ok) {
Login(JSON.parse(await refreshResult.text()))
return await send(apiOptions)
} else if (refreshResult.status === 401) {
Logout()
window.location.reload()
return { code: 0, text: ""}
}
}
}
const text = await result.text()
return { code: result.status, text: text }
}
I suppose that in apiParams for POST you have property 'data' assigned, and later you have if-condition that completely replaces request headers object.
Change it to:
options.headers['Content-Type'] = 'application/json';
To keep authorization in headers
The first time check your apiOptions.data
i think , its null when you call POST/Patch request
Just put console.log("...") In the if statement , Then try for resolve your Error
If your problem not resolved, put a replay under my post
I use a spring boot with angular .right now is i have a method which allows to bring the data from api service every second but but a message that is displayed:
core.js:6185 ERROR TypeError: You provided 'undefined' where a stream
was expected. You can provide an Observable, Promise, Array, or
Iterable.
//service
ListAutombilesAuto(){
let headers=new HttpHeaders({'Authorization':this.jwt});
return this.fiches=this.http.get(this.BaseUrl+'/Automobiles',{headers:headers}).pipe
(
merge(this.fiches, interval(1000).pipe(switchMap(() => this.fiches)))
);
}
//component
ListAuto(){
this.auth.ListAutombilesAuto().subscribe(data=>{
this.fiches=data;
console.log(this.fiches);
}) // api call
}
is there any solutions?
thank you for help
You are referencing this.fiches within its first assignment, therefore its value is still undefined.
Try fix your code as following:
ListAutombilesAuto() {
let headers = new HttpHeaders({ 'Authorization': this.jwt });
this.fiches = this.http.get(this.BaseUrl + '/Automobiles', { headers: headers });
return this.fiches.pipe
(
merge(this.fiches, interval(1000).pipe(switchMap(() => this.fiches)))
);
}
But actually why not keep it simple:
ListAutombilesAuto() {
let headers = new HttpHeaders({ 'Authorization': this.jwt });
return timer(0, 1000)
.pipe(
switchMap(() => this.http.get(this.BaseUrl + '/Automobiles', { headers }))
)
}
When the token expires, I want to get a new token based on refresh_token. I have read that this can be obtained with axios.interceptors.
Please check if:
Have I correctly configured axios.interceptors?
Have I placed it in the right place, i.e. above theItems class.
axios.interceptors.response is assigned to theinterceptor variable. What should I do with this variable?
In addition to `axios.interceptors', I need to get a new token. The token is valid for 24 hours.
Do I have to wait 24 hours to test whether it works, or is it possible in a different way, faster?
Where should I put 'client_id', 'secret_id', 'grant_type'?
Code here: https://stackblitz.com/edit/react-pkea41
import axios from 'axios';
axios.defaults.baseURL = localStorage.getItem('domain');
const interceptor = axios.interceptors.response.use(
response => response,
error => {
// Reject promise if usual error
if (errorResponse.status !== 401) {
return Promise.reject(error);
}
/*
* When response code is 401, try to refresh the token.
* Eject the interceptor so it doesn't loop in case
* token refresh causes the 401 response
*/
axios.interceptors.response.eject(interceptor);
return axios.post('/api/refresh_token', {
'refresh_token': JSON.parse(localStorage.getItem('token'))['refresh_token']
}).then(response => {
/*saveToken();*/
localStorage.setItem('token', JSON.stringify(response.data));
error.response.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
return axios(error.response.config);
}).catch(error => {
/*destroyToken();*/
localStorage.setItem('token', '');
this.router.push('/login');
return Promise.reject(error);
}).finally(createAxiosResponseInterceptor);
}
);
class Items extends Component {
constructor (props) {
super(props);
this.state = {
}
}
render () {
return (
<div >
</div>
)
}
}
render(<Items />, document.getElementById('root'));
This is what I did before. Your configuration is a little different from mine.
const baseURL = localStorage.getItem('domain');
const defaultOptions = {
baseURL,
method: 'get',
headers: {
'Content-Type': 'application/json',
}
};
// Create Instance
const axiosInstance = axios.create(defaultOptions);
// Get token from session
const accessToken = ...
// Set the auth token for any request
instance.interceptors.request.use(config => {
config.headers.Authorization = accessToken ? `Bearer ${accessToken}` : '';
return config;
});
// Last step: handle request error general case
instance.interceptors.response.use(
response => response,
error => {
// Error
const { config, response: { status } } = error;
if (status === 401) {
// Unauthorized request: maybe access token has expired!
return refreshAccessToken(config);
} else {
return Promise.reject(error);
}
}
});
I think this part should be separated with Components - it will be placed on helpers or utils.
Also, you have to wait for 24 hrs because refreshToken() method is never called before 24 hrs.
You don't need to process client_id, secret_id, grant_type right here.
Please check if I have correctly configured axios.interceptors.
I think it works. But I suggest that you should test it carefully.This is a good article to refer https://blog.liplex.de/axios-interceptor-to-refresh-jwt-token-after-expiration/
Have I placed it in the right place, i.e. above theItems class. ?
You should create a service function to wrap Axios and API configs,and interceptor of course
axios.interceptors.response is assigned to the interceptor variable. What should I do with this variable?
It is just a variable used to define the interceptor. Don't care about it. If you want to avoid assigning it, just do it inside a function like this Automating access token refreshing via interceptors in axios
I have to wait 24 hours to test whether it works, or is it possible in a different way, faster?
You can change the token saved in your localStorage, and do that
Where should I put 'client_id', 'secret_id', 'grant_type'?
If you store it inside localStorage, it's accessible by any script inside your page (which is as bad as it sounds as an XSS attack can let an external attacker get access to the token).
Don't store it in local storage (or session storage). If any of the 3rd part scripts you include in your page gets compromised, it can access all your users' tokens.
The JWT needs to be stored inside an HttpOnly cookie, a special kind of cookie that's only sent in HTTP requests to the server, and it's never accessible (both for reading or writing) from JavaScript running in the browser.
Please check if I have correctly configured axios.interceptors.
From what I can see the configuration seems ok, as it's the same of this answer https://stackoverflow.com/a/53294310/4229159
Have I placed it in the right place, i.e. above theItems class. ?
That is something that I can't answer, every application is different, it's not the best place to put it, but might be OK for an example. In your app however it should be together with all the API calls (for example)
axios.interceptors.response is assigned to theinterceptor variable. What should I do with this variable?
As you can see, the variable that got answered from the call to /refresh_token for assigned to config.headers['Authorization'] = 'Bearer ' + response.data.access_token; if you backend reads from there the auth value you should be fine
I have to wait 24 hours to test whether it works, or is it possible in a different way, faster?
You should wait unless the backend can change that, and expire the token in less time (EG in 5 or 2 minutes)
Where should I put 'client_id', 'secret_id', 'grant_type'?
Seems like the backend should have that, unless they are public ones... You are probably the best to know whether that belongs to the config for the call or if you are authenticating with them. If you are authenticating with them and they are the ones that grant you a token, then you shouldn't put it in the client side, as it is a security risk
1) Configuration looks fine to me. But your solution won't work when there are multiple parallel requests and all of them trying to refresh auth token at the same time. Believe me this is a issue is really hard to pin point. So better be covered upfront.
2) No. Not the right place. Create a separate service (I call it api.service) and do all the network/api commutation using that.
3) There is no use of interceptor variable. You can avoid assigning it to a variable.
4) If have control over the API you can reduce the timeout for a bit. Also i think 24 hours is bit too long. Else no option I guess.
5) Not sure you have to deal with them.
Bellow is a working code of api.service.ts. You might have to change few things here and there to fit that in to your application. If you get the concept clearly it wont be hard. Also it cover multiple parallel request problem as well.
import * as queryString from 'query-string';
import axios, { AxiosRequestConfig, Method } from 'axios';
import { accountService } from '../account.service'; //I use account service to authentication related services
import { storageService } from './storage.service'; //I use storage service to keep the auth token. inside it it uses local storage to save values
var instance = axios.create({
baseURL: 'your api base url goes here',
});
axios.defaults.headers.common['Content-Type'] = 'application/json';
export const apiService = {
get,
post,
put,
patch,
delete: deleteRecord,
delete2: deleteRecord2
}
function get<T>(controller: string, action: string = '', urlParams: string[] = [], queryParams: any = null) {
return apiRequest<T>('get', controller, action, null, urlParams, queryParams);
}
function post<T>(controller: string, action: string = '', data: any, urlParams: string[] = [], queryParams: any = null) {
return apiRequest<T>('post', controller, action, data, urlParams, queryParams);
}
function put<T>(controller: string, action: string = '', data: any, urlParams: string[] = [], queryParams: any = null) {
return apiRequest<T>('put', controller, action, data, urlParams, queryParams);
}
function patch<T>(controller: string, action: string = '', data: any, urlParams: string[] = [], queryParams: any = null) {
return apiRequest<T>('patch', controller, action, data, urlParams, queryParams);
}
function deleteRecord(controller: string, action: string = '', urlParams: string[] = [], queryParams: any = null) {
return apiRequest<any>('delete', controller, action, null, urlParams, queryParams);
}
function deleteRecord2<T>(controller: string, action: string = '', urlParams: string[] = [], queryParams: any = null) {
return apiRequest<T>('delete', controller, action, null, urlParams, queryParams);
}
function apiRequest<T>(method: Method, controller: string, action: string = '', data: any, urlParams: string[] = [], queryParams: any = null) {
var url = createUrl(controller, action, urlParams, queryParams);
var options = createRequestOptions(url, method, data);
return instance.request<T>(options)
.then(res => res && res.data)
.catch(error => {
if (error.response) {
//handle error appropriately: if you want to display a descriptive error notification this is the place
} else {
//handle error appropriately: if you want to display a a generic error message
}
throw error;
});
}
function createUrl(controller: string, action: string = '', urlParams: string[] = [], queryParams: any = null) {
let url = controller + (action ? '/' + action : '');
urlParams.forEach(param => {
url += '/' + param;
});
let params = '';
if (queryParams) {
params += '?' + queryString.stringify(queryParams);
}
return url += params;
}
function createRequestOptions(url: string, method: Method, data: any, responseType?: any) {
var authToken = storageService.getAuthToken();
var jwtToken = authToken != null ? authToken.authToken : '';
var options: AxiosRequestConfig = {
url,
method,
data,
headers: {
'Authorization': 'bearer ' + jwtToken
},
}
if (responseType) {
options.responseType = responseType;
}
return options;
}
let isRefreshing = false;
let failedQueue: any[] = [];
const processQueue = (error: any, token: string = '') => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
}
instance.interceptors.response.use(undefined, (error) => {
const originalRequest = error.config;
if (originalRequest && error.response && error.response.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise(function (resolve, reject) {
failedQueue.push({ resolve, reject })
}).then(authToken => {
originalRequest.headers.Authorization = 'bearer ' + authToken;
return axios(originalRequest);
}).catch(err => {
return err;
})
}
originalRequest._retry = true;
isRefreshing = true;
return new Promise(function (resolve, reject) {
accountService.refreshToken()
.then(result => {
if (result.succeeded) {
originalRequest.headers.Authorization = 'bearer ' + result.authToken;
axios(originalRequest).then(resolve, reject);
processQueue(null, result.authToken);
} else {
reject(error);
}
}).catch((err) => {
processQueue(err);
reject(err);
}).then(() => { isRefreshing = false });
});
}
return Promise.reject(error);
});
Cheers,
I'm struggling to understand this..
In my requests.service I have this function that calls my backend:
loginUser(username, pw): Observable <any>{
const body = {
username: username,
password: pw
}
let headers = new HttpHeaders();
headers = headers.append("Authorization", "Basic " + btoa('test:test'));
headers = headers.append("Content-Type", "application/json");
return this.http.post('https://website.com/1/user/login', body, {headers: headers})
.subscribe((data)=>{ //use methods in our service
console.log(data)
this.userData = data;
}, (err)=> console.log(err));
}
This works, the data is returned.
Now, in login.page.ts I want to call loginUser but I also want a callback on it so I know when it has successfully been run, so I do:
this.req.loginUser(info.email, info.password).then((data) => {
console.log(data)
})
but I get the error:
this.req.loginUser(...).then is not a function
If I just console.log it with no callback it works fine, but I need to know when the call has been successful.
Any ideas?
It's an observable coming back not a promise. So .then is not applicable.
this.req.loginUser(info.email, info.password).pipe().subscribe((data) => {
console.log(data) // whenever data returns do something with it
})
We use pipe() to be able to do more things with subscriptions such as:
.pipe(
take(1) // will allow the subscription to return 1 time then unsubscribe
takeUntil() // subscribe until a condition is met
map() // map your observable
etc.
)
Since loginUser returns an Observable, you need to subscribe to it.
this.req.loginUser(info.email, info.password).subscribe((data) => {
console.log(data)
})
you need to update the loginUser like this in case you want to use the observable and you don't need to subscribe inside the loginUser
loginUser(username, pw): Observable <any>{
const body = {
username: username,
password: pw
}
let headers = new HttpHeaders();
headers = headers.append("Authorization", "Basic " + btoa('test:test'));
headers = headers.append("Content-Type", "application/json");
const url = 'https://website.com/1/user/login';
return this.http.post(url, body, {headers: headers}); // 👈
}
invoke the method like this 👇
this.req.loginUser(info.email, info.password).subscribe((data)=>{
console.log(data);
this.userData = data;
}, (err)=>{
console.log(err)
});
in case you want to use the then method and you want to invoke the method imidaily
use toPromise method to convert the observable to promise
loginUser(username, pw): Promise <any>{
const body = {
username: username,
password: pw
}
let headers = new HttpHeaders();
headers = headers.append("Authorization", "Basic " + btoa('test:test'));
headers = headers.append("Content-Type", "application/json");
const url = 'https://website.com/1/user/login';
return this.http.post(url, body, {headers: headers}).toPromise(); // 👈
}
now you can use the then method
this.req.loginUser(info.email, info.password).then((data)=>{
console.log(data);
this.userData = data;
}).
catch( err => console.log(err));
💣💥
The key difference between two ways if it observable this 👉 this.req.login
User(info.email, info.password) will not run until you subscribe but
in case of promise this 👉 this.req.login User(info.email, info.password)
will run the method even without using then the request will send to
the server 🔥🔥
Problem
My code is searching the database for a record and returns error when it cannot find an existing entry. It needs to check if the request is empty before parsing and return an empty array if it did not find a record and return [results] array if it does find one. How do I solve this problem?
This is for a Zapier integration with Zoho CRM that will search a custom module by Account_Name for an existing record and create one if it does not already exist.
Code
const options = {
url: `https://www.zohoapis.com/crm/v2/Accounts/search?criteria=(Account_Name:equals:${bundle.inputData.Account_Name})`,
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Zoho-oauthtoken ${bundle.authData.access_token}`,
'Accept': 'application/json'
},
params: {
}
}
return z.request(options)
.then((response) => {
response.throwForStatus();
const results = [z.JSON.parse(response.content)];
return [results];
});
You can try using a catch if your promise fails to resolve.
Like:
return z.request(options)
.then((response) => {
response.throwForStatus();
const results = [z.JSON.parse(response.content)];
return [results];
})
.catch(err => {
/*
check if the error is specifically where no entry in db was found.
Or if the error is always for that purpose
*/
console.log(err) // handle error approriately, maybe send to sentry?
return []; //emtpy array, when db errors out?
});
if response.content is null when it doesn't find anything:
.then((response) => {
...
return (response.content) ?
[z.JSON.parse(response.content)] :
Error("invalid request");
}
if response.content is an empty object when it doesn't find anything:
.then((response) => {
...
return (Object.keys(response.content).length) ?
[z.JSON.parse(response.content)]
: Error("invalid request");
}