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.
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.
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!