ASP.NET OWIN OpenID Connect无法创建用户身份验证

10

我有一个ASP.NET 4.6的web应用程序,我正在尝试使用OWIN添加OpenId Connect。

我添加了我的Owin启动类,一切似乎都配置正确,但是我遇到的问题是ASP Identity/Authenticated用户从未被创建。我最终陷入了无限循环中,其中OpenId回调页面重定向回原始页面,然后重定向到登录页面,依此类推。

这是我的启动类:

public void Configuration(IAppBuilder app)
    {


     app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);


        app.UseKentorOwinCookieSaver();
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Login.aspx"),
            ExpireTimeSpan = TimeSpan.FromDays(7)
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {                
            ClientId = _clientId,
            ClientSecret = _clientSecret,
            Authority = _authority,
            RedirectUri = _redirectUri, // LoginCallback
            PostLogoutRedirectUri = "http://localhost:60624/Logout.aspx",

            ResponseType = OpenIdConnectResponseType.CodeIdToken,
            Scope = "openid profile email",

            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name"
            },

            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                AuthorizationCodeReceived = async n =>
                {
                    // Exchange code for access and ID tokens
                    var tokenClient = new TokenClient($"{_authority}/as/token.oauth2", _clientId, _clientSecret);

                    var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, _redirectUri);
                    if (tokenResponse.IsError)
                    {
                        throw new Exception(tokenResponse.Error);
                    }

                    var userInfoClient = new UserInfoClient($"{_authority}/idp/userinfo.openid");
                    var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);

                    var claims = new List<Claim>(userInfoResponse.Claims)
                      {
                        new Claim("id_token", tokenResponse.IdentityToken),
                        new Claim("access_token", tokenResponse.AccessToken)
                      };

                    n.AuthenticationTicket.Identity.AddClaims(claims);



                    //// create the identity
                    //var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);

                    //System.Web.HttpContext.Current.GetOwinContext().Authentication.SignIn(new AuthenticationProperties
                    //{
                    //    IsPersistent = true
                    //}, identity);
                }
            }
        });
    }

这是Login.aspx页面:

 protected void Page_Load(object sender, EventArgs e)
    {

        if (!Request.IsAuthenticated)
        {
            HttpContext.Current.GetOwinContext().Authentication.Challenge(
              new AuthenticationProperties { RedirectUri = Request["ReturnUrl"] ?? "Default.aspx" },
              OpenIdConnectAuthenticationDefaults.AuthenticationType);
        }        
    }

页面流程如下:
1)请求:http://localhost:60624/Page.aspx,响应:302 - 重定向到 Login.aspx。
2)请求:http://localhost:60624/Login.aspx?ReturnUrl=%2FPage.aspx,响应:302 - 重定向到 https://auth.myprovider.com。一些 cookie 在此处设置在响应头中:

Set-Cookie: OpenIdConnect.nonce.KIsuj4RUmGKJIynLrkEScxBvGrZzkMo6ylZ%2F4lRknPM%3D=xxxxxxxxx; path=/; expires=Mon, 22-Apr-2019 14:12:00 GMT; HttpOnly Set-Cookie: OpenIdConnect.nonce.KIsuj4RUmGKJIynLrkEScxBvGrZzkMo6ylZ%2F4lRknPM%3D=yyyyyyyyy; expires=Mon, 22-Apr-2019 14:12:00 GMT; path=/; HttpOnly

3)认证提供商,登录,它将 302 重定向到 /LoginCallback。
4)请求:http://localhost:60624/LoginCallback,响应:302 - 重定向到 /Page.aspx。在这里清除了第 2 步设置的 cookie:

Set-Cookie: OpenIdConnect.nonce.KIsuj4RUmGKJIynLrkEScxBvGrZzkMo6ylZ%2F4lRknPM%3D=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT Set-Cookie: OpenIdConnect.nonce.KIsuj4RUmGKJIynLrkEScxBvGrZzkMo6ylZ%2F4lRknPM%3D=; expires=Thu, 01-Jan-1970 00:00:00 GMT; path=/

5)返回到 Page.aspx,用户未经过身份验证;回到步骤 1。
我已经进行了一些调试,并且在启动时 AuthorizationCodeReceived 已经触发,后端成功调用了用户信息终结点。我尝试从该通知中调用 System.Web.HttpContext.Current.GetOwinContext().Authentication.SignIn(),但似乎没有任何作用。
此时,我卡住了。为什么用户身份验证的身份验证 cookie 没有被设置?这似乎应该自动发生。我是否应该手动创建它? (如何手动创建身份验证 cookie 而不是默认方法?)

编辑:在查阅了@Zaxxon的回复后,我得以让它正常工作。在AuthorizationCodeReceived通知中有两个问题:

  1. 我需要创建ClaimsIdentity。在我上面提交的原始代码中,我已经将其注释掉了,但是也是不正确的。
  2. 我必须用新创建的身份替换AuthenticationTicket。然后将声明添加到这个新身份中。

以下是可正常工作的代码:

ClaimsIdentity identity = new ClaimsIdentity(DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.GivenName, ClaimTypes.Role);
 n.AuthenticationTicket = new AuthenticationTicket(identity, n.AuthenticationTicket.Properties);
 n.AuthenticationTicket.Identity.AddClaims(claims);

服务器上已有的 cookie 可能无效或已过期。建议使用 IE 删除所有 cookie 并重试。我认为您正在遇到异常情况,应在出现异常时退出代码而不是重试。错误 302 看起来像是某种端口转发算法。请参阅:https://en.wikipedia.org/wiki/List_of_HTTP_status_codes。 - jdweng
没有错误。在这个过程中,HTTP 302 是正常的重定向响应,因为它在页面流之间移动。我在这个过程中没有看到任何异常,即使清除了 cookies。 - swbradshaw
你是否看到了200 OK?我怀疑你可能会遇到异常情况,建议添加异常处理程序。同时,检查事件查看器以查看是否有任何错误。 - jdweng
我看不到200 OK,因为用户从未被认证。正如我所提到的,在经过身份验证流程后,它会重复自身,因为在身份验证期间从未设置cookie。在OWIN过程中,我应该在哪里添加异常处理程序? - swbradshaw
你需要使用类似Wireshark或Fiddler的嗅探器,首先确认您正在发送请求。一旦请求被发送到服务器,服务器应该发送一个带有状态(如200 OK或失败状态)的响应。根据您的描述,我现在不确定请求是否已被发送。我也无法确定它是否安全(使用SSL或TTLS)或非安全。然后,我也无法确定您是否使用http 1.0(流模式)或http 1.1(块模式)。服务器可能没有运行,因此从cmd.exe使用>netstat -a并验证端口60624上是否有监听器。 - jdweng
显示剩余2条评论
3个回答

7

我曾经需要使用VB.Net实现一个概念验证,这相当令人痛苦。以下是我的测试代码(即非生产代码),参考了一些其他C#互联网示例:

Imports System.Security.Claims
Imports System.Threading.Tasks
Imports IdentityModel
Imports IdentityModel.Client
Imports Microsoft.AspNet.Identity
Imports Microsoft.AspNet.Identity.Owin
Imports Microsoft.IdentityModel.Protocols.OpenIdConnect
Imports Microsoft.Owin
Imports Microsoft.Owin.Security
Imports Microsoft.Owin.Security.Cookies
Imports Microsoft.Owin.Security.Notifications
Imports Microsoft.Owin.Security.OAuth
Imports Microsoft.Owin.Security.OpenIdConnect
Imports Owin

Partial Public Class Startup
    Private Shared _oAuthOptions As OAuthAuthorizationServerOptions
    Private Shared _publicClientId As String

    Private Shared _clientId As String
    Private Shared _clientSecret As String

    ' Enable the application to use OAuthAuthorization. You can then secure your Web APIs
    Shared Sub New()

        _clientId = System.Configuration.ConfigurationManager.AppSettings("OAuth:ClientID").ToString()
        _clientSecret = System.Configuration.ConfigurationManager.AppSettings("OAuth:SecretKey").ToString()

        PublicClientId = _clientId

        OAuthOptions = New OAuthAuthorizationServerOptions() With {
            .TokenEndpointPath = New PathString("/Token"), 'New PathString("https://authtesteria.domain.com/as/token.oauth2"), ' 
            .AuthorizeEndpointPath = New PathString("/Account/Authorize"), 'New PathString("https://authtesteria.domain.com/as/authorization.oauth2"), '
            .Provider = New ApplicationOAuthProvider(PublicClientId),
            .AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            .AllowInsecureHttp = True
        }
    End Sub

    Public Shared Property OAuthOptions() As OAuthAuthorizationServerOptions
        Get
            Return _oAuthOptions
        End Get
        Private Set
            _oAuthOptions = Value
        End Set
    End Property

    Public Shared Property PublicClientId() As String
        Get
            Return _publicClientId
        End Get
        Private Set
            _publicClientId = Value
        End Set
    End Property

    ' For more information on configuring authentication, please visit https://go.microsoft.com/fwlink/?LinkId=301864
    Public Sub ConfigureAuth(app As IAppBuilder)
        ' Configure the db context, user manager and signin manager to use a single instance per request
        app.CreatePerOwinContext(AddressOf ApplicationDbContext.Create)
        app.CreatePerOwinContext(Of ApplicationUserManager)(AddressOf ApplicationUserManager.Create)
        app.CreatePerOwinContext(Of ApplicationSignInManager)(AddressOf ApplicationSignInManager.Create)

        ' Enable the application to use a cookie to store information for the signed in user
        ' and to use a cookie to temporarily store inforation about a user logging in with a third party login provider
        ' Configure the sign in cookie
        ' OnValidateIdentity enables the application to validate the security stamp when the user logs in.
        ' This is a security feature which is used when you change a password or add an external login to your account.
        app.UseCookieAuthentication(New CookieAuthenticationOptions() With {
            .AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            .Provider = New CookieAuthenticationProvider() With {
                .OnValidateIdentity = SecurityStampValidator.OnValidateIdentity(Of ApplicationUserManager, ApplicationUser)(
                    validateInterval:=TimeSpan.FromMinutes(30),
                    regenerateIdentity:=Function(manager, user) user.GenerateUserIdentityAsync(manager))},
            .LoginPath = New PathString("/Account/Login")})


        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie)

        ' Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
        app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5))

        ' Enables the application to remember the second login verification factor such as phone or email.
        ' Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
        ' This is similar to the RememberMe option when you log in.
        app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie)

        ' Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(OAuthOptions)

        Dim controller As New AccountController()

        'Dim validator As OpenIdConnectProtocolValidator = New OpenIdConnectProtocolValidator()
        'validator.ShowPII = False

        Dim oidcAuth As New Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions() With {
            .ClientId = _clientId,
            .ClientSecret = _clientSecret,
            .Authority = "https://authtesteria.domain.com",
            .Notifications = New Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationNotifications() With {
                .RedirectToIdentityProvider = AddressOf OnRedirectToIdentityProvider,
                .MessageReceived = AddressOf OnMessageReceived,
                .SecurityTokenReceived = AddressOf OnSecurityTokenReceived,
                .SecurityTokenValidated = AddressOf OnSecurityTokenValidated,
                .AuthorizationCodeReceived = AddressOf OnAuthorizationCodeReceived,
                .AuthenticationFailed = AddressOf OnAuthenticationFailed
        }}
        app.UseOpenIdConnectAuthentication(oidcAuth)

    End Sub

    Private Function OnRedirectToIdentityProvider(arg As RedirectToIdentityProviderNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
        Debug.WriteLine("*** RedirectToIdentityProvider")

        If arg.ProtocolMessage.RequestType = Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout Then
            Dim idTokenHint = arg.OwinContext.Authentication.User.FindFirst("id_token")

            If idTokenHint IsNot Nothing Then
                arg.ProtocolMessage.IdTokenHint = idTokenHint.Value
            End If
        End If
        Return Task.FromResult(0)
    End Function

    Private Function OnMessageReceived(arg As MessageReceivedNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
        Debug.WriteLine("*** MessageReceived")
        Return Task.FromResult(0)
    End Function

    Private Function OnAuthorizationCodeReceived(arg As AuthorizationCodeReceivedNotification) As Task
        Debug.WriteLine("*** AuthorizationCodeReceived")
        'Upon successful sign in, get & cache a token if you want here
        Return Task.FromResult(0)
    End Function

    Private Function OnAuthenticationFailed(arg As AuthenticationFailedNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
        Debug.WriteLine("*** AuthenticationFailed")
        Return Task.FromResult(0)
    End Function

    Private Function OnSecurityTokenReceived(arg As SecurityTokenReceivedNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
        Debug.WriteLine("*** SecurityTokenReceived")
        Return Task.FromResult(0)
    End Function

    Private Async Function OnSecurityTokenValidated(arg As SecurityTokenValidatedNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
        Debug.WriteLine("*** SecurityTokenValidated")
        'Verify the user signing in should have access or not.  Here I just pass folk thru.
        Dim nid = New ClaimsIdentity(
              DefaultAuthenticationTypes.ApplicationCookie, 'arg.AuthenticationTicket.Identity.AuthenticationType,
              ClaimTypes.GivenName,
              ClaimTypes.Role)

        Dim tokenClient = New TokenClient("https://authtesteria.domain.com/as/token.oauth2",
             _clientId,
             _clientSecret)

        Dim tokenResponse = Await tokenClient.RequestAuthorizationCodeAsync(arg.ProtocolMessage.Code, arg.ProtocolMessage.RedirectUri)

        ' get userinfo data
        Dim userInfoClient = New IdentityModel.Client.UserInfoClient("https://authtesteria.domain.com/idp/userinfo.openid")

        Dim userInfo = Await userInfoClient.GetAsync(tokenResponse.AccessToken)
        userInfo.Claims.ToList().ForEach(Sub(ui) nid.AddClaim(New Claim(ui.Type, ui.Value)))

        '' keep the id_token for logout
        'nid.AddClaim(New Claim("id_token", arg.ProtocolMessage.IdToken))

        '' add access token for sample API
        'nid.AddClaim(New Claim("access_token", arg.ProtocolMessage.AccessToken))

        '' keep track of access token expiration
        'nid.AddClaim(New Claim("expires_at", DateTimeOffset.Now.AddSeconds(Integer.Parse(arg.ProtocolMessage.ExpiresIn)).ToString()))

        '' add some other app specific claim
        'nid.AddClaim(New Claim("app_specific", "some data"))

        nid.AddClaim(New Claim(ClaimTypes.Role, "group1"))

        arg.AuthenticationTicket = New AuthenticationTicket(nid, arg.AuthenticationTicket.Properties)
        arg.AuthenticationTicket.Properties.RedirectUri = HttpContext.Current.Session("PageRedirect").ToString() 
    End Function
End Class

现在我这样触发登录:
Private Sub SomePageName_Load(sender As Object, e As EventArgs) Handles Me.Load
    If Not IsPostBack Then
        If User.Identity.IsAuthenticated Then
            Console.WriteLine(User.Identity.GetUserName())
        Else
            Session("PageRedirect") = Request.Url
            Response.Redirect("/")
        End If
    End If
End Sub

我们有一些差异:

  1. 我使用OnSecurityTokenValidated,但不确定是否重要
  2. 我用当前页面的Request.Url填充一个Session变量,
  3. 然后在启动时在OnSecurityTokenValidated的通知参数中使用它: arg.AuthenticationTicket.Properties.RedirectUri = … (请参见我的代码)。

希望这对你有所帮助。享受!


我本来想提交一些编辑,修复你的答案上的语法高亮(强制VB高亮),但是Stack Overflow声称建议的编辑队列已满。(我在想那个队列到底是针对这个答案、问题还是整个Stack Overflow...) - Zarepheth

0
在我的情况下,我根本不需要在启动文件中添加任何内容到Notifications。我漏掉了这个:
app.UseCookieAuthentication(new CookieAuthenticationOptions() 
{
    CookieManager = new SystemWebCookieManager()
});

在Katana中有一些bug会阻止cookie正确保存,所以在寻找了三天后,添加这一行代码解决了我的问题!


0

我将RedeemCode设置为True,SaveTokens设置为True,并将其放入通知中,最终成功让它工作了。所有的示例都没有包括这个,我不确定为什么,但可能是代码流程所必需的。您不必重新添加声明,否则它们会重复。

另外,如果您想要自动填充User.Identity.Name,请不要忘记设置TokenValidationParameters NameClaimType="name"。

SecurityTokenValidated = (context) =>
            {
                var ctx = context.OwinContext;
                
                var applicationCookieIdentity = new ClaimsIdentity(context.AuthenticationTicket.Identity, null, DefaultAuthenticationTypes.ApplicationCookie, null, null);
                ctx.Authentication.SignIn(applicationCookieIdentity);

                return Task.FromResult(0);
            },

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接