简体   繁体   English

IdentityServer3 和外部通过 OpenIDConnect 登录

[英]IdentityServer3 and external login through OpenIDConnect

In ASP.NET MVC app, I am trying to implement authentication against external OIDC service.在 ASP.NET MVC 应用程序中,我正在尝试针对外部 OIDC 服务实施身份验证。 For my testing I am using IdentityServer3 ( https://identityserver.github.io/Documentation/ ) and public OIDC demo server: https://mitreid.org/对于我的测试,我使用IdentityServer3 ( https://identityserver.github.io/Documentation/ ) 和公共 OIDC 演示服务器: https ://mitreid.org/

I cloned this sample from GitHub: https://github.com/IdentityServer/IdentityServer3.Samples/tree/master/source/MVC%20Authentication我从 GitHub 克隆了这个示例: https ://github.com/IdentityServer/IdentityServer3.Samples/tree/master/source/MVC%20Authentication

Then added the following code to register the public OIDC server as external login provider:然后添加以下代码将公共 OIDC 服务器注册为外部登录提供程序:

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

The code works, i get the option to login via external OIDC server.代码有效,我可以选择通过外部 OIDC 服务器登录。 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.但是,在浏览器导航回https://localhost:44319/后,用户未通过身份验证 - User.Identity.IsAuthenticated为 false。

Question: What should be correct value of RedirectUri property?问题: RedirectUri 属性的正确值应该是多少? Does OpenIdConnect middleware have capability to parse the authantication info passed in from external server or it must be coded manually? OpenIdConnect 中间件是否能够解析从外部服务器传入的身份验证信息,还是必须手动编码? 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:我研究了代码并调试了好几个小时(我是新手),我了解到:

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.所以我只需要实现标准的授权代码流程——交换 id 令牌的代码、获取声明、创建身份验证票证并重定向到 IdentityServer /identity/callback 端点。 When I've done this, everything started working.当我这样做时,一切都开始工作了。 IdentityServer is awesome!身份服务器很棒!

I inherited new set of classes from OpenIdConnect middleware and did override some methods.我从 OpenIdConnect 中间件继承了一组新的类,并覆盖了一些方法。 The key method is async Task<AuthenticationTicket> AuthenticateCoreAsync() in OpenIdConnectAuthenticationHandler .关键方法是OpenIdConnectAuthenticationHandler中的async Task<AuthenticationTicket> AuthenticateCoreAsync() 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);
    }
}

and in Startup.cs在 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
            }                
        );
    }
    ....
}
....
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM