How to use Auth0 with react-admin? - reactjs

I'm trying to implement authentication using Auth0 in a react-admin v3 app. I need to implement an authProvider that talks with Auth0. This sounds like something that should be available somewhere, but the closest I could find was https://github.com/alexicum/merge-admin/blob/master/src/Auth/index.js, which is about 2 years old (the SDKs have changed since then).
Is there an Auth0 authProvider somewhere I can reuse, or do I have to implement it myself?
Thanks!

for reference sake, here's an example of a way to integrate react admin with auth0-react package
index.js
import { Auth0Provider } from "#auth0/auth0-react";
ReactDOM.render(
<Auth0Provider
domain="XXXXX.auth0.com"
clientId="XXXXX"
audience="https://XXXXX"
redirectUri={window.location.origin}
>
<React.StrictMode>
<App />
</React.StrictMode>
</Auth0Provider>,
document.getElementById("root")
);
App.js
import { withAuth0, withAuthenticationRequired } from "#auth0/auth0-react";
import ApolloClient from "apollo-boost";
// I'm using Hasura w/ JWT Auth, so here's an example of how to set Authorization Header
async componentDidMount() {
const token = await this.props.auth0.getAccessTokenSilently();
const client = new ApolloClient({
uri: "https://HASURA_URL/v1/graphql",
headers: {
Authorization: `Bearer ${token}`
},
});
buildHasuraProvider({ client }).then((dataProvider) =>
this.setState({ dataProvider })
);
}
export default withAuthenticationRequired(withAuth0(App));

I've created a sample application with Auth0 and react-admin way of auth
https://github.com/spintech-software/react-admin-auth0-example
Here is auth provider code for reference
import authConfig from "./authConfig";
import {Auth0Client} from '#auth0/auth0-spa-js';
const auth0 = new Auth0Client({
domain: authConfig.domain,
client_id: authConfig.clientID,
cacheLocation: 'localstorage',
useRefreshTokens: true
});
const CallbackURI = "http://localhost:3000/login"
export default {
// called when the user attempts to log in
login: (url) => {
if (typeof url === 'undefined') {
return auth0.loginWithRedirect({
redirect_uri: CallbackURI
})
}
return auth0.handleRedirectCallback(url.location);
},
// called when the user clicks on the logout button
logout: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) { // need to check for this as react-admin calls logout in case checkAuth failed
return auth0.logout({
redirect_uri: window.location.origin,
federated: true // have to be enabled to invalidate refresh token
});
}
return Promise.resolve()
})
},
// called when the API returns an error
checkError: ({status}) => {
if (status === 401 || status === 403) {
return Promise.reject();
}
return Promise.resolve();
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) {
return Promise.resolve();
}
return auth0.getTokenSilently({
redirect_uri: CallbackURI
})
})
},
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => {
return Promise.resolve()
},
};

My answer is following react-admin approach where I use its authProvider like below. There are two main steps:
Get needed data from useAuth0 hook.
Convert authProvider into function where it takes the above values, and return an object like default.
// In App.js
import authProvider from './providers/authProvider';// my path is changed a bit
const App = () => {
const {
isAuthenticated,
logout,
loginWithRedirect,
isLoading,
error,
user,
} = useAuth0();
const customAuthProvider = authProvider({
isAuthenticated,
loginWithRedirect,
logout,
user,
});
return (
<Admin
{...otherProps}
authProvider={customAuthProvider}
>
{...children}
</Admin>
);
}
// My authProvider.js
const authProvider = ({
isAuthenticated,
loginWithRedirect,
logout,
user,
}) => ({
login: loginWithRedirect,
logout: () => logout({ returnTo: window.location.origin }),
checkError: () => Promise.resolve(),
checkAuth: () => (isAuthenticated ? Promise.resolve() : Promise.reject()),
getPermissions: () => Promise.reject('Unknown method'),
getIdentity: () =>
Promise.resolve({
id: user.id,
fullName: user.name,
avatar: user.picture,
}),
});
export default authProvider;
That's it.

It's more convenient to wrap the react-admin app with auth0 native login, and then provide react-admin dataProvider an http client that reads the jwt token stored in local storage by auth0.

Related

React-Redux logout modal/prompt on JWT expiry

My React-Redux app is using JWTs to handle authentication like so:
import axios from 'axios';
import { getLocalAccessToken, getLocalRefreshToken, updateLocalTokens } from './token-service';
async function attachAccessToken(reqConfig) {
const accessToken = getLocalAccessToken();
if (accessToken) reqConfig.headers.authorization = `Bearer ${accessToken}`;
return reqConfig;
}
export default (opts) => {
const API = axios.create(opts);
API.interceptors.request.use(attachAccessToken);
API.interceptors.response.use(
(res) => res.data,
async (err) => {
const { status, config: reqConfig, data: message } = err.response;
const isAuthTokenErr = !reqConfig.url.includes('/login') && status === 401 && !reqConfig._isRetried;
if (!isAuthTokenErr) return Promise.reject(message);
else {
reqConfig._isRetried = true;
try {
const { data: newTokens } = await axios.post('/auth/reauthorize', { refreshToken: getLocalRefreshToken() });
updateLocalTokens(newTokens);
return API(reqConfig); //retry initial request with new access token
} catch (reauthErr) {
const message = reauthErr.response.data;
return Promise.reject('Session expired. Please sign-in again');
}
}
}
);
return API;
};
Clients are given access to protected routes (views) as long as there is a user in the Redux store i.e.
import { Redirect, Route } from 'react-router-dom';
import { useSelector } from 'react-redux';
export default function ProtectedRoute({ component: Component, ...rest }) {
const isLoggedIn = useSelector(state => state.auth.user);
return (
<Route
{...rest}
render={(routeProps) => {
if (isLoggedIn) return <Component />;
else return <Redirect to="/auth/login" />;
}}
/>
);
}
When the refresh token expires, the client will no longer be able to access protected endpoints on the backend as desired.
That being said, on the frontend, the client remains logged in and able to access protected routes (views), albeit with no data loading.
My question is, for any given 401 unauthenticated response (apart from bad login credentials), how can I use the reauthErr to either:
a) Log the client out automatically i.e. clear the client's JWTs in LocalStorage as well as remove the user from the Redux store?
Or
b) more elegantly, prompt the client with a modal, for example, that their session has expired and that they will need to login again?
Thank you!

best way to authenticate with SWR (firebase auth)

I'm doing project with React , firebase auth social signin(google, github provider) and backend(spring boot)
I'm wondering how can i use useSWR for global state for google userData
Here's my Code This is Login page simply i coded
In this page, I fetch userData(email, nickname ,, etc) with header's idToken(received from firebase auth) and backend validates idToken and send me a response about userData
This is not problem I guess.. But
// import GithubLogin from '#src/components/GithubLogin';
import GoogleLogin from '#src/components/GoogleLogin';
import { auth, signOut } from '#src/service/firebase';
import { fetcherWithToken } from '#src/utils/fetcher';
import React, { useEffect, useState } from 'react';
import useSWR from 'swr';
const Login = () => {
const [token, setToken] = useState<string | undefined>('');
const { data: userData, error } = useSWR(['/api/user/me', token], fetcherWithToken);
useEffect(() => {
auth.onAuthStateChanged(async (firebaseUser) => {
const token = await firebaseUser?.getIdToken();
sessionStorage.setItem('user', token!);
setToken(token);
});
}, []);
return (
<div>
<button onClick={signOut}>Logout</button>
<h2>Login Page</h2>
<GoogleLogin />
</div>
);
};
export default Login;
Here's Code about fetcher using in useSWR parameter
export const fetcherWithToken = async (url: string, token: string) => {
await axios
.get(url, {
headers: {
Authorization: `Bearer ${token}`,
Content-Type: 'application/json',
},
withCredentials: true,
})
.then((res) => res.data)
.catch((err) => {
if (err) {
throw new Error('There is error on your site');
}
});
};
problem
I want to use userData from useSWR("/api/user/me", fetcherWithToken) in other page! (ex : Profile Page, header's Logout button visibility)
But for doing this, I have to pass idToken (Bearer ${token}) every single time i use useSWR for userData. const { data: userData, error } = useSWR(['/api/user/me', token], fetcherWithToken);
Like this.
What is the best way to use useSWR with header's token to use data in other pages too?
seriously, I'm considering using recoil, context api too.
but I don't want to.
You can make SWR calls reusable by wrapping them with a custom hook. See the SWR docs page below.
Make It Reusable
When building a web app, you might need to reuse the data in many
places of the UI. It is incredibly easy to create reusable data hooks
on top of SWR:
function useUser (id) {
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading: !error && !data,
isError: error
}
}
And use it in your components:
function Avatar ({ id }) {
const { user, isLoading, isError } = useUser(id)
if (isLoading) return <Spinner />
if (isError) return <Error />
return <img src={user.avatar} />
}

Inserting access token from session into API request

I have built a web app that allows me to login with Spotify and also gain the users access token which is required for the use of the API. However I am unsure on how to insert the users token from their session into the API's headers.
I can see in the console that the authorization request does return a user token and I have verified they do work.
I am using Next-Auth for authenticating the user through Spoitfy.
My current [...nextauth].js file
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import Spotify from "../../../providers/spotify"
import Twitch from "../../..//providers/twitch"
export default (req, res) => NextAuth(req, res, {
providers: [
Spotify({
clientId: process.env.SPOTIFY_CLIENT_ID,
clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
}),
Twitch({
clientId: process.env.TWITCH_CLIENT_ID,
clientSecret: process.env.TWITCH_CLIENT_SECRET,
})
],
callbacks: {
async jwt(token, _, session) {
if (session) {
token.id = session.id
token.accessToken = session.accessToken
}
console.log(token);
return token
},
async session(session, user) {
session.user = user
return session
}
},
})
My current file for making the API request using axios
import axios from "axios";
import { VStack, Heading, Text, Box, Center } from "#chakra-ui/layout";
import { signIn, signOut, useSession } from 'next-auth/client'
const Spotify = ({ Spotify }) => { if(!Spotify){ return <div>Loading...</div> }
// eslint-disable-next-line react-hooks/rules-of-hooks
const [ session, loadings ] = useSession();
console.log(Spotify);
return (
<VStack>
{Spotify.map((Spoty) => (
<div key={Spoty.id}>
<Heading>{Spoty.name}</Heading>
<Text>Link: {Spoty.href}</Text>
<Text>Owner: {Spoty.owner.display_name}</Text>
</div>
))}
</VStack>
);
};
Spotify.getInitialProps = async (ctx) => {
try {
const res = await axios.get("https://api.spotify.com/v1/me/playlists", {
headers: {
Authorization: `Bearer ${session.user.accessToken}`
}
});
const Spotify = res.data.items;
return { Spotify };
} catch (error) {
return { error };
}
};
export default Spotify;

How to remove response_mode = web_message from loginwithPopup url?

I am working on authentication using Auth0 and react. I am using loginWithPopup() for the login popup screen. But every time I end up getting misconfiguration error(like you can see in the attachment). But if I remove the response_mode = web_message from the URL it works, is there any way to remove response_mode from code. I am using the react-auth0-spa.js given my auth0 quick start
import React, { Component, createContext } from 'react';
import createAuth0Client from '#auth0/auth0-spa-js';
// create the context
export const Auth0Context = createContext();
// create a provider
export class Auth0Provider extends Component {
state = {
auth0Client: null,
isLoading: true,
isAuthenticated: false,
user: false,
};
config = {
domain: "dev-ufnn-q8r.auth0.com",
client_id: "zZh4I0PgRLQqLKSPP1BUKlnmfJfLqdoK",
redirect_uri: window.location.origin,
//audience: "https://reachpst.auth0.com/api/v2/"
};
componentDidMount() {
this.initializeAuth0();
}
// initialize the auth0 library
initializeAuth0 = async () => {
const auth0Client = await createAuth0Client(this.config);
const isAuthenticated = await auth0Client.isAuthenticated();
const user = isAuthenticated ? await auth0Client.getUser() : null;
this.setState({ auth0Client, isLoading: false, isAuthenticated, user });
};
loginWithPopup = async () => {
try {
await this.state.auth0Client.loginWithPopup();
}
catch (error) {
console.error(error);
}
this.setState({
user: await this.state.auth0Client.getUser(),
isAuthenticated: true,
});
};
render() {
const { auth0Client, isLoading, isAuthenticated, user } = this.state;
const { children } = this.props;
const configObject = {
isLoading,
isAuthenticated,
user,
loginWithPopup: this.loginWithPopup,
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
logout: (...p) => auth0Client.logout(...p)
};
return (
<Auth0Context.Provider value={configObject}>
{children}
</Auth0Context.Provider>
);
}
}
After a bit of research, I found an answer to my own question. So if we use response_mode = web_message then we need to configure our callback URL in allowed web origin field as well. In my case, I am using loginWithPopup() so which typically adds response_mode = web_message in the login URL because loginWithPopup() from auth0 SDK is a combination of PKCE + web_message
https://auth0.com/docs/protocols/oauth2 (under how response mode works?)
https://auth0.com/blog/introducing-auth0-single-page-apps-spa-js-sdk (under behind the curtain)

ReactJS with IdentityServer4 and oidc-client-js Error: "No code in response"

I am trying to get ReactJS working with IdentityServer4 via oidc-client-js library.
PROBLEM
I click the login button and I get redirected to IdentityServer4, once I login, I get redirect to /login-callback and then from there I get redirected to / but I get the following error in the console:
I am not sure what I'm doing wrong, but I've tried all sorts and nothing seems to work.
CODE
All code is open-source and sits here.
App.jsx
// other code ommited, but this is a route
<AuthHandshake path="/login-callback" />
AuthHandshake.jsx
import React from "react";
import { AuthConsumer } from "../AuthProvider/AuthProvider";
function AuthHandshake() {
return <AuthConsumer>{value => value.loginCallback()}</AuthConsumer>;
}
export default AuthHandshake;
AuthProvider.jsx
import React, { useState } from "react";
import { navigate } from "#reach/router";
import { UserManager, WebStorageStateStore } from "oidc-client";
import AuthContext from "../../contexts/AuthContext";
import { IDENTITY_CONFIG } from "../../utils/authConfig";
IDENTITY_CONFIG.userStore = new WebStorageStateStore({
store: window.localStorage
});
const userManager = new UserManager(IDENTITY_CONFIG);
const login = () => {
console.log("Login button click handled.");
userManager.signinRedirect();
};
const logout = () => {
userManager.signoutRedirect();
};
export function AuthProvider(props) {
const [user, setUser] = useState(null);
const loginCallback = () => {
userManager.signinRedirectCallback().then(
user => {
window.history.replaceState(
{},
window.document.title,
window.location.origin
);
setUser(user);
navigate("/");
},
error => {
console.error(error);
}
);
};
const providerValue = {
login: login,
logout: logout,
loginCallback: loginCallback,
isAuthenticated: user
};
const Provider = () => {
if (user) {
return (
<AuthContext.Provider value={providerValue}>
{props.children}
</AuthContext.Provider>
);
} else {
return <div className="auth-provider">{props.children}</div>;
}
};
return (
<>
<AuthContext.Provider value={providerValue}>
{props.children}
</AuthContext.Provider>
</>
);
}
export const AuthConsumer = AuthContext.Consumer;
On the IdentityServer side, I have set the post logout redirect to the same thing /login-callback
new Client
{
ClientId = "bejebeje-react-local",
ClientName = "Bejebeje ReactJS SPA Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RequireConsent = false,
RedirectUris = { "http://localhost:1234/login-callback" },
PostLogoutRedirectUris = { "http://localhost:1234/logout-callback" },
AllowedCorsOrigins = { "http://localhost:1234" },
AllowedScopes = { "openid", "profile", "bejebeje-api-local" },
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse,
}
Where am I going wrong?
Try this here..
https://github.com/JwanKhalaf/Bejebeje.React/blob/bug/fix-no-code-response-on-redirect/src/components/AuthProvider/AuthProvider.jsx#L15
Basically, when you are sending a login request from the client (OIDC-client.js), the client sends a unique identifier (State) along with the login request in the URL. The client saves this value in the storage option you choose (local/session). After successful sign-in, server issues tokens in the response redirect url and it also includes the unique identifier client initially sent in login request. Watch the login request url and response url in chrome Developer Tools.
When OIDC Client Library receives the sign-in response, it grabs the unique identifier from the URL and matches with the value it kept in the local/session storage at the time of login request. If they don't match, the client will not accept the token issued by the server.
This is just one level of additional security to make sure, the client is not accepting any tokens issued by any random server or any bookmarked URL.
Hope this helps.!!
usermanager.signinRedirect({ state: { bar: 15 } });
I had to add response_mode: 'query' to the UserManager.
var mgr = new Oidc.UserManager({ response_mode: 'query' });
mgr.signinRedirectCallback().then(res => {
window.location = "/signin-callback";
}).catch(error => {
window.location = "/";
})

Resources