Using DotNetOpenAuth for NetSuite SuiteSignOn (Outbound Single Sign-on) - c#

I am trying to figure out how to use DotNetOpenAuth (DNOA) to interface to NetSuite's SuiteSignOn. I have a java example I am trying to duplicate the function of, but I am new to OAuth. Here is what I have to work with:
This is the high level of what NetSuite wants to happen:
User logs in to NetSuite, initiating a NetSuite session.
User clicks on one of the following in the NetSuite user interface:
o A subtab that provides SuiteSignOn access
o A page displaying a portlet that provides SuiteSignOn access
o A link for a Suitelet that provides SuiteSignOn access
o An action button that results in the execution of a user event script that provides SuiteSignOn access
NetSuite generates a token, and sends this token to the external application as the value for the oauth_token URL parameter. This outbound HTTP call also includes a dcand an env URL parameter. These values can be mapped to the URL to be used for NetSuite access (see Mappings of dc and env URL Parameter Values). If any data fields were previously defined as required context for the connection, NetSuite sends values for these fields at the same time.
The external application sends back to NetSuite the token, the consumer key, and its shared secret, along with other information such as the timestamp and nonce, in order to verify the user. The consumer key is a unique identifier for the application provider, generated by NetSuite when the application provider sets up a SuiteSignOn connection. The shared secret is a password defined by the application provider during this setup.
NetSuite responds to the verification, sending any user identification information that was previously defined as necessary for the connection, in XML format. This information may include standard fields like email address or name, or custom fields.
The external application sends the HTML for the landing page, and the page displays. Or, if there is a problem, an error is returned instead.
NetSuite HTTP Outbound Call (got this figured out).
When a user accesses a SuiteSignOn connection point, NetSuite issues an outbound call to start the handshake. The following is an example of this call:
GET /SSO/demoApp.php?oauth_token=01046c1211661d6c6b415040422f0daf09310e3ea4ba&dc=001&env=PRODUCTION HTTP/1.1
Host: externalsystem.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:19.0) Gecko/20100101 Firefox/19.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
External Application HTTP Verify Call (trying to prepare this with DotNetOpenAuth).
Upon receipt of the NetSuite HTTP outbound call, the external application needs to issue an HTTP verify call. The following is an example of this call:
GET /app/common/integration/ssoapplistener.nl HTTP/1.0
Host: system.netsuite.com
Authorization: OAuth oauth_token="01046c1211661d6c6b415040422f0daf09310e3ea4ba", oauth_consumer_key="3moWE2ukbW4lohz7", oauth_signature_method="PLAINTEXT", oauth_signature="foobar1%26", oauth_timestamp="1364997730", oauth_nonce="392380036"
NetSuite HTTP Verify Call Response (I can code this).
Upon receipt of the verify call from the external application, NetSuite sends a response. The following is an example of this response:
HTTP/1.1 200 OK
Date: Tue, 16 Apr 2013 13:30:41 GMT
Server: Apache/2.2.17
Set-Cookie: lastUser=1326288_79_3; expires=Tuesday, 23-Apr-2013 13:30:42 GMT; path=/
Set-Cookie: NS_VER=2013.1.0; domain=system.netsuite.com; path=/
X-Powered-By: Servlet/2.5 JSP/2.1
P3P: CP="CAO PSAa OUR BUS PUR"
Vary: User-Agent
Connection: close
Content-Type: text/html; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<outboundSso>
<entityInfo>
<ENTITYLASTNAME>Smith</ENTITYLASTNAME>
<ENTITYINTERNALID>79</ENTITYINTERNALID>
<ENTITYACCOUNT>1326288</ENTITYACCOUNT>
<ENTITYFIRSTNAME>John</ENTITYFIRSTNAME>
<ENTITYEMAIL>jsmith#netsuite.com</ENTITYEMAIL>
</entityInfo>
</outboundSso>
The excerpts of a Java example using OAuth 1.0a that I'm trying to port to .net/DotNetOpenAuth:
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;
import net.oauth.client.OAuthClient;
import net.oauth.http.HttpMessage;
<<snip>>
OAuthConsumer consumer = new OAuthConsumer(null, CONSUMER_KEY, SHARED_SECRET, null);
consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, "PLAINTEXT");
OAuthAccessor oauthAccessor = new OAuthAccessor(consumer);
//Get the token from NetSuite
oauthAccessor.accessToken = request.getParameter("oauth_token");
<<snip>>
OAuthMessage rqt = null;
rqt = oauthAccessor.newRequestMessage("POST", ssoVerifyUrl, null);
HttpMessage message =
rqt.toHttpRequest(OAuthClient.ParameterStyle.AUTHORIZATION_HEADER);
verifyConnection.setRequestProperty("Authorization",
message.getHeader("Authorization"));
Being new to OAuth and DotNetOpenAuth, I'm fumbling around.
What is the proper replacement for OAuthConsumer in DNOA in this situation? WebConsumer? DesktopConsumer?
Assuming I need such a consumer, how much of the ServiceProviderDescription do I need to provide? I only have one endpoint (/app/common/integration/ssoapplistener.nl), I'm not sure if that is a Request, Access, or other type of endpoint.
What is the proper replacement for OAuthAccessor in DNOA?
Thanks for any assistance,
Bo.

Ok, after a lot of digging and experimenting, I got DotNetOpenAuth to work with NetSuite's SuiteSignOn. It may not be perfect, but it does work!
I got my tokenmanager from this post:
https://developer.yahoo.com/forum/Fantasy-Sports-API/Authenticating-with-NET-using-DotNetOpenAuth/1279209867000-4eee22f1-25fd-3589-9115-1a835add3212
using DotNetOpenAuth.OAuth;
using DotNetOpenAuth.OAuth.ChannelElements;
using DotNetOpenAuth.OAuth.Messages;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Extensions.OAuth;
// In my Page_Load method, I receive the GET request from NetSuite:
public partial class sso_page : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
// This is what the NetSuite SuiteSignOn ConnectionPoint sends:
// GET /administratorportal/SSO/sso_page.aspx?oauth_token=08046c1c166a7a6c47471857502d364b0d59415418156f15db22f76dcfe648&dc=001&env=SANDBOX
// see the NetSuite SuiteSignOn doc about dc & env processing to build endpoints
ServiceProviderDescription provider = GetServiceDescription();
// Set up OAuth with our keys and stuff
string token = Request.Params["oauth_token"];
string consumerKey = "yourconsumerkey"; // this has to match what is defined on our NetSuite account - ConnectionPoint to CRMLink
string sharedSecret = "yoursharedsecret"; // this has to match what is defined on our NetSuite account - ConnectionPoint to CRMLink - Careful - NO funny chars like '!'
// I got this InMemoryTokenManager from another DotNetOpenAuth post in SO
InMemoryTokenManager _tokenManager = new InMemoryTokenManager(consumerKey, sharedSecret);
AuthorizationApprovedResponse authApprovedResponse = new AuthorizationApprovedResponse();
authApprovedResponse.RequestToken = token;
_tokenManager.StoreOpenIdAuthorizedRequestToken(consumerKey, authApprovedResponse);
WebConsumer consumer = new WebConsumer(provider, _tokenManager);
// this is the SSO address in netsuite to use. Should be production or sandbox, based on the values of dc and env
string uri = "https://system.sandbox.netsuite.com/app/common/integration/ssoapplistener.nl";
MessageReceivingEndpoint endpoint = new MessageReceivingEndpoint(uri, methods);
WebRequest verifyRequest = consumer.PrepareAuthorizedRequest(endpoint, token );
HttpWebResponse responseData = verifyRequest.GetResponse() as HttpWebResponse;
XDocument responseXml;
responseXml = XDocument.Load(responseData.GetResponseStream());
// process the SSO values that come back from NetSuite in the XML They should look something
// like the following:
/* XML response should look like this:
<?xml version="1.0" encoding="UTF-8"?>
<outboundSso>
<entityInfo>
<ENTITYINTERNALID>987654</ENTITYINTERNALID>
<ENTITYNAME>Fred</ENTITYNAME>
<ENTITYEMAIL>fred#yourcompany.com</ENTITYEMAIL>
</entityInfo>
</outboundSso>
*/
// If that data looks good, you can mark the user as logged in, and redirect to whatever
// page (like SSOLandingPage.aspx) you want, which will be shown inside a frame on the NetSuite page.
Response.Redirect("~/SSOLandingPage.aspx", false);
// If that data looks bad, invalid user/login? Then you could respond with an error or redirect to a login.aspx page or something.
There is some other error handling and different returns depending on what happens, but the above is the basics of receiving an SSO login from NetSuite SuiteSignOn.
This was a hardcoded ServiceProviderDescription I used. You need to read the NetSuite SuiteSignOn doc to understand how to dynamically build these endpoints based on values of dc and env, I did not do that here yet.
// I'm not completely sure why I need all these endpoints below, and since I provide an endpoint as such:
// MessageReceivingEndpoint endpoint = new MessageReceivingEndpoint(uri, methods );
// these don't seem like I need them. But I need a ServiceProviderDescription to create a consumer, so...
private ServiceProviderDescription GetServiceDescription()
{
return new ServiceProviderDescription
{
AccessTokenEndpoint = new MessageReceivingEndpoint("https://system.sandbox.netsuite.com/app/common/integration/ssoapplistener.nl", HttpDeliveryMethods.GetRequest),
RequestTokenEndpoint = new MessageReceivingEndpoint("https://system.sandbox.netsuite.com/app/common/integration/ssoapplistener.nl", HttpDeliveryMethods.GetRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://system.sandbox.netsuite.com/app/common/integration/ssoapplistener.nl", HttpDeliveryMethods.GetRequest),
ProtocolVersion = ProtocolVersion.V10a,
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new PlaintextSigningBindingElement() }
};
}

Related

ServiceStack Client Put request and query parameters

I'm using the latest ServiceStack client lib in a .Net project and I'm having some issue making a PUT request.
Especially it doesn't seem to take into account the type of parameter defined in the RequestDto object and put all params in the body (despite the param being defined as type ="query").
My Request object (auto-generated) looks like this:
[Route("/test/name", "PUT")]
public partial class PutTestName
: IReturn<PutTestNameResponse>
{
///<summary>
///the user id
///</summary>
[ApiMember(Description = "the user id", ParameterType = "query")]
public virtual string UserId { get; set; }
///<summary>
///the name
///</summary>
[ApiMember(Description = "the name", ParameterType = "query")]
public virtual string Name { get; set; }
}
I make the call like this:
_apiClient.Put(new PutTestName(){UserId ="xyz..", Name="Bob"});
and I get "Resource not found" exception in return.
When I run the query manually using Postman (and putting both parameters in the Querystring) it works ok.
When debugging the C# client with fiddler I can see that no parameter is set to the query string and they are both passed in the body.
Edit: This what the Fiddler Request Raw looks like:
PUT https://xxxx/test/name HTTP/1.1
User-Agent: ServiceStack .NET Client 4.56
Accept-Encoding: gzip,deflate
Ocp-Apim-Subscription-Key: 783....
Content-Encoding: gzip
Accept: application/json
Content-Type: application/json
Host: xxx.net
Content-Length: 115
Expect: 100-continue
Connection: Keep-Alive
{"UserId":"xxx","Name":"Bob"}
There is Azure API Management between the ServiceStack API and my call but I don't think this is the issue. The client code is setting the parameters in the body while they're supposed to be in the query.
If the same request works with POST then it's likely that WebDav is enabled and interfering with your PUT Request in which case you should disable WebDav so the request can reach ServiceStack unimpeded.
For debugging HTTP Interoperability issues like this, you should inspect (and provide here) the Raw HTTP Response Headers using a tool like Fiddler, Chrome Web Inspector or WireShark. If the HTTP Response Headers doesn't include an X-Powered-By: ServiceStack.. Header than it's likely the request has been intercepted and blocked before it reaches ServiceStack, e.g. IIS/ASP.NET or a forwarding proxy.
The client code is setting the parameters in the body while they're
supposed to be in the query.
ServiceStack only sends parameters in the body for HTTP Verbs that don't have a request body like GET or DELETE, for Verbs with Request bodies, e.g. POST or PUT ServiceStack's JsonServiceClient will POST JSON as expected.
ServiceStack Services will accept parameters posted in either QueryString, JSON Request Body or x-www-form-urlencoded Content-Type. If you're not calling a ServiceStack Service you should be using a generic HTTP Client like HTTP Utils which will allow you to control exactly how the Request is sent, e.g:
var response = absoluteUrl.ToPutUrl(new PutTestName {...});
Will send the results as x-www-form-urlencoded.
ServiceStack's .NET Service Clients are only for sending requests to ServiceStack Services.

Windows 10 MessageWebsocket adding cookie

I'm writing a Windows 10 app in which I'm trying to connect a websocket to the IRCCloud api.
The IRCCloud api requires the following request headers:
GET / HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
User-Agent: [REDACTED]
Cookie: session=[REDACTED]
Sec-WebSocket-Key: [REDACTED]==
Host: api.irccloud.com
Origin: https://api.irccloud.com
Sec-WebSocket-Version: 13
I'm using the following code to connect the websocket to the api:
MessageWebSocket Socket = new MessageWebSocket();
Socket.SetRequestHeader("Origin", "https://api.irccloud.com");
Socket.SetRequestHeader("Cookie", "session=" + info.Session);
Socket.MessageReceived += Socket_MessageReceived;
await Socket.ConnectAsync(new Uri("wss://" + Host + Path));
However, when I connect I get an authentication error due to the Websocket adding a cookie all by itself with the name __cfduid. IRCClouds API is very specific in that the session cookie should be the ONLY cookie that's in the request header. The session key is retrieved with a Windows.Web.Http.HttpClient.
I verified all my other parameters with the IRCCloud devs, who are saying they are correct, and they also pointed me to the cookie. So my question is, how do I remove the cookie? There's no public method for it in the MessageWebSocket class that seems to work for it.
The WebSockets API uses the same underlying stack as Windows.Web.Http namespace, so, you will need to remove the cookies manually using something like this:
var filter = new HttpBaseProtocolFilter();
var cookieManager = filter.CookieManager;
var uri = new Uri("http://example.com/foo/bar");
foreach (var cookie in cookieManager.GetCookies(uri))
{
cookieManager.DeleteCookie(cookie);
}

(partial) HTTP Request isn't recognized by C# HTTP Server

In my visual studio 13 console application, I receive following byte stream on a TCP-Socket(receiving from a embedded device):
POST /setup HTTP/1.1
Content-Length: 6
Content-Type: application/setup+tlv8
TLV8-data
Although it seem to be a valid http request, none of my following attempts have successful been recognize it as a http request: (on regular HTTP Requests they work perfectly)
.NET HttpListener class (does not even inform me that any request has been invoked)
Grapevine (same thing, with any routes given on POST or GET) https://github.com/scottoffen/Grapevine
Alchemy (OnConnect method has been invoked, but in the according UserContext i was just seeing request path: / . Similar to this reported issue: https://github.com/Olivine-Labs/Alchemy-Websockets/issues/70
So far, I'm actually only interested in the requested path from the POST or GET as well as the attached content (tlv formatted) from the body.
Am I wrong in configuring? such as: I need to tell the proper content-type ?
Is there any way to get rid of writing a own simple text parser ?
Code sample in case of grapevine:
private void init()
{
s = new PairServer();
s.Host = "172.28.22.78";
s.Port = "52025";
s.Start();
}
providing following server class:
public class PairServer : RestServer
{
[RestRoute(Method = HttpMethod.POST, PathInfo = #"^/setup")]
[RestRoute(Method = HttpMethod.GET, PathInfo = #"^/setup")]
public void PairSetup(HttpListenerContext context)
{
// will not reach here
}
[RestRoute(Method = HttpMethod.POST)]
public void AnyRoute(HttpListenerContext context)
{
// Not even here
}
Although it seem to be a valid http request
No, that's not a valid HTTP request. A valid HTTP request, as the specification states, must include a Host request header:
A client MUST include a Host header field in all HTTP/1.1 request
messages . If the requested URI does not include an Internet host name
for the service being requested, then the Host header field MUST be
given with an empty value. An HTTP/1.1 proxy MUST ensure that any
request message it forwards does contain an appropriate Host header
field that identifies the service being requested by the proxy. All
Internet-based HTTP/1.1 servers MUST respond with a 400 (Bad Request)
status code to any HTTP/1.1 request message which lacks a Host header
field.
So make sure that your client is following the specification:
POST /setup HTTP/1.1
Host: example.com
Content-Length: 6
Content-Type: application/setup+tlv8
TLV8-data

JsonServiceClient not using cookie placed in cookiecontainer

I've posted a couple of questions previously to get where I am now:
Reconnecting to Servicestack session in an asp.net MVC4 application
and
nullreference exception when adding session cookie to ServiceStack
Quick background on the app: This is an asp.net MVC application accessing data on a remote servicestack installation.
At this point I am successfully authenticating with SS, saving the session key in a cookie, and inserting that cookie into the CookieContainer of a new JsonServiceClient instance.
However when I try to grab some data via the new JsonServiceClient instance:
CallList = client.Get(new ServiceCallRequest()).Result;
The remote ServiceStack instance seems to be redirecting me to the default ASP login area (/Auth/login or something similar). This redirect in itself isn't a problem, but it does seem to indicate that the client isn't using the SS session already established on the remote machine.
This is the code that is actually inserting the cookie into the client cookie container and calling for a list of objects:
public List<ServiceCallModel> LoadList()
{
try
{
var cookie = HttpContext.Request.Cookies.Get(SessionFeature.PermanentSessionId);
var client = new JsonServiceClient([api address]);
cookie.Domain = ".domain.com";
var cookie1 = new Cookie(SessionFeature.PermanentSessionId, cookie.Value);
cookie1.Domain = ".domain.com";
client.CookieContainer.Add(cookie1);
List<ServiceCallModel> CallList = new List<ServiceCallModel>();
CallList = client.Get(new ServiceCallRequest()).Result;
return CallList;
}
catch (Exception ex)
{
return new List<ServiceCallModel>();
}
}
I can verify that this remote resource works with a monotouch Android application using the C# client. The only difference of course is that the Android client is persistent; the one in question here isn't.
The above example always returns a WebServiceException ("Not Found") (Which I assume is actually a 401/403 that has been redirected annoyingly by ASP).
Does this seem reasonable, or am I missing/misunderstanding some functionality of the JsonServiceClient/ServiceStack?
Thanks much
Update
Using Fiddler, I can confirm that the cookie is being stored in the browser and sent back to the MVC application in the request header:
GET / HTTP/1.1
Host: [web app address]
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: ASP.NET_SessionId=3457C511FECBECCD3C055C21;.MONOAUTH=PzE6iBuLIbv1evgACfpUwpC1D7opCANroPDQN/mXHNvGAgkjqq04Tdd8EnnGTL7y3lWYWY4+GWaXGDT0Fm7+eJxRdpy LJMaUQ6BiYmb3dxRi1B3f/qkmPMbFIoC7vC9M; ss-pid=lzJ+o9vG/7F3YZ9JNN2F
At this point I am trying to find out of that same ss-pid value is then making it back to the API server in a request header.
Update 2
Using tcpdump I was able to see that the ss-pid value is in fact making it all the way back to the API server (The "remote servicestack instance"). So now I think I need to troubleshoot that, not the client. Any thoughts?
Snippet of tcpdump output:
0x0090: 6e65 740d 0a43 6f6f 6b69 653a 2073 732d net..Cookie:.ss-
0x00a0: 7069 643d 6c7a 4a2b 6f39 7647 2f37 4633 pid=lzJ+o9vG/7F3
0x00b0: 595a 394a 4e4e 3246 0d0a 4163 6365 7074 YZ9JNN2F..Accept
I know that the ss-pid values are different in each part of this post. They were obtained at different times
Update 3
I've also changed the LogFormat in the vhost config file to spit out the value for the cookie called "ss-pid" (At the end of the log entry).
The resulting logs on the ServiceStack remote API server looks like this:
172.16.0.17 - - [08/Oct/2013:12:26:52 -0400] "GET /sc/0 HTTP/1.1" 500 3082 "-" "-" "HFMtFpPQkpE0Br6/fEFg"
172.16.0.17 - - [08/Oct/2013:12:27:06 -0400] "GET /sc/0 HTTP/1.1" 302 394 "-" "-" "HFMtFpPQkpE0Br6/fEFg"
172.16.0.17 - - [08/Oct/2013:12:27:07 -0400] "GET /login.aspx?ReturnUrl=%2fsc%2f0 HTTP/1.1" 404 451 "-" "-" "HFMtFpPQkpE0Br6/fEFg"
This "500" status on the first request sticks out. I will be investigating this now.
Update 4
The 500 status seems to be a case of Microsoft.Web.Infrastructure.dll being included in the bin directory. Deleted that to resolve the 500 response, but this did not fix the overall problem.
ServiceStack places two Session cookies in the Request, 'ss-id' and 'ss-pid'. This line in your code...
var cookie = HttpContext.Request.Cookies.Get(SessionFeature.PermanentSessionId);
will grab the 'ss-pid' cookie. But, you probably want the grab the 'ss-id' cookie which is the cookie for your current authenticated session.
var cookie = HttpContext.Request.Cookies.Get(SessionFeature.SessionId);
Take a look here for more information on ServiceStack Sessions.

How to simulate browser HTTP POST request and capture result in C#

Lets say we have a web page with a search input form, which submits data to server via HTTP GET. So that's mean server receive search data through query strings. User can see the URL and can also initialize this request by himself (via URL + Query strings).
We all know that. Here is the question.
What if this web page submits data to the server via HTTP POST? How can user initialize this request by himself?
Well I know how to capture HTTP POST (that's why network sniffers are for), but how can I simulate this HTTP POST request by myself in a C# code?
You could take a look at the WebClient class. It allows you to post data to an arbitrary url:
using (var client = new WebClient())
{
var dataToPost = Encoding.Default.GetBytes("param1=value1&param2=value2");
var result = client.UploadData("http://example.com", "POST", dataToPost);
// do something with the result
}
Will generate the following request:
POST / HTTP/1.1
Host: example.com
Content-Length: 27
Expect: 100-continue
Connection: Keep-Alive
param1=value1&param2=value2

Categories