How authentication works (Part 1)

mannawar

Mannawar Hussain

Posted on June 23, 2024

How authentication works (Part 1)

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 });
        }
    }
)
Enter fullscreen mode Exit fullscreen mode

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

registerGoogle: (values: any) => requests.post('account/registergoogle', values),
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);

  }
Enter fullscreen mode Exit fullscreen mode

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)
            };
        }
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
mannawar
Mannawar Hussain

Posted on June 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related