Question:
I am implementing Google OAuth2.0 authentication in my ASP.NET Core MVC application. The process goes through the following steps:
- The user logs into my application using a user ID and password.
- If authenticated, the user accesses the
/sendemails
endpoint. - The user is redirected to Google for authentication.
- After selecting the Google account, it is supposed to redirect to my callback endpoint
https://localhost:7268/sendemails/oauthcallback
.
Problem:
Instead of successfully redirecting back to my application after Google authentication, I encounter a 404 error which is a page i did that means Access Denied (user is not authenticated in my system).
OAuth Flow Overview:
- User hits
https://localhost:7268/sendemails
. - Redirects to Google OAuth for account selection.
- Fails to redirect to
https://localhost:7268/sendemails/oauthcallback
and shows a 404 error which is a page i did that means Access Denied (user is not authenticated in my system).
Code Setup
Startup.cs
builder.Services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
options.SlidingExpiration = true;
options.ExpireTimeSpan =
TimeSpan.FromMinutes(Convert.ToInt16(builder.Configuration["TimeoutSession:CookieMinutes"]));
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddGoogleOpenIdConnect(options =>
{
options.ClientId = googleCredentials.ClientId;
options.ClientSecret = googleCredentials.ClientSecret;
options.CallbackPath = "/sendemails/oauthcallback";
options.SaveTokens = true;
});
Controller: PrepareEmailController.cs
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using UMS.BackEnd.DatabaseHelper.Interfaces;
using UMS.Models.ViewModel.EmailViewModels;
using Google.Apis.Auth.AspNetCore3;
using UMS.BackEnd.GoogleHelper;
namespace UMS.Controllers
{
[Authorize("ViewSendEmailsPolicy")]
[Route("sendemails")]
public class PrepareEmailController : Controller
{
private readonly IPrepareEmailRepository _repository;
private readonly GoogleCredentials _googleCredentials;
public PrepareEmailController(IPrepareEmailRepository repository)
{
_repository = repository;
string credentialsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "gmailCreds", "credentials.json");
_googleCredentials = GoogleCredentials.LoadFromJson(credentialsPath);
}
public async Task<IActionResult> PrepareEmailForm()
{
var accessToken = HttpContext.Session.GetString("AccessToken");
if (string.IsNullOrEmpty(accessToken))
{
return Challenge(GoogleOpenIdConnectDefaults.AuthenticationScheme);
}
try
{
var employeeId = User.Identity.Name;
if (employeeId != User.Identity.Name) return Unauthorized();
var userId = await _repository.GetUserId(employeeId);
var isUserInstructor = await _repository.IsUserInstructor(employeeId);
if (isUserInstructor)
{
var studentEmails = await _repository.GetStudentEmailsByInstructor(employeeId);
var emailsFrom = await _repository.GetAllEmailFroms(userId);
var emailSubjects = await _repository.GetAllEmailSubjects(userId);
var emailBodies = await _repository.GetAllEmailBodies(userId);
var viewModel = new PrepareEmailFormViewModel()
{
StudentEmails = studentEmails,
EmailFroms = emailsFrom,
EmailSubjects = emailSubjects,
EmailBodies = emailBodies
};
return View(viewModel);
}
else
{
var emailsFrom = await _repository.GetAllEmailFroms(userId);
var emailSubjects = await _repository.GetAllEmailSubjects(userId);
var emailBodies = await _repository.GetAllEmailBodies(userId);
var viewModel = new PrepareEmailFormViewModel()
{
EmailFroms = emailsFrom,
EmailSubjects = emailSubjects,
EmailBodies = emailBodies
};
return View(viewModel);
}
}
catch (ApplicationException ex)
{
TempData["ErrorMessage"] = ex.Message;
return NotFound();
}
catch (Exception ex)
{
Console.WriteLine("An unexpected error occurred: " + ex.Message);
TempData["ErrorMessage"] = "Failed to prepare email data";
return NotFound();
}
}
[Route("oauthcallback")]
public async Task<IActionResult> OAuthCallback(string code)
{
if (string.IsNullOrEmpty(code))
{
TempData["ErrorMessage"] = "Authorization code is missing.";
return RedirectToAction("PrepareEmailForm");
}
try
{
var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = _googleCredentials.ClientId,
ClientSecret = _googleCredentials.ClientSecret
}
});
var tokenResponse = await flow.ExchangeCodeForTokenAsync("user", code, "https://localhost:7268/sendemails/oauthcallback", CancellationToken.None);
HttpContext.Session.SetString("AccessToken", tokenResponse.AccessToken);
Console.WriteLine("---->Access Token Stored: " + tokenResponse.AccessToken);
return RedirectToAction("PrepareEmailForm");
}
catch (Exception ex)
{
TempData["ErrorMessage"] = $"Error during OAuth callback: {ex.Message}";
return RedirectToAction("PrepareEmailForm");
}
}
[HttpPost("send-email")]
public async Task<IActionResult> SendEmail(string to, string subject, string body)
{
var accessToken = HttpContext.Session.GetString("AccessToken");
if (string.IsNullOrEmpty(accessToken))
{
return Challenge(GoogleOpenIdConnectDefaults.AuthenticationScheme);
}
try
{
await GmailServiceHelper.SendEmail(to, subject, body, accessToken);
TempData["SuccessMessage"] = "Email sent successfully!";
return RedirectToAction("PrepareEmailForm");
}
catch (Exception ex)
{
TempData["ErrorMessage"] = $"Error sending email: {ex.Message}";
return RedirectToAction("PrepareEmailForm");
}
}
}
}
Steps Tried:
- Verified the redirect URI in Google Cloud Console matches
https://localhost:7268/sendemails/oauthcallback
. - Ensured the
GoogleOpenIdConnectDefaults.AuthenticationScheme
is set up correctly inStartup.cs
. - Checked the Console output and no logs are being made in
OAuthCallback
.
Observations:
- The OAuth flow initiates correctly, and I can select the Google account.
- Upon selecting the account, the application fails to hit the
OAuthCallback
endpoint, resulting in a 404 error.
What am I missing?
Is there an issue with the routing or the way I've set up the authentication callback? How can I resolve this 404 error which means my user is no longer authenticated to my system and successfully complete the Google OAuth flow? I am really confused and this workflow of authenticating with google then with my system again is really confusing.