Fluxo de autenticação com Token JWT no React Native
Uma das funcionalidades que me gerou um pouco de dor de cabeça quando comecei a estudar o React Native foi a autenticação. Isso porquê os plugins de navegação raramente tem uma estrutura pré-formatada para essa feature.
Depois de testar algumas formas de dividir as rotas entre os usuários autenticados e os não-autenticados, cheguei em uma estrutura que considero fácil de aplicar em uma aplicação utilizando o plugin react-navigation.
Durante o exemplo desse post não vou utilizar o Redux, mas você vai conseguir utilizá-lo facilmente seguindo a documentação de integração. Além disso, vou utilizar o React Native Elements para facilitar a criação dos layouts.
Nosso exemplo funcionará da seguinte forma: Ao acessar o app, o usuário verá uma tela de login, clicando no botão logar vamos fazer uma chamada fake à uma API REST que irá nos retornar um token JWT (JSON Web Token). Esse token será gravado em nosso storage do React Native e o usuário será redirecionado para a tela que requer autenticação. A partir desse momento, quando o aplicativo for inicializado com o token, o usuário será redirecionado automaticamente à tela autenticada. Criaremos um exemplo parecido com esse:
Configurando React Navigation
Vamos começar instalando o plugin do react-navigation utilizando esse comando no terminal:
yarn add react-navigation
Agora, vamos criar algumas rotas para definir nosso fluxo. Irei definir um StackNavigator para cada tipo de rota, um para as rotas de login (antes da autenticação) e outro para as rotas autenticadas.
// src/routes.js
import { createStackNavigator } from 'react-navigation';
import Login from './pages/login';
import Logged from './pages/logged';
export const SignedOutRoutes = createStackNavigator({
Login: {
screen: Login,
navigationOptions: {
title: "Entrar"
}
},
});
export const SignedInRoutes = createStackNavigator({
Logged: {
screen: Logged,
navigationOptions: {
title: "Meu perfil"
}
},
});
Por enquanto, os arquivos pages/login.js
e pages/logged.js
podem ser preenchidos com conteúdos vazios.
Agora, no nosso componente principal vamos importar nossas rotas para exibir as páginas ao usuário. Por enquanto vamos mostrar apenas a página de login, pois ainda não sabemos se o usuário está logado ou não.
// src/index.js
import React from 'react';
import { View } from 'react-native';
import { SignedOutRoutes, SignedInRoutes } from './routes';
export default class App extends React.Component {
render() {
return <SignedOutRoutes />
}
}
Agora vamos criar um arquivo que fará o papel de endpoint da nossa aplicação. Já que não temos uma API para fazer a autenticação, criarei um arquivo que possui funções que simulam o login e o logout.
Além disso, terei uma terceira função que apenas realizar a checagem se existe um token guardado para indicar que o usuário está logado.
// src/services/auth.js
import { AsyncStorage } from 'react-native';
export const TOKEN_KEY = "@RocketSeat:token";
export const onSignIn = () => AsyncStorage.setItem(TOKEN_KEY, "true");
export const onSignOut = () => AsyncStorage.removeItem(TOKEN_KEY);
export const isSignedIn = async () => {
const token = await AsyncStorage.getItem(TOKEN_KEY);
return (token !== null) ? true : false;
};
No arquivo acima, utilizamos o AsyncStorage para gravar um token no armazenamento do dispositivo do usuário quando ele logar, e removemos esse token no processo de logout. Para checar se o usuário está autenticado apenas verificamos se o token existe.
Criando o RootNavigator
No arquivo de rotas precisamos criar um novo método para englobar nossas rotas autenticadas e não-autenticadas em um único objeto, dessa forma, poderemos redirecionar o usuário do login para rota logada. Se não fizermos isso, quando o usuário estiver deslogado, o app não enxergará as rotas autenticadas e não teremos como navegar com o clique no botão.
No final do arquivo routes.js
, além do conteúdo que já adicionamos à ele, vamos adicionar um novo método chamado createRootNavigator
e o arquivo ficará assim:
// src/routes.js
import { createStackNavigator } from 'react-navigation';
import Login from './pages/login';
import Logged from './pages/logged';
export const SignedOutRoutes = createStackNavigator({
Login: {
screen: Login,
navigationOptions: {
title: "Entrar"
}
},
});
export const SignedInRoutes = createStackNavigator({
Logged: {
screen: Logged,
navigationOptions: {
title: "Meu perfil"
}
},
});
export const createRootNavigator = (signedIn = false) => {
return createStackNavigator({
SignedIn: { screen: SignedInRoutes },
SignedOut: { screen: SignedOutRoutes }
},
{
headerMode: "none",
mode: "modal",
initialRouteName: signedIn ? "SignedIn" : "SignedOut",
navigationOptions: {
gesturesEnabled: false
}
});
};
Agora no nosso arquivo principal vamos utilizar esse método para exibir a rota correta ao usuário acessar nosso app.
// src/index.js
import React from 'react';
import { View } from 'react-native';
import { isSignedIn } from "./services/auth";
import { createRootNavigator, SignedOutRoutes, SignedInRoutes } from './routes';
export default class App extends React.Component {
state = {
signed: false,
signLoaded: false,
};
componentWillMount() {
isSignedIn()
.then(res => this.setState({ signed: res, signLoaded: true }))
.catch(err => alert("Erro"));
}
render() {
const { signLoaded, signed } = this.state;
if (!signLoaded) {
return null;
}
const Layout = createRootNavigator(signed);
return <Layout />;
}
}
O que estamos fazendo aqui é definir um estado inicial para nossa aplicação e checando se o usuário está logado utilizando nosso serviço que simula uma API de autenticação.
Quando obtemos resposta do serviço anotamos na variável signLoaded
que já carregamos a informação de autenticação e exibimos as rotas de acordo com o retorno dessa função que é guardado na variável signed
.
Estilizando as páginas
Agora podemos estilizar nossas páginas e criar os botões para logar e sair do app.
// src/pages/logged.js
import React from "react";
import { View } from "react-native";
import { Card, Button, Text } from "react-native-elements";
import { onSignOut } from "../services/auth";
export default ({ navigation }) => (
<View style={{ paddingVertical: 20 }}>
<Card title="John Doe">
<View
style={{
backgroundColor: "#bcbec1",
alignItems: "center",
justifyContent: "center",
width: 80,
height: 80,
borderRadius: 40,
alignSelf: "center",
marginBottom: 20
}}
>
<Text style={{ color: "white", fontSize: 28 }}>JD</Text>
</View>
<Button
backgroundColor="#03A9F4"
title="Sair"
onPress={() => onSignOut().then(() => navigation.navigate("SignedOut"))}
/>
</Card>
</View>
);
// src/pages/login.js
import React from "react";
import { View } from "react-native";
import { Card, Button, FormLabel, FormInput } from "react-native-elements";
import { onSignIn } from "../services/auth";
export default ({ navigation }) => (
<View style={{ paddingVertical: 20 }}>
<Card>
<FormLabel>E-mail</FormLabel>
<FormInput placeholder="Digite seu e-mail" />
<FormLabel>Senha</FormLabel>
<FormInput secureTextEntry placeholder="Digite sua senha" />
<Button
buttonStyle={{ marginTop: 20 }}
backgroundColor="#03A9F4"
title="Entrar"
onPress={() => {
onSignIn().then(() => navigation.navigate("SignedIn"));
}}
/>
</Card>
</View>
);
Agora, se tudo ocorreu bem, seu app deve estar parecido com o GIF que coloquei no início do post:
Essa é uma das formas de controlar a autenticação do usuário, existem outras, mas depois de testar várias maneiras de obter o mesmo resultado, concluí que esse é o melhor jeito por enquanto.
Ah, se você está controlando a autenticação do seu app de outra forma que você ache legal não esquece de deixar aí nos comentários.