Create a React PWA with Social Login Authentication

Progressive Web Apps (PWAs) offer the speed, reliability, and offline functionality of native apps—all delivered through the web. However, security is as important as performance, especially regarding user authentication. Modern authentication is essential in a world where users expect instant, secure access across multiple devices and platforms.
Identity providers, like Okta, offer secure, scalable, and developer-friendly tools for implementing authentication. Federated identity allows users to sign in using existing social accounts.
In this article, we’ll walk through how to build a React-based PWA with offline support and integrate it with Google Social Login using Okta. You’ll learn how to deliver a fast, reliable user experience with modern identity features built in. Let’s get started.
Table of Contents
- Creating an Okta integration
- Create the React app
- Secure routes in your React app with React Router
- Authenticate using OAuth 2.0 and OpenID Connect (OIDC)
- Federated identity using Social Login
- Set up your React app as a PWA
- Build a secure todo list React PWA
- Authenticate with Social Login from a React PWA
- Learn more about React, PWA, Social Login, and Federated Identity
What you’ll need
This is a beginner-friendly tutorial, so you’ll mostly need the willingness to learn! However, you’d need access to a few things:
- Node.js and NPM. Any LTS version should be fine, but in this tutorial, I use Node 22 and NPM v10
- A command terminal
- Basic JavaScript and TypeScript knowledge
- An IDE of your choice. I use PHPStorm, but you can use VSCode or something similar.
- A Google Cloud Console Account. You can set up one using your Gmail account.
Creating an Okta integration
Before you begin, you’ll need an Okta Integrator Free Plan account. To get one, sign up for an Integrator account. Once you have an account, sign in to your Integrator account. Next, in the Admin Console:
- Go to Applications > Applications
- Click Create App Integration
- Select OIDC - OpenID Connect as the sign-in method
- Select Single-Page Application as the application type, then click Next
-
Enter an app integration name
- In the Grant type section, ensure that both Authorization Code and Refresh Token are selected
- Configure the redirect URIs:
- Sign-in redirect URIs:
http://localhost:5173/login/callback
- Sign-out redirect URIs:
http://localhost:5173
- Sign-in redirect URIs:
- In the Controlled access section, select the appropriate access level
- Click Save
Where are my new app's credentials?
Creating an OIDC Single-Page App manually in the Admin Console configures your Okta Org with the application settings. You may also need to configure trusted origins for http://localhost:5173
in Security > API > Trusted Origins.
After creating the app, you can find the configuration details on the app’s General tab:
- Client ID: Found in the Client Credentials section
- Issuer: Found in the Issuer URI field for the authorization server that appears by selecting Security > API from the navigation pane.
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
NOTE: You can also use the Okta CLI Client or Okta PowerShell Module to automate this process. See this guide for more information about setting up your app.
Create the React app
We’ll use a Vite template to scaffold the project. The example app for this tutorial is a todo application called “Lister”. To create a React app named “Lister”, run the following command in your terminal to scaffold the project:
npm create vite@5.4 lister
Select React and TypeScript as the variant.
Follow the instructions after running the command to navigate into your app directory and installing dependencies.
We have extra dependencies to add. Run the following commands in your terminal.
Install React Router by running
npm install react-router-dom@5.3.4
Install React Router types by running
npm install --save-dev @types/react-router-dom@5.3.3
To use Okta authentication with our React app, let’s install the Okta SDKs by running
npm install @okta/okta-react@6.9.0 @okta/okta-auth-js@7.8.1
I wrote this post using Vite 5.4, React 18.3, Okta React 6.9, and Okta AuthJS SDK 7.8.
With this, you now have the base React project set up.
Secure routes in your React app with React Router
Open the project in your IDE. Let’s navigate to App.tsx
and paste in the following code:
import './App.css';
import { Route, Switch, useHistory } from 'react-router-dom';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import { LoginCallback, Security } from '@okta/okta-react';
import Home from './pages/Home.tsx';
const oktaAuth = new OktaAuth({
clientId: import.meta.env.VITE_OKTA_CLIENT_ID,
issuer: `https://${import.meta.env.VITE_OKTA_DOMAIN}`,
redirectUri: window.location.origin + '/login/callback',
scopes: ['openid', 'profile', 'email', 'offline_access'],
});
function App() {
const history = useHistory();
const restoreOriginalUri = (_oktaAuth: OktaAuth, originalUri: string) => {
history.replace(toRelativeUrl(originalUri || '/', window.location.origin));
};
return (
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
<Switch>
<Route path="/login/callback" component={LoginCallback}/>
<Route path="/" exact component={Home}/>
</Switch>
</Security>
);
}
export default App
We set up the Okta authentication SDK packages in the App
Component. Pay attention to the OktaAuth
config:
const oktaAuth = new OktaAuth({
clientId: import.meta.env.VITE_OKTA_CLIENT_ID,
issuer: `https://${import.meta.env.VITE_OKTA_DOMAIN}`,
redirectUri: window.location.origin + '/login/callback',
scopes: ['openid', 'profile', 'email', 'offline_access'],
});
If you encounter any issues with the login, a good place to start debugging is from here. We’ll use environment variables to define our OIDC configuration in the app for convenience. In the root of your Lister project, create an .env
file and edit it to look like so:
VITE_OKTA_DOMAIN={yourOktaDomain}
VITE_OKTA_CLIENT_ID={yourOktaClientID}
Replace {yourOktaDomain} with your Okta domain for example, dev-123.okta.com
or trial-123.okta.com
. Note the variable doesn’t include the HTTP protocol. Replace {yourOktaClientID} with the Okta client ID from the Okta application you created.
Before moving forward, let’s set up React Router in our project root. Navigate to src/main.tsx
and replace the existing code with the following code snippet:
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { BrowserRouter } from "react-router-dom";
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<BrowserRouter>
<App/>
</BrowserRouter>,
)
In the App.tsx
earlier, we imported Home
from ./pages/Home.tsx
and used it in our routing. Let’s create the Home
component. In the src
folder, create a pages
folder, and in that, create a Home.tsx
file.
const Home = () => {
return (<h2>You are home</h2>);
}
export default Home;
This is a minimal home component that represents our home page.
Authenticate using OAuth 2.0 and OpenID Connect (OIDC)
Next, we want to add the ability to sign in and out with our Okta without social login as a starting point. We’ll add the social login connection later.
To do that, we’ll create the SignIn
component and a generic Layout
component to control user access based on their authentication. Navigate to your src
folder, then create a components
folder to hold child components.
In the newly created components
folder, create the Layout.tsx
, Layout.css
, and SignIn.tsx
files.
Open the Layout.tsx
file and add the following code:
import './Layout.css';
import { useOktaAuth } from "@okta/okta-react";
import SignIn from "./SignIn.tsx";
import { Link } from "react-router-dom";
import logo from '../assets/react.svg';
const Layout = ({children}) => {
const { authState, oktaAuth} = useOktaAuth();
const signout = async () => await oktaAuth.signOut();
return authState?.isAuthenticated ? (<>
<div className="navbar">
<Link to="/"><img src={logo} className="logo" /></Link>
<div className="right">
<Link to="/profile">Profile</Link>
<button onClick={signout} className="no-outline">Sign Out</button>
</div>
</div>
<div className="layout">
{...children}
</div>
</>) : <SignIn/>;
}
export default Layout;
This component imports the useOktaAuth
from the @okta/okta-react
package. This React hook helps us the user’s authenticated state and gives them access to the child components of the Layout
component. The hook also lets us sign in or out our users.
At the top of the file, we import Layout.css
. Open Layout.css
so fill in the CSS we need:
.layout {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.layout.sign-in {
margin-top: 35vh;
}
.logo {
height: 32px;
will-change: filter;
transition: filter 300ms;
}
.navbar {
display: flex;
justify-content: space-between;
}
These minor stylings help the Layout.tsx
navbar look proper. Let’s not forget the SignIn
component used in the Layout
component.
Paste the following code into SignIn.tsx
:
import { useOktaAuth } from "@okta/okta-react";
import logo from '../assets/react.svg';
const SignIn = () => {
const { oktaAuth} = useOktaAuth();
const signin = async () => await oktaAuth.signInWithRedirect();
return (
<div className="sign-in layout">
<h2> <img src={logo} className="logo" alt="Logo"/> Lister</h2>
<button className="outlined" onClick={signin}>Sign In</button>
</div>
);
}
export default SignIn;
Here, we use the same useOktaAuth
hook to sign in our user. Lastly, we update src/App.tsx
to use our new Layout
component. We wrap the Layout component around the routes that require authentication. Your code now looks like this:
import './App.css';
import { Route, Switch, useHistory } from 'react-router-dom';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import { LoginCallback, Security } from '@okta/okta-react';
import Home from './pages/Home.tsx';
import Profile from './pages/Profile.tsx';
import Layout from "./components/Layout.tsx";
const oktaAuth = new OktaAuth({
clientId: import.meta.env.VITE_OKTA_CLIENT_ID,
issuer: `https://${import.meta.env.VITE_OKTA_DOMAIN}`,
redirectUri: window.location.origin + '/login/callback',
scopes: ['openid', 'profile', 'email'],
}) ;
function App() {
const history = useHistory();
const restoreOriginalUri = (_oktaAuth: OktaAuth, originalUri: string) => {
history.replace(toRelativeUrl(originalUri || '/', window.location.origin));
};
return (
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
<Switch>
<Route path="/login/callback" component={LoginCallback}/>
<Layout>
<Route path="/" exact component={Home}/>
<Route path="/profile" component={Profile}/>
</Layout>
</Switch>
</Security>
);
}
export default App
Be careful not to wrap the callback route in the Layout
component, or else you’ll experience some weirdness during logins. If you look at the code above, you see we added a route for a profile component. Let’s create that component!
Navigate to src/pages
and create the Profile.tsx
and Profile.css
files. In your Profile.tsx
file, paste these in:
import './Profile.css';
import { useState, useEffect } from "react";
import { useOktaAuth } from "@okta/okta-react";
import { IDToken, UserClaims } from "@okta/okta-auth-js";
const Profile= () => {
const { authState, oktaAuth} = useOktaAuth();
const [userInfo, setUserInfo] = useState<UserClaims | null>(null);
useEffect(() => {
if(!authState || !authState.isAuthenticated) setUserInfo(null);
else setUserInfo((authState.idToken as IDToken).claims);
}, [authState, oktaAuth]);
return (userInfo) ? (
<div>
<div className="profile">
<h1>My User Profile (ID Token Claims)</h1>
<p>
Below is the information from your ID token which was obtained during the
<a href="https://developer.okta.com/docs/guides/implement-auth-code-pkce">PKCE Flow</a>
{' '}
and is now stored in local storage.
</p>
<p>
This route is protected with the
{' '}
<code><SecureRoute></code>
{' '}
component, which will ensure that this page cannot be accessed until you have
authenticated.
</p>
<table>
<thead>
<tr>
<th>Claim</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{Object.entries(userInfo).map((claimEntry) => {
const claimName = claimEntry[0];
const claimValue = claimEntry[1];
const claimId = `claim-${claimName}`;
return (
<tr key={claimName}>
<td>{claimName}</td>
<td id={claimId}>{claimValue.toString()}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
) : (<div>
<p>Fetching user profile...</p>
</div>)
};
export default Profile;
And in your Profile.css
file, add the following styles:
td, th {
text-align: left;
padding: 1px 10px
}
td:first-child, th:first-child {
border-right: 1px solid #dcdcdc;
}
table {
max-width: 600px;
}
.profile {
margin: auto;
}
.profile h1, p {
text-align: left;
width: fit-content;
}
The Profile
component shows you the information in the useOktaAuth
. When building your profile page, you will probably use only a handful of that information.
Lastly, paste this helper CSS code into the index.css
file in your folder root; it’s just minor styling tweaks to improve your app’s appearance.
#root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a, button {
font-weight: 500;
color: #213547;
text-decoration: inherit;
}
a:hover, button:hover {
color: #535bf2;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
button.outlined {
border: 1px solid;
}
button.no-outline {
border: none;
}
button.no-outline:focus, button.no-outline:focus-visible, button.no-outline:hover, button.no-outline:active {
border: none;
outline: none;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
Run npm run dev
in the console. The command serves your app at http://localhost:5173
and you should be able to sign in with your Okta account.
With these, all we need to do now is integrate Social Login and then make this app a PWA, both of which are straightforward!
Federated identity using Social Login
Social login is an authentication method that allows users to sign into an application using their existing credentials from platforms like Google, Facebook, or Apple. It simplifies the login process, reduces password fatigue, and enhances security by leveraging trusted identity providers. In our case, we are choosing Google as our social login provider.
Configure Google as an Identity Provider in Okta
First, we’d need to sign up for Google Workspace and create a Google project. After that, we configure Google as an Identity Provider (IDP). Follow the instructions to set up Google for Social Login from Okta Developer documentation.
When you define the OAuth consent screen in Google Cloud, use the following configuration:
- Add
http://localhost:5173
to your authorized JavaScript Origins - this is the test server for our React application. - Add
https://{yourOktaDomain}/oauth2/v1/authorize/callback
to the Authorized redirect urls session. Replace{yourOktaDomain}
with your actual Okta domain.
When adding the required scopes in Google Cloud, include the ./auth/userinfo.email
, ./auth/userinfo.profile
, and the openid
scopes.
After setting up Google Cloud, you’ll configure Okta. Use the following values:
- Enable automatic account linking to make it easier for users with an Okta account to sign in with Google.
- Add routing rules to allow all logins to use Google Social Login. For this tutorial, we’re keeping the routing conditions permissive; however, you should be a lot more stringent on a production application. You can check the routing page to configure routing to fit your use case better.
Test authenticating with Google Social Login
If you run npm run dev
and click the sign-in button, you should see the “Sign In With Google” button and your usual Okta sign-in / sign-up screen!
Set up your React app as a PWA
Lastly, let’s make our app a PWA so we can use it offline. First, we need to add a new dependency. Open the command terminal to the project’s root and run the following command.
npm install vite-plugin-pwa@1.0.1
Next, we update our vite.config.ts in your project root to include PWA configuration and add manifest icons:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from "vite-plugin-pwa";
// https://vitejs.dev/config/
const manifestIcons = [
{
src: 'pwa-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'pwa-512.png',
sizes: '512x512',
type: 'image/png',
}
]
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true
},
manifest: {
name: 'Lister',
short_name: 'lister',
icons: manifestIcons,
}
})
],
})
You can get cute favicons from an icon generator and replace the manifestIcons
source images with those. You can also look at the Vite PWA documentation to better understand each option’s meaning and how to use it.
With these changes, end your current npm script and run npm run dev
again; everything should be peachy. Now we have an app with Social Login capabilities.
Since this application is a todo list application, let’s add the todo list feature. Since our app is a PWA, our users should be able to use the application even when offline. To make the data accessible offline, we can store it locally on the client using browser storage and then sync the data with our servers using service workers (let us know if you want to see a tutorial using service workers).
Build a secure todo list React PWA
Since we will persist the todo list data, creating a model is a good idea. This model serves as a layer of abstraction over the DB calls. In this section, we’ll save the data to local storage; in the future, we may want to switch to another technology. A model helps us make this change in the implementation without changing code when consuming the model. Now let’s create the model: navigate to the src
folder and create a folder named models
. In that folder, create a Task.model.ts
. We’ll call each item in the todo list a task.
The task model file should look like this:
export interface Task {
name: string;
description: string;
done: boolean;
}
const key = 'lister-tasks';
export default {
addTask: (task: Task) => {
const currentTasksJSON = localStorage.getItem(key);
if (!currentTasksJSON) {
localStorage.setItem(key, JSON.stringify([task]));
return;
}
const currentTasks = JSON.parse(currentTasksJSON);
currentTasks.push(task);
localStorage.setItem(key, JSON.stringify(currentTasks));
},
all: (): Task[] => {
const currentTasksJSON = localStorage.getItem(key);
if (!currentTasksJSON) return [];
return JSON.parse(currentTasksJSON);
},
save: (tasks: Task[]) => localStorage.setItem(key, JSON.stringify(tasks)),
}
The model is a small wrapper over LocalStorage
. The first part of the model defines the Task interface – all we need for a task is its name, description, and done state. The key variable is the localStorage
item name; I chose to use lister-tasks
for mine.
Remember, don’t store sensitive user data (e.g., passwords) on the client side but on a secure server!
Next up, we update the home page at src/pages/Home.tsx
to look like this:
import './Home.css';
import { useEffect, useState } from "react";
import TaskModel, { Task } from "../models/Task.model.ts";
const EMPTY_TASK: Task = { name: "", description: "", done: false } as const;
const Home = () => {
const [tasks, setTasks] = useState<Task[]>(TaskModel.all().reverse());
const [addMode, setAddMode] = useState(false);
const [form, setForm] = useState<Task>(EMPTY_TASK);
const [expanded, setExpanded] = useState<boolean[]>(new Array(tasks.length).fill(false));
useEffect(() => TaskModel.save(tasks), [tasks]);
const toggleTask = (id: number) => {
const _tasks = [...tasks];
_tasks[id].done = !_tasks[id].done;
setTasks(_tasks);
}
const addNewTask = (e: Event) => {
e.preventDefault();
setExpanded(new Array(tasks.length + 1).fill(false));
setTasks([...tasks, form]);
setForm(EMPTY_TASK);
setAddMode(!addMode);
}
const toggleExpansion = (id: number) => {
const _expanded = [...expanded];
_expanded[id] = !_expanded[id];
setExpanded(_expanded);
}
return (<>
<h2 className="tab-heading">
<button className={`no-outline ${!addMode && 'active'}`}
onClick={() => setAddMode(false)}>Task List
</button>
<button className={`no-outline ${addMode && 'active'}`} onClick={() => setAddMode(true)}>New
Task +
</button>
</h2>
{addMode && <form className="tab" action="#" onSubmit={addNewTask}>
<div className="form-fields">
<div className="form-group">
<label htmlFor="name">Name</label>
<input type="text" name="name" id="name" placeholder="Task name"
onChange={(e) => setForm({...form, name: e.target.value})} required/>
</div>
<div className="form-group full">
<label htmlFor="description">Description</label>
<textarea rows="5" maxLength="800"
onChange={(e) => setForm({...form, description: e.target.value})}
className="form-control" name="description"
id="description" placeholder="describe the task..."></textarea>
</div>
</div>
<div className="form-group">
<input type="submit" value="Submit"/>
</div>
</form>}
{!addMode && <ul className="tab task-list">
{tasks.map((task, idx) =>
<li key={idx} className={`${task.done && 'done'}`}>
<div className="title-card">
<input type="checkbox" name={'task' + idx} checked={task.done}
onChange={() => toggleTask(idx)}/>
<p className="name">{task.name}</p>
<p className="expand" onClick={() => toggleExpansion(idx)}>▼</p>
</div>
{expanded[idx] && <p className="description">{task.description}</p>}
</li>)}
</ul>}
</>);
}
export default Home;
The first three lines of the code are the necessary imports. Next, we create a default empty task. The rest of the component is a basic CRUD page, with the required state for creating, reading, updating, and deleting tasks. I used a useEffect
hook to save the tasks to local storage whenever they change. If you look at the component, a Home.css
import is at the top. Let’s create that file in the same directory and paste these into the content:
.form-fields {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.form-group{
margin: 5px 0;
}
.form-fields .form-group{
width: 100%;
display: flex;
flex-direction: column;
}
.form-group.check-group {
display: flex;
}
/**Submit button styling**/
input:not([type="submit"]):not([type="checkbox"]), select, textarea {
display: block;
max-width: 100%;
padding: 6px 12px;
font-size: 16px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
margin-bottom: 5px;
}
form input[type="submit"] {
display: block;
background-color: #213547;
box-shadow: 0 0 0 0 #213547;
text-transform: capitalize;
letter-spacing: 1px;
border: none;
color: #fff;
font-size: .9em;
text-align: center;
padding: 10px;
width: 50%;
margin: 15px 0 0 auto;
transition: background-color 250ms ease;
border-radius: 5px;
}
.tab {
max-width: 500px;
margin: auto;
}
form label {
text-align: left;
max-width: 100%;
margin-bottom: 5px;
font-size: 16px;
font-weight: 300;
line-height: 24px;
}
.tab-heading {
border-bottom: 1px solid #D9E4EEFF;
}
.tab-heading button{
width: 50%;
}
.tab-heading button:first-child{
text-align: right;
}
.tab-heading button:last-child{
text-align: left;
}
.tab-heading button:hover {
background: #dcdcdc;
border-radius: 0;
}
.tab-heading button.active{
background: rgba(217, 228, 238, 0.42);
}
.tab-heading button.active:first-child {
border-right: 1px solid rgba(217, 228, 238, 0.9);
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.tab-heading button.active:last-child{
border-left: 1px solid rgba(217, 228, 238, 0.9);
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.task-list {
list-style-type: none;
}
.task-list p.description {
text-align: left;
margin-top: 0;
font-size: 0.8rem;
}
.task-list li {
display: flex;
flex-direction: column;
border-bottom: 2px solid rgba(217, 228, 238, 0.7);
padding: 5px 10px;
}
.task-list li .title-card {
display: flex;
}
.task-list li.done .title-card *:not(.expand){
text-decoration: line-through;
}
.task-list li:hover {
background: rgba(234, 243, 252, 0.59);
}
.task-list li input[type=checkbox] {
margin-right: 15px;
cursor: pointer;
}
.task-list li p.name {
font-size: 1.2rem;
}
.task-list li p.expand {
color: #46617a;
font-size: 1rem;
margin-left: auto;
cursor: pointer;
}
The above are helper styles. I used a tabular design for the todo list component so CRUD can be on the same page without using modal popups. Once all the files are in place, you’ll see the Todo List home page when you log in with Okta.
Once you have all the required manifest icons in your project, when you serve the app, you’ll see a prompt in the browser to install it on your machine! If you don’t want to create icons, use the ones in the sample repo.
Authenticate with Social Login from a React PWA
Great job making it this far! Along the way, we’ve explored how social login works with Okta and Google and how to set up a basic PWA using React, Vite, and the Vite PWA plugin. As a bonus, we now have a handy little todo list app to help keep our day on track!
Of course, a production-ready application would involve more advanced service worker configurations and a proper database setup, but our current implementation is adequate for an introduction. Now it’s your turn to have fun: open the app in your browser, try signing in with Okta or Google, and test the install prompt to see how smoothly it runs as a standalone app. Happy coding!
Learn more about React, PWA, Social Login, and Federated Identity
If you want to learn more about the ways you can incorporate authentication and authorization security in your apps, you might want to check out these resources:
- The Ultimate Guide to Progressive Web Applications
- Use Redux to Manage Authenticated State in a React App
- Android Login Made Easy with OIDC
Remember to follow us on Twitter and subscribe to our YouTube channel for fun and educational content. We also want to hear from you about topics you want to see and questions you may have. Leave us a comment below! Until next time! Toodles!
Okta Developer Blog Comment Policy
We welcome relevant and respectful comments. Off-topic comments may be removed.