我已成功使用IdentityModel.OidcClient v2中的WinForms示例来调用使用IdentityServer4保护的API。
IS配置了两个外部提供商,Google和ADFS;实施基于IS4快速入门。
身份验证工作正常,WinForms应用程序接收有效的刷新令牌并能够调用安全的API,但我对外部登录回调行为感到困惑。
成功登录后,嵌入式浏览器关闭,默认浏览器打开(我的笔记本电脑中的Chrome),并到达ExternalLoginCallback。
然后WinForms获取刷新令牌,但随后chrome选项卡保持打开状态并重定向到IS登录页面。
如何阻止显示/关闭Chrome浏览器窗口? 我是否必须调整ExternalLogin操作?
更新
添加客户端代码和lib /服务器信息:
WinForm客户端 IdentityModel v 3.0.0 IdentityModel.OidcClient 2.4.0 asp.net mvc服务器用 IdentityServer4版本2.1.1 IdentityServer4.EntityFramework 2.1.1
关注WinForm客户端代码:
public partial class SampleForm : Form
{
private OidcClient _oidcClient;
private HttpClient _apiClient;
public SampleForm()
{
InitializeComponent();
var options = new OidcClientOptions
{
Authority = "http://localhost:5000",
ClientId = "native.hybrid",
ClientSecret = "secret",
Scope = "openid email offline_access myscope myapi1 myapi2",
RedirectUri = "http://localhost/winforms.client",
ResponseMode = OidcClientOptions.AuthorizeResponseMode.FormPost,
Flow = OidcClientOptions.AuthenticationFlow.Hybrid,
Browser = new WinFormsEmbeddedBrowser()
};
_oidcClient = new OidcClient(options);
}
private async void LoginButton_Click(object sender, EventArgs e)
{
AccessTokenDisplay.Clear();
OtherDataDisplay.Clear();
var result = await _oidcClient.LoginAsync(new LoginRequest());
if (result.IsError)
{
MessageBox.Show(this, result.Error, "Login", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
AccessTokenDisplay.Text = result.AccessToken;
var sb = new StringBuilder(128);
foreach (var claim in result.User.Claims)
{
sb.AppendLine($"{claim.Type}: {claim.Value}");
}
if (!string.IsNullOrWhiteSpace(result.RefreshToken))
{
sb.AppendLine($"refresh token: {result.RefreshToken}");
}
OtherDataDisplay.Text = sb.ToString();
_apiClient = new HttpClient(result.RefreshTokenHandler);
_apiClient.BaseAddress = new Uri("http://localhost:5003/");
}
}
private async void LogoutButton_Click(object sender, EventArgs e)
{
//await _oidcClient.LogoutAsync(trySilent: Silent.Checked);
//AccessTokenDisplay.Clear();
//OtherDataDisplay.Clear();
}
private async void CallApiButton_Click(object sender, EventArgs e)
{
if (_apiClient == null)
{
return;
}
var result = await _apiClient.GetAsync("identity");
if (result.IsSuccessStatusCode)
{
OtherDataDisplay.Text = JArray.Parse(await result.Content.ReadAsStringAsync()).ToString();
}
else
{
OtherDataDisplay.Text = result.ReasonPhrase;
}
}
}
更新2
ExternalLoginCallback代码:
public async Task<IActionResult> ExternalLoginCallback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
_logger.LogError(result.Failure, "External athentication error.");
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var externalUser = result.Principal;
var claims = externalUser.Claims.ToList();
....LOOKING FOR THE USER (OMITTED FOR BREVITY)....
var additionalClaims = new List<Claim>();
// if the external system sent a session id claim, copy it over
// so we can use it for single sign-out
var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
if (sid != null)
{
additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
}
// if the external provider issued an id_token, we'll keep it for signout
AuthenticationProperties props = null;
var id_token = result.Properties.GetTokenValue("id_token");
if (id_token != null)
{
props = new AuthenticationProperties();
props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } });
}
// issue authentication cookie for user
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, userId, user.Id.ToString(), user.Username));
await HttpContext.SignInAsync(user.Id.ToString(), user.Username, provider, props, additionalClaims.ToArray());
_logger.LogInformation("User {user} logged in with external provider.", userId);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
// validate return URL and redirect back to authorization endpoint or a local page
var returnUrl = result.Properties.Items["returnUrl"];
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
}
IdentityServer上的客户端配置,序列化:
{
"Enabled": true,
"ClientId": "native.hybrid",
"ProtocolType": "oidc",
"RequireClientSecret": true,
"ClientName": "Application",
"LogoUri": null,
"RequireConsent": false,
"AllowRememberConsent": true,
"AllowedGrantTypes": [
"hybrid"
],
"RequirePkce": false,
"AllowPlainTextPkce": false,
"AllowAccessTokensViaBrowser": true,
"RedirectUris": [
"http://localhost/winforms.client"
],
"FrontChannelLogoutUri": null,
"FrontChannelLogoutSessionRequired": true,
"BackChannelLogoutUri": null,
"BackChannelLogoutSessionRequired": true,
"AllowOfflineAccess": true,
"AllowedScopes": [
"openid",
"email",
"profile",
"myscope",
"offline_access",
"myapi1",
"myapi2"
],
"AlwaysIncludeUserClaimsInIdToken": false,
"IdentityTokenLifetime": 300,
"AccessTokenLifetime": 3600,
"AuthorizationCodeLifetime": 300,
"AbsoluteRefreshTokenLifetime": 2592000,
"SlidingRefreshTokenLifetime": 1296000,
"ConsentLifetime": null,
"RefreshTokenUsage": 1,
"UpdateAccessTokenClaimsOnRefresh": false,
"RefreshTokenExpiration": 1,
"AccessTokenType": 0,
"EnableLocalLogin": true,
"IdentityProviderRestrictions": [
"Google",
"WsFederation"
],
"IncludeJwtId": false,
"Claims": [],
"AlwaysSendClientClaims": false,
"ClientClaimsPrefix": "client_",
"PairWiseSubjectSalt": null,
"Properties": {}
}
答案 0 :(得分:0)
我认为ResponseMode
会困扰你。为什么不从OIDC客户端设置中删除它。流程也可以用于现在(只需确保在IDS端正确配置)。另外 - 监视Identity Server的日志,查找是否存在任何错误。
答案 1 :(得分:0)
我可以回答很长的时间,但最终,您使用的快速入门代码是此问题的根本原因。 确切地说,正是这段代码导致了这个问题:
// validate return URL and redirect back to authorization endpoint or a local page
var returnUrl = result.Properties.Items["returnUrl"];
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
应该改为:
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context != null)
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl });
}
}
return Redirect(returnUrl);
这也意味着您需要扩展类方法:
public static class Extensions
{
/// <summary>
/// Determines whether the client is configured to use PKCE.
/// </summary>
/// <param name="store">The store.</param>
/// <param name="clientId">The client identifier.</param>
/// <returns></returns>
public static async Task<bool> IsPkceClientAsync(this IClientStore store, string clientId)
{
if (!string.IsNullOrWhiteSpace(clientId))
{
var client = await store.FindEnabledClientByIdAsync(clientId);
return client?.RequirePkce == true;
}
return false;
}
}
缺少的视图模型:
public class RedirectViewModel
{
public string RedirectUrl { get; set; }
}
此缺少的javascript文件,其内容位于wwwroot / js / signin-redirect.js
window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url");
最后是位于“视图/共享”中的新剃刀页面Redirect.cshtml
@model RedirectViewModel
<h1>You are now being returned to the application.</h1>
<p>Once complete, you may close this tab</p>
<meta http-equiv="refresh" content="0;url=@Model.RedirectUrl" data-url="@Model.RedirectUrl">
<script src="~/js/signin-redirect.js"></script>
这应该可以解决问题,或者您可以更新快速入门代码。但这不是您自己的代码中的问题。
答案 2 :(得分:0)
我遇到了同样的问题,对我有用的是将RedirectUri更改为不以http开头的其他值。
var options = new OidcClientOptions
{
Authority = "<path to ids>",
ClientId = "<your client id>",
Flow = OidcClientOptions.AuthenticationFlow.Hybrid,
ClientSecret = "<your super secret phrase>",
Scope = "<your scopes>",
RedirectUri = "winformsclients://callback", // <-- HERE IS THE ANSWER
Browser = new WinFormsWebView(),
PostLogoutRedirectUri = "winformsclients://callback", // <-- HERE IS THE ANSWER
};
var oidcClient = new OidcClient(options);
上述解决方案的重要之处在于,需要使用此虚拟uri更新IdentityServer(数据库中)上的客户端配置。
必须配置的表是:
ClientCorsOrigins , ClientRedirectUris 和 ClientPostLogoutRedirectUris
您还可以从下面的链接下载示例应用程序并对其进行自定义。
https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/main/WinFormsWebView
答案 3 :(得分:0)
我在尝试使用 Google 进行身份验证时遇到了类似的问题。此处记录了可能的解决方法/修复:
https://github.com/IdentityModel/IdentityModel.OidcClient/issues/283