feat(auth): Microsoft OAuth sign-in/link + self-serve signup via Google/Microsoft
Wave 7. Mirrors the existing Google ID-token-exchange pattern (Program.cs smart-scheme dispatch, JWT bearer scheme, AuthController exchange/link/ unlink endpoints, ApplicationUser fields, reconciler columns) for Microsoft Entra ID + personal accounts via the multi-tenant "common" endpoint. Google/Microsoft sign-in previously only worked for accounts already linked to an existing local user -- there was no way to actually sign up via OAuth. Both exchange endpoints now create a new user when no match is found and Auth:AllowRegistration is true, same gate as email/password registration. Frontend: new MicrosoftAuthCard (MSAL popup flow -- Microsoft has no vanilla-JS equivalent to Google's Identity Services script) wired into the login page's provider tabs and the profile page's account-linking section. REACT_APP_MICROSOFT_CLIENT_ID env var, Auth:MicrosoftClientId config gate on the backend.
This commit is contained in:
@@ -162,6 +162,7 @@ builder.Services.AddSingleton<IJobCvMatchService, JobCvMatchService>();
|
||||
builder.Services.AddSingleton<ICvAiClassifier, CvAiClassifier>();
|
||||
builder.Services.AddSingleton<ICvAiNormalizer, CvAiNormalizer>();
|
||||
builder.Services.AddSingleton<IGoogleTokenValidator, GoogleTokenValidator>();
|
||||
builder.Services.AddSingleton<IMicrosoftTokenValidator, MicrosoftTokenValidator>();
|
||||
builder.Services.AddScoped<IGmailOAuthService, GmailOAuthService>();
|
||||
builder.Services.AddSingleton<IGmailJobMatchingService, GmailJobMatchingService>();
|
||||
builder.Services.AddSingleton<IGmailCorrespondenceEnrichmentService, NoOpGmailCorrespondenceEnrichmentService>();
|
||||
@@ -209,6 +210,7 @@ builder.Services.AddScoped<JobImportService>();
|
||||
|
||||
var requireAuth = builder.Configuration.GetValue("Auth:Require", false);
|
||||
var googleClientId = (builder.Configuration["Auth:GoogleClientId"] ?? "").Trim();
|
||||
var microsoftClientId = (builder.Configuration["Auth:MicrosoftClientId"] ?? "").Trim();
|
||||
|
||||
var jwtKey = (builder.Configuration["Auth:JwtKey"] ?? "").Trim();
|
||||
var ephemeralJwtKey = false;
|
||||
@@ -234,7 +236,7 @@ builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.ForwardDefaultSelector = ctx =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(googleClientId))
|
||||
if (string.IsNullOrWhiteSpace(googleClientId) && string.IsNullOrWhiteSpace(microsoftClientId))
|
||||
return "local";
|
||||
|
||||
var auth = ctx.Request.Headers.Authorization.ToString();
|
||||
@@ -250,9 +252,11 @@ builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
var iss = jwt.Issuer ?? "";
|
||||
return iss is "accounts.google.com" or "https://accounts.google.com"
|
||||
? "google"
|
||||
: "local";
|
||||
if (!string.IsNullOrWhiteSpace(googleClientId) && iss is "accounts.google.com" or "https://accounts.google.com")
|
||||
return "google";
|
||||
if (!string.IsNullOrWhiteSpace(microsoftClientId) && iss.StartsWith("https://login.microsoftonline.com/", StringComparison.OrdinalIgnoreCase))
|
||||
return "microsoft";
|
||||
return "local";
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -322,6 +326,23 @@ if (!string.IsNullOrWhiteSpace(googleClientId))
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(microsoftClientId))
|
||||
{
|
||||
builder.Services.AddAuthentication().AddJwtBearer("microsoft", options =>
|
||||
{
|
||||
// Validate Microsoft (Entra ID / personal account) ID tokens as bearer tokens.
|
||||
// "common" authority + ValidateIssuer=false: multi-tenant issuer varies per tenant id.
|
||||
options.Authority = "https://login.microsoftonline.com/common/v2.0";
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = microsoftClientId,
|
||||
ValidateLifetime = true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
if (requireAuth)
|
||||
|
||||
Reference in New Issue
Block a user