Step-by-Step: Google OAuth in ASP.NET with Angular and JWT Authentication

Step-by-Step: Google OAuth in ASP.NET with Angular and JWT Authentication

Implement Google Authentication in ASP.NET Using JWT - Complete Tutorial with Code Sample

Apr 28, 2024ยท

9 min read

Introduction

This article covers the process of authentication in a website, detailing the technical aspects of user verification, authorization, and token generation. It provides a step-by-step guide on setting up Google OAuth credentials, configuring Angular for social login, establishing JWT authentication, and implementing API endpoints for login, token refresh, and sign-out. The use of refresh tokens, interceptor code for token renewal, and backend server setup for JWT authentication are also explained.

How does authentication work in a website?

In general, when using a web application, you will be asked to enter your login credentials. These credentials will then be sent to the server for verification. If the verification process is successful, you will be granted access to the secure pages of the website. Once you are logged in, your session will remain active until you either log out or your session expires.

Technically what happens in the authentication process?

The user credentials are sent to the server for authentication and authorization. Once the user credentials are verified, the user's authorization (role or permissions) is checked and finally, the server returns the authenticated response to the client with auth token (Ex. JWT Token), refresh token, and expiry details.
Auth tokens will be sent to the server on each request. For security reasons, the Auth token expiry time will be set as a low value (Ex. 5 minutes). However, auth tokens will be refreshed using refresh tokens within the above interval.

What are the things this blog post covers?

  • Create and configure the project in the Google console.

  • Set up Angular with the necessary package.

  • Establish JWT authentication.

  • Reason for using Refresh Token

  • API setup for JWT authentication - Login, Refresh Token, Signout.

Create and configure the project in the Google console

Let's create Google OAuth credentials in the Google Console. First, navigate to the https://console.cloud.google.com/apis

Click the Select a project --> New Project --> Enter the project name and click Create.

Once the project is created, it will be auto-selected as below.

Go to the Credentials section --> Click Create Credentials --> Select OAuth client ID --> New screen will appear with Application type dropdown --> Select Web application. and fill in the remaining values below

In the new screen input the name of your OAuth 2.0 client, Javascript origin (the frontend URL where the Google redirects after successful login), server API URL as below, and submit the form.

Once the form is submitted, we will get a new screen with the Client ID and Client secret. Save in values for later use in the application.

Google Button Creation

Go to the Google Configurator website and create a Google sign-in button with the help of Google Client ID and login URL. Under Select sign-in methods select any one of the options and click Get Code.

Set up Angular with the necessary package

Create a new Angular project with ng new project_name the command. In this post, we are using angular version 16.

We will use the following social login and authentication module for Angular 16. Install the suitable version for the current angular version.

npm i @abacritt/angularx-social-login@2.1.0

In the app module, import SocialLoginModule and configure the Google client id.

import { SocialLoginModule, SocialAuthServiceConfig } from '@abacritt/angularx-social-login';
import {
  GoogleLoginProvider
} from '@abacritt/angularx-social-login';

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    SocialLoginModule
  ],
  providers: [
    {
      provide: 'SocialAuthServiceConfig',
      useValue: {
        autoLogin: false,
        providers: [
          {
            id: GoogleLoginProvider.PROVIDER_ID,
            provider: new GoogleLoginProvider(
              'clientId' //Your Google client id
            )
          }
        ],
        onError: (err) => {
          console.error(err);
        }
      } as SocialAuthServiceConfig,
    }
  ],
  bootstrap: [...]
})
export class AppModule { }

Next, on the app.component.html page create a Google sign-in button with the previously generated code.

  <asl-google-signin-button type='standard' size='medium' shape="pill">
    <div id="g_id_onload"
         data-client_id="Your-Google-client-Id"
         data-context="signin"
         data-ux_mode="popup"
         data-login_uri="http://localhost,http://localhost:4200"
         data-auto_prompt="false">
    </div>

    <div class="g_id_signin"
         data-type="standard"
         data-shape="rectangular"
         data-theme="filled_blue"
         data-text="signin_with"
         data-size="large"
         data-logo_alignment="left">
    </div>
  </asl-google-signin-button>

Establish JWT authentication

Now the angular application is ready with the Google sign-in button. Once the user completes the Google sign-in, it will be redirected to our application URL http://localhost:4200 (You can change this to any login page and handle the response) with an authenticated token.

//app.component.ts
 ngOnInit() {
   this.authState();
 }
 authState() {
   this.authService.authState.subscribe((user) => {
     this.user = user;
     this.loggedIn = (user != null);
     this.authToken = user.idToken;
     this.authorizeService.login(user.idToken);
     //this.authorizeService.accessToken = user.idToken;
     console.log(user);
   });
 }

Since app.component.ts going to be called at first, the above code is added to the above page. We will get the user object as a response from Google with necessary information like Name, Email, photoUrl, idToken, etc. Here we are only going to use idToken.

Next, we pass idToken to the API server via login a method to authenticate the user. The server verifies the idToken and returns the JWT token along with the refresh token.

We should use interceptors for sending JWT tokens with all the requests, to verify the unauthorized response and renew the token.

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

  if(this.authService.isTokenExpired())
    return this.handle401Error(req, next);

  return next.handle(req).pipe(catchError(error => {
    if (error instanceof HttpErrorResponse && !req.url.includes('/login-google') && error.status === 401) {
      console.log("401");
      return this.handle401Error(req, next);
    }

    return throwError(error);
  }));

  return next.handle(req);
}

private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
  if (!this.isRefreshing && this.authService.getAccessToken() && this.authService.getRefreshToken()) {
    this.isRefreshing = true;

    return this.authService.getRefreshTokenRequest().pipe(
      switchMap((response: AuthenticatedResponse) => {
        this.isRefreshing = false;
        this.authService.setToken(response);

        request = request.clone({
          setHeaders: {
            'Content-Type': 'application/json; charset=utf-8',
            'Accept': 'application/json',
            'Authorization': `Bearer ${this.authService.getAccessToken()}`,
          },
        });
        return next.handle(request);
      }),
      catchError((err) => {
        this.isRefreshing = false;
        console.log("this.authService.logOut(); - triggered")
        this.authService.logOut();
        return throwError(err);
      })
    );
  }
  return next.handle(request);

}
๐Ÿ’ก
The above interceptor code automatically requests and gets the new JWT token if the unauthorized response is received. No need to manually check the token expiration and renew the tokens.

We will store the above tokens on the client. JWT token should be sent along with all the subsequent requests to authenticate the same. The refresh token will be sent only when the JWT token has expired. The server regenerates both refresh and JWT tokens and sends them to the clients.

Why refresh tokens?

  • JWT token will have a minimum expiry time (Ex. 5 minutes) for security reasons. So to renew the JWT token, we use the refresh token

  • There is no way to revoke the JWT token, hence revoking the refresh token stops the JWT token renewal and forces the user to log in again.

  • Users do not need to provide the credentials again each time since we have already verified the login and received the authenticated tokens.

  • Even if the JWT token is compromised, it can be used for a very short time until it expires

A refresh token is similar to a JWT token but has a longer expiry time (Ex. 7 days) and it is stored in the database for the logged-in user. If the JWT token is expired, the client will send the refresh token to the server. After verification, the new JWT and refresh token are generated and sent to the client for further usage.

To renew the JWT token without authenticating the users again (with Google or any other means), we use the refresh token to identify the authenticated users. Usually, the JWT token will have a minimum expiry time (Ex. 5 minutes) for security reasons.

API setup for JWT authentication - Login, Refresh Token, Signout

The backend API server plays an important role in authentication by generating JWT authentication, refreshing tokens, and signing out by revoking tokens.

JWT and Google Auth configuration

Install the below packages from nuget package manger.

Microsoft.AspNetCore.Authentication.JwtBearer 
Google.Apis.Auth

Configure the appsettings.json with Google client id and secret along with JWT configuration.

 "Authentication": {
   "Google": {
     "ClientId": "854824497783-xxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
     "ClientSecret": "GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
   }
 },
 "Jwt": {
   "Issuer": "https://rajasekar.dev/",
   "Audience": "https://rajasekar.dev/",
   "Key": "This is a secure key, requires a key size of at least '128' bits"
 }

Login Endpoint

Google sends the idToken to this endpoint to get the new JWT auth-token. First, we should validate the idToken. Create claims based on user email, optionally check the user's role in the database, and update the claims accordingly.

Pass the claims to the method GenerateToken for creating JWT token. The method GenerateRefreshToken will be used to generate a new refresh token and the same can be stored in the database for the logged-in user which will be used to invalidate the token later.

//Validate Google Token - expired or not
async Task<GoogleJsonWebSignature.Payload?> ValidateGoogleToken(string idToken)
{
    try
    {
        var settings = new GoogleJsonWebSignature.ValidationSettings()
        {
            Audience = new List<string>() { googleAuth.GetSection("clientId").Value }
        };
        var payload = await GoogleJsonWebSignature.ValidateAsync(idToken, settings);
        return payload;
    }
    catch (Exception ex)
    {
        //log an exception
        return null;
    }
}

List<Claim> GetClaims(string userEmail, string userRole)
{
    return new List<Claim>()
    {
        new Claim("Id", Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.Sub, userEmail),
        new Claim(JwtRegisteredClaimNames.Email, userEmail),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(ClaimTypes.Role, userRole)
    };
}

string GenerateToken(IEnumerable<Claim> Claims)
{
    var issuer = builder.Configuration["Jwt:Issuer"];
    var audience = builder.Configuration["Jwt:Audience"];
    var key = Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(Claims),
        Expires = DateTime.UtcNow.AddMinutes(5), //should be at least 5 minutes - https://github.com/IdentityServer/IdentityServer3/issues/1251
        Issuer = issuer,
        Audience = audience,
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
    };
    var tokenHandler = new JwtSecurityTokenHandler();
    var token = tokenHandler.CreateToken(tokenDescriptor);
    var stringToken = tokenHandler.WriteToken(token);
    return stringToken;
}

static string GenerateRefreshToken()
{
    var randomNumber = new byte[64];
    using var rng = RandomNumberGenerator.Create();
    rng.GetBytes(randomNumber);
    return Convert.ToBase64String(randomNumber);
}

app.MapPost("/login-google", [AllowAnonymous] async (HttpContext context, GoogleLogin g) =>
{
    var result = await ValidateGoogleToken(g.IdToken);
    if (result == null)
        return Results.Unauthorized();

    var token = GenerateToken(GetClaims(result.Email, "user"));
    var refreshToken = GenerateRefreshToken();
    //TODO - store refreshToken in database for the user

    return Results.Ok(new User(result.Name, result.Picture, token, refreshToken));
});

Refresh Token

When the auth-token is expired then the client should send the expired auth-token and the current refresh-token to the server for getting new JWT tokens.

We should check the validity of the refresh token in the database if the token is valid we will allow to generate a new JWT auth-token, else the request will be rejected with Unauthorized response. The client application will be logged out if the Unauthorized the response is received.

app.MapPost("/refresh-token", (TokenModel tokenModel) =>
{

    var principal = GetPrincipalFromExpiredToken(tokenModel.Token);
    var username = principal.Identity?.Name;
    if (string.IsNullOrWhiteSpace(username))
        return Results.BadRequest("Invalid client request");

    //TODO - Get user with refresh token by given "username"
    var newToken = GenerateToken(principal.Claims);
    var newRefreshToken = GenerateRefreshToken();
    //TODO - store new refreshToken in database for the above user

    return Results.Ok(new TokenModel(newToken, newRefreshToken));
}).RequireAuthorization();

Sign out

To sign out from the application, you need to delete both the authentication and refresh tokens. You can delete the authentication token from the client machine by removing the local storage value. Additionally, you should delete the refresh token from the database to ensure complete sign-out.

app.MapPost("/user/sign-out", (string userEmail) =>
{
    //TODO - update refreshToken as NULL in database for the user

    return Results.NoContent();
}).RequireAuthorization();

To-Dos:

Conclusion

This detailed guide provides a comprehensive overview of implementing Google Authentication in ASP.NET using Angular and JWT. It covers setting up Google OAuth credentials, configuring Angular for social login, establishing JWT authentication, implementing API endpoints for login, token refresh, and sign-out, and the importance of refresh tokens in the authentication process. By following the step-by-step instructions and understanding the technical aspects discussed in this article, developers can successfully integrate Google Authentication in their ASP.NET applications.

References:

The Ultimate Guide to handling JWTs on frontend clients (GraphQL) (hasura.io)

The best way to securely manage user sessions (supertokens.com)

https://code-maze.com/using-refresh-tokens-in-asp-net-core-authentication/

Authenticate with Google in Angular 17 via OAuth2 | by Kushal Ghosh | Medium

@abacritt/angularx-social-login - npm (npmjs.com)

Angular 12 Refresh Token with Interceptor and JWT example - BezKoder