Cookie-based authentication via REST API in react-admin - reactjs

I'm new to react-admin. I already read through all the questions here in stackoverflow, and google'd for my question too, but did not find any useful solution.
I am setting up React-admin to replace an existing admin page for one of my projects. I use cookie-based authentication via REST API.
Is it possible (and if yes how?) to use it in react-admin? Can someone please lead me to the right direction?
Cheers!

It is possible of course. You just have to make fetch use cookies.
react-admin uses fetch to send http requests to your back-end. And fetch does not send cookies by default.
So to make fetch send cookies, you have to add the credentials: 'include' option for every fetch call the app makes.
(if your admin and api are not on the same domain, you will have to enable CORS on your back-end.)
See react-admin's doc for how to customize requests on the dataProvider here: https://github.com/marmelab/react-admin/blob/master/docs/Authentication.md#sending-credentials-to-the-api
import { fetchUtils, Admin, Resource } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const token = localStorage.getItem('token');
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
}
const dataProvider = simpleRestProvider('http://localhost:3000', httpClient);
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
...
</Admin>
);
You'll have to customize this to add options.credentials = 'include' like so :
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({
Accept: 'application/json'
});
}
options.credentials = 'include';
return fetchUtils.fetchJson(url, options);
}
You will have to do the same thing for the authProvider.
Something like
// in src/authProvider.js
export default (type, params) => {
// called when the user attempts to log in
if (type === AUTH_LOGIN) {
const { username, password } = params;
const request = new Request(`${loginUri}`, {
method: 'POST',
body: JSON.stringify({ username: username, password }),
credentials: 'include',
headers: new Headers({ 'Content-Type': 'application/json' }),
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) throw new Error(response.statusText);
localStorage.setItem('authenticated', true);
});
}
// called when the user clicks on the logout button

Related

React Remix not sending cookies to remote server

I am trying to set up authentication with Remix as my pure frontend and a django backend.
When the user signs in successfully, the backend sends a cookie with the response and this is set in the browser redirect with remix
const signIn = async (credentials: LoginCreds) => {
try {
const response = await fetch(generateFullBackendUrl('/auth/signin'), {
method: 'POST',
body: JSON.stringify(credentials),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'include'
});
return response;
} catch (e) {
console.log(e);
}
}
const response = await authService.signIn({
email,
password
})
const cookies = response?.headers.get('set-cookie');
if(cookies){
return redirect('profile', {
headers: {
'Set-Cookie': cookies
}
});
However when I try to make subsequent fetch calls in my loader the cookies are not sent to the backend as I would expect the browser would
await fetch(generateFullBackendUrl('api/users/me'), {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'include'
})
Front end is running on port 3000
Backend running on port 4000
Im wondering why the fetch request in the loader does not send the cookies with the request
You need to read the cookie header from the loader request and pass it to your fetch headers.
There’s no way Fetch can automatically know what headers to send when used server side.
There is a quick workaround but not so elegant solution:
in your loader use this:
export const loader: LoaderFunction = async({request}) => {
const response = await fetch(`/api/books?${new URLSearchParams({
limit: '10',
page: '1',
})}`, {
headers: {
'Cookie': request.headers.get('Cookie') || ''
}
});
console.log(response)
if(response.ok) return await response.json();
}

unexpected 401 on PATCH request of discord api

I'm trying to programmatically add a role to a user to indicate their account as 'linked' or 'verified' in relation to my web app. To do so, I take the following steps:
in the frontend, send the user to the authorization url to authorize discord
when authorized, discord returns to my redirectURI, which calls my /discord/callback route
in this route, I get an access_token for the user from /oauth2/token
with the access_token, I get the discord_user object from /users/#me/guilds/${GUILD_ID}/member
So far, all of those requests are successful, and I get the data I expect. However, when I try to PATCH the user, and add a new role, for some reason I get a 401 unauthorized error, and I'm not entirely sure why. The endpoint I'm using is: https://discord.com/developers/docs/resources/guild#modify-guild-member
Here's my code for clarity:
const GUILD_ID = '<removed>';
const redirectURI = 'http://localhost:8000/auth/discord/callback';
const encodedRedirectURI = encodeURIComponent(redirectURI);
const url = `https://discord.com/api/oauth2/authorize?response_type=code&client_id=${process.env.DISCORD_CLIENT}&scope=identify%20guilds.members.read%20guilds.join&redirect_uri=${redirectURI}&prompt=consent`
router.get('/discord/callback', async (req, res) => {
const { code } = req.query;
try {
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('client_id', process.env.DISCORD_CLIENT);
params.append('client_secret', process.env.DISCORD_SECRET);
params.append('code', code);
params.append('redirect_uri', 'http://localhost:8000/auth/discord/callback');
let access_token;
try {
const tokenRequest = await fetch(`https://discord.com/api/v8/oauth2/token`, {
method: 'POST',
body: params,
headers: {
'Content-type': 'application/x-www-form-urlencoded'
}
}).then(r => r.json());
access_token = tokenRequest.access_token;
} catch {
throw new Error('Failed to get access token.');
}
let discord_user;
try {
discord_user = await fetch(`https://discord.com/api/v8/users/#me/guilds/${GUILD_ID}/member`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${access_token}`
}
}).then(r => r.json());
} catch {
throw new Error('Failed to get user from guild');
}
const discord_user_id = discord_user.user.id;
try {
await fetch(`https://discord.com/api/v8/guilds/${GUILD_ID}/members/${discord_user_id}`, {
method: 'PATCH',
body: JSON.stringify({
roles: [
'foo',
...discord_user.roles
],
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${access_token}`
}
}).then(r => r.json())
.then((data) => {
console.log(data); // 401 unauthorized
});
} catch {
throw new Error('Failed to add role to user');
}
return res.redirect('/settings?state=success');
} catch {
return res.redirect('/settings?state=failure');
}
});
Figured it out:
The role needs to be the ID of the role, not the name of the role, e.g.: '957197947676803072' instead of 'foo'
The role setting needs to be done by a bot:
await fetch(`https://discord.com/api/v8/guilds/${GUILD_ID}/members/${discord_user_id}`, {
method: 'PATCH',
body: JSON.stringify({
roles: ['957197947676803072', ...discord_user.roles]
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bot ${process.env.DISCORD_BOT_TOKEN}`
}
}).then(r => r.json())
I also needed to give that bot account admin permissions and manage roles permissions. You can do this either in the invite link or in your discord's server settings.

react admin returns Unauthorized 401 error upon CRUD operations

I am working on a react-admin project. The backend is written using Django rest framework which runs on a docker container. The authentication endpoints for access and refresh tokens are written using djangorestframework-simplejwt and served at http://localhost:8000/api/token/ and http://localhost:8000/api/token/refresh/ respectively.
I have written my own authProvider.js and dataProvider.js for react admin. The login and checkAuth functions for authProvider.js looks like this
// in src/authProvider.js
import jwt from "jsonwebtoken";
export default {
login: async ({ username, password }) => {
const request = new Request('http://localhost:8000/api/token/', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
const response = await fetch(request);
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
const { refresh, access } = await response.json();
localStorage.setItem('refreshToken', refresh);
localStorage.setItem('accessToken', access);
},
logout: ...
checkAuth: async () => {
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
if (accessToken && refreshToken) {
const { exp } = await jwt.decode(accessToken);
if (exp > (new Date().getTime() / 1000) - 10) {
return Promise.resolve();
} else {
const request = new Request('http://localhost:8000/api/token/refresh/', {
method: 'POST',
body: JSON.stringify({ "refresh": refreshToken }),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
const response = await fetch(request)
.then(response => {
if (response.status !== 200) {
throw new Error(response.statusText);
}
return response.json();
})
.then(({ token }) => {
localStorage.setItem('accessToken', token);
return Promise.resolve();
});
return response;
}
}
return Promise.reject();
},
checkError: ...
getPermissions: () => Promise.resolve(),
}
Retrieving data works fine. But whenever I perform a create, edit and delete operation, I am automatically logged out with a 401 Unauthorized error. Error message from docker server log
Unauthorized: /api/products/2
"PUT /api/products/2 HTTP/1.1" 401
Error from browser console: PUT HTTP://localhost:8000/api/products/2 401 (Unauthorized)
Prior to adding authProvider and using docker container as backend, CRUD data mutations worked fine, using a local python venv as backend. So I assume the dataProvider.js is not responsible here.
I have not been able to figure this out for quite some time. Can anyone help me figure out what I might be doing wrong here? Thank you for your time.
EDIT 1: It seems the access token is not sent from the frontend during API request, hence the server returning 401 Unauthorized
You need to modify your dataProvider to include the token (in a token, a cookie, or in a GET parameter, depending on what your backend requires). This is explained in the react-admin auth documentation:
import { fetchUtils, Admin, Resource } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const { token } = JSON.parse(localStorage.getItem('auth'));
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
};
const dataProvider = simpleRestProvider('http://localhost:3000', httpClient);
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
...
</Admin>
);

I can't make the post or put with fetch or axios to the swagger-ui API to create a login page

I am trying to make a login page with react using a swagger api: this is the code that i have but it give me this error: POST https://dev.tuten.cl/TutenREST/rest/user/testapis%40tuten.cl 400 (Bad Request)
when i make the test in the API, it gives me an ok, but with my function the result it's 400 BAD REQUEST i can't understand why it's not working:
Can anyone help me?
const onSignIn = async ({ email, password, app }) => {
const result = await fetch('https://dev.tuten.cl/TutenREST/rest/user/testapis%40tuten.cl', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
app,
}),
});
if (result.ok) {
const promiseData = await result.json();
console.log(promiseData);
} else {
console.log('Access is not allowed');
}
};

LocalStorage not working after storing token

I am storing JWT token in LocalStorage after Login success is dispatched and routing to next component. But the next component API call is not able to take LocalStored token.
If i refresh the page and click again, it is accepting the token. Dont know the issue.
This is Axios Instance and Login Dispatch respectively
const instance = axios.create({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8',
'x-access-token': localStorage.getItem('accessToken'),
},
withCredentials: true,
validateStatus: (status) => status === 200
});
export function checkLogin(data, history) {
return function (dispatch) {
return dispatch(makeAPIRequest(loginAPI, data)).then(function (response) {
if (response.data.success == 1) {
localStorage.removeItem('accessToken')
localStorage.setItem('accessToken', response.data.data.token)
dispatch({ type: STORE_SESSION_TOKEN, authenticated: response.data.data.auth, token: response.data.data.token,userDetails: response.data.data.user });
history.push('/dashboard')
}
})
}
}
Expecting to take token from Localstorage from very next call from Dashboard. But that doesn't happen. says no token and redirects to Login
I think the problem is when you create the axios instance, the token is not available in localStorage.
You should try the default headers of axios:
export const AUTHORIZATION = 'authorization'
export const API = axios.create({
baseURL: `http://localhost:3000/api`
})
export const authorize = (token) => {
if (token) {
API.defaults.headers.Authorization = `Bearer ${token}`
localStorage.setItem(AUTHORIZATION, token)
}
}
export const unauthorize = () => {
delete API.defaults.headers.Authorization
localStorage.removeItem(AUTHORIZATION)
}
authorize(localStorage.getItem(AUTHORIZATION))
When you receive the token from the API in your actions, you have to save it to localStorage.
You need to get your header dynamically, because the token is not there while creating the instance and it might change later:
const JSON_HEADERS = {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8'
};
const getTokenFromStorage = () => localStorage.getItem('token');
const getHeaders = () => getTokenFromStorage().then((token) => {
if (!token) {
return JSON_HEADERS;
}
return {
... JSON_HEADERS,
'x-access-token': token
};
});
export const makeAPIRequest = ({ method, url, data }) => {
return axios({
method,
url,
data,
headers: getHeaders()
});
}

Resources