IdentityServer SSO - Trusted application

2019-06-06 08:33发布

问题:

I need to SSO (single sign on) a user coming from an application of mine (identity provider using ASPNET Session State) and redirect them to another application of mine (service provider) that is configured to use implicit flow with IdentityServer4. I need to achieve this without requiring the user to log back in and without providing the user's password.

My initial thought was that I could use a client secret for the identity provider to redirect the user to the IdentityServer4 authentication end point with the access token as a query parameter and then use a custom validator or extension grant to issue an identity token for use with the service provider application without needing to also provide the user's password.

I've managed to issue an access token to the identity provider and then redirect the user to IdentityServer4, but issuing an identity token has proven difficult for me. I've poured over the samples and documentation and I'm confused to say the least.

I'm looking for direction on the appropriate approach to this scenario and perhaps a comprehensive example in C#. I've come to understand I can use a hybrid flow to issue an access token as well as an identity token. I think my biggest struggle is how to redirect the user and, based on the access token, issue the user an identity token (and if this is even an acceptable approach).

Simply put: I'd like to redirect the user from Application A to IdentityServer4 to Application B based on trust with the identity provider (via client secret?).

Note: I understand this could be considered an opinion-based question, but based on my research I believe there is one single best practice and that's what I'm asking for.

回答1:

I managed to get this working by the following flow:

  1. Authorize the user in Application A (Identity Provider)
  2. Obtain Access Token from Identity Server 4 via Token Endpoint and shared secret.
  3. Add access token as a query string parameter since headers are not preserved on redirect.
  4. Redirect the user to an Account controller method that accepts identifying information such as username. This method is protected by a custom middleware class that checks the query string for an access token parameter. If the token exists, it is added to the authentication header; this authorizes the user to hit this controller method.
  5. The controller method will then sign the user in and redirect them to the /connect/authorize/login endpoint.
  6. Finally, the login endpoint sets the cookie and redirects the user to Application B (Service Provider), whose URL is specified via the redirect_uri query parameter.

Configuration for shared secret:

Add appropriate grant type, secret and new scope name to the client. The new scope will help in debugging Access token issues in your logs (especially if you have multiple applications hitting your ID4 server). Also make sure to add the Service Provider's URL to the client RedirectUris, otherwise you'll receive an "invalid redirect" error.

            AllowedGrantTypes = new List<string> { GrantType.Implicit, GrantType.ClientCredentials },
            ClientSecrets = new List<Secret> {
                new Secret(_clientSecrets.ExternalIdpSecret.Sha256(), clientID)
            },
            AllowedScopes = new List<string>
            {
                "newScopeName"
            },
            RedirectUris = new List<string>
            {
                $"http://localhost:<portnumber>"
            }

Next, add your custom middleware.

public class QueryStringOAuthBearerMiddleware
{
    private readonly RequestDelegate next;

    public QueryStringOAuthBearerMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        this.BeginInvoke(context);
        await this.next.Invoke(context);
        this.EndInvoke(context);
    }

    private void BeginInvoke(HttpContext context)
    {
        if (context.Request.Query.ContainsKey("accesstokenparametername"))
        {
            var accessToken = context.Request.Query.First(p => p.Key == "accesstokenparametername");

            if (!string.IsNullOrEmpty(accessToken.Value))
            {
                context.Request.Headers.Add("Authorization", "Bearer " + accessToken.Value);
            }
        }
    }

    private void EndInvoke(HttpContext context)
    {
    }
}

And add the middleware to your configuration.

        app.UseMiddleware<QueryStringOAuthBearerMiddleware>();

Create your login method.

    [HttpGet]
    [Authorize]
    public async Task<IActionResult> Login2(string userName, string returnURL)
    {
        await _httpContextWrapper.SignInAsync(userName);

        return Redirect(returnURL);
    }

Configuration for Client application (IDP):

Your client side code should look like this:

var disco = await DiscoveryClient.GetAsync("http://localhost:<portnumber>");
var tokenClient = new TokenClient(disco.TokenEndpoint, "clientIdentifier", "IUsedAGuidHere");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("newScopeName");

var redirectURL = string.Format("http://localhost:2228/account/Login2?userName=<UserIDValue>&returnURL={1}&accesstokenparametername={0}",
            tokenResponse.AccessToken,
            Server.UrlEncode(
                string.Format("/connect/authorize/login?client_id={3}&redirect_uri={2}&response_type=id_token%20token&scope=<ImplicitFlowScopes>&state={0}&nonce={1}",
                CryptoRandom.CreateUniqueId(),
                CryptoRandom.CreateUniqueId(),
                Server.UrlEncode("http://localhost:<PortNumber>"),
                "ClientIdentifier")));

Response.Redirect(redirectURL, false);

Note: Please understand you won't be able to take this code AS-IS and make it work. I've heavily modified it to protect the security of my resources.



回答2:

I think I might take care of the Authentication with Application A first, then forward on to the next app...

Application A --> IdentityServer --> Application A --> Application B.

You could include some custom parameters in your returnUrl which Application A could read upon return from IdentityServer that would trigger the redirect to Application B.