首页 文章

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

提问于
浏览
2

我已成功使用IdentityModel.OidcClient v2中的WinForms示例来调用使用IdentityServer4保护的API .

IS配置了两个外部提供商,Google和ADFS;实现基于IS4快速入门 .

身份验证工作正常,WinForms应用程序接收有效的刷新令牌,并能够调用安全的API,但我对外部登录回调行为感到困惑 .

成功登录后,嵌入式浏览器关闭,默认浏览器打开(我的笔记本电脑中的Chrome),并到达ExternalLoginCallback .

然后WinForms获取刷新令牌,但随后chrome选项卡保持打开状态并重定向到IS登录页面 .

如何阻止显示/关闭Chrome浏览器窗口?我是否必须调整ExternalLogin操作?

Update

添加客户端代码和lib /服务器信息:

具有IdentityModel v 3.0.0的WinForm客户端IdentityModel.OidcClient 2.4.0具有IdentityServer4版本2.1.1的asp.net mvc服务器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;
        }
    }
}

Update 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": {}

}

2 回答

  • 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;
        }
    }
    

    缺少的viewmodel:

    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");
    

    最后一个位于Views / Shared中的新剃刀页面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>
    

    这应该可以解决问题,或者您可以更新快速入门代码 . 但这不是你自己的代码中的问题 .

  • 0

    我认为 ResponseMode 是困扰你的事情 . 为什么不从OIDC客户端设置中删除它 . 流程也可以用于现在(只需确保在IDS端正确配置) . 此外 - 监视Identity Server的日志,查找任何错误 .

相关问题