Felipe de Senna
Posted on March 28, 2021
Com o crescimento constante dos sistemas na web cria-se uma necessidade e preocupação com a segurança desses sistemas. Uma forma de criar uma camada de segurança é a criação de autenticação via login.
E uma alternativa de autenticação é o Azure AD (Active Directory), com ele é possível criar configurações específicas para ter essa camada de segurança para seu sistema web.
No Azure AD é possível criar um grupo de usuários e nele adicionar todas as contas de e-mail que podem acessar o sistema web, você pode adicionar qualquer conta que esteja dentro dos domínios da Microsoft, e-mails pessoais e corporativos.
Sendo assim, nesse artigo eu vou demonstrar como fazer as configurações para ativar o Active Directory dentro do Portal do Azure e em seguida as configurações para integrar o Azure AD com o ReactJS.
O primeiro passo é a configuração dentro do Portal do Azure, após entrar no portal é necessário acessar o diretório/assinatura a qual será criada a configuração, em seguida pesquise por Azure Active Directory e selecione, agora selecione a opção Registros de aplicativo e clique em Novo registro.
Nesse momento irá abrir uma tela para cadastrar algumas informações sobre seu aplicativo, são elas: nome, quem pode usar este aplicativo e URI de redirecionamento que é opcional e finalizamos clicando em Registrar.
A imagem abaixo demonstra como ficará esta primeira etapa.
Assim que o registro do aplicativo for concluído o portal do azure irá redirecionar a página para a visão geral do aplicativo criado, uma página semelhante a imagem abaixo:
Nessa tela o importante a destacar é o ID do aplicativo (cliente), esse código é utilizado como parte da validação de token de segurança entre o sistema web e o Active Directory.
Para começar o projeto ReactJS vamos utilizar o npx create-react-app my-app para criar o projeto base da aplicação e logo em seguida criaremos uma pasta pages e dentro dela teremos duas pastas para ter rotas diferentes do projeto depois, são elas:
src/pages/SignIn/index.js
import React, { useCallback } from 'react';
import { useAuth } from '../../hooks/auth';
import logo from '../../assets/logo.svg';
import '../../assets/styles.css';
const SignIn = () => {
const { signIn } = useAuth();
const handleSignIn = useCallback(() => {
const accessToken = localStorage.getItem('@AzureAd:accessToken');
if (!accessToken) {
signIn();
}
}, [signIn]);
return (
<div className="App">
<img src={logo} alt="ReactJS, Azure AD" className="App-logo" />
<button type="button" onClick={handleSignIn}>Entrar</button>
</div>
);
};
export default SignIn;
src/pages/Dashboard/index.js
import React from 'react';
import { useAuth } from '../../hooks/auth';
import logo from '../../assets/logo.svg';
import '../../assets/styles.css';
const Dashboard = () => {
const { signOut, accountInfo } = useAuth();
return (
<div>
<header className="App-header">
<img src={logo} alt="ReactJS, Azure AD" className="App-logo" />
<div>
<p>Bem-vindo,
<strong> {accountInfo.user.displayName}</strong>
</p>
</div>
<button type="button" className="App-button" onClick={signOut}>sair</button>
</header>
</div>
);
};
export default Dashboard;
Vale notar que nessas duas telas utilizamos o useAuth que é um hook que foi criado para ter acesso ao login e informações do usuário logado, mais tarde vamos falar sobre esse hook.
Agora criamos uma pasta routes, nela vamos configurar a opção de rotas do projeto e definir qual rota será publica ou privada. A rota privada será acessada somente quando o usuário fizer o login que é autenticado pelo Azure AD.
src/routes/Route.js
import React from 'react';
import { Route as ReactDOMRoute, Redirect } from 'react-router-dom';
import { useAuth } from '../hooks/auth';
const Route = ({
isPrivate = false,
component: Component,
...rest
}) => {
const { accountInfo } = useAuth();
return (
<ReactDOMRoute
{...rest}
render={({ location }) => {
return isPrivate === !!accountInfo.user.email ? (
<Component />
) : (
<Redirect
to={{
pathname: isPrivate ? '/' : '/dashboard',
state: { from: location },
}}
/>
);
}}
/>
);
};
export default Route;
Na Route.js utilizamos o hook useAuth para recuperar as informações do usuário logado e verificar se essas informações são validas a partir do e-mail dele.
Se o e-mail do usuário for valido ele é redirecionado para a tela Dashboard onde aparece uma mensagem de boas vindas junto ao nome do usuário que logou e se essa validação do e-mail for invalida o usuário é redirecionado para a tela SignIn onde ele pode fazer o login para se autenticar.
src/routes/index.js
import React from 'react';
import { Switch } from 'react-router-dom';
import Route from './Route';
import SignIn from '../pages/SignIn';
import Dashboard from '../pages/Dashboard';
const Routes = () => (
<Switch>
<Route path="/" exact component={SignIn} />
<Route path="/dashboard" component={Dashboard} isPrivate />
</Switch>
);
export default Routes;
E para finalizar as configurações de rotas criamos um index.js para exportar as configurações de rotas e os componentes que ele irá exibir de acordo com a rota do projeto.
Para isso utilizamos a lib react-router-dom que é muito utilizada em projeto ReactJS para fazer configurações de rotas e na página index.js configuramos o que foi desenvolvido anteriormente e que recebe os valores de path que recebe o nome da rota, component que recebe o componente a ser renderizado e por fim o isPrivate que define qual rota é privada ou publica, que no nosso caso a rota /dashboard será privada.
Após as configurações de páginas e rotas partiremos para criação do hook useAuth que vai ter as configurações para efetuar login, logout, acesso ao token e acesso ao usuário que são autenticados no Azure AD.
Para começar vamos criar uma pasta hooks e nela teremos algumas arquivos.
No auth.js vamos fazer as configurações de comunicação e validação com o Azure AD e utilizaremos a lib @azure/msal-browser para fazer isso.
Basicamente no auth.js temos quatro métodos: signIn, signOut, getAccessToken, getUserProfile e um useEffect que faz uma primeira validação ao acessar a aplicação para saber se já existe um usuário logado.
signIn é um método assíncrono que faz uma chamada para a lib @azure/msal-browser abrir o login de usuário em modo Popup passando as informações de configurações do Azure Ad que falaremos depois e recuperando esse usuário com o método getUserProfile.
getUserProfile que é chamado pelo signIn envia as informações de request para o getAccessToken que retorna se o token é válido ou não, se esse token existir ele chama uma configuração do graphService para ter acesso as informações do usuário de acordo com o token, após isso ele salva o token no localStorage para utilizar em outro momento e ainda salva um estado utilizando o useState com as informações do usuário como displayName de mail.
getAccessToken faz a validação utilizando o getAllAccounts para recuperar o token do usuário e ele tem dois métodos para isso um é o acquireTokenSilent que valida o token sem interação do usuário e o outro é o acquireTokenPopup que valida o token a partir do momento que o usuário faz o login no Popup que foi aberto pelo sistema.
signOut esse método simples executa a remoção do token que foi salvo no localStorage e o logout da lib @azure/msal-browser para fazer o logout daquele usuário logado.
E por fim utilizamos o para exportar os métodos signIn, signOut, accountInfo que serão utilizados no projeto, esse último sendo um estado com as informações do usuário.
src/hooks/auth.js
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { PublicClientApplication } from '@azure/msal-browser';
import {
msalConfig,
loginRequest,
} from '../utils/configAzureAd';
import { getUserDetails } from '../utils/graphService';
const AuthContext = createContext({});
const msalInstance = new PublicClientApplication(msalConfig);
const AuthProvider = ({ children }) => {
const [accountInfo, setAccountInfo] = useState({
isAuthenticated: false,
user: {},
error: null,
});
const signIn = async () => {
try {
await msalInstance.loginPopup(
{
scopes: loginRequest.scopes,
prompt: "select_account"
});
await getUserProfile();
}
catch (err) {
setAccountInfo({
isAuthenticated: false,
user: {},
error: err,
});
}
}
const signOut = () => {
localStorage.removeItem('@AzureAd:accessToken');
msalInstance.logout();
}
const getAccessToken = async (scopes) => {
try {
const accounts = msalInstance.getAllAccounts();
if (accounts.length <= 0) throw new Error('Login required');
const silentResult = await msalInstance.acquireTokenSilent({
scopes: scopes,
account: accounts[0]
});
return silentResult.accessToken;
} catch (err) {
if (err) {
const interactiveResult = await msalInstance.acquireTokenPopup({
scopes: scopes,
});
return interactiveResult.accessToken;
} else {
throw err;
}
}
}
const getUserProfile = useCallback(async () => {
try {
const accessToken = await getAccessToken(loginRequest.scopes);
if (accessToken) {
const user = await getUserDetails(accessToken);
localStorage.setItem('@AzureAd:accessToken', accessToken);
setAccountInfo({
isAuthenticated: true,
user: {
displayName: user.displayName,
email: user.mail || user.userPrincipalName,
},
error: null
});
}
}
catch (err) {
setAccountInfo({
isAuthenticated: false,
user: {},
error: err,
});
}
}, []);
useEffect(() => {
const accounts = msalInstance.getAllAccounts();
if (accounts && accounts.length > 0) {
getUserProfile();
}
}, [getUserProfile]);
return (
<AuthContext.Provider value={{ signIn, signOut, accountInfo }}>
{children}
</AuthContext.Provider>
);
}
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
export { AuthProvider, useAuth };
Um ponto importante do auth.js é que ele é criado como contextAPI para ser possível repassar suas informações.
src/hooks/index.js
import React from 'react';
import { AuthProvider } from './auth';
const AppProvider = ({ children }) => (
<AuthProvider>
{children}
</AuthProvider>
);
export default AppProvider;
O index.js é importante para exportar os métodos signIn, signOut, accountInfo que foram desenvolvidos no auth.js, com esses dois arquivos auth.js e index.js criamos um conceito no ReactJS que é chamado de contextAPI onde é possível criar funções específicas e que serão utilizadas em mais de um lugar do projeto.
No auth.js usamos alguns dados que são acessos do Azure AD e uma configuração para recuperar informações do usuário que são disponibilizadas pela lib @microsoft/microsoft-graph-client após o usuário realizar o login na aplicação.
E para isso vamos criar uma pasta utils para fazer essas configurações.
No configAzureAd.js temos as informações do clientId e redirectUri que são disponibilizadas ao fazer o registro de um aplicativo no Azure AD e também temos a configuração do loginRequest onde falamos em qual escopo o projeto terá acesso, que nesse caso é apenas para leitura das informações desse usuário.
src/utils/configAzureAd.js
export const msalConfig = {
auth: {
clientId: process.env.REACT_APP_CLIENT_ID,
redirectUri: process.env.REACT_APP_REDIRECT_URI,
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false,
},
}
export const loginRequest = {
scopes: ['user.read'],
}
Para segurança dos dados sensíveis utilizados no configAzureAd.js foi criado um arquivo .env na raíz do projeto, essas informações estão disponíveis ao cadastrar um resgistro de aplicativo no Azure AD.
.env
# Config Azure AD
REACT_APP_CLIENT_ID=ID_do_aplicativo
REACT_APP_REDIRECT_URI=URIs_de_Redirecionamento
No graphService.js só fazemos uma validação com o token recuperado após o login do usuário para recuperar da API graph as informações do usuário como nome e e-mail.
src/utils/graphService.js
const graph = require('@microsoft/microsoft-graph-client');
function getAuthenticatedClient(accessToken) {
const client = graph.Client.init({
authProvider: (done) => {
done(null, accessToken);
}
});
return client;
}
export async function getUserDetails(accessToken) {
const client = getAuthenticatedClient(accessToken);
const user = await client
.api('/me')
.select('displayName,mail,userPrincipalName')
.get();
return user;
}
E para finalizar no App.js importamos do Routes as configurações de rotas que a aplicação terá acesso e também o AppProvider que vai disponibilizar os métodos de signIn, signOut, accountInfo que serão utilizados em outros componentes utilizando o contextAPI para passar as informações.
src/App.js
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import AppProvider from './hooks';
import Routes from './routes';
const App = () => (
<Router>
<AppProvider>
<Routes />
</AppProvider>
</Router>
);
export default App;
E assim finalizamos o projeto com autenticação via Azure AD, trazendo uma visão de como funciona o Azure AD e suas configurações e o seu funcionamento com o ReactJS. Espero ter ajudado!
Vale lembrar que com o próprio Azure AD você consegue criar outras configurações de permissões para quem vai acessar a aplicação, criação de grupos de usuários, e até mesmo fazer configurações para validar o token do login em uma API Rest criada pelo back end por exemplo.
Referência:
https://docs.microsoft.com/pt-br/graph/tutorials/react
O projeto completo pode ser baixado no github:
https://github.com/felipedesenna/react-authentication-azuread
Posted on March 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 23, 2024
May 9, 2023