IdentityServer3和通过OpenIDConnect进行的外部登录 [英] IdentityServer3 and external login through OpenIDConnect

查看:177
本文介绍了IdentityServer3和通过OpenIDConnect进行的外部登录的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在ASP.NET MVC应用程序中,我正在尝试针对外部OIDC服务实现身份验证.对于我的测试,我使用的是 IdentityServer3 ( https://identityserver.github.io/文档/)和公共OIDC演示服务器: https://mitreid.org/

In ASP.NET MVC app, I am trying to implement authentication against external OIDC service. For my testing I am using IdentityServer3 (https://identityserver.github.io/Documentation/) and public OIDC demo server: https://mitreid.org/

我从GitHub克隆了此示例: https://github.com/IdentityServer/IdentityServer3.Samples/tree/master/source/MVC%20Authentication

I cloned this sample from GitHub: https://github.com/IdentityServer/IdentityServer3.Samples/tree/master/source/MVC%20Authentication

然后添加了以下代码,以将公共OIDC服务器注册为外部登录提供程序:

Then added the following code to register the public OIDC server as external login provider:

private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
    app.UseOpenIdConnectAuthentication(
        new OpenIdConnectAuthenticationOptions
        {
            AuthenticationType = "<AuthTypeName>",
            Authority = "https://mitreid.org/",
            Caption = "MIT Test Server",
            ClientId = "<Client Id>",
            ClientSecret = "<Client Secret>",
            RedirectUri = "https://localhost:44319/", //NOT SURE WHAT TO PUT HERE
            ResponseType = "code",
            Scope = "openid email profile",
            SignInAsAuthenticationType = signInAsType
        });
}

代码有效,我可以选择通过外部OIDC服务器登录.浏览器将重定向到外部服务器登录页面,输入登录名和密码后,将显示同意页面.但是,在浏览器导航回 https://localhost:44319/后,用户未通过身份验证-User.Identity.IsAuthenticated是错误的.

The code works, i get the option to login via external OIDC server. The browser redirects to the external server login page and when login and password is entered, the consent page is shown. However, after the browser navigates back to https://localhost:44319/ the user is not authenticated - User.Identity.IsAuthenticated is false.

问题:什么是RedirectUri属性的正确值? OpenIdConnect中间件是否具有解析从外部服务器传入的身份验证信息的功能,或者必须手动对其进行编码?有任何示例代码如何执行此操作吗?

Question: What should be correct value of RedirectUri property? Does OpenIdConnect middleware have capability to parse the authantication info passed in from external server or it must be coded manually? Is there any sample code how to do this?

推荐答案

我正在研究代码并调试了好几个小时(对此我是陌生的),我了解到:

I was studying the code and debugging quite a few hours (I am new to this) and I learned that:

  • 此问题与Microsoft实现的OpenIdConnect OWIN中间件有关(使用HTTP POST发送消息,但是MIT服务器执行HTTP GET
  • 来自Microsoft的中间件期望从OIDC服务器获得的消息中包含ID令牌以及代码,但是MIT服务器仅发送该代码.
  • 看起来RedirectUri可以是/identity下的任何路径,因为对每个请求都命中了中间件方法AuthenticateCoreAsync(),它
  • This problem is related to OpenIdConnect OWIN Middleware implemented by Microsoft (https://github.com/aspnet/AspNetKatana/tree/dev/src/Microsoft.Owin.Security.OpenIdConnect).
  • The middleware from Microsoft expect that the OIDC server sends the message using HTTP POST, but the MIT server does HTTP GET
  • The middleware from Microsoft expect that there is id token along with code in the message obtained from OIDC server, but the MIT server sends only the code.
  • Looks like the RedirectUri can be any path under /identity because the middleware method AuthenticateCoreAsync() is hit on every request and it does compare request path to configured Options.CallbackPath (which is set from RedirectURI)

因此,我只需要实现标准的授权代码流程-将代码交换为id令牌,获取声明,创建身份验证票证并重定向到IdentityServer/identity/callback端点.完成此操作后,一切都开始工作. IdentityServer太棒了!

So I just had to implement the standard authorization code flow - exchange the code for id token, get claims, create authentication ticket and redirect to IdentityServer /identity/callback endpoint. When I've done this, everything started working. IdentityServer is awesome!

我从OpenIdConnect中间件继承了一组新的类,并且确实重写了一些方法.关键方法是OpenIdConnectAuthenticationHandler中的async Task<AuthenticationTicket> AuthenticateCoreAsync().我在下面粘贴了代码,以防对某人有所帮助.

I inherited new set of classes from OpenIdConnect middleware and did override some methods. The key method is async Task<AuthenticationTicket> AuthenticateCoreAsync() in OpenIdConnectAuthenticationHandler. I pasted the code below in case it would help to someone.

public class CustomOidcHandler : OpenIdConnectAuthenticationHandler
{
    private const string HandledResponse = "HandledResponse";

    private readonly ILogger _logger;
    private OpenIdConnectConfiguration _configuration;

    public CustomOidcHandler(ILogger logger) : base(logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// Invoked to process incoming authentication messages.
    /// </summary>
    /// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns>
    protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
    {
        // Allow login to be constrained to a specific path. Need to make this runtime configurable.
        if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path))
            return null;

        OpenIdConnectMessage openIdConnectMessage = null;
        if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
            openIdConnectMessage = new OpenIdConnectMessage(Request.Query);

        if (openIdConnectMessage == null)
            return null;

        ExceptionDispatchInfo authFailedEx = null;
        try
        {
            return await CreateAuthenticationTicket(openIdConnectMessage).ConfigureAwait(false);
        }
        catch (Exception exception)
        {
            // We can't await inside a catch block, capture and handle outside.
            authFailedEx = ExceptionDispatchInfo.Capture(exception);
        }

        if (authFailedEx != null)
        {
            _logger.WriteError("Exception occurred while processing message: ", authFailedEx.SourceException);

            // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification.
            if (Options.RefreshOnIssuerKeyNotFound && authFailedEx.SourceException.GetType() == typeof(SecurityTokenSignatureKeyNotFoundException))
                Options.ConfigurationManager.RequestRefresh();

            var authenticationFailedNotification = new AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
            {
                ProtocolMessage = openIdConnectMessage,
                Exception = authFailedEx.SourceException
            };
            await Options.Notifications.AuthenticationFailed(authenticationFailedNotification).ConfigureAwait(false);
            if (authenticationFailedNotification.HandledResponse)
                return GetHandledResponseTicket();

            if (authenticationFailedNotification.Skipped)
                return null;

            authFailedEx.Throw();
        }

        return null;
    }

    private async Task<AuthenticationTicket> CreateAuthenticationTicket(OpenIdConnectMessage openIdConnectMessage)
    {
        var messageReceivedNotification =
            new MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
            {
                ProtocolMessage = openIdConnectMessage
            };
        await Options.Notifications.MessageReceived(messageReceivedNotification).ConfigureAwait(false);
        if (messageReceivedNotification.HandledResponse)
        {
            return GetHandledResponseTicket();
        }
        if (messageReceivedNotification.Skipped)
        {
            return null;
        }

        // runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we
        // should process.
        AuthenticationProperties properties = GetPropertiesFromState(openIdConnectMessage.State);
        if (properties == null)
        {
            _logger.WriteWarning("The state field is missing or invalid.");
            return null;
        }

        // devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users.
        if (!string.IsNullOrWhiteSpace(openIdConnectMessage.Error))
        {
            throw new OpenIdConnectProtocolException(
                string.Format(CultureInfo.InvariantCulture,
                    openIdConnectMessage.Error,
                    "Exception_OpenIdConnectMessageError", openIdConnectMessage.ErrorDescription ?? string.Empty,
                    openIdConnectMessage.ErrorUri ?? string.Empty));
        }


        // tokens.Item1 contains id token
        // tokens.Item2 contains access token
        Tuple<string, string> tokens = await GetTokens(openIdConnectMessage.Code, Options)
            .ConfigureAwait(false);
        if (string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken))
            openIdConnectMessage.IdToken = tokens.Item1;

        var securityTokenReceivedNotification =
            new SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context,
                Options)
            {
                ProtocolMessage = openIdConnectMessage,
            };
        await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification).ConfigureAwait(false);
        if (securityTokenReceivedNotification.HandledResponse)
            return GetHandledResponseTicket();

        if (securityTokenReceivedNotification.Skipped)
            return null;

        if (_configuration == null)
            _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.Request.CallCancelled)
                .ConfigureAwait(false);

        // Copy and augment to avoid cross request race conditions for updated configurations.
        TokenValidationParameters tvp = Options.TokenValidationParameters.Clone();
        IEnumerable<string> issuers = new[] {_configuration.Issuer};
        tvp.ValidIssuers = tvp.ValidIssuers?.Concat(issuers) ?? issuers;
        tvp.IssuerSigningTokens = tvp.IssuerSigningTokens?.Concat(_configuration.SigningTokens) ?? _configuration.SigningTokens;

        SecurityToken validatedToken;
        ClaimsPrincipal principal =
            Options.SecurityTokenHandlers.ValidateToken(openIdConnectMessage.IdToken, tvp, out validatedToken);
        ClaimsIdentity claimsIdentity = principal.Identity as ClaimsIdentity;

        var claims = await GetClaims(tokens.Item2).ConfigureAwait(false);

        AddClaim(claims, claimsIdentity, "sub", ClaimTypes.NameIdentifier, Options.AuthenticationType);
        AddClaim(claims, claimsIdentity, "given_name", ClaimTypes.GivenName);
        AddClaim(claims, claimsIdentity, "family_name", ClaimTypes.Surname);
        AddClaim(claims, claimsIdentity, "preferred_username", ClaimTypes.Name);
        AddClaim(claims, claimsIdentity, "email", ClaimTypes.Email);

        // claims principal could have changed claim values, use bits received on wire for validation.
        JwtSecurityToken jwt = validatedToken as JwtSecurityToken;
        AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, properties);

        if (Options.ProtocolValidator.RequireNonce)
        {
            if (String.IsNullOrWhiteSpace(openIdConnectMessage.Nonce))
                openIdConnectMessage.Nonce = jwt.Payload.Nonce;

            // deletes the nonce cookie
            RetrieveNonce(openIdConnectMessage);
        }

        // remember 'session_state' and 'check_session_iframe'
        if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState))
            ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState;

        if (!string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe))
            ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] =
                _configuration.CheckSessionIframe;

        if (Options.UseTokenLifetime)
        {
            // Override any session persistence to match the token lifetime.
            DateTime issued = jwt.ValidFrom;
            if (issued != DateTime.MinValue)
            {
                ticket.Properties.IssuedUtc = issued.ToUniversalTime();
            }
            DateTime expires = jwt.ValidTo;
            if (expires != DateTime.MinValue)
            {
                ticket.Properties.ExpiresUtc = expires.ToUniversalTime();
            }
            ticket.Properties.AllowRefresh = false;
        }

        var securityTokenValidatedNotification =
            new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context,
                Options)
            {
                AuthenticationTicket = ticket,
                ProtocolMessage = openIdConnectMessage,
            };

        await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification).ConfigureAwait(false);
        if (securityTokenValidatedNotification.HandledResponse)
        {
            return GetHandledResponseTicket();
        }
        if (securityTokenValidatedNotification.Skipped)
        {
            return null;
        }
        // Flow possible changes
        ticket = securityTokenValidatedNotification.AuthenticationTicket;

        // there is no hash of the code (c_hash) in the jwt obtained from the server
        // I don't know how to perform the validation using ProtocolValidator without the hash
        // that is why the code below is commented
        //var protocolValidationContext = new OpenIdConnectProtocolValidationContext
        //{
        //    AuthorizationCode = openIdConnectMessage.Code,
        //    Nonce = nonce
        //};
        //Options.ProtocolValidator.Validate(jwt, protocolValidationContext);

        if (openIdConnectMessage.Code != null)
        {
            var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options)
            {
                AuthenticationTicket = ticket,
                Code = openIdConnectMessage.Code,
                JwtSecurityToken = jwt,
                ProtocolMessage = openIdConnectMessage,
                RedirectUri =
                    ticket.Properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey)
                        ? ticket.Properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey]
                        : string.Empty,
            };
            await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification)
                .ConfigureAwait(false);
            if (authorizationCodeReceivedNotification.HandledResponse)
            {
                return GetHandledResponseTicket();
            }
            if (authorizationCodeReceivedNotification.Skipped)
            {
                return null;
            }
            // Flow possible changes
            ticket = authorizationCodeReceivedNotification.AuthenticationTicket;
        }

        return ticket;
    }

    private static void AddClaim(IEnumerable<Tuple<string, string>> claims, ClaimsIdentity claimsIdentity, string key, string claimType, string issuer = null)
    {
        string subject = claims
            .Where(it => it.Item1 == key)
            .Select(x => x.Item2).SingleOrDefault();
        if (!string.IsNullOrWhiteSpace(subject))
            claimsIdentity.AddClaim(
                new System.Security.Claims.Claim(claimType, subject, ClaimValueTypes.String, issuer));
    }


    private async Task<Tuple<string, string>> GetTokens(string authorizationCode, OpenIdConnectAuthenticationOptions options)
    {
        // exchange authorization code at authorization server for an access and refresh token
        Dictionary<string, string> post = null;
        post = new Dictionary<string, string>
        {
            {"client_id", options.ClientId},
            {"client_secret", options.ClientSecret},
            {"grant_type", "authorization_code"},
            {"code", authorizationCode},
            {"redirect_uri", options.RedirectUri}
        };

        string content;
        using (var client = new HttpClient())
        {
            var postContent = new FormUrlEncodedContent(post);
            var response = await client.PostAsync(options.Authority.TrimEnd('/') + "/token", postContent)
                .ConfigureAwait(false);
            content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        }
        // received tokens from authorization server
        var json = JObject.Parse(content);
        var accessToken = json["access_token"].ToString();
        string idToken = null;
        if (json["id_token"] != null)
            idToken = json["id_token"].ToString();

        return new Tuple<string, string>(idToken, accessToken);
    }

    private async Task<IEnumerable<Tuple<string, string>>> GetClaims(string accessToken)
    {
        string userInfoEndpoint = Options.Authority.TrimEnd('/') + "/userinfo";
        var userInfoClient = new UserInfoClient(new Uri(userInfoEndpoint), accessToken);
        var userInfoResponse = await userInfoClient.GetAsync().ConfigureAwait(false);
        var claims = userInfoResponse.Claims;

        return claims;
    }

    private static AuthenticationTicket GetHandledResponseTicket()
    {
        return new AuthenticationTicket(null, new AuthenticationProperties(new Dictionary<string, string>() { { HandledResponse, "true" } }));
    }

    private AuthenticationProperties GetPropertiesFromState(string state)
    {
        // assume a well formed query string: <a=b&>OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d>
        int startIndex = 0;
        if (string.IsNullOrWhiteSpace(state) || (startIndex = state.IndexOf("OpenIdConnect.AuthenticationProperties", StringComparison.Ordinal)) == -1)
        {
            return null;
        }

        int authenticationIndex = startIndex + "OpenIdConnect.AuthenticationProperties".Length;
        if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=')
        {
            return null;
        }

        // scan rest of string looking for '&'
        authenticationIndex++;
        int endIndex = state.Substring(authenticationIndex, state.Length - authenticationIndex).IndexOf("&", StringComparison.Ordinal);

        // -1 => no other parameters are after the AuthenticationPropertiesKey
        if (endIndex == -1)
        {
            return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex).Replace('+', ' ')));
        }
        else
        {
            return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex, endIndex).Replace('+', ' ')));
        }
    }
}


public static class CustomOidcAuthenticationExtensions
{
    /// <summary>
    /// Adds the <see cref="OpenIdConnectAuthenticationMiddleware"/> into the OWIN runtime.
    /// </summary>
    /// <param name="app">The <see cref="IAppBuilder"/> passed to the configuration method</param>
    /// <param name="openIdConnectOptions">A <see cref="OpenIdConnectAuthenticationOptions"/> contains settings for obtaining identities using the OpenIdConnect protocol.</param>
    /// <returns>The updated <see cref="IAppBuilder"/></returns>
    public static IAppBuilder UseCustomOidcAuthentication(this IAppBuilder app, OpenIdConnectAuthenticationOptions openIdConnectOptions)
    {
        if (app == null)
            throw new ArgumentNullException(nameof(app));

        if (openIdConnectOptions == null)
            throw new ArgumentNullException(nameof(openIdConnectOptions));

        return app.Use(typeof(CustomOidcMiddleware), app, openIdConnectOptions);
    }
}

和Startup.cs

and in Startup.cs

public class Startup
{
....
public void Configuration(IAppBuilder app)
{
    ....

     private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
    {
        app.UseCustomOidcAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                AuthenticationType = "<name>",
                Authority = "<OIDC server url>",
                Caption = "<caption>",
                ClientId = "<client id>",
                ClientSecret = "<client secret>",
                // might be https://localhost:44319/identity/<anything>
                RedirectUri = "https://localhost:44319/identity/signin-customoidc",
                ResponseType = "code",
                Scope = "openid email profile address phone",
                SignInAsAuthenticationType = signInAsType
            }                
        );
    }
    ....
}
....
}

这篇关于IdentityServer3和通过OpenIDConnect进行的外部登录的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆