El siguiente tutorial es para construir un portal serverless básico con las siguiente características.

  • React Framework
  • React-Route
  • Bootstrap
  • Página Pública
  • Página Privada
  • Autenticación por AWS Cognito

Existe mucha documentación en sobre react y autenticación con AWS, sin embargo para poder discriminar las paginas publicas y privadas utilizando React-Route no resulta fácil de encontrar. Es por ello este tutorial indica los pasos mínimos para lograrlo de forma sencilla.

La documentación adicional se puede encontrar aquí:

https://aws-amplify.github.io/docs/js/react

https://aws-amplify.github.io/docs/js/authentication

Creamos el proyecto de react

yarn create react-app basicpage

cd ./basicpage

Iniciamos Amplify

amplify init
? Enter a name for the project basicpage
? Enter a name for the environment dev
? Choose your default editor: Atom Editor
? Choose the type of app that you’re building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path: src
? Distribution Directory Path: build
? Build Command: npm run-script build
? Start Command: npm run-script start
Using default provider awscloudformation 

 Agregamos  hosting S3

amplify add hosting
? Select the environment setup: DEV (S3 only with HTTP)
? hosting bucket name basicpage-xxx-hostingbucket
? index doc for the website index.html
? error doc for the website index.html

 

Agregamos autenticación

amplify add auth
Using service: Cognito, provided by: awscloudformation
The current configured provider is Amazon Cognito.
Do you want to use the default authentication and security configuration? Yes, 

 

 

Subimos la primera actualización

amplify push

Agregamos las dependencias necesarias 

yarn add aws-amplify aws-amplify-react

yarn add react-bootstrap bootstrap

 

yarn add react-router-dom

 

Hasta aquí todo es el procedimiento recomendado por la documentación de AWS.

 https://aws-amplify.github.io/docs/js/react

Ahora editamos el archivo App.js, que es el que contiene la barra de navegación, las páginas de ejemplo y los constraints de conección

Código 
import React, { Component } from 'react';
import './App.css';
import Amplify, { Auth} from 'aws-amplify';
import awsmobile from './aws-exports';
import { Redirect, BrowserRouter as Router, Route, Link } from "react-router-dom";
import { Authenticator } from 'aws-amplify-react';
import {Navbar, Nav} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
Amplify.configure(awsmobile);
class App extends Component {
constructor(props, context) {
super(props, context);
this.state={signed:null}
Auth.currentAuthenticatedUser({
bypassCache: false // Optional, By default is false. If set to true, this call will send a request to Cognito to get the latest user data
}).then(user => this.setState({signed:true, user:user} ) )
.catch(err => this.setState({signed:false}) );
}
render(){
if(this.state.signed===null)return null;//Previene doble render
return (
<Router>
<Navbar bg="light" expand="lg">
<Navbar.Brand >TestPage</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Link to="/" className="nav-link">Home</Link>
{!this.state.signed ? //Constraint si no esta logeado
<Link to="/LoginPage" className="nav-link">Login</Link>
:""}
{this.state.signed ? //Constraint si esta logeado
<Link to="/UserPage" className="nav-link">User</Link>
:""}
</Nav>
</Navbar.Collapse>
{this.state.signed ?
<Navbar.Collapse className="justify-content-end">
<Navbar.Text>
Logged in as: {this.state.user.username}
</Navbar.Text>
<Link to="/LogoutPage" className="nav-link">Logout</Link>
</Navbar.Collapse>
:""}
</Navbar>
<Route exact path="/" component={HomePage} />
<Route path="/LoginPage" component={LoginPage} />
<Route path="/UserPage" component={UserPage} />
<Route path="/LogoutPage" component={LogoutPage} />
</Router>
);
}
}
function HomePage() { //Contenido publico
return (
<div>
<h2>Home</h2>
</div>
);
}
class LoginPage extends Component { //Pagina de Login AWS
constructor(props, context) {
super(props, context);
this.state={signed:null}
Auth.currentSession()
.then(data => this.setState({signed:true}) )
.catch(err => this.setState({signed:false}) );
}
testLogin(authState){
console.log(authState);
if(authState==="signedIn"){
window.location = "/UserPage";//refresca la pagina y envia a USERPAGE
}
}
render() {
if(this.state.signed===null)return null;//Previene doble render
if(this.state.signed) return <Redirect to='/UserPage'/>; //Redirecciona si ya esta logeado
return(<Authenticator
onStateChange={(authState) => this.testLogin(authState) }
></Authenticator>)
}
}
class LogoutPage extends Component { //Pagina de transicion para Logout
constructor(props, context) {
super(props, context);
this.state={signed:null}
Auth.signOut()
.then(data => this.testLogout(data) )
.catch(err => this.testLogout(err) );
}
testLogout(data){
console.log(data);
window.location = "/"; //refresca la pagina y envia a home
}
render(){
return null;
}
}
class UserPage extends Component { //Pagina Privada
constructor(props, context) {
super(props, context);
this.state={signed:null}
Auth.currentSession()
.then(data => this.setState({signed:true}) )
.catch(err => this.setState({signed:false}) );
}
render() {
if(this.state.signed===null)return null;//Previene doble render
if(!this.state.signed) return <Redirect to='/LoginPage'/>; //Redirecciona si no esta logeado
return (
<div>
<h2>User Page</h2>
</div>
);
}
}
export default App;
//export default withAuthenticator(App, true);
view raw App.js hosted with ❤ by GitHub

Para verificar la autenticación no utilizamos 

 

export default withAuthenticator(App, true);

Como indica la documentación, ya que esto provoca que toda el sitio este autenticado para funcionar, utilizamos entonces en el constructor  de cada Componente del Router

Auth.currentAuthenticatedUser

Auth.currentSession()

Que verifican la sesión o el usuario respectivamente, funcionan de forma asincrónica es por ello que al devolver el resultado asignamos valor a la variable «signed» que al ser parte de la variable especial «state» nos permite ejecutar el render cuando se haya realizado la autenticación.

De tal forma

if(this.state.signed===null)return null;

Previene que se renderice de forma repetida al ser nulo y cuando se verifique la autenticación se puede renderizar de forma normal.

La autenticación se la realiza gracias al componente

<Authenticator
onStateChange={(authState) => this.testLogin(authState) }
></Authenticator>

que está perfectamente documentado en

https://aws-amplify.github.io/docs/js/authentication

 Para verificar cuando ha finalizado el proceso de autentificación, utilizamos como callback la función 

testLogin(authState){
 console.log(authState);
  if(authState===»signedIn»){
  window.location = «/UserPage»;//refresca la pagina y envia a USERPAGE
 }
} 

Podemos notar que cuando «authState» es igual a ‘signedin’ forzamos el refresco de la página en vez de utilizar Routes, esto se debe a que la barra de navegación no se refresca ni valida la autenticación y se requiere forzar un refresco completo.

Es hora de probar

amplify serve

 

 Basta con agregar un usuario por la consola de AWS Cognito, o registrarse directamente en la página.

Una vez funcionando el código podemo publicar en AWS.

amplify publish

Este comando pública y nos da el enlace para utilizarlo.

Son realmente pasos muy sencillos y claros, casi todo está en la documentación pero siempre es importante tener una estructura básica que se adapte a los sitios usados comúnmente.

EDITADO

En el siguiente código la autenticación se realiza una única vez en el componente padre y se pasa el estado de autenticación por medio de props.

Código
import React, { Component } from 'react';
import './App.css';
import Amplify, { Auth} from 'aws-amplify';
import awsmobile from './aws-exports';
import { Redirect, BrowserRouter as Router, Route, Link } from "react-router-dom";
import { Authenticator } from 'aws-amplify-react';
import {Navbar, Nav} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
Amplify.configure(awsmobile);
class App extends Component {
constructor(props, context) {
super(props, context);
this.state={signed:null}
this.testLogin();
//this.testLogin = this.testLogin.bind(this);
}
testLogin(){
Auth.currentAuthenticatedUser({
bypassCache: false // Optional, By default is false. If set to true, this call will send a request to Cognito to get the latest user data
}).then(user => this.setState({signed:true, user:user} ) )
.catch(err => this.setState({signed:false}) );
}
render(){
if(this.state.signed===null)return null;//Previene doble render
return (
<Router>
<Navbar bg="light" expand="lg">
<Navbar.Brand >TestPage</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Link to="/" className="nav-link">Home</Link>
{!this.state.signed ? //Constraint si no esta logeado
<Link to="/LoginPage" className="nav-link">Login</Link>
:""}
{this.state.signed ? //Constraint si esta logeado
<Link to="/UserPage" className="nav-link">User</Link>
:""}
</Nav>
</Navbar.Collapse>
{this.state.signed ?
<Navbar.Collapse className="justify-content-end">
<Navbar.Text>
Logged in as: {this.state.user.username}
</Navbar.Text>
<Link to="/LogoutPage" className="nav-link">Logout</Link>
</Navbar.Collapse>
:""}
</Navbar>
<Route exact path="/" render={()=><HomePage signed={this.state.signed} testLogin={()=>this.testLogin()}/> } />
<Route path="/LoginPage" render={()=><LoginPage signed={this.state.signed} testLogin={()=>this.testLogin()}/> } />
<Route path="/UserPage" render={()=><UserPage signed={this.state.signed} testLogin={()=>this.testLogin()}/> } />
<Route path="/LogoutPage" render={()=><LogoutPage signed={this.state.signed} testLogin={()=>this.testLogin()}/> } />
</Router>
);
}
}
function HomePage() { //Contenido publico
return (
<div>
<h2>Home</h2>
</div>
);
}
class LoginPage extends Component { //Pagina de Login AWS
constructor(props, context) {
super(props, context);
}
testAuthState(authState){
console.log(authState);
if(authState==="signedIn"){
this.props.testLogin();
}
}
render() {
console.log(this.props);
if(this.props.signed===null)return null;//Previene doble render
if(this.props.signed) return <Redirect to='/UserPage'/>; //Redirecciona si ya esta logeado
return(<Authenticator
onStateChange={(authState) => this.testAuthState(authState) }
></Authenticator>)
}
}
class LogoutPage extends Component { //Pagina de transicion para Logout
constructor(props, context) {
super(props, context);
Auth.signOut()
.then(data => this.props.testLogin() )
.catch(err => this.props.testLogin() );
}
render(){
if(!this.props.signed) return <Redirect to='/'/>; //Redirecciona si ya esta logeado
return null;
}
}
class UserPage extends Component { //Pagina Privada
constructor(props, context) {
super(props, context);
}
render() {
if(this.props.signed===null)return null;//Previene doble render
if(!this.props.signed) return <Redirect to='/LoginPage'/>; //Redirecciona si no esta logeado
return (
<div>
<h2>User Page</h2>
</div>
);
}
}
export default App;
//export default withAuthenticator(App, true);
view raw App.js hosted with ❤ by GitHub