AJAX call against REST endpoint secured with Thinktecture's IdentityServer STS

我与影子孤独终老i 提交于 2019-12-21 19:42:28

问题


I'm having some dramas making a call against a ServiceStack REST service which I've secured with an out of the box IdentityServer STS.

I'm making an AJAX call against the REST endpoint, and I'm not sure how one might setup a logon procedure to get a security token to pass. The REST endpoint is on a different domain than the website making the call. The info that I've found so far all seems to revolve around the procedure where the client makes a call to the secured resource gets a 302 redirect to the identityserver logon page, then after successful authentication gets a 302 redirect to either the realm or the reply depending on the configuration. I've hooked all this up correctly and it works great if I'm simply browsing through the REST services. However with regards to my web app, AJAX and 302s aren't exactly best friends, so ideally what I think I'd like to have is a REST endpoint from the same ServiceStack website that takes a username and password and returns a security token without the complication of any redirects (I'll handle the 401 redirects within my web application itself which I'll get when I turn off passiveRedirectEnabled in the web.config). Any thoughts on how one might achieve this with IdentityServer?

Cheers, Clint.


回答1:


Completing the answer with the full REST endpoint:

In the ServiceStack web app:

Route to the logon endpoint in AppHost.cs with something like:

public override void Configure(Container container)
{
    Routes.Add<Logon>("/logon", "POST");
}

Then there's a simple username/password Request DTO

public class Logon
{
    public string UserName { get; set; }
    public string Password { get; set; }
}

And the response DTO

Response DTO only needs to handle the POST - yes, you could add the URL/Password as parameters in the URL for a GET request, but this does not sound like it's recommended. In fact, you'd probably normally put this info in the Authorization header of the HTTP request but this makes your job in ServiceStack a little harder.

public class LogonService : Service
{
    public object Post(Logon request)
    {
        var securityToken = GetSaml2SecurityToken(request.UserName, request.Password, "https://myserver/identityserverwebapp/issue/wstrust/mixed/username", "http://myserver/servicestackwebapp/");

        return SerializeRequestSecurityTokenResponse(securityToken);
    }

    private RequestSecurityTokenResponse GetSaml2SecurityToken(string username, string password, string endpointAddress, string realm)
    {
        var factory = new WSTrustChannelFactory(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
            new EndpointAddress(endpointAddress))
        {
            TrustVersion = TrustVersion.WSTrust13
        };

        factory.Credentials.UserName.UserName = username;
        factory.Credentials.UserName.Password = password;

        var channel = (WSTrustChannel)factory.CreateChannel();
        RequestSecurityTokenResponse requestSecurityTokenResponse;

        channel.Issue(new RequestSecurityToken
        {
            TokenType = "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0",
            AppliesTo = new EndpointReference(realm),
            RequestType = RequestTypes.Issue,
            KeyType = KeyTypes.Bearer,
        }, out requestSecurityTokenResponse);

        return requestSecurityTokenResponse;
    }

    private string SerializeRequestSecurityTokenResponse(RequestSecurityTokenResponse requestSecurityTokenResponse)
    {
        var serializer = new WSTrust13ResponseSerializer();
        var context = new WSTrustSerializationContext(FederatedAuthentication.FederationConfiguration.IdentityConfiguration.SecurityTokenHandlerCollectionManager);
        var stringBuilder = new StringBuilder(128);

        using (var writer = XmlWriter.Create(new StringWriter(stringBuilder), new XmlWriterSettings { OmitXmlDeclaration = true}))
        {
            serializer.WriteXml(requestSecurityTokenResponse, writer, context);
            writer.Flush();
            return stringBuilder.ToString();
        }
    }
}

The ServiceStack web app Web.config should look fairly similar to this:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  </configSections>
  <location path="FederationMetadata">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>
  <!-- to allow the logon route without requiring authentication first. -->
  <location path="logon">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>
  <system.web>
    <httpHandlers>
      <add path="*" type="ServiceStack.WebHost.Endpoints.ServiceStackHttpHandlerFactory, ServiceStack" verb="*" />
    </httpHandlers>
    <compilation debug="true" />
    <authentication mode="None" />
    <authorization>
      <deny users="?" />
    </authorization>
    <httpRuntime targetFramework="4.5" requestValidationMode="4.5" />
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
        <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
        <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
    </modules>
    <validation validateIntegratedModeConfiguration="false" />
    <handlers>
      <add path="*" name="ServiceStack.Factory" type="ServiceStack.WebHost.Endpoints.ServiceStackHttpHandlerFactory, ServiceStack" verb="*" preCondition="integratedMode" resourceType="Unspecified" allowPathInfo="true" />
    </handlers>
  </system.webServer>
  <system.identityModel>
    <identityConfiguration>
      <audienceUris>
        <add value="http://myserver/servicestackwebapp/" />
      </audienceUris>
      <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <trustedIssuers>
          <add thumbprint="B6E05E14243FB7D76D5B660532520FB94679AA01" name="http://mycertificatefriendlyname" />
        </trustedIssuers>
      </issuerNameRegistry>
      <certificateValidation certificateValidationMode="None" />
      <securityTokenHandlers>
        <securityTokenHandlerConfiguration saveBootstrapContext="true" />
      </securityTokenHandlers>
    </identityConfiguration>
  </system.identityModel>
  <system.identityModel.services>
    <federationConfiguration>
      <cookieHandler requireSsl="false" />
      <wsFederation passiveRedirectEnabled="false" issuer="https://myserver/identityserverwebapp/issue/wsfed" realm="http://myserver/servicestackwebapp/" requireHttps="false" />
    </federationConfiguration>
  </system.identityModel.services>
</configuration>

And finally, to authenticate a simple Javascript client app with the REST endpoint, POST the username and password to the logon endpoint of the servicestackwebapp, and then when you receive the response, post that back to the realm - doing so sets up the FedAuth cookies for your current session so you don't have to think about the token management client-side anymore.

$.ajax({
    type: "POST",
    url: "/servicestackwebapp/logon",
    dataType: "text",
    data: { UserName: "myuser", Password: "mypassword" },
    success: function (data) {
        $.ajax({
            type: "POST",
            url: "/servicestackwebapp/",
            data: "wa=wsignin1.0&wresult=" + encodeURIComponent(data)
        });
    }
});

Also, I should note that all of the HTTP endpoints above should instead be going over HTTPS - don't be silly like I've done in my example and send clear-text claims over HTTP.

Also after I'd implemented my solution I found this: http://msdn.microsoft.com/en-us/library/hh446531.aspx ... I wish I'd found it before, but it's reassuring to know I've implemented something similar to the Microsoft example - we diverge at the point where they convert to a Simple Web Token - I keep it as a SAML token and pass that (serialized) to the client instead.




回答2:


My solution so far:

I've exposed an endpoint on the REST service that makes a call against the WS-Trust endpoint that IdentityServer provides by default. In .NET 4.5, you'll need to have a reference to Thinktecture.IdentityModel as UserNameWSTrustBinding is not available in System.IdentityModel see: What's the .NET 4.5 equivalent to UserNameWSTrustBinding?

The code to get the SAML2 security token from the endpoint looks like this:

    private SecurityToken GetSamlSecurityToken(string username, string password, string endpointAddress, string realm)
    {
        var factory = new WSTrustChannelFactory(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
            new EndpointAddress(endpointAddress))
        {
            TrustVersion = TrustVersion.WSTrust13
        };

        factory.Credentials.UserName.UserName = username;
        factory.Credentials.UserName.Password = password;

        var channel = factory.CreateChannel();

        var securityToken = channel.Issue(new RequestSecurityToken
        {
            TokenType = "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0",
            AppliesTo = new EndpointReference(realm),
            RequestType = RequestTypes.Issue,
            KeyType = KeyTypes.Bearer,
        });

        return securityToken;
    }

This will authenticate based on a username and password and the endpointAddress parameter will look something like:

https://myserver/identityserverapp/issue/wstrust/mixed/username

Then I'm serializing the security token as follows:

    private string SerializeSecurityToken(SecurityToken securityToken)
    {
        var serializer = new WSSecurityTokenSerializer();
        var stringBuilder = new StringBuilder();

        using (var writer = XmlWriter.Create(new StringWriter(stringBuilder)))
        {
            serializer.WriteToken(writer, securityToken);
            return stringBuilder.ToString();
        }
    }

I believe the only remaining bit is establishing the FedAuth cookie(s), which I believe are set on first post of the security token to the secured web app.

Please weigh in with any improvements or suggestions. Thanks!



来源:https://stackoverflow.com/questions/19287900/ajax-call-against-rest-endpoint-secured-with-thinktectures-identityserver-sts

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!