我有一个 .NET 7 应用程序,它有 2 个独立的项目。一个项目是包含所有后端代码的 API,另一个项目是主要包含 javascript 的前端。 API 和前端在不同的 URL 上运行。在 API 中,我们实现了 IdentityServer4 来进行 OAUTH2 身份验证。用户可以通过前端使用用户名和密码登录,然后前端调用API来请求访问令牌。然后,此令牌将添加到对 API 的所有后续请求的授权标头中。
我正在尝试实现允许用户通过其 Google 帐户登录的功能,而不是用户名和密码。我添加了一个到前端的链接,它将用户重定向到 API 的
external-login
端点。该端点是一个控制器函数,它返回一个以 Google 作为提供者的 ChallengeResult。它还具有一个回调 URL,用于在用户通过 Google 进行身份验证后将用户重定向回 API 中的回调端点,这将验证所有内容,然后将用户重定向到前端应用程序。到目前为止,这一切都有效,我可以通过 Google 进行身份验证,一切都经过验证,然后我会被发送回我的前端。
我遇到的问题是前端不知道用户已通过Google成功登录。当用户通过用户名/密码登录时,我调用 IdentityServer4 的 /token 端点,并返回带有 JWT 令牌、过期日期等的结果。我将其保存在浏览器的本地存储中,并使用它来验证所有其他请求到 API。当我通过 Google 对用户进行身份验证时,不会发生这种情况,因为这适用于重定向。因此前端不会收到 JWT 令牌,因此无法验证对 API 的请求。
我这样做是因为我发现的一切都告诉我这样做,我还没有找到任何其他方法来做到这一点。我很难找到正确解释正在发生的事情以及如何自定义该代码的示例。
我想我可能这样做完全错误,也许我还需要调用 /token 端点进行 Google 身份验证,但我不确定如何准确执行此操作,我找不到任何示例。谁能给我解释一下吗?
这是我目前拥有的API中的代码(其中很多代码来自我在IdentityServer4文档中找到的示例,我还不知道我实际需要多少代码):
[Route("api/v3/[controller]")]
[ApiController]
[Consumes(MediaTypeNames.Application.Json)]
[Produces(MediaTypeNames.Application.Json)]
public class UsersController : ControllerBase
{
/// <summary>
/// Endpoint for external login providers such as Google.
/// This uses IdentityServer4 to handle the authentication.
/// </summary>
/// <param name="provider"></param>
/// <returns></returns>
[HttpGet]
[Route("external-login")]
[AllowAnonymous]
public IActionResult ExternalLogin(string provider)
{
var properties = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
AllowRefresh = true,
IsPersistent = true
};
return Challenge(properties, provider);
}
[HttpGet]
[Route("external-login-callback")]
[AllowAnonymous]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
var externalUser = result.Principal;
// lookup our user and external provider info
// try to determine the unique id of the external user (issued by the provider)
// the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
throw new Exception("Unknown userid");
var provider = result.Properties.Items[".AuthScheme"];
var providerUserId = userIdClaim.Value;
// find external user
/*var user = _users.FindByExternalProvider(provider, providerUserId);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
//
// remove the user id claim so we don't include it as an extra claim if/when we provision the user
var claims = externalUser.Claims.ToList();
claims.Remove(userIdClaim);
user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
}*/
var user = new TestUser()
{
SubjectId = "1",
Username = "Test",
Password = "Test",
IsActive = true,
Claims = new List<Claim>()
{
new(ClaimTypes.GivenName, "Test"),
new(ClaimTypes.Name, "Test"),
new(ClaimTypes.Role, "Test"),
new(ClaimTypes.GroupSid, "main"),
new(ClaimTypes.Sid, "Test"),
new(IdentityConstants.AdminAccountName, "Test"),
new(HttpContextConstants.IsTestEnvironmentKey, "true"),
new(JwtClaimTypes.Subject, "1")
}
};
// this allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>
{
new(JwtClaimTypes.Subject, providerUserId)
};
var localSignInProps = new AuthenticationProperties();
CaptureExternalLoginContext(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
//var token = localSignInProps.GetTokenValue("id_token") ?? "";
if (!result.Properties.Items.TryGetValue("returnUrl", out var returnUrl))
{
returnUrl = $"https://localhost:44377/";
}
// check if external login is in the context of an OIDC request
var context = await interaction.GetAuthorizationContextAsync(returnUrl);
await events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
/*if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage(returnUrl);
}
}*/
var token = new Token(IdentityServerConstants.TokenTypes.IdentityToken);
//var userCredential = new UserCredential(user);
token.Claims = user.Claims;
token.AccessTokenType = AccessTokenType.Reference;
token.ClientId = "wiser";
token.CreationTime = DateTime.UtcNow;
token.Audiences = new[] {"api1"};
token.Lifetime = 3600;
//token.CreateJwtPayload()
return Redirect(returnUrl);
}
}
ps。我正在谈论的应用程序实际上是开源的,因此如果有帮助的话我可以发送该应用程序的 GitHub URL。
你对这个话题有进一步的了解吗,我目前也在研究这个问题