Skip to content

Commit

Permalink
OAuth2 login support
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Jun 4, 2023
1 parent 5798035 commit 4546534
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 3 deletions.
8 changes: 8 additions & 0 deletions src/App.js
Expand Up @@ -13,6 +13,8 @@ import EditUserPage from './pages/EditUserPage';
import ChangePasswordPage from './pages/ChangePasswordPage';
import LoginPage from './pages/LoginPage';
import RegistrationPage from './pages/RegistrationPage';
import SocialLoginPage from './pages/SocialLoginPage';
import SocialCallbackPage from './pages/SocialCallbackPage';
import ResetRequestPage from './pages/ResetRequestPage';
import ResetPage from './pages/ResetPage';

Expand All @@ -31,6 +33,12 @@ export default function App() {
<Route path="/register" element={
<PublicRoute><RegistrationPage /></PublicRoute>
} />
<Route path="/oauth2/:provider" element={
<PublicRoute><SocialLoginPage /></PublicRoute>
} />
<Route path="/oauth2/:provider/callback" element={
<PublicRoute><SocialCallbackPage /></PublicRoute>
} />
<Route path="/reset-request" element={
<PublicRoute><ResetRequestPage /></PublicRoute>
} />
Expand Down
14 changes: 13 additions & 1 deletion src/MicroblogApiClient.js
Expand Up @@ -38,7 +38,7 @@ export default class MicroblogApiClient {
'Authorization': 'Bearer ' + localStorage.getItem('accessToken'),
...options.headers,
},
credentials: options.url === '/tokens' ? 'include' : 'omit',
credentials: options.url.startsWith('/tokens') ? 'include' : 'omit',
body: options.body ? JSON.stringify(options.body) : null,
});
}
Expand Down Expand Up @@ -90,6 +90,18 @@ export default class MicroblogApiClient {
return 'ok';
}

async oauth2Login(provider, code, state) {
const response = await this.post('/tokens/oauth2/' + provider, {
code,
state,
}, {});
if (!response.ok) {
return response.status === 401 ? 'fail' : 'error';
}
localStorage.setItem('accessToken', response.body.access_token);
return 'ok';
}

async logout() {
await this.delete('/tokens');
localStorage.removeItem('accessToken');
Expand Down
2 changes: 1 addition & 1 deletion src/components/Header.js
Expand Up @@ -28,7 +28,7 @@ export default function Header() {
Profile
</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item as={NavLink} to="/password">
<NavDropdown.Item as={NavLink} to="/password" disabled={!user.has_password}>
Change Password
</NavDropdown.Item>
<NavDropdown.Item onClick={logout}>
Expand Down
11 changes: 10 additions & 1 deletion src/contexts/UserProvider.js
Expand Up @@ -28,13 +28,22 @@ export default function UserProvider({ children }) {
return result;
}, [api]);

const oauth2Login = useCallback(async (provider, code, state) => {
const result = await api.oauth2Login(provider, code, state);
if (result === 'ok') {
const response = await api.get('/me');
setUser(response.ok ? response.body : null);
}
return result;
}, [api]);

const logout = useCallback(async () => {
await api.logout();
setUser(null);
}, [api]);

return (
<UserContext.Provider value={{ user, setUser, login, logout }}>
<UserContext.Provider value={{ user, setUser, login, oauth2Login, logout }}>
{children}
</UserContext.Provider>
);
Expand Down
1 change: 1 addition & 0 deletions src/pages/LoginPage.js
Expand Up @@ -53,6 +53,7 @@ export default function LoginPage() {
return (
<Body>
<h1>Login</h1>
<p>No patience to type? Login with <Link to="/oauth2/google">Google</Link> or with <Link to="/oauth2/github">GitHub</Link>.</p>
<Form onSubmit={onSubmit}>
<InputField
name="username" label="Username or email address"
Expand Down
41 changes: 41 additions & 0 deletions src/pages/SocialCallbackPage.js
@@ -0,0 +1,41 @@
import { useState, useEffect, useRef } from 'react';
import { useParams, Navigate, useLocation } from 'react-router-dom';
import { useUser } from '../contexts/UserProvider';
import { useFlash } from '../contexts/FlashProvider';

export default function SocialCallbackPage() {
const { oauth2Login } = useUser();
const flash = useFlash();
const location = useLocation();
const firstRender = useRef(true);

const [redirectUrl, setRedirectUrl] = useState(null);
const { provider } = useParams();
const searchParams = new URLSearchParams(location.search);
const code = searchParams.get('code');
const state = searchParams.get('state');

useEffect(() => {
if (!firstRender.current) {
return;
}
firstRender.current = false;

(async () => {
const result = await oauth2Login(provider, code, state);
if (result === 'fail') {
flash('Could not log you in.', 'danger');
}
else if (result === 'ok') {
let next = '/';
if (location.state && location.state.next) {
next = location.state.next;
}
setRedirectUrl(next);
}

})();
}, [oauth2Login, provider, code, state, flash, location]);

return redirectUrl === null ? null : <Navigate to={redirectUrl} />;
}
10 changes: 10 additions & 0 deletions src/pages/SocialLoginPage.js
@@ -0,0 +1,10 @@
import { useParams } from 'react-router-dom';
import { useApi } from '../contexts/ApiProvider';

export default function SocialLoginPage() {
const { provider } = useParams();
const api = useApi();

window.location.href = api.base_url + `/tokens/oauth2/${provider}`;
return null;
}

0 comments on commit 4546534

Please sign in to comment.