IDentityServer4与混合WinForms OidcClient2 - auth流程在登录后打开浏览器

时间:2018-01-19 10:55:31

标签: identityserver4

我已成功使用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": {}

}

4 个答案:

答案 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