Skip to main content

Nidam React SPA

Continuing working on the React application we started on the registration front page. We will put everything together.

Checking if the User is logged in at Startup

Let's take a look at the top level components. AuthenticationStartup is the component that is rendered first. Its job is very simple: Call the resource sever public endpoint /me. While waiting for the response show a splash screen. When the response is returned then renders the children AppRoutes which holds the routing.

src/index.js
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter basename="/react-ui" >
<AuthenticationStartup>
{/* emotion: this must be added to get rid of the top margin that won't go away when everything is set to margin: 0*/}
<Global styles={css` body { margin: 0; } `} />
<AppRoutes/>
</AuthenticationStartup>
</BrowserRouter>
</Provider>
</React.StrictMode>
);

If the response of /me has populated data. Then the store property authentication.authenticated is set to true, otherwise it says false. We will use this property to decide whether to render private or public routes. Also, the response is saved at the store property authentication.userInfo.

src/authentication/AuthenticationStartup.js
const AuthenticationStartup = props => {
const dispatch = useDispatch();

const isLoggedInLoading = useSelector((state) => state.authentication.isLoggedInLoading);
const userInfo = useSelector((state) => state.authentication.userInfo);
const isLoggedInError = useSelector((state) => state.authentication.isLoggedInError);

const [phase, setPhase] = useState(1);
const [showSplashScreen, setShowSplashScreen] = useState(true);


useEffect(() => {
dispatch(authenticationSagas.isLoggedIn());
setPhase(2);
setShowSplashScreen(true);
}, []);

useEffect(() => {
if (phase === 2 && !isLoggedInLoading) {
if(isLoggedInError === null){
if(userInfo?.username === ""){
// if empty then unauthenticated phase 3
setPhase(3);
} else {
// if not then authenticated phase 4
setPhase(4);
// userinfo is already loaded in store, add an authenticated flag and set to true
dispatch(authenticated());
}
}
setPhase(3);
setShowSplashScreen(false);
disableSplashScreen();
}
}, [isLoggedInError, phase, isLoggedInLoading, userInfo]);

return showSplashScreen ? <LayoutSplashScreen/> : props.children;
}

export default AuthenticationStartup;

const disableSplashScreen = () => {
const splashScreen = document.getElementById('splash-screen')
if (splashScreen) {
splashScreen.style.setProperty('display', 'none')
}
}

Routing

We have two distinct routings:

  • not authenticated: Sign up and error pages.
  • authenticated: Secret and error pages.
src/routing/AppRoutes.js
const AppRoutes = () => {
const authenticated = useSelector((state) => state.authentication.authenticated);

return (<Routes>
<Route path='error/404' element={<ErrorPage />} />
{authenticated ? (
<>
<Route path="secret" element={<Private/>} />
<Route index element={<Navigate to='/secret' />} />
</>
) : (
<>
<Route path="signup" element={<SignUp/>} />
<Route index element={<Navigate to='/signup' />} />
</>
)}
<Route path='*' element={<Navigate to='/error/404' />} />
</Routes>);
}

The sign up page was explained on the Registration Front Page. Next, the login Process is discussed.

All Roads lead to the Login Page

Start nidam-spa using npm start then navigate to http://localhost:7080/react-ui

danger

Remember nidam-spa must be accessed through the reverse proxy and not directly.

You will be redirected to the sign up page http://localhost:7080/react-ui/signup, which shows the Login button. Login Button

Let's look at the code of this component.

src/routing/AppRoutes.js
const Login = props => {
const login = (event) => {
event.preventDefault();

const currentPath = "/";
let url = new URL(process.env.REACT_APP_LOGIN_URL);

url.searchParams.append(
"post_login_success_uri",
`${process.env.REACT_APP_BASE_URI}${currentPath}`
)

window.location.href = url.toString();
}

return <Link onClick={e => login(e)} style={{cursor: "pointer"}}>
Already have an account? Sign in
</Link>
}

export default Login;

And

.env
PORT=4001
REACT_APP_BACKEND_REGISTRATION_URL=http://localhost:4000/

REACT_APP_REVERSE_PROXY_URI=http://localhost:7080
REACT_APP_BASE_PATH='/react-ui'
REACT_APP_BASE_URI=${REACT_APP_REVERSE_PROXY_URI}${REACT_APP_BASE_PATH}

REACT_APP_RESOURCE_SERVER_URI=http://localhost:7080/bff/api

REACT_APP_LOGIN_URL=${REACT_APP_REVERSE_PROXY_URI}/bff/oauth2/authorization/token-generator

This component builds the login URL from .env properties. And adds a search parameter that the BFF will redirect to after successful authentication.

http://localhost:7080/bff/oauth2/authorization/token-generator?post_login_success_uri=http%3A%2F%2Flocalhost%3A7080%2Freact-ui%2F

When the user clicks the Login Button. He will be redirected to this address. Here, the authorization server takes over and displays the login form. Enter a valid email and password, and click Sign in.

The Result

successful authentication page

Sign in Video

If we check the cookies we will find the SESSION cookie.

SESSION cookie

The secret page displayed here makes a call to the secured endpoint /demo and the resource server successfully returns the JwtAuthenticationToken object:

{
"authorities": [
{
"authority": "manage-users"
},
{
"authority": "manage-projects"
}
],
"details": {
"remoteAddress": "127.0.0.1",
"sessionId": null
},
"authenticated": true,
"principal": {
"tokenValue": "eyJraWQiOiJjYWI1ZTU2My0zM2RiLTQ1YTUtOWZkMy05Yzc2MmU2OTI5NWEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtZWhkaUBuaWRhbS5jb20iLCJhdWQiOiJjbGllbnQiLCJuYmYiOjE3MTM4NzMzMzksInNjb3BlIjpbIm9wZW5pZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjcwODAvYXV0aCIsImV4cCI6MTcxMzkxNjUzOSwiaWF0IjoxNzEzODczMzM5LCJqdGkiOiJiYWI5Y2I5MS1iZjMwLTQwMWUtYTMxOS04ZjJiMmI1ZDg1MDQiLCJhdXRob3JpdGllcyI6WyJtYW5hZ2UtdXNlcnMiLCJtYW5hZ2UtcHJvamVjdHMiXX0.EFJShYhsd4_GT9ENc59YYWWRmY_czrjhpnUH3xKB6ISeGLMJ13ylAv_girVajbcBtlI4ck1FU8Sb9A6x3V3BEDzCHFh_j7dGRA6uTLGEDMKk1Ci46oYYyWmqmQ5hm3LJZ9W66sg1M4oRMIPGh7tgtpm9LNcS_RRG6ja76eswovVfq18yQmx8PmlbQ0IpJiiZUetqRBErU09Wc63kHxmTtO_pWPVtS_qK7xs1u-KAlj0zTxTirowuVvadm1sK2Q9vDwzKdaBXquNPDyDjAGKpwQF-_jIK5RNQjvKferchBpyB-gMQcEY0MdAruIse12xTHRfx8TBRDy4TYnXp515mPg",
"issuedAt": "2024-04-23T11:55:39Z",
"expiresAt": "2024-04-23T23:55:39Z",
"headers": {
"kid": "cab5e563-33db-45a5-9fd3-9c762e69295a",
"alg": "RS256"
},
"claims": {
"sub": "mehdi@nidam.com",
"aud": [
"client"
],
"nbf": "2024-04-23T11:55:39Z",
"scope": [
"openid"
],
"iss": "http://localhost:7080/auth",
"exp": "2024-04-23T23:55:39Z",
"iat": "2024-04-23T11:55:39Z",
"jti": "bab9cb91-bf30-401e-a319-8f2b2b5d8504",
"authorities": [
"manage-users",
"manage-projects"
]
},
"id": "bab9cb91-bf30-401e-a319-8f2b2b5d8504",
"subject": "mehdi@nidam.com",
"notBefore": "2024-04-23T11:55:39Z",
"audience": [
"client"
],
"issuer": "http://localhost:7080/auth"
},
"credentials": {
"tokenValue": "eyJraWQiOiJjYWI1ZTU2My0zM2RiLTQ1YTUtOWZkMy05Yzc2MmU2OTI5NWEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtZWhkaUBuaWRhbS5jb20iLCJhdWQiOiJjbGllbnQiLCJuYmYiOjE3MTM4NzMzMzksInNjb3BlIjpbIm9wZW5pZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjcwODAvYXV0aCIsImV4cCI6MTcxMzkxNjUzOSwiaWF0IjoxNzEzODczMzM5LCJqdGkiOiJiYWI5Y2I5MS1iZjMwLTQwMWUtYTMxOS04ZjJiMmI1ZDg1MDQiLCJhdXRob3JpdGllcyI6WyJtYW5hZ2UtdXNlcnMiLCJtYW5hZ2UtcHJvamVjdHMiXX0.EFJShYhsd4_GT9ENc59YYWWRmY_czrjhpnUH3xKB6ISeGLMJ13ylAv_girVajbcBtlI4ck1FU8Sb9A6x3V3BEDzCHFh_j7dGRA6uTLGEDMKk1Ci46oYYyWmqmQ5hm3LJZ9W66sg1M4oRMIPGh7tgtpm9LNcS_RRG6ja76eswovVfq18yQmx8PmlbQ0IpJiiZUetqRBErU09Wc63kHxmTtO_pWPVtS_qK7xs1u-KAlj0zTxTirowuVvadm1sK2Q9vDwzKdaBXquNPDyDjAGKpwQF-_jIK5RNQjvKferchBpyB-gMQcEY0MdAruIse12xTHRfx8TBRDy4TYnXp515mPg",
"issuedAt": "2024-04-23T11:55:39Z",
"expiresAt": "2024-04-23T23:55:39Z",
"headers": {
"kid": "cab5e563-33db-45a5-9fd3-9c762e69295a",
"alg": "RS256"
},
"claims": {
"sub": "mehdi@nidam.com",
"aud": [
"client"
],
"nbf": "2024-04-23T11:55:39Z",
"scope": [
"openid"
],
"iss": "http://localhost:7080/auth",
"exp": "2024-04-23T23:55:39Z",
"iat": "2024-04-23T11:55:39Z",
"jti": "bab9cb91-bf30-401e-a319-8f2b2b5d8504",
"authorities": [
"manage-users",
"manage-projects"
]
},
"id": "bab9cb91-bf30-401e-a319-8f2b2b5d8504",
"subject": "mehdi@nidam.com",
"notBefore": "2024-04-23T11:55:39Z",
"audience": [
"client"
],
"issuer": "http://localhost:7080/auth"
},
"token": {
"tokenValue": "eyJraWQiOiJjYWI1ZTU2My0zM2RiLTQ1YTUtOWZkMy05Yzc2MmU2OTI5NWEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtZWhkaUBuaWRhbS5jb20iLCJhdWQiOiJjbGllbnQiLCJuYmYiOjE3MTM4NzMzMzksInNjb3BlIjpbIm9wZW5pZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjcwODAvYXV0aCIsImV4cCI6MTcxMzkxNjUzOSwiaWF0IjoxNzEzODczMzM5LCJqdGkiOiJiYWI5Y2I5MS1iZjMwLTQwMWUtYTMxOS04ZjJiMmI1ZDg1MDQiLCJhdXRob3JpdGllcyI6WyJtYW5hZ2UtdXNlcnMiLCJtYW5hZ2UtcHJvamVjdHMiXX0.EFJShYhsd4_GT9ENc59YYWWRmY_czrjhpnUH3xKB6ISeGLMJ13ylAv_girVajbcBtlI4ck1FU8Sb9A6x3V3BEDzCHFh_j7dGRA6uTLGEDMKk1Ci46oYYyWmqmQ5hm3LJZ9W66sg1M4oRMIPGh7tgtpm9LNcS_RRG6ja76eswovVfq18yQmx8PmlbQ0IpJiiZUetqRBErU09Wc63kHxmTtO_pWPVtS_qK7xs1u-KAlj0zTxTirowuVvadm1sK2Q9vDwzKdaBXquNPDyDjAGKpwQF-_jIK5RNQjvKferchBpyB-gMQcEY0MdAruIse12xTHRfx8TBRDy4TYnXp515mPg",
"issuedAt": "2024-04-23T11:55:39Z",
"expiresAt": "2024-04-23T23:55:39Z",
"headers": {
"kid": "cab5e563-33db-45a5-9fd3-9c762e69295a",
"alg": "RS256"
},
"claims": {
"sub": "mehdi@nidam.com",
"aud": [
"client"
],
"nbf": "2024-04-23T11:55:39Z",
"scope": [
"openid"
],
"iss": "http://localhost:7080/auth",
"exp": "2024-04-23T23:55:39Z",
"iat": "2024-04-23T11:55:39Z",
"jti": "bab9cb91-bf30-401e-a319-8f2b2b5d8504",
"authorities": [
"manage-users",
"manage-projects"
]
},
"id": "bab9cb91-bf30-401e-a319-8f2b2b5d8504",
"subject": "mehdi@nidam.com",
"notBefore": "2024-04-23T11:55:39Z",
"audience": [
"client"
],
"issuer": "http://localhost:7080/auth"
},
"name": "mehdi@nidam.com",
"tokenAttributes": {
"sub": "mehdi@nidam.com",
"aud": [
"client"
],
"nbf": "2024-04-23T11:55:39Z",
"scope": [
"openid"
],
"iss": "http://localhost:7080/auth",
"exp": "2024-04-23T23:55:39Z",
"iat": "2024-04-23T11:55:39Z",
"jti": "bab9cb91-bf30-401e-a319-8f2b2b5d8504",
"authorities": [
"manage-users",
"manage-projects"
]
}
}

Logout

To Logout, we call /bff/logout. we specify a header called X-POST-LOGOUT-SUCCESS-URI to tell the BFF where to redirect to after successful logout, in this case back to the home of our SPA http://localhost:7080/react-ui. This was made simply by using com.c4-soft.springaddons/spring-addons-starter-oidc. Read this for more details.

src/container/Logout.js
const Logout = props => {

const [disabled, setDisabled] = useState(false);

// There is no need to use Saga in this case.
const logout = async () => {
setDisabled(true);
const response = await axios.post(
"/bff/logout",
{},
{
headers: {
"X-POST-LOGOUT-SUCCESS-URI": process.env.REACT_APP_BASE_URI,
},
}
);
// console.log("logout response: ", JSON.stringify(response.headers["location"]));
window.location.href = response.headers["location"];
setDisabled(false);
};

return <Button variant="contained" sx={{width: "25%"}} disabled={disabled} onClick={logout}>Logout</Button>
}

The End

Now you have a Spring OAuth 2 Secured Backend System with a React SPA to go with it.