I'm working on a C# VS2012 Framework 4.5 MVC application that is trying to become PCI compliant using Payflow Pro (https://pilot-payflowpro.paypal.com). We've been using PayflowPro for years, and this is what I have to use. From my reading it seems that I should use the Transparent Redirect so I'm not posting anything private to my webserver, though I don't know if I need that with how I'm hoping to handle this. I also have a few questions...
How I think this all works:
My understanding is that you need a securetoken (communication to Paypal, trip 1). Then you post the secure data (CC, exp, security code) including the securetoken (communication to Paypal, trip 2) and receive the authorization and transactionID of the sale.
How I'm hoping to do it:
I'm intending on having a form that will have all the info (user details, shipping details, and CC info), and when the user presses the purchase button, I'll use AJAX to process trip 1 to my server (no secure user info sent). Here I'll create the URL + params and send paypal my un/pw info to retrieve the token (all from my server). The response will be returned to the client and, if successful, I'll then directly communicate via AJAX to Paypal's Gateway server, this time sending the secure CC info + token (trip #2). Based on the response to trip #2, I'll let the user know what's up with their purchase. Trip 2 shouldn't need my Paypal UN/PW info as it could easily be see on the client, and I'm including the SecureToken which SHOULD identify the original transaction. From what I've explained I don't see a need for Transparent Redirect. Or am I missing something here?
Also, what Transaction Type do I want to use? Create an 'Authorization' for trip #1, then a 'Sale' for trip #2?
So here's the nitty gritty coding type stuff:
For my R&D testing I'm building my own name/value pair parameter string (see below) and communicating to the gateway server via WebRequest through their sandbox/test url (pilot-payflowpro.paypal.com). I do get a successful response and SECURETOKEN back. Initial request (shown below) for secure token is TRXTYPE = A (Authorization), no card info is sent. Do I want to authorize first?
Here are my parameters (might include shipto info as well, but it's not listed below):
USER=myAuthUserName
&VENDOR=myAuthUserName
&PARTNER=myPartner
&PWD=myPassword
&AMT=21.43
&BILLTOFIRSTNAME=FName
&BILLTOLASTNAME=LName
&BILLTOSTREET=123 Main Street
&BILLTOSTREET2=Apt 203B
&BILLTOCITY=MyCity
&BILLTOSTATE=CA
&BILLTOZIP=77777
&BILLTOPHONENUM=4444444444
&EMAIL=myemail#somedomain.com
&CURRENCY=USD
**&TRXTYPE=A**
&SILENTTRAN=TRUE
&CREATESECURETOKEN=Y
&SECURETOKENID=a99998afe2474b1b82c8214c0824df99
As I said, I get a successful response and move to the next step of sending the secure data (CC#, EXPDATE, security code). When I remove my UN/PW/VENDOR/Partner info from the params I get an error due to invalid user authentication. But, seeing I'm dynamically building this 2nd call I can't have my paypal un/pw there. What am I missing? Anyone offer assistance with this or the other questions from above?
Please let me know if I need any clarification to be added. Thanks in advance for your time!
After spending a bunch of time with a Paypal engineer I've successfully figured out a solution for the Paypal's Payflow Transparent Redirect without hosted pages (have own payment page). Again, here's the documentation which, per the engineer, is pretty confusing: Payflow API Documentation. Also, the code isn't optimized as it was just a R&D app, but as a whole, it is working for me. Just an example and explanation, and I'm sure there are better ways of doing individual steps. Hope this helps and allows you to bypass some of the roadblocks that have been slowing down your Paypal Payflow integration.
YES, it is PCI compliant in that no secure customer data will hit your own servers. Remember that PCI compliance is pretty complicated and involved but this is big part of it. Ok, so I'll explain what I did to make this work in a MVC C# environment. I'll explain the steps here, then include code below.
CLIENT: Client finishes adding items to the cart and presses BUY button. Javascript handles the button click, doesn't submit, and takes you to the next step.
CLIENT --> SERVER: AJAX function POSTS to server method to contact Paypal for the single-use secure token. This communication identifies YOU (the merchant) to paypal with your authentication, a unique transaction id (a guid), and non secure details about the transaction (total, billing info, shipping info, return URL details). This way, all your merchant personal acct info is secure (web server to Paypal).
SERVER --> CLIENT: From the transaction above you'll receive a parameter string that contains the secure token (among other stuff, see method with example). Using this piece of info, I dynamically create my url that I'll eventually need on the client for the transparent redirect part, and send the url string back to the client.
CLIENT: Using the url that was returned in step #3, I complete the URL by adding the needed credit card parameters using jQuery.
CLIENT --> PAYPAL: This is where I didn't understand what to do. While step #2 was a post, this step will be a REDIRECT. Sure, that seems appropriate seeing it's called 'transparent redirect', but that part just didn't make sense to me. So, once your entire URL is complete, you'll literally redirect the window to Paypal for processing your transaction.
PAYPAL --> SERVER: PayPal posts back to one of the URLs you included in step 2 (to a public method on one of my controllers), and I read the response object and parse the parameters.
Easy, right? Perhaps, but for me step 5 caused me big problems. I was using a POST and didn't understand why I kept getting errors on the response. It was an html page with something about an invalid merchant or authentication. Remember to redirect, not post for step #5.
CODE:
STEP 1: onclick attribute on button to call GetToken function.
STEP 2 and STEP 3:
client-side:
function GetToken() {
$.ajax({
url: '#Url.Action("GetToken", "MyController")',
type: 'POST',
cache: 'false',
contentType: 'application/json; charset=utf-8',
dataType: 'text',
success: function (data) {
// data is already formatted in parameter string
SendCCDetailsToPaypal(data);
},
//error:
//TODO Handle the BAD stuff
});}
Server Side:
I have separate methods used to build all the parameter values needed for the token request. First three build: authentication, transaction details, transparent redirect. I keep urls and payflow acct info in a web.config file. Last method, ProcessTokenTransaction, does all the heavy lifting to contact Paypal via WebRequest, and then parse it into the URL that will be sent back to the client. This method should be refactored for a cleaner delivery, but I'll leave that up to you. ParseResponse is a method that populates a simple model that I created, and returns that model.
URL for token (sandbox): https://pilot-payflowpro.paypal.com
THIS IS DIFFERENT THAN THE TOKEN URL!! Used in the PaypalTranactionAPI config value.
URL for transaction: (sandbox) https://pilot-payflowlink.paypal.com
private string PrepareApiAuthenticationParams()
{
var paypalUser = ConfigurationManager.AppSettings["PaypalUser"];
var paypalVendor = ConfigurationManager.AppSettings["PaypalVendor"];
var paypalPartner = ConfigurationManager.AppSettings["PaypalPartner"];
var paypalPw = ConfigurationManager.AppSettings["PaypalPwd"];
//var amount = (decimal)19.53;
var apiParams = #"USER=" + paypalUser
+ "&VENDOR=" + paypalVendor
+ "&PARTNER=" + paypalPartner
+ "&PWD=" + paypalPw
+ "&TENDER=C"
+ "&TRXTYPE=A"
+ "&VERBOSITY=HIGH";
// find more appropriate place for this param
//+ "&VERBOSITY=HIGH";
return apiParams;
}
private string PrepareTransactionParams(CustomerDetail detail)
{
var currencyType = "USD";
var transactionParams = #"&BILLTOFIRSTNAME=" + detail.FirstName
+ "&BILLTOLASTNAME=" + detail.LastName
+ "&BILLTOSTREET=" + detail.Address1
+ "&BILLTOSTREET2=" + detail.Address2
+ "&BILLTOCITY=" + detail.City
+ "&BILLTOSTATE=" + detail.State
//+ "&BILLTOCOUNTRY=" + detail.Country + // NEEDS 3 digit country code
+ "&BILLTOZIP=" + detail.Zip
+ "&BILLTOPHONENUM=" + detail.PhoneNum
+ "&EMAIL=" + detail.Email
+ "&CURRENCY=" + currencyType
+ "&AMT=" + GET_VALUE_FROM_DB
+ "&ERRORURL= " + HostUrl + "/Checkout/Error"
+ "&CANCELURL=" + HostUrl + "/Checkout/Cancel"
+ "&RETURNURL=" + HostUrl + "/Checkout/Success";
// ADD SHIPTO info for address validation
return transactionParams;
}
private string PrepareTransparentParams(string requestId, string transType)
{
var transparentParams = #"&TRXTYPE=" + transType +
"&SILENTTRAN=TRUE" +
"&CREATESECURETOKEN=Y" +
"&SECURETOKENID=" + requestId;
return transparentParams;
}
// Method to build parameter string, and create webrequest object
public string ProcessTokenTransaction()
{
var result = "RESULT=0"; // default failure response
var transactionType = "A";
var secureToken = string.Empty;
var requestId = Guid.NewGuid().ToString().Replace("-", string.Empty);
var baseUrl = ConfigurationManager.AppSettings["PaypalGatewayAPI"];
var apiAuthenticationParams = PrepareApiAuthenticationParams();
// Create url parameter name/value parameter string
var apiTransactionParams = PrepareTransactionParams(detail);
// PCI compliance, Create url parameter name/value parameter string specific to TRANSAPARENT PROCESSING
var transparentParams = PrepareTransparentParams(requestId, transactionType);
var url = baseUrl;
var parameters = apiAuthenticationParams + apiTransactionParams + transparentParams;
// base api url + required
var request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "text/name"; // Payflow?
request.Headers.Add("X-VPS-REQUEST-ID", requestId);
byte[] bytes = Encoding.UTF8.GetBytes(parameters);
request.ContentLength = bytes.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(bytes, 0, bytes.Length);
requestStream.Close();
WebResponse response = request.GetResponse();
Stream stream = response.GetResponseStream();
StreamReader reader = new StreamReader(stream);
try
{
// sample successful response
// RESULT=0&RESPMSG=Approved&SECURETOKEN=9pOyyUMAwRUWmmv9nMn7zhQ0h&SECURETOKENID=5e3c50a4c3d54ef8b412e358d24c8915
result = reader.ReadToEnd();
var token = ParseResponse(result, requestId, transactionType);
var transactionUrl = ConfigurationManager.AppSettings["PaypalTransactionAPI"];
secureToken = transactionUrl + "?SECURETOKEN=" + token.SecureToken + "&SECURETOKENID=" + requestId;
//ameValueCollection parsedParams = HttpUtility.ParseQueryString(result);
stream.Dispose();
reader.Dispose();
}
catch (WebException ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
finally { request.Abort(); }
return secureToken;
}
private TokenResponse ParseResponse(string response, string requestId, string transactionType)
{
var nameValues = HttpUtility.ParseQueryString(response);
int result = -999; // invalid result to guarantee failure
int.TryParse(nameValues.Get(TokenResponse.ResponseParameters.RESULT.ToString()), out result);
// retrieving response message
var responseMessage = nameValues.Get(TokenResponse.ResponseParameters.RESPMSG.ToString());
// retrieving token value, if any
var secureToken = nameValues.Get(TokenResponse.ResponseParameters.SECURETOKEN.ToString());
var reference = nameValues.Get(TokenResponse.ResponseParameters.PNREF.ToString());
var authCode = nameValues.Get(TokenResponse.ResponseParameters.AUTHCODE.ToString());
var cscMatch = nameValues.Get(TokenResponse.ResponseParameters.CSCMATCH.ToString());
// populating model with values
var tokenResponse = new TokenResponse
{
Result = result,
ResponseMessage = responseMessage,
SecureToken = secureToken,
TransactionIdentifierToken = requestId,
TransactionType = transactionType,
ReferenceCode = reference,
AuthorizationCode = authCode,
CSCMatch = cscMatch
};
return tokenResponse;
}
STEP 4 and STEP 5:
Back to Client Side:
Here I use the URL built from the previous steps and add the final needed params (secure credit card info) using jQuery and then REDIRECT to Paypal.
function SendCCDetailsToPaypal(secureParm) {
//alert('in SendCCDetailsToPaypal:' + secureParm);
var secureInfo = '&ACCT=' + $('#ccNumber').val() + '&EXPDATE=' + $("#expMonth").val() + $("#expYear").val() + "&CSC=" + $('#ccSecurityCode').val();
secureInfo = secureParm + secureInfo;
window.location.replace(secureInfo);
}
STEP 6:
Paypal will post back to one of the following methods: Cancel, Error, or Return (name the methods anything you want in the token request). Parse the Response and look at the variables returned from Paypal, particularly the RESULT and RESPMSG. Read the documentation for specifics as you can incorporate address validation and a bunch of other features. Based on the response, display what's appropriate.
server side:
public ActionResult Cancel()
{
var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));
//return View("Return", result);
}
public ActionResult Error()
{
var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));
return View("Return", result);
}
public ActionResult Return()
{
var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));
return View("Return", result);
}
Hope this helps, and good luck! I'll answer clarification questions as I'm able. Thanks for checking this out, and remember to pay it forward.
I was able to use RichieMN's answer to get a working Transparent Redirect happening. However, the problem with doing a redirect with window.location.replace in the SendCCDetailsToPaypal function is that you're passing the data on a GET string.
This works on the PayFlow Gateway side, but when they send the customer's browser back to your ResponseURL, your Apache logs will show the whole payflowlink.paypal.com URL, including the GET string as the referrer in your Apache access logs! That GET string includes the Credit Card number and now you have just lost your PCI compliance!
To alleviate this problem, you can either put the SecureToken and SecureTokenID into your Credit Card entry form, and POST it directly to payflowlink.paypal.com, or you can rewrite the SendCCDetailsToPaypal function to build a form and submit it, like this:
function SendCCDetailsToPaypal() {
var parameters = {
"SECURETOKEN": secureToken,
"SECURETOKENID": secureTokenID,
"ACCT": $("#ccNumber").val(),
"EXPDATE": $("#expMonth").val() + $("#expYear").val(),
"CSC": $("#ccSecurityCode").val()
};
var form = $('<form></form>');
form.attr("method", "post");
form.attr("action", "https://pilot-payflowlink.paypal.com");
$.each(parameters, function(key, value) {
var field = $('<input></input>');
field.attr("type", "hidden");
field.attr("name", key);
field.attr("value", value);
form.append(field);
});
$(document.body).append(form);
form.submit();
}
Since that form transfers the data via POST, when your server gets the result POST back, the referrer does not contain any sensitive data, and your PCI compliance is maintained.
Related
When using RestSharp to query account details in your MailChimp account I get a "401: unauthorized" with "API key is missing", even though it clearly isn't!
We're using the same method to create our RestClient with several different methods, and in all requests it is working flawlessly. However, when we're trying to request the account details, meaning the RestRequest URI is empty, we get this weird error and message.
Examples:
private static RestClient CreateApi3Client(string apikey)
{
var client = new RestClient("https://us2.api.mailchimp.com/3.0");
client.Authenticator = new HttpBasicAuthenticator(null, apiKey);
return client;
}
public void TestCases() {
var client = CreateApi3Client(_account.MailChimpApiKey);
var req1 = new RestRequest($"lists/{_account.MailChimpList}/webhooks", Method.GET);
var res1 = client.Execute(req1); // works perfectly
var req2 = new RestRequest($"automations/{account.MailChimpTriggerEmail}/emails", Method.GET);
var res2 = client.Execute(req2); // no problem
var req3 = new RestRequest(Method.GET);
var res3 = client.Execute(req3); // will give 401, api key missing
var req4 = new RestRequest(string.Empty, Method.GET);
var res4 = client.Execute(req4); // same here, 401
}
When trying the api call in Postman all is well. https://us2.api.mailchimp.com/3.0, GET with basic auth gives me all the account information and when debugging in c# all looks identical.
I'm trying to decide whether to point blame to a bug in either RestSharp or MailChimp API. Has anyone had a similar problem?
After several hours we finally found what was causing this..
When RestSharp is making the request to https://us2.api.mailchimp.com/3.0/ it's opting to omit the trailing '/'
(even if you specifically add this in the RestRequest, like: new RestRequest("/", Method.GET))
so the request was made to https://us2.api.mailchimp.com/3.0
This caused a serverside redirect to 'https://us2.api.mailchimp.com/3.0/' (with the trailing '/') and for some reason this redirect scrubbed away the authentication header.
So we tried making a
new RestRequest("/", Method.GET)
with some parameters (req.AddParameter("fields", "email")) to make it not scrub the trailing '/', but this to was failing.
The only way we were able to "fool" RestSharp was to write it a bit less sexy like:
new RestRequest("/?fields=email", Method.GET)
I am working on New web application which is Using Web API as Business Layer and Knock out Js as client side frame work to binding. I have a requirement like Pass the certain search criteria to Web API Controller and get the Data from DB and Create and Send the Excel/MS-Word file on the fly as a downloadable content.
I am new to both the Web API and Knock out, I am searching on the Net and get partial solution and I am looking here to get more optimal solution for this use case.
Below is my code:
Client:
function GetExcelFile() {
var $downloadForm = $("<form method='POST'>")
.attr("action", baseUrl + "api/FileHandler/GetExcelFileTest")
.attr("target", "_blank")
$("body").append($downloadForm);
$downloadForm.submit();
$downloadForm.remove();
}
On Button Click having this code snippet to create a form on the fly and Get response from Web API.
Web API Code:
[HttpPost]
public HttpResponseMessage GetExcelFileTest()
{
var response = new HttpResponseMessage();
//Create the file in Web App Physical Folder
string fileName = Guid.NewGuid().ToString() + ".xls";
string filePath = HttpContext.Current.Server.MapPath(String.Format("~/FileDownload/{0}", fileName));
StringBuilder fileContent = new StringBuilder();
//Get Data here
DataTable dt = GetData();
if (dt != null)
{
string str = string.Empty;
foreach (DataColumn dtcol in dt.Columns)
{
fileContent.Append(str + dtcol.ColumnName);
str = "\t";
}
fileContent.Append("\n");
foreach (DataRow dr in dt.Rows)
{
str = "";
for (int j = 0; j < dt.Columns.Count; j++)
{
fileContent.Append(str + Convert.ToString(dr[j]));
str = "\t";
}
fileContent.Append("\n");
}
}
// write the data into Excel file
using (StreamWriter sw = new StreamWriter(fileName.ToString(), false))
{
sw.Write(fileContent.ToString());
}
IFileProvider FileProvider = new FileProvider();
//Get the File Stream
FileStream fileStream = FileProvider.Open(filePath);
//Set response
response.Content = new StreamContent(fileStream);
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName = fileName;
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/ms-excel");
response.Content.Headers.ContentLength = fileStream.Length;
//Delete the file
//if(File.Exists(filePath))
//{
// File.Delete(filePath);
//}
return response;
}
Using this code I am able to download an Excel File. Still I have some more open questions to make this code optimal.
Q1) I need to Pass view model(Search Criteria) to API Controller Using the dynamically create form ? (OR) Any better ways to get Excel file from Web API.
Q2) I am sure it's not a good way to create Excel file in Physical folder and Get FileStream and send as a respone. How to do on the fly ? OR any other optimal ways.
Please suggest me to do better ways.. Thanks
Q1) You can quite easily pass the view-model, but it's also similarly easy to pull that information from the posted form.
Passing the view-model
If you want to pass the view-model to a WebAPI method then remember that said method must take as a parameter an object with the same properties. So if the object that you wish to post back always has the same properties then it's trivial to build a server-side class with the same properties and receive an instance of that class.
To post back this client-side object you can do something like this (uses jQuery, which I see you're already using):
$.ajax({
contentType: "application/json",
data: my-view-model.toJSON(),
type: "POST",
url: baseUrl + "api/FileHandler/GetExcelFileTest" });
I haven't attached any success or error handlers here because the JavaScript isn't concerned with the return, but you might wish to add some handlers in case an exception is thrown in your WebAPI method. I recommend doing that by adding the following to the above $.ajax() call:
statusCode: {
500: function(jqXhr, textStatus, errorThrown) {
},
[other HTTP error codes]
}
[Read the documentation for the $.ajax() call here.]
One additional tip here: when you call my-view-model.toJSON() (or self.toJSON(), if called from within your view-model) Knockout will first of all determine if your view-model contains a toJSON() method. If so, it will use this method; if not then it will call the browser's implementation of this function. However, the browser's implementation of this function will serialise everything, which can be particularly length if you have, for example, long select lists in your view-model. Therefore, if you wish only to send back a subset of the view-model's properties then define your own toJSON function on your view-model like so:
var toJSON = function() {
return {
Property1: ...,
Property2: ...
};
}
[Read more about converting a view-model to JSON here.]
Posting the form as-is
If you don't wish to expend the effort to do the view-model wiring then you can just post the form exactly like you have in your question. You can then retrieve the values from the form by using
Request.Form["my-field"];
Q2)
You're probably right in pointing out that it's not wise to create the Excel file in the physical folder. However, as far as I'm aware (interested if someone says otherwise) you'll have to use a 3rd-party library for this. Microsoft do offer an Office automation library but I have a suspicion that you also need Office to be installed at the same location.
Creating Excel spreadsheets dynamically is something I've done several times but for the actual creation I use Aspose.Cells, which requires a license. Although I do create a physical version and then delete it, I believe Aspose.Cells may allow you to create it as a stream. But take a look around, there are certainly other libraries which offer Excel automation.
Returning the File from the Server
Calling $.ajax({...}) alone won't allow you to present the user with a "Save as..." dialog. What I do in this situation - and this won't work if you wish to store the generated file only in memory (FileStream, for example) and not on the file system - is to respond to the $.ajax({...}) call with a filename for the generated file.
The next step is to direct the user towards that filename.
So I have something like this in my JavaScript:
$.ajax({
dataType: "json",
type: "GET", // you'll probably want POST in your case
url: ...,
success: function(response) {
if (response && response.Uri && response.Uri.length) {
window.location.href = [root URL] + response.Uri;
}
}
});
But don't be alarmed by this redirect. That window.location.href points directly to a folder on the server, no controller needed. Because the browser then receives a file it presents the "Save as..." dialog while remaining on the same webpage.
I want to do an integration test for the below action.
How can I pass my requestDto object in the integration test?
Neither the GetAsync nor SendAsync method has an overload parameter to pass a custom object to the server.
[Route("{startDate:datetime}")]
[HttpGet]
public HttpResponseMessage Get(DateTime startDate, [FromBody]LessonplannerGetRequest request)
{
request.StartDate = startDate;
var lessonplannerResponse = _service.GetPeriodsByWeekStartDate(request);
return Request.CreateResponse<LessonplannerResponse>(HttpStatusCode.OK, lessonplannerResponse);
}
[Test]
public void Get_Lessons_By_Date()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Get, _server.BaseAddress + "/api/lessonplanner/2014-01-14");
var myRequestDto = new LessonplannerGetRequest();
// Act => QUESTION: HOW do I pass the myRequestDto ???
var response = _client.SendAsync(request, new CancellationToken()).Result;
// Assert
Assert.That(response.StatusCode == HttpStatusCode.OK);
}
UPDATE
As Darrel Miller said:"Technically HTTP says you can send a body, it just says the body doesn't mean anything and cannot be used. HttpClient won't let you send one."
I post here my integration test with HttpClient doing a Get request with complex type + FromBody:
// Arrange
var request = new HttpRequestMessage(HttpMethod.Get, _server.BaseAddress + "/api/lessonplanner/2014-01-14");
var myRequestDto = new LessonplannerGetRequest{ FirstDayOfWeek = DayOfWeek.Sunday, SchoolyearId = 1, StartDate = DateTime.Today};
request.Content = new ObjectContent<LessonplannerGetRequest>(myRequestDto, new JsonMediaTypeFormatter());
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Act
var response = _client.SendAsync(request, new CancellationToken()).Result;
// Assert
Assert.That(response.StatusCode == HttpStatusCode.OK);
Of course is this is not the Http way some might consider doing it differentlly sending complex type via FromUri/query string.
HTML specifications says you cannot send a GET with a body.
HTTP specs allows it.
WebAPI allows it, because it is a service/REST and implements HTTP but not HTML, but many clients and browser won't allow it because they implement both specs and try to be strict.
As for the specifications (RFC1866, page 46; HTML 4.x section 17.13.3) itself, it states:
If the method is "get" and the action is an HTTP URI, the user agent takes the value of action, appends a `?' to it, then appends the form data set, encoded using the "application/x-www-form-urlencoded" content type.
(e.g. if you do a <form> with GET, it will parse all the form params and set them in the query string ?a=b).
In term of pure HTTP and in the context of REST services, nothing prevents that behavior, but not all clients will be able to handle it. It's mostly a best-practice advise when it comes to REST/WebAPI to not handle body data from HttpGet, only URI data (the opposite, POST /action?filter=all is usually tolerated for metadata/action qualifiers, but that's another discussion).
So yeah, it's at your own risk, even if used only internally. As not all clients handle it (e.g. HttpRequestMessage), so you might run into trouble like you have.
You should NOT pass a GET body with HTTPClient.
I am trying to write an activity in Google+ using the dotnet-client. The issue is that I can't seem to get the configuration of my client app correctly. According to the Google+ Sign-In configuration and this SO question we need to add the requestvisibleactions parameter. I did that but it did not work. I am using the scope https://www.googleapis.com/auth/plus.login and I even added the scope https://www.googleapis.com/auth/plus.moments.write but the insert still did not work.
This is what my request url looks like:
https://accounts.google.com/ServiceLogin?service=lso&passive=1209600&continue=https://accounts.google.com/o/oauth2/auth?scope%3Dhttps://www.googleapis.com/auth/plus.login%2Bhttps://www.googleapis.com/auth/plus.moments.write%26response_type%3Dcode%26redirect_uri%3Dhttp://localhost/%26state%3D%26requestvisibleactions%3Dhttp://schemas.google.com/AddActivity%26client_id%3D000.apps.googleusercontent.com%26request_visible_actions%3Dhttp://schemas.google.com/AddActivity%26hl%3Den%26from_login%3D1%26as%3D-1fbe06f1c6120f4d<mpl=popup&shdf=Cm4LEhF0aGlyZFBhcnR5TG9nb1VybBoADAsSFXRoaXJkUGFydHlEaXNwbGF5TmFtZRoHQ2hpa3V0bwwLEgZkb21haW4aB0NoaWt1dG8MCxIVdGhpcmRQYXJ0eURpc3BsYXlUeXBlGgdERUZBVUxUDBIDbHNvIhTeWybcoJ9pXSeN2t-k8A4SUbfhsygBMhQivAmfNSs_LkjXXZ7bPxilXgjMsQ&scc=1
As you can see from there that there is a request_visible_actions and I even added one that has no underscore in case I got the parameter wrong (requestvisibleactions).
Let me say that my app is being authenticated successfully by the API. I can get the user's profile after being authenticated and it is on the "insert moment" part that my app fails. My insert code:
var body = new Moment();
var target = new ItemScope();
target.Id = referenceId;
target.Image = image;
target.Type = "http://schemas.google.com/AddActivity";
target.Description = description;
target.Name = caption;
body.Target = target;
body.Type = "http://schemas.google.com/AddActivity";
var insert =
new MomentsResource.InsertRequest(
// this is a valid service instance as I am using this to query the user's profile
_plusService,
body,
id,
MomentsResource.Collection.Vault);
Moment result = null;
try
{
result = insert.Fetch();
}
catch (ThreadAbortException)
{
// User was not yet authenticated and is being forwarded to the authorization page.
throw;
}
catch (Google.GoogleApiRequestException requestEx)
{
// here I get a 401 Unauthorized error
}
catch (Exception ex)
{
} `
For the OAuth flow, there are two issues with your request:
request_visible_actions is what is passed to the OAuth v2 server (don't pass requestvisibleactions)
plus.moments.write is a deprecated scope, you only need to pass in plus.login
Make sure your project references the latest version of the Google+ .NET client library from here:
https://developers.google.com/resources/api-libraries/download/stable/plus/v1/csharp
I have created a project on GitHub showing a full server-side flow here:
https://github.com/gguuss/gplus_csharp_ssflow
As Brettj said, you should be using the Google+ Sign-in Button as demonstrated in the latest Google+ samples from here:
https://github.com/googleplus/gplus-quickstart-csharp
First, ensure you are requesting all of the activity types you're writing. You will know this is working because the authorization dialog will show "Make your app activity available via Google, visible to you and: [...]" below the text that starts with "This app would like to". I know you checked this but I'm 90% sure this is why you are getting the 401 error code. The following markup shows how to render the Google+ Sign-In button requesting access to Add activities.
<div id="gConnect">
<button class="g-signin"
data-scope="https://www.googleapis.com/auth/plus.login"
data-requestvisibleactions="http://schemas.google.com/AddActivity"
data-clientId="YOUR_CLIENT_ID"
data-accesstype="offline"
data-callback="onSignInCallback"
data-theme="dark"
data-cookiepolicy="single_host_origin">
</button>
Assuming you have a PlusService object with the correct activity type set in data-requestvisibleactions, the following code, which you should be able to copy/paste to see it work, concisely demonstrates writing moments using the .NET client and has been tested to work:
Moment body = new Moment();
ItemScope target = new ItemScope();
target.Id = "replacewithuniqueforaddtarget";
target.Image = "http://www.google.com/s2/static/images/GoogleyEyes.png";
target.Type = "";
target.Description = "The description for the activity";
target.Name = "An example of add activity";
body.Target = target;
body.Type = "http://schemas.google.com/AddActivity";
MomentsResource.InsertRequest insert =
new MomentsResource.InsertRequest(
_plusService,
body,
"me",
MomentsResource.Collection.Vault);
Moment wrote = insert.Fetch();
Note, I'm including Google.Apis.Plus.v1.Data for convenience.
Ah it's that simple! Maybe not? I am answering my own question and consequently accept it as the answer (after a few days of course) so others having the same issue may be guided. But I will definitely up-vote Gus' answer for it led me to the fix for my code.
So according to #class answer written above and as explained on his blog the key to successfully creating a moment is adding the request_visible_actions parameter. I did that but my request still failed and it is because I was missing an important thing. You need to add one more parameter and that is the access_type and it should be set to offline. The OAuth request, at a minimum, should look like: https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/plus.login&response_type=code&redirect_uri=http://localhost/&request_visible_actions=http://schemas.google.com/AddActivity&access_type=offline.
For the complete and correct client code you can get Gus' example here or download the entire dotnet client library including the source and sample and add what I added below. The most important thing that you should remember is modifying your AuthorizationServerDescription for the Google API. Here's my version of the authenticator:
public static OAuth2Authenticator<WebServerClient> CreateAuthenticator(
string clientId, string clientSecret)
{
if (string.IsNullOrWhiteSpace(clientId))
throw new ArgumentException("clientId cannot be empty");
if (string.IsNullOrWhiteSpace(clientSecret))
throw new ArgumentException("clientSecret cannot be empty");
var description = GoogleAuthenticationServer.Description;
var uri = description.AuthorizationEndpoint.AbsoluteUri;
// This is the one that has been documented on Gus' blog site
// and over at Google's (https://developers.google.com/+/web/signin/)
// This is not in the dotnetclient sample by the way
// and you need to understand how OAuth and DNOA works.
// I had this already, see my original post,
// I thought it will make my day.
if (uri.IndexOf("request_visible_actions") < 1)
{
var param = (uri.IndexOf('?') > 0) ? "&" : "?";
description.AuthorizationEndpoint = new Uri(
uri + param +
"request_visible_actions=http://schemas.google.com/AddActivity");
}
// This is what I have been missing!
// They forgot to tell us about this or did I just miss this somewhere?
uri = description.AuthorizationEndpoint.AbsoluteUri;
if (uri.IndexOf("offline") < 1)
{
var param = (uri.IndexOf('?') > 0) ? "&" : "?";
description.AuthorizationEndpoint =
new Uri(uri + param + "access_type=offline");
}
// Register the authenticator.
var provider = new WebServerClient(description)
{
ClientIdentifier = clientId,
ClientSecret = clientSecret,
};
var authenticator =
new OAuth2Authenticator<WebServerClient>(provider, GetAuthorization)
{ NoCaching = true };
return authenticator;
}
Without the access_type=offline my code never worked and it will never work. Now I wonder why? It would be good to have some explanation.
I've been racking my brain for a couple of days now on how to approach a new requirement.
I have two websites. The first one lets the user fill out an application. The second website is an internal website use to manage the users applications. I need to develop a "web service" that sends the application data from website 1 to website 2 and return a response to website 2 of success or failure. I have never done a web service before and I'm a bit confused on where to start. I've been reading various examples online but they all seem to be just a starting point for building a webservice... no specific examples.
So for posting the data website 1, what would my controller method look like? Do I use Json to post the data to website 2? What would and example of that look like? Is there some form of redirect in the method that points to website 2?
So for posting the response back to website 2 what would that controller method look like? I assume I would use Json again to send the response back to website 1? Is there some form of redirect in the method that points back to website 1?
I would use JSON and POST the application to the web service.
First I am assuming the application data is contained in some type of object. Use JSON.Net to serialize the object into JSON. It will look something like the following code.
var application = new Application();
string serializedApplication = JsonConvert.Serialize(application);
Second is to POST the code your endpoint(webservice, mvc action). To this you'll need to make a HTTPRequest to the endpoint. The following code is what I use to make to POST the code.
public bool Post(string url, string body)
{
//Make the post
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
var bytes = Encoding.Default.GetBytes(body);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
Stream stream = null;
try
{
request.KeepAlive = false;
request.ContentLength = bytes.Length;
request.ContentType = "application/x-www-form-urlencoded";
request.Timeout = -1;
request.Method = "POST";
stream = request.GetRequestStream();
stream.Write(bytes, 0, bytes.Length);
}
finally
{
if (stream != null)
{
stream.Flush();
stream.Close();
}
}
bool success = GetResponse(request);
return success;
}
public bool GetResponse(HttpWebRequest request)
{
bool success;
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
using (Stream responseStream = response.GetResponseStream())
{
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created)
{
throw new HttpException((int)response.StatusCode, response.StatusDescription);
}
var end = string.Empty;
using (StreamReader reader = new StreamReader(responseStream))
{
end = reader.ReadToEnd();
reader.Close();
success = JsonConvert.DeserializeObject<bool>(end);
}
response.Close();
}
}
return success;
}
So now you have can POST JSON to an endpoint and receive a response the next step is to create the endpoint. The following code will get you started on an endpoint in mvc that will receive an application and process it.
[HttpPost]
public ActionResult SubmitApplication()
{
//Retrieve the POSTed payload
string body;
using (StreamReader reader = new StreamReader(Request.InputStream))
{
body = reader.ReadToEnd();
reader.Close();
}
var application = JsonConvert.Deserialize<Application>(body);
//Save the application
bool success = SaveApplication(application);
//Send the server a response of success or failure.
return Json(success);
}
The above code is a good start. Please note, I have not tested this code.
You have obviously more than one client for the data & operations. so a service is what you are looking for.
ASP.NET MVC is a good candidate for developing RESTful services. If you (and your Manager) are ready to use beta version, Then Checkout ASP.NET-Web API.
If you want to stay with a stable product, Go for MVC3. you may need to write some custom code to return the data in XML as well as JSON to server different kind of clients. There are some tutorials out there.
So create a Service (ASP.NET MVC / WCF Service) .You may then create 2 client apps, one for the external clients and another for the Internal users. Both of this apps can call methods in the Service to Create/ Read the user accounts / or whatever operation you want to do.
To make the apps more interactive and lively , you may conside including a wonderful thing called SiganalR, which helps you to get some real time data without continuosly polling the data base/ middle tier very in every n seconds !