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
Table of contents
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);
}
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:
- Fingerprint logic to be added to avoid XSS attack
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