Send custom messages to Azure Service Bus from MS CRM Plugins (Sandbox) - c#

I know I can register a new "Service Endpoint" in MS CRM and use that to send messages to Azure Service Bus, but this... isn't really what I'm looking for. The above method ends up sending a serialized RemoteExecutionContext.
In my case, I want to have full control over what the Service Bus messages will contain. This means serializing my own classes.
I've tried using the WindowsAzure.ServiceBus nugget (and ILmerging the new DLL) but this only works in a non-sandboxed setting (on-premise CRM), but I'd also like for my solution to work in CRM Online. When attempting to use the same code in CRM Online then attempting to create a TopicClient throws an error:
System.Security.SecurityException: That assembly does not allow partially trusted callers
Is there any way around the above problem?

Here is a way to do this without additional classes and without using ILMerge. I verified this works in CRM online version 9.1.0.646 (Dynamics 365 Customer Engagement)
Your plugin project will need the following references and using statements.
References: System.Net, System.Net.Http
Usings: System.Net, System.Net.Http, System.Security.Cryptography, System.Globalization
In this sample code, I have built up a string variable named json that is the JSON message I want to post. Then the following code will send the message.
string asbUri =
"https://<azurenamespace>.servicebus.windows.net/<topicname>/messages";
TimeSpan ts = new TimeSpan(0, 0, 90);
string sasToken = GetSASToken("sb://<azurenamespace>.servicebus.windows.net", "
<nameofSASkey>", "<SASKeyValue>", ts);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", sasToken);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, asbUri)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
HttpResponseMessage response = client.SendAsync(request).Result;
private static string GetExpiry(TimeSpan ttl)
{
TimeSpan expirySinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1) + ttl;
return Convert.ToString((int)expirySinceEpoch.TotalSeconds);
}
public static string GetSASToken(string resourceUri, string keyName, string key,
TimeSpan ttl)
{
var expiry = GetExpiry(ttl);
//string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
//NOTE: UrlEncode is not supported in CRM, use System.Uri.EscapeDataString instead
string stringToSign = Uri.EscapeDataString(resourceUri).ToLowerInvariant() + "\n"
+ expiry;
HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
var signature =
Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature
sr={0}&sig={1}&se={2}&skn={3}",
Uri.EscapeDataString(resourceUri).ToLowerInvariant(),
Uri.EscapeDataString(signature), expiry, keyName);
return sasToken;
}

I found a way that's compatible with Sandboxed MS CRM.
The idea is to use the Azure REST endpoint to send messages to. It's fairly easy to authenticate and use... at least if you have a working example, which I was able to find here.
It's a fairly decent sample, albeit a bit messy. Still, it shows how to get the basics working, which is authentication and the actual calls.
(Minor note: reading topic messages from ASB based on the sample was not working reliably for me - it would work once and then would not work until the auth key timed out... this didn't bother me as I needed to send messages only, but if this is a functionality you need then this might be not as straightforward.)

I know this is an old question but I had to do something similar recently and I used the SharedVariable Collection to pass additional details and parameter to the ServiceBus.
This is an example:
context.SharedVariables.Add("AttachmentType", attachmentType);

For CRM Online you could take the logic of message conversion/processing outside of sandbox. It will require to have some external compute. Considering you're already using CRM online, that shouldn't be an issue.
An approach you could take is to convert CRM constructed RemoteExecutionContext to whatever type you want. There's a sample of how to integrate Dynamics 365 with NServiceBus, which take this approach as well. The compute I was referring to would be the equivalent of the CRMAdapterEndpoint endpoint from the sample. The endpoint is using a Mapper object to convert JSON serialized RemoteExecutionContext to custom types, ContactCreate and ContactUpdate. That would allow you to achieve what you want.

Related

C#: Download Release Asset from Github

I want to download release asset zipball´s in a C# application for further use.
I´m using Octokit to get all release informations from the repo, including the respective browserdownload_url.
After some research it seemed to me, that you cant download this release asset zip´s via octokit, so trying with httpclient as suggested by some SO posts, that were asking these questions.
The release zip´s are on a Github Enterprise Repository, so they require Authentication.
And that is probably my issue, i cant make the authentication work with the httpClient...
The request always responds with Code 404
(which is the regular behaviour if you try by putting the url into the regular browser without logging in)
My actual implementation looks like this
public void DownloadRelease(string dlUrl, string targetPath)
{
var githubToken = "aaaaaaaaaaabbbbbbbbbcccccccccdddddddddd"; //Token created in the github developer settings with all available rights
//dlUrl = https://github.server.de/organization/project/releases/download/v1.2.34/release.zip
using (var client = new System.Net.Http.HttpClient())
{
var credentials = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}:", githubToken);
credentials = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(credentials));
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
//client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", githubToken);
var contents = client.GetByteArrayAsync(dlUrl).Result;
System.IO.File.WriteAllBytes(targetPath, contents);
}
}
Update:
At the End we followed the way of using the curl way:
https://docs.github.com/en/enterprise-server#3.0/rest/reference/repos#download-a-repository-archive-zip
And one more mistake on my end: There were releases without downloadable Asset IDs which i didnt catch in the code.
Based on the documentation (https://docs.github.com/en/enterprise-server#2.22/rest/overview/other-authentication-methods#via-oauth-and-personal-access-tokens) my best guess is, that your crendentials are wrong.
The docs say, the format should be username:token, yet you are only using token followed by a colon : - that doesn't look right to me, either.
So essentially you need to refactor your credentials variable a bit:
var credentials = $"{username}:{githubToken}";

Convert a Kerberos based WindowsIdentity into a Base64 string

If I have the following code:
var token = System.Security.Principal.WindowsIdentity.GetCurrent();
I get a WindowsIdentity that is Kerberos based. I need to make a call with a header like this:
Authorize: Negotiate <Kerberos Token Here>
Is there a way to convert that token object into a Base64 string?
As the maintainer of the Kerberos.NET as mentioned in the other answer, you can always go that route. However, if you want SSO using the currently signed in Windows identity you have to go through Windows' SSPI stack.
Many .NET clients already support this natively using Windows Integrated auth, its just a matter of finding the correct knobs. It's unclear what client you're using so I can't offer any suggestions beyond that.
However if this is a custom client you have to call into SSPI directly. There's a handful of really good answers for explaining how to do that such as: Client-server authentication - using SSPI?.
The aforementioned Kerberos.NET library does have a small wrapper around SSPI: https://github.com/dotnet/Kerberos.NET/blob/develop/Kerberos.NET/Win32/SspiContext.cs
It's pretty trivial:
using (var context = new SspiContext($"host/yourremoteserver.com", "Negotiate"))
{
var tokenBytes = context.RequestToken();
var header = "Negotiate " + Convert.ToBase64String(tokenBytes);
...
}
I could not get this to work, but I was able to get a token using the excellent Kerberos.NET NuGet package. With that I was able to get it like this:
var client = new KerberosClient();
var kerbCred = new KerberosPasswordCredential("UserName", "p#ssword", "domain");
await client.Authenticate(kerbCred);
Console.WriteLine(client.UserPrincipalName);
var ticket = await client.GetServiceTicket("http/ServerThatWantsTheKerberosTicket.domain.net");
return Convert.ToBase64String(ticket.EncodeGssApi().ToArray());
As an aside, I needed help figuring out what the SPN value was for the GetServiceTicket and the project maintainer was fantastically helpful (and fast!).

LuisV3.PredictionOptions doesn't let me timezone - how to do it?

I figured I'd upgrade my LuisRecognizer to use LuisRecognizerOptionsV3. However I can't seem to set prediction options the way I like - how do I set the timezone? The v3 prediction options lack this field.
In my bot I am currently doing:
var predictionOptions = new LuisPredictionOptions();
predictionOptions.TimezoneOffset = turnContext.Activity.LocalTimestamp.Value.Offset.TotalMinutes;
and I can't figure out the equivalent in v3's version of the data structure.
The timezoneOffset parameter was mostly provided as a way to determine what day it is for the user in case they say something like "today" or "tomorrow." It also helps when the user enters a relative time like "in three hours." When using the timezoneOffset parameter, the returned entity is in the provided timezone rather than universal time.
In LUIS v3, instead of providing an offset you provide a DateTime reference and LUIS uses that to process relative time. You can see that documented here. Note that the datetimeReference property is only available in POST requests and not GET requests because you provide it in the request body and not as a query parameter.
Also note that the datetimeReference property is not currently available in the Bot Builder SDK. You can write your own code to access the LUIS API directly with an HttpClient, but if you'd still like a prebuilt SDK to handle things then you can use this NuGet package: Microsoft.Azure.CognitiveServices.Language.LUIS.Runtime 3.0.0
Here's an example of how to use it:
var appId = new Guid("<LUIS APP ID>");
var client = new LUISRuntimeClient(new ApiKeyServiceClientCredentials("<SERVICE KEY>"));
client.Endpoint = "https://westus2.api.cognitive.microsoft.com";
var options = new PredictionRequestOptions(activity.LocalTimestamp.Value.DateTime);
var request = new PredictionRequest("Book a flight in three hours", options);
var response = await client.Prediction.GetSlotPredictionAsync(appId, "PRODUCTION", request);
Console.WriteLine(JsonConvert.SerializeObject(response.Prediction.Entities, Formatting.Indented));

VersionOne: Getting message that VersionOneAPIConnector is obsolete and I should use V1Connector instead. But how?

I am trying to upgrade my c# programs to VersionOne.SDK.API version 15.3.0.0. I am getting a message that VersionOneAPIConnector is obsolete and that I should use V1Connector instead. When the ObjectModel API was discontinued, I had changed to doing everything with VersionOneAPIConnector using query.v1 (using HttpPost) and rest-1.v1/Data(using SendData). After getting the new version, I figured out how to use V1Connector and pass it to Services and then to use ExecutePassThroughQuery() to pass what had been the 2nd argument to HttpPost for my query.v1 code like this:
Original code:
Stream resultStream = _cx.HttpPost("/query.v1", System.Text.Encoding.UTF8.GetBytes(GetQueryForMembers()));
using (StreamReader reader = new StreamReader(resultStream, Encoding.UTF8))
{
result = reader.ReadToEnd();
}
New Code:
result = _service.ExecutePassThroughQuery(GetQueryForMembers());
Where GetQueryforMembers() is:
public static string GetQueryForMembers()
{
return #"
from: Member
select:
- Email
where:
AssetState: Active";
}
But, I can't figure out how to use V1Connector/IServices for my rest/SendData code. All the Rest examples on the VersionOne site now show using it from the API Console, not from stand-alone code. It looks like at least some of the things I was doing with Rest can be done via Services.executeOperation() or Services.Meta, but the V1 employee that I was working with to get rid of my Object Model code a few years ago had recommended only using the API for the connection and to otherwise use Rest and Query, so that I didn't have to worry about compatibility with .NET versions. But, from looking at the examples currently provided and the functionality available on the V1Connector/IServices classes, it looks like maybe V1 wants us to use Meta API rather than REST now? Or is there a way to use REST directly that I am missing? From looking at the source, I see there is an internal UseDataAPI() on the V1Connector that IServices uses. But, it only uses it in Save, ExecuteOperation and Retrieve and it doesn't look like any of those do what I'm trying to do.
This is an example, if what I was doing before with REST (where _passed_cx is a VersionOneAPIConnector)
string newDefect = string.Concat("<Asset href=\"/MentorG01/rest-1.v1/New/Defect\">",
"<Attribute name=\"Name\" act=\"set\">", cdata_attribute_value, "</Attribute>",
"<Relation name=\"Scope\" act=\"set\">",
"<Asset href=\"/MentorG01/rest-1.v1/Data/Scope/", project_oid.Split(':')[1], "\" idref=\"", project_oid, "\" />",
"</Relation>",
"</Asset>");
string url = "rest-1.v1/Data/Defect";
Stream resultStream = _passed_cx.SendData(url, newDefect);
Anyone know how to use rest-1.v1 with V1Connector? If so, can you provide an example?

Use google credentials to login into UWP C# app

I'm trying to make a login for a UWP app that I'm developing for a client that has a #<theircompay>.com email that uses G Suite. It doesn't have to access any user data, they just want it as an authentication so that only people that have a company email can access the app.
It would be great if they could login from within the app without having to use a web browser, and even better if it could remember them so they wouldn't have to login every single time.
I've been looking at OAuth 2.0 and several other solutions google has but can't really understand which one to use and much less how.
I looked into this answer but it doesn't seem like a good idea to ship your certificate file with your app.
So basically if this can be done, what (if any) certificates or credentials do I need to get from Google, and how would I handle them and the login through my C# code?
Edit
The app is 100% client side, no server backend
Taking a look at Google's GitHub it seems that .Net API is still not ready for UWP (however if you traverse the issues you will find that they are working on it, so it's probably a matter of time when official version is ready and this answer would be obsolete).
As I think getting simple accessToken (optionaly refresing it) to basic profile info should be sufficient for this case. Basing on available samples from Google I've build a small project (source at GitHub), that can help you.
So first of all you have to define your app at Google's developer console and obtain ClientID and ClientSecret. Once you have this you can get to coding. To obtain accessToken I will use a WebAuthenticationBroker:
string authString = "https://accounts.google.com/o/oauth2/auth?client_id=" + ClientID;
authString += "&scope=profile";
authString += $"&redirect_uri={RedirectURI}";
authString += $"&state={state}";
authString += $"&code_challenge={code_challenge}";
authString += $"&code_challenge_method={code_challenge_method}";
authString += "&response_type=code";
var receivedData = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.UseTitle, new Uri(authString), new Uri(ApprovalEndpoint));
switch (receivedData.ResponseStatus)
{
case WebAuthenticationStatus.Success:
await GetAccessToken(receivedData.ResponseData.Substring(receivedData.ResponseData.IndexOf(' ') + 1), state, code_verifier);
return true;
case WebAuthenticationStatus.ErrorHttp:
Debug.WriteLine($"HTTP error: {receivedData.ResponseErrorDetail}");
return false;
case WebAuthenticationStatus.UserCancel:
default:
return false;
}
If everything goes all right and user puts correct credentials, you will have to ask Google for tokens (I assume that you only want the user to put credentials once). For this purpose you have the method GetAccessToken:
// Parses URI params into a dictionary - ref: http://stackoverflow.com/a/11957114/72176
Dictionary<string, string> queryStringParams = data.Split('&').ToDictionary(c => c.Split('=')[0], c => Uri.UnescapeDataString(c.Split('=')[1]));
StringContent content = new StringContent($"code={queryStringParams["code"]}&client_secret={ClientSecret}&redirect_uri={Uri.EscapeDataString(RedirectURI)}&client_id={ClientID}&code_verifier={codeVerifier}&grant_type=authorization_code",
Encoding.UTF8, "application/x-www-form-urlencoded");
HttpResponseMessage response = await httpClient.PostAsync(TokenEndpoint, content);
string responseString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
Debug.WriteLine("Authorization code exchange failed.");
return;
}
JsonObject tokens = JsonObject.Parse(responseString);
accessToken = tokens.GetNamedString("access_token");
foreach (var item in vault.RetrieveAll().Where((x) => x.Resource == TokenTypes.AccessToken.ToString() || x.Resource == TokenTypes.RefreshToken.ToString())) vault.Remove(item);
vault.Add(new PasswordCredential(TokenTypes.AccessToken.ToString(), "MyApp", accessToken));
vault.Add(new PasswordCredential(TokenTypes.RefreshToken.ToString(), "MyApp", tokens.GetNamedString("refresh_token")));
TokenLastAccess = DateTimeOffset.UtcNow;
Once you have the tokens (I'm saving them in PasswordVault for safety), you can later then use them to authenticate without asking the user for his credentials. Note that accessToken has limited lifetime, therefore you use refreshToken to obtain a new one:
if (DateTimeOffset.UtcNow < TokenLastAccess.AddSeconds(3600))
{
// is authorized - no need to Sign In
return true;
}
else
{
string token = GetTokenFromVault(TokenTypes.RefreshToken);
if (!string.IsNullOrWhiteSpace(token))
{
StringContent content = new StringContent($"client_secret={ClientSecret}&refresh_token={token}&client_id={ClientID}&grant_type=refresh_token",
Encoding.UTF8, "application/x-www-form-urlencoded");
HttpResponseMessage response = await httpClient.PostAsync(TokenEndpoint, content);
string responseString = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
JsonObject tokens = JsonObject.Parse(responseString);
accessToken = tokens.GetNamedString("access_token");
foreach (var item in vault.RetrieveAll().Where((x) => x.Resource == TokenTypes.AccessToken.ToString())) vault.Remove(item);
vault.Add(new PasswordCredential(TokenTypes.AccessToken.ToString(), "MyApp", accessToken));
TokenLastAccess = DateTimeOffset.UtcNow;
return true;
}
}
}
The code above is only a sample (with some shortcuts) and as mentioned above - a working version with some more error handling you will find at my GitHub. Please also note, that I haven't spend much time on this and it will surely need some more work to handle all the cases and possible problems. Though hopefully will help you to start.
Answer from Roamsz is great but didnt work for me because I found some conflicts or at least with the latest build 17134 as target, it doesn't work. Here are the problem, in his Github sample, he is using returnurl as urn:ietf:wg:oauth:2.0:oob . this is the type of url, you can't use with web application type when you create new "Create OAuth client ID" in the google or firebase console. you must use "Ios" as shown below. because web application requires http or https urls as return url.
from google doc
According to his sample he is using Client secret to obtain access token, this is not possible if you create Ios as type. because Android and Ios arent using client secret. It is perfectly described over here
client_secret The client secret obtained from the API Console. This
value is not needed for clients registered as Android, iOS, or Chrome
applications.
So you must use type as Ios, No Client Secret needed and return url is urn:ietf:wg:oauth:2.0:oob or urn:ietf:wg:oauth:2.0:oob:auto difference is that auto closes browser and returns back to the app. other one, code needs to be copied manually. I prefer to use urn:ietf:wg:oauth:2.0:oob:auto
Regarding code: please follow his github code. Just remove the Client Secret from the Access Token Request.
EDIT: it looks like I was right that even offical sample is not working after UWP version 15063, somebody created an issue on their github
https://github.com/Microsoft/Windows-universal-samples/issues/642
I'm using pretty straightforward code with Google.Apis.Oauth2.v2 Nuget package. Note, that I'm using v.1.25.0.859 of that package. I tried to update to the lastest version (1.37.0.1404), but this surprisingly doesn't work with UWP. At the same time v. 1.25.0.859 works just fine.
So, unless there's a better option, I would recommend to use a bit old, but working version of Nuget package.
This is my code:
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
new Uri("ms-appx:///Assets/User/Auth/google_client_secrets.json"),
new[] { "profile", "email" },
"me",
CancellationToken.None);
await GoogleWebAuthorizationBroker.ReauthorizeAsync(credential, CancellationToken.None);
Then you can retrieve access token from: credential.Token.AccessToken.

Categories