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.