问题
I have a web application and web api services that authenticate through ADFS. They are contained in the same IIS application, and the web app makes calls back to the web api services without a problem.
I'm now trying to call the same services from a different application, but am having trouble passing the token. I am able to authenticate and retrieve a SAML token with the following code:
var stsEndpoint = "https://MyAdfsServer/adfs/services/trust/13/UsernameMixed";
var reliantPartyUri = "https://MyDomain/AppRoot/";
var factory = new Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannelFactory(
new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
new EndpointAddress(stsEndpoint));
factory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrust13;
// Username and Password here...
factory.Credentials.UserName.UserName = @"Domain\UserName";
factory.Credentials.UserName.Password = "Password";
var rst = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
AppliesTo = new EndpointAddress(reliantPartyUri),
KeyType = KeyTypes.Bearer,
};
var channel = factory.CreateChannel();
var token = channel.Issue(rst) as GenericXmlSecurityToken;
var saml = token.TokenXml.OuterXml;
However, I'm not sure how to pass the saml in to the web api call. I've tried this:
using (var handler = new HttpClientHandler()
{
ClientCertificateOptions = ClientCertificateOption.Automatic,
AllowAutoRedirect = false
})
{
using (var client = new HttpClient(handler))
{
client.BaseAddress = new Uri("https://MyDomain/AppRoot/api/");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("SAML", saml);
HttpResponseMessage response = client.GetAsync("MyService/Get/").Result;
// Get the results...
var result = response.Content.ReadAsStringAsync().Result;
var status = response.StatusCode;
}
}
This is returning a status code of 302 and trying to redirect me to the ADFS server for authentication. Is there another way to pass the SAML token to the web api service?
回答1:
(SET)
string samlString = "blah blah blah";
byte[] bytes = Encoding.UTF8.GetBytes(samlString);
string base64SamlString = Convert.ToBase64String(bytes);
myHttpClient.DefaultRequestHeaders.Add("X-My-Custom-Header", base64SamlString);
(GET)
IEnumerable<string> headerValues = request.Headers.GetValues("X-My-Custom-Header");
if (null != headerValues)
{
var encoding = Encoding.GetEncoding("iso-8859-1");
string samlToken = encoding.GetString(Convert.FromBase64String(headerValues.FirstOrDefault()));
}
回答2:
I have been tackling the same challenge. The heart of the issue is that SAML is a specification that builds on SOAP; it is intended for message protection across multiple SOAP processing nodes, regardless of transport.
On the other hand, REST is all about transport. Thus, the security of REST endpoints rests (no pun intended) on transport security, e.g., SSL.
We have implemented a "hack" where we use a NetworkCredential with a fixed username like "saml_user" and set the password to the SAML token. But this is a short term thing. Do you really want to add 8 to 10 KB on every call?
Simple Web Tokens are much better suited for REST security. In fact, Microsoft's Guide to Claims-based Identity and Access Control (2nd Edition) has an entire chapter devoted to bridging from a SAML token provider to a Simple Web Token. See Accessing REST Services from a Windows Phone Device.
回答3:
When you want to access a resource which is protected via SSO (like ADFS, I assume), I found it easiest to use following approach: Show a WebBrowser
element, let user enter credentials, then grab global cookies, and pass them into new HttpClient
which performs the actual HTTP operation.
Here a complete code sample which downloads all build statuses from a SAML-protected Jenkins server:
private void Start()
{
var t = new Thread(ThreadProc);
t.SetApartmentState(ApartmentState.STA);
t.Start();
t.Join();
}
public async void ThreadProc()
{
try
{
var urlBase = "https://JENKINS/";
var url = urlBase + "job/JOBNAME/api/json?depth=1&tree=lastBuild[timestamp],builds[number,result,timestamp,url,actions[lastBuiltRevision[SHA1,branch[name]],totalCount,failCount,skipCount],building,duration]";
var form = new Form();
var browser = new System.Windows.Forms.WebBrowser();
browser.SetBounds(0, 0, 400, 400);
form.Size = new System.Drawing.Size(400, 400);
form.Controls.AddRange(new Control[] { browser });
form.FormBorderStyle = FormBorderStyle.FixedDialog;
form.StartPosition = FormStartPosition.CenterScreen;
form.MinimizeBox = false;
form.MaximizeBox = false;
// Navigate to base URL. It should internally forward to login form. After logging in, close browser window.
browser.Navigate(urlBase);
form.ShowDialog();
var cookieString = GetGlobalCookies(urlBase);
var cookieContainer = new System.Net.CookieContainer();
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = new Uri(urlBase, UriKind.Absolute) })
{
cookieContainer.SetCookies(client.BaseAddress, cookieString);
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var responseStream = await response.Content.ReadAsStreamAsync();
using (var reader = new System.IO.StreamReader(responseStream))
{
var responseString = await reader.ReadToEndAsync();
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
[System.Runtime.InteropServices.DllImport("wininet.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
private static extern bool InternetGetCookieEx(string pchURL, string pchCookieName,
System.Text.StringBuilder pchCookieData, ref uint pcchCookieData, int dwFlags, IntPtr lpReserved);
private const int INTERNET_COOKIE_HTTPONLY = 0x00002000;
public string GetGlobalCookies(string uri)
{
uint uiDataSize = 2048;
var sbCookieData = new System.Text.StringBuilder((int)uiDataSize);
if (InternetGetCookieEx(uri, null, sbCookieData, ref uiDataSize,
INTERNET_COOKIE_HTTPONLY, IntPtr.Zero)
&&
sbCookieData.Length > 0)
{
return sbCookieData.ToString().Replace(";", ",");
}
return null;
}
来源:https://stackoverflow.com/questions/22000244/pass-saml-token-into-web-api-call