How authentication works (Part 1)
Tags: react, redux, aspnet, sqlserver
Published: 2024-06-23 06:42:06

Recently, i have been developing FullStack app for a client. I would like to share the key takeaways on that as a part of giving back to the community!

Tech Stack used- React 18, Redux, Aspnet core 7, Sql-server database.

I am taking Google login/Register as an example here. But the general flow remain same for Apple Login or mobile otp login or any other login mechanism. I will summarize as well in the last as well. When the user clicks on Button as here.
```
                    <Box sx={{ display: 'flex', justifyContent: 'center', mb: 2 }}>
                        <GoogleAuth onSuccess={handleGoogleSuccess} onError={handleGoogleFailure} />
                    </Box>
```
Inside the handleGoogleSuccess method i am using jwtDecode library which breaks the jwt token into three parts and i have mapped it is as below.
```
  const tokenId = response.credential;
        const decodedToken = jwtDecode<GoogleTokenPayload>(tokenId);

        const email = decodedToken.email;
        const displayName = decodedToken.name;
        const username = decodedToken.sub;

  dispatch(signInWithGoogle({ tokenId, email, displayName, username }))
            .unwrap()
            .then(() => {
                toast.success('Registration successful!');
                navigate('/Book');
            })
            .catch((error: any) => {
                toast.error('Some error occurred - Please try again later');
                console.error('Google sign in failed:', error);
            });

        console.log('Google sign in successful:', response);
```
As we know jwt is basically made up of three parts viz. Header, payload and signature. using payload i have mapped above properties to send in backend. More details about jwt can be read in the link below.
_Ref- https://jwt.io/_

Next, I am using Redux for centralizing all account related stuff for user. The exact part of code which is responsible for sending payload to backend is below.

```
export const signInWithGoogle = createAsyncThunk<User, GoogleSignInPayLoad>(
    'account/signInWithGoogle',
    async (payload, thunkAPI) => {
        try {
            const {tokenId, email, displayName, username} = payload;
            const user = await agent.Account.registerGoogle({ GoogleTokenId: tokenId, email, displayName, username });
            localStorage.setItem('user', JSON.stringify(user));
            return user;
        } catch(error: any){
            return thunkAPI.rejectWithValue({ error: error.data });
        }
    }
)
```

All the three payload viz. email, displayName, username alongwith tokenId is sent via agent method as here. And if request is successful the localstorage is set with the logged in user credential as shown below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/erkwbjl8tyxoypfhg0yq.png)
```
registerGoogle: (values: any) => requests.post('account/registergoogle', values),
```

Just to give brief on redux store set up. Root reducer are pure functions which is defined inside ConfigureStore function as below. `Reducers are pure function which takes current state and action as an argument and return a new state result.`
```
export const store = configureStore({
    reducer: {
        book: bookSlice.reducer,
        account: accountSlice.reducer,
        subscription: basketSlice.reducer,
        progress: progressSlice.reducer
    },
})

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;  

export default store;
```
Here this line `export type RootState = ReturnType<typeof store.getState>;` represents the type of entire redux state in a type safe manner across the application.

This line `export type AppDispatch = typeof store.dispatch;` is just telling typeScript of new type AppDispatch.`store.dispatch` is function provided by redux that is used to dispatch action to the store. Hence, AppDispatch will be typed by the exact type of the dispatch function from redux store.

Further, coming to this line `export const useAppDispatch = () => useDispatch<AppDispatch>();` useAppDispatch is custom React hook which calls internally useDispatch hook with AppDispatch type. As it is exported it can be easily used inside other modules or component by just importing it.

Lastly, this line `export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; ` useSelector is another hook provided by React-Redux. Its function is to extract data from the redux store state in functional components. However, useSelector inherently is not typed to infer the type of redux state automatically. To achieve type safety and avoid repetitive type annotation, TypedUseSelectorHook is provided.

Then we can further easily use `useAppSelector` in any of our component like this as below. 
`import { useAppSelector } from './store';`
To use this first import inside your component and then `const user = useAppSelector(state => state.user);` Here, we are selecting user slice from redux store and state has a type RootState.

_More or redux can be read here- https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers_

Next, the request will go to backend via agent.ts method registerGoogle. 

It will hit inside the controller action method as defined here.
```
  [AllowAnonymous]
  [HttpPost("registergoogle")]
  public async Task<ActionResult<UserDto>> RegisterGoogle(GoogleRegisterDto googleRegisterDto)
  {

      GoogleJsonWebSignature.Payload payload;
      try
      {
          payload = await GoogleJsonWebSignature.ValidateAsync(googleRegisterDto.GoogleTokenId);
      }
      catch (Exception ex)
      {
          return Unauthorized(new { Error = "Invalid google token" });
      }
      if (payload.Email != googleRegisterDto.Email)
      {
          return Unauthorized(new { Error = "Email doesnt match google token" });
      }

      var existingUser = await _userManager.Users.FirstOrDefaultAsync(x => x.UserName == googleRegisterDto.Username || x.Email == googleRegisterDto.Email || x.Us_DisplayName == googleRegisterDto.DisplayName);

      if(existingUser != null)
      {
          if(existingUser.Email == googleRegisterDto.Email)
          {
              return CreateUserObject(existingUser);
          }else
          {
              ModelState.AddModelError("username", "Username taken");
              return ValidationProblem();
          }
      }

      var user = new AppUser
      {
          Us_DisplayName = googleRegisterDto.DisplayName,
          Email = googleRegisterDto.Email,
          UserName = googleRegisterDto.Username,
          Us_Active = true,
          Us_Customer = true,
          Us_SubscriptionDays = 0
      };

      var result = await _userManager.CreateAsync(user);

      if (result.Succeeded)
      {
          return CreateUserObject(user);
      }

      return BadRequest(result.Errors);
  
  }
```
Here, since any user can register hence the attribute `AllowAnonymous` is used else protect the route by any mechanism or attribute like use `Authorize` 
In the backend i am using nuget package `Google.Apis.Auth` and these lines ```      GoogleJsonWebSignature.Payload payload;
      try
      {
          payload = await GoogleJsonWebSignature.ValidateAsync(googleRegisterDto.GoogleTokenId);
      }
      catch (Exception ex)
      {
          return Unauthorized(new { Error = "Invalid google token" });
      }``` are responsible for checking `googleRegisterDto.GoogleTokenId` is valid and belongs to user attempting to register.

This line of code checks in database ```  var existingUser = await **_userManager.Users**.FirstOrDefaultAsync(x => x.UserName == googleRegisterDto.Username || x.Email == googleRegisterDto.Email || x.Us_DisplayName == googleRegisterDto.DisplayName);``` if database already has the record related to the user attempting to register.

Further, down this line ```if(existingUser != null)
{
    if(existingUser.Email == googleRegisterDto.Email)
    {
        return CreateUserObject(existingUser);
    }``` checks if existing user is not null and and the email from the payload which user sends from frontend matches then CreateUserObject function is called which is as below.

```
        private UserDto CreateUserObject(AppUser user)
        {
            return new UserDto
            {
                DisplayName = user.Us_DisplayName,
                Image = user.Md_ID.ToString(),
                Token = _tokenService.CreateToken(user),
                Username = user.UserName,
                FirebaseUID = user.Us_FirebaseUID,
                FirebaseToken = user.Us_FirebaseToken,
                Language = user.Us_language,
                IsSubscribed = (user.Us_SubscriptionExpiryDate != null && user.Us_SubscriptionExpiryDate > DateTime.Now)
            };
        }
```
Above, most crucial is this one `Token = _tokenService.CreateToken(user)` where the token is created for the user attempting to register whose records have already matched with the user saved in db, for further interaction with app.

Now, coming further down this line of code 
` var user = new AppUser
            {
                Us_DisplayName = googleRegisterDto.DisplayName,
                Email = googleRegisterDto.Email,
                UserName = googleRegisterDto.Username,
                Us_Active = true,
                Us_Customer = true,
                Us_SubscriptionDays = 0
            };
`

If the user attempting to register is new and his records is not found in db. Then the above code will be executed and a user is created with role as just user. Note this line `var result = await _userManager.CreateAsync(user);` will create a new user and save in db.

Down this line, ```if (result.Succeeded)
            {
                return `(user);
            }``` if everything goes well till now `CreateUserObject` method will be called where a token will be generated for the user which will be used during his further interaction with app.

Else, bad request with corresponding error message will be displayed as here `return BadRequest(result.Errors);`

Note- Here i used google login, which is most common for login these days. But, the underlying logic remains same for any other login mechanism.

And finally, in the front end side in i have another reducer `signout` as here ``` reducers: {
        signOut: (state) => {
            state.user = null;
            localStorage.removeItem('user');
            router.navigate('/');
        },
        setUser: (state, action) => {
            const claims = JSON.parse(atob(action.payload.token.split('.')[1]));
            const roles = claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'];
            state.user = {...action.payload, roles: typeof(roles) === 'string' ? [roles] : roles};
        }
    },``` which can be triggered inside any component which is handling log out. In my case, i am using a menu component where i dispatch signout. You can use anywhere where you plan user to trigger logout. Once the `dispatch(signOut());` from the component it will clear the localstorage as here `localStorage.removeItem('user');` and user will be redirected to index page as here `router.navigate('/');`.

**To summarize:**
Step-1 Front end- use any mechanism or package to decode the google token. Once decoded match its part as per the model or dto's defined in backend.
Step-2 Send the request to backend via directly by axios or redux(centralized state management) using agent(which centralizes request to backend).
Step-3 Once the endpoint in backend is hit, check if token id is valid and ensure it is issued by google only. If valid, then check if user record is already present in our db or not. If present, generate a token with minimum role as a user for further communication. If not then create a user and save its info on db and then generate token with minimum role for their further interaction with app.

I hope this basic flow would be helpful to someone. Thanks for your time!