Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 3
Spring BootNovember 08, 20182 mins readWelcome to the 3rd and last part of the Spring Boot OAuth2 social login series. In this article, we’ll be building the frontend client with React.
You can find the complete source code of the application on Github.
Creating the React application
Let’s create the React app using create-react-app
CLI client. You can install create-react-app
using npm
like this -
npm install -g create-react-app
Now create the React app by typing the following command -
create-react-app react-social
We’ll be using react-router-dom
for client-side routing and react-s-alert
for showing alerts. Download these dependencies by typing the following commands -
cd react-social
npm install react-router-dom react-s-alert --save
Directory Structure
Here is the directory structure of the application for your reference -
Understanding the frontend code
I won’t be going into the details of every piece of code in this article. The code is very simple and self-explanatory. You can download the code from Github and see for yourself.
But Let’s look at some important components of the application -
index.js
This is the entry point of our application -
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './app/App';
import registerServiceWorker from './registerServiceWorker';
import { BrowserRouter as Router } from 'react-router-dom';
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById('root')
);
registerServiceWorker();
It renders the App
component in a DOM element with id root
(This DOM element is available inside public/index.html
file). The App
component is wrapped inside react-router’s Router
to enable client-side routing.
src/app/App.js
App.js is the main top-level component of our application. It defines the basic layout and the routes. It also loads the details of the currently authenticated user from backend and passes the detail to its child components.
import React, { Component } from 'react';
import {
Route,
Switch
} from 'react-router-dom';
import AppHeader from '../common/AppHeader';
import Home from '../home/Home';
import Login from '../user/login/Login';
import Signup from '../user/signup/Signup';
import Profile from '../user/profile/Profile';
import OAuth2RedirectHandler from '../user/oauth2/OAuth2RedirectHandler';
import NotFound from '../common/NotFound';
import LoadingIndicator from '../common/LoadingIndicator';
import { getCurrentUser } from '../util/APIUtils';
import { ACCESS_TOKEN } from '../constants';
import PrivateRoute from '../common/PrivateRoute';
import Alert from 'react-s-alert';
import 'react-s-alert/dist/s-alert-default.css';
import 'react-s-alert/dist/s-alert-css-effects/slide.css';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
authenticated: false,
currentUser: null,
loading: true
}
this.loadCurrentlyLoggedInUser = this.loadCurrentlyLoggedInUser.bind(this);
this.handleLogout = this.handleLogout.bind(this);
}
loadCurrentlyLoggedInUser() {
getCurrentUser()
.then(response => {
this.setState({
currentUser: response,
authenticated: true,
loading: false
});
}).catch(error => {
this.setState({
loading: false
});
});
}
handleLogout() {
localStorage.removeItem(ACCESS_TOKEN);
this.setState({
authenticated: false,
currentUser: null
});
Alert.success("You're safely logged out!");
}
componentDidMount() {
this.loadCurrentlyLoggedInUser();
}
render() {
if(this.state.loading) {
return <LoadingIndicator />
}
return (
<div className="app">
<div className="app-top-box">
<AppHeader authenticated={this.state.authenticated} onLogout={this.handleLogout} />
</div>
<div className="app-body">
<Switch>
<Route exact path="/" component={Home}></Route>
<PrivateRoute path="/profile" authenticated={this.state.authenticated} currentUser={this.state.currentUser}
component={Profile}></PrivateRoute>
<Route path="/login"
render={(props) => <Login authenticated={this.state.authenticated} {...props} />}></Route>
<Route path="/signup"
render={(props) => <Signup authenticated={this.state.authenticated} {...props} />}></Route>
<Route path="/oauth2/redirect" component={OAuth2RedirectHandler}></Route>
<Route component={NotFound}></Route>
</Switch>
</div>
<Alert stack={{limit: 3}}
timeout = {3000}
position='top-right' effect='slide' offset={65} />
</div>
);
}
}
export default App;
src/user/login/Login.js
The Login
component allows users to login using an OAuth2 provider or an email and password.
import React, { Component } from 'react';
import './Login.css';
import { GOOGLE_AUTH_URL, FACEBOOK_AUTH_URL, GITHUB_AUTH_URL, ACCESS_TOKEN } from '../../constants';
import { login } from '../../util/APIUtils';
import { Link, Redirect } from 'react-router-dom'
import fbLogo from '../../img/fb-logo.png';
import googleLogo from '../../img/google-logo.png';
import githubLogo from '../../img/github-logo.png';
import Alert from 'react-s-alert';
class Login extends Component {
componentDidMount() {
// If the OAuth2 login encounters an error, the user is redirected to the /login page with an error
// Here we display the error and then remove the error query parameter from the location.
if(this.props.location.state && this.props.location.state.error) {
setTimeout(() => {
Alert.error(this.props.location.state.error, {
timeout: 5000
});
this.props.history.replace({
pathname: this.props.location.pathname,
state: {}
});
}, 100);
}
}
render() {
if(this.props.authenticated) {
return <Redirect
to={{
pathname: "/",
state: { from: this.props.location }
}}/>;
}
return (
<div className="login-container">
<div className="login-content">
<h1 className="login-title">Login to SpringSocial</h1>
<SocialLogin />
<div className="or-separator">
<span className="or-text">OR</span>
</div>
<LoginForm {...this.props} />
<span className="signup-link">New user? <Link to="/signup">Sign up!</Link></span>
</div>
</div>
);
}
}
class SocialLogin extends Component {
render() {
return (
<div className="social-login">
<a className="btn btn-block social-btn google" href={GOOGLE_AUTH_URL}>
<img src={googleLogo} alt="Google" /> Log in with Google</a>
<a className="btn btn-block social-btn facebook" href={FACEBOOK_AUTH_URL}>
<img src={fbLogo} alt="Facebook" /> Log in with Facebook</a>
<a className="btn btn-block social-btn github" href={GITHUB_AUTH_URL}>
<img src={githubLogo} alt="Github" /> Log in with Github</a>
</div>
);
}
}
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: ''
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleInputChange(event) {
const target = event.target;
const inputName = target.name;
const inputValue = target.value;
this.setState({
[inputName] : inputValue
});
}
handleSubmit(event) {
event.preventDefault();
const loginRequest = Object.assign({}, this.state);
login(loginRequest)
.then(response => {
localStorage.setItem(ACCESS_TOKEN, response.accessToken);
Alert.success("You're successfully logged in!");
this.props.history.push("/");
}).catch(error => {
Alert.error((error && error.message) || 'Oops! Something went wrong. Please try again!');
});
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div className="form-item">
<input type="email" name="email"
className="form-control" placeholder="Email"
value={this.state.email} onChange={this.handleInputChange} required/>
</div>
<div className="form-item">
<input type="password" name="password"
className="form-control" placeholder="Password"
value={this.state.password} onChange={this.handleInputChange} required/>
</div>
<div className="form-item">
<button type="submit" className="btn btn-block btn-primary">Login</button>
</div>
</form>
);
}
}
export default Login
OAuth2RedirectHandler.js
This component is loaded when the user has completed the OAuth2 authentication flow with the server. The server redirects the user to this page with an access token if the authentication was successful, or an error if it failed.
import React, { Component } from 'react';
import { ACCESS_TOKEN } from '../../constants';
import { Redirect } from 'react-router-dom'
class OAuth2RedirectHandler extends Component {
getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
var results = regex.exec(this.props.location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
};
render() {
const token = this.getUrlParameter('token');
const error = this.getUrlParameter('error');
if(token) {
localStorage.setItem(ACCESS_TOKEN, token);
return <Redirect to={{
pathname: "/profile",
state: { from: this.props.location }
}}/>;
} else {
return <Redirect to={{
pathname: "/login",
state: {
from: this.props.location,
error: error
}
}}/>;
}
}
}
export default OAuth2RedirectHandler;
On successful authentication, this component reads the token from the query string, saves it in localStorage and redirects the user to the /profile
page.
If the authentication failed, then this component redirects the user to the /login
page with an error -
Running the react app
You can run the react-social app using npm
like this -
cd react-social
npm install && npm start
The above command will install any missing dependencies and start the app on port 3000
.
If you just want to build the app, then type the following command -
npm run build
The above command will create an optimized production build in a directory named build/
.
Conclusion
That’s all folks! In this tutorial series, you learned how to add social as well as email and password based login to your spring boot application.
I hope you enjoyed this series. Thank you for reading.
Until next time…