Currently I'm trying to create user activity from uwp application. But every time I encounter this response.
{
"error": {
"code": "AuthenticationError",
"message": "Error authenticating with resource",
"innerError": {
"date": "2022-12-28T09:20:16",
"request-id": "some id",
"client-request-id": "some id"
}
}
}
Here is my c# code example. Request content string was taken from microsoft docs
public sealed class UserActivityProvider : IUserActivityProvider
{
private const string activityId = "SendMessageUserActivity";
private static HttpClient httpClient = new HttpClient();
public UserActivityProvider()
{
}
private async Task<string> GetAccessTokenAsync(Account account)
{
var accessToken = string.Empty;
var publicClientApplication = PublicClientApplicationBuilder.Create(MicrosoftConstants.ClientId)
.WithRedirectUri(MicrosoftConstants.RedirectUri)
.Build();
var scopes = new string[]
{
"UserActivity.ReadWrite.CreatedByApp"
};
AuthenticationResult? authToken = null;
try
{
authToken = await publicClientApplication.AcquireTokenSilent(scopes, account.Email).ExecuteAsync();
}
catch (Exception)
{
authToken = await publicClientApplication.AcquireTokenInteractive(scopes).ExecuteAsync();
}
if (authToken != null)
{
accessToken = authToken.AccessToken;
}
return accessToken;
}
public async Task CreateUserActivityAsync(Account account, CreatingMessageUserActivityParameters userActivityParameters)
{
var accessToken = await GetAccessTokenAsync(account);
if (accessToken != string.Empty)
{
var contentForCreatingActivity = new StringContent("{\r\n \"appActivityId\": \"SendMessageUserActivity\",\r\n \"activitySourceHost\": \"https://www.contoso.com\",\r\n \"userTimezone\": \"Africa/Casablanca\",\r\n \"appDisplayName\": \"Contoso, Ltd.\",\r\n \"activationUrl\": \"https://www.contoso.com/article?id=12345\",\r\n \"contentUrl\": \"https://www.contoso.com/article?id=12345\",\r\n \"fallbackUrl\": \"https://www.contoso.com/article?id=12345\",\r\n \"contentInfo\": {\r\n \"#context\": \"https://schema.org\",\r\n \"#type\": \"Article\",\r\n \"author\": \"Jennifer Booth\",\r\n \"name\": \"How to Tie a Reef Knot\"\r\n },\r\n \"visualElements\": {\r\n \"attribution\": {\r\n \"iconUrl\": \"https://www.contoso.com/icon\",\r\n \"alternateText\": \"Contoso, Ltd.\",\r\n \"addImageQuery\": false\r\n },\r\n \"description\": \"How to Tie a Reef Knot. A step-by-step visual guide to the art of nautical knot-tying.\",\r\n \"backgroundColor\": \"#ff0000\",\r\n \"displayText\": \"Contoso How-To: How to Tie a Reef Knot\",\r\n \"content\": {\r\n \"$schema\": \"https://adaptivecards.io/schemas/adaptive-card.json\",\r\n \"type\": \"AdaptiveCard\",\r\n \"body\": [\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"Contoso MainPage\"\r\n }\r\n ]\r\n }\r\n }\r\n}", Encoding.UTF8, "application/json");
httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
var response = await httpClient.PutAsync($"https://graph.microsoft.com/beta/me/activities/{activityId}", contentForCreatingActivity);
var stringifiedResponse = await response.Content.ReadAsStringAsync();
}
}
}
And here is also get method for retrieving all activities and it's also return bad request
public async Task<string?> IsUserActivityExistsAsync(Account account)
{
string? resultSubject = null;
var accessToken = await GetAccessTokenAsync(account);
if (accessToken != string.Empty)
{
httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
var response = await httpClient.GetAsync("https://graph.microsoft.com/v1.0/me/activities");
var stringifiedResponse = await response.Content.ReadAsStringAsync();
}
return resultSubject;
}
All articles referencing that I have to provide correct scope but I took that scope from official microsoft docs and there wouldn't be a mistake.
Response for getting activities
{
"error": {
"code": "UnknownError",
"message": "{\"ErrorCode\":2,\"ErrorMessage\":\"Substrate operation failed. Url: https://substrate.office.com/api/v2.0/users('******(73)')/CurrentCollections('******(10)') Status: Unauthorized. Error Code: invalid_tenant Error Message: The tenant for tenant guid 'tenant' does not exist., SubstrateError: null\"}",
"innerError": {
"date": "2022-12-29T05:44:57",
"request-id": "id",
"client-request-id": "id"
}
}
}
Response for adding activities
{
"error": {
"code": "UnknownError",
"message": "{\"ErrorCode\":35,\"ErrorMessage\":\"General error occurred. Contact product team.\"}",
"innerError": {
"date": "2022-12-29T05:59:02",
"request-id": "id",
"client-request-id": "id"
}
}
}
Please make sure that UserActivity.ReadWrite.CreatedByApp MS Graph API permissions are assigned to your app in Azure AD as Delegated type (not Application)
UPDATED:
Not sure if you're using correct authentication provider, for delegated type (check options here https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS)
Try to acquire token by updating your code with the following changes
var publicClientApplication = PublicClientApplicationBuilder
.Create(MicrosoftConstants.ClientId)
.WithTenantId("YOUR_TENANT_ID")
.Build();
and
var authToken = await publicClientApplication
.AcquireTokenByIntegratedWindowsAuth(scopes)
.ExecuteAsync()
.Result;
Note: Execute code under user account belonging to organization
As you said you are getting empty data when you decoded the access token in jwt.ms , which means you are doing something wrong while acquiring access token , follow the doc to get token via Interactive provider , and then again check by decoding in jwt.ms .
var scopes = new[] { "User.Read" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "common";
// Value from app registration
var clientId = "YOUR_CLIENT_ID";
// using Azure.Identity;
var options = new InteractiveBrowserCredentialOptions
{
TenantId = tenantId,
ClientId = clientId,
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
// MUST be http://localhost or http://localhost:PORT
// See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-Core
RedirectUri = new Uri("http://localhost"),
};
// https://learn.microsoft.com/dotnet/api/azure.identity.interactivebrowsercredential
var interactiveCredential = new InteractiveBrowserCredential(options);
var graphClient = new GraphServiceClient(interactiveCredential, scopes);
Related
I'm trying to retrieve the reviews from our business account.
For this I'm using the Google.Apis.Auth.OAuth2.Mvc library for .net https://www.nuget.org/packages/Google.Apis.Auth.MVC/ and following this example https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-asp.net-mvc.
The library is supposed to use the refresh token automatically but for some reason after 1 hour when the access token expires we lost access to the reviews.
Here is my implementation:
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "xxxxxxxxxxxxxxxxxxxxxxxxx",
ClientSecret = "xxxxxxxxxxxxxxxxxxxxx"
},
Scopes = new string[] { "https://www.googleapis.com/auth/plus.business.manage" },
DataStore = new FileDataStore(System.Web.HttpContext.Current.Server.MapPath("/App_Data/MyGoogleStorage"), true)
//DataStore = new FileDataStore(System.Web.HttpContext.Current.Server.MapPath("/App_Data/Drive.Api.Auth.Store"))
});
public override string GetUserId(Controller controller)
{
return "our email address";
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
public async Task<ActionResult> IndexAsync(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
if (result.Credential != null)
{
var accessToken = result.Credential.Token.AccessToken;
var client = new RestClient("https://mybusiness.googleapis.com/v4/accounts/116326379071192580211/locations/6608127685860731136/reviews?access_token=" + accessToken);
var request = new RestRequest(Method.GET);
IRestResponse response = client.Execute(request);
GoogleReviewsModel googleReviews = Newtonsoft.Json.JsonConvert.DeserializeObject<GoogleReviewsModel>(response.Content);
return View("Index", googleReviews);
}
else
{
return new RedirectResult(result.RedirectUri);
}
}
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
{
get { return new AppFlowMetadata(); }
}
}
The reviews are from our own company so we don't to logins from different users. What I want to achieve is to login the first time with our company logins and then automatically refresh the access token with the refresh token so the reviews are always visible in the website,
Thanks a lot!
EDIT:
After 1 hour the response I get from the following code is this:
var accessToken = result.Credential.Token.AccessToken;
var client = new RestClient("https://mybusiness.googleapis.com/v4/accounts/116326379071192580211/locations/6608127685860731136/reviews?access_token=" + accessToken);
var request = new RestRequest(Method.GET);
IRestResponse response = client.Execute(request);
""message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.\",\n "status": "UNAUTHENTICATED"
result.Credential contains both the access token and the refresh token so it seems to read the file in app_data. But the access code seems to be expired and is not being refreshed at this point and is not asking to login again neither:
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
You are not actually "telling the library" to refresh the access token, you are directly using the token that had been stored, and is now expired. Your code that looks like this:
var accessToken = result.Credential.Token.AccessToken;
should look like this:
var accessToekn = await result.Credential.GetAccessTokenForRequestAsync();
The GetAccessTokenForRequestAsync method will check if the access token needs refreshing and do so when needed.
I have two projects. One is an Identity server 4 who handle users and authentication. The second need to use the first to login and ask for a token to access an API.
When I need to refresh the token I don't now how to handle the new access token. How I can set to the authentification asp dot net core the new token. All the refresh process are made on a AuthorizationHandler.
I tried to modify the claims on identity but doesnt work. I tried to stock access token and refresh token inside my own cookie but I have trouble because when I refresh the token I can only use them at the next request (I didn't achieve to modify the request.cookies only the response.cookies.
public static async Task SetToken(TokenKind token,string value, HttpContext context)
{
context.Response.Cookies.Append(GetTokenCookieName(token), value);
}
public static async Task<string> GetRefreshTokenAsync(HttpContext context)
{
return await SearchToken(TokenKind.Refresh,context);
}
private static async Task<string> SearchToken(TokenKind token, HttpContext context)
{
var tokenName = GetTokenName(token);
var test = context.Request.Cookies;
var apiToken = context.Request.Cookies.FirstOrDefault(x => x.Key == GetTokenCookieName(token)).Value;
if (apiToken == null)
{
// Save token into cookie
var tokenValue = await context.GetTokenAsync(GetTokenName(TokenKind.Access));
await SetToken(TokenKind.Access, tokenValue, context);
var refreshTokenValue = await context.GetTokenAsync(GetTokenName(TokenKind.Refresh));
await SetToken(TokenKind.Refresh, refreshTokenValue, context);
switch (token)
{
case TokenKind.Access:
return tokenValue;
case TokenKind.Refresh:
return refreshTokenValue;
default:
return null;
break;
}
}
else
{
return apiToken;
}
}
private async Task<bool> RefreshToken(AuthorizationFilterContext mvcContext, HttpClient client)
{
var refreshToken = await TokenUtils.GetRefreshTokenAsync(mvcContext.HttpContext);
//await mvcContext.HttpContext.GetTokenAsync("refresh_token");
var variables = new Dictionary<string, string>
{
{ "grant_type", "refresh_token" },
{ "client_id", _configuration["ApplicationOptions:ClientId"] },
{ "client_secret", _configuration["ApplicationOptions:ClientSecret"] },
{ "refresh_token", refreshToken }
};
var content = new FormUrlEncodedContent(variables);
var url = _configuration["ApplicationOptions:AuthorizeServer"] + "/connect/token";
var response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode == false)
{
var errorString = await response.Content.ReadAsStringAsync();
var errorData = JsonConvert.DeserializeObject<dynamic>(errorString);
return false;
}
var contentAsString = await response.Content.ReadAsStringAsync();
var responseData = JsonConvert.DeserializeObject<dynamic>(contentAsString);
var newAccessToken = (string)responseData.access_token;
var newRefreshToken = (string)responseData.refresh_token;
await TokenUtils.SetAccessToken(newAccessToken, mvcContext.HttpContext);
await TokenUtils.SetRefreshToken(newRefreshToken, mvcContext.HttpContext);
var result = await mvcContext.HttpContext.AuthenticateAsync();
if (result.Succeeded)
{
result.Properties.StoreTokens(new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = newAccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = newRefreshToken
}
});
return true;
}
else
{
return false;
}
}
I would like to know what is the good pratice to store (or replace the actual access token) with the .net core authentification. I'm sure I'm not doing it the right way. At the end I want to handle correctly my token and my refresh token for not ask the user to login again. Now with my solution my project will denied the access when it need to refresh the token and next request will be granted (because the cookie need to be resend from the user).
Thank to Ruard van Elburg I found the solution (here's the complete answer)
And that's what I used to replace my tokens:
// Save the information in the cookie
var info = await mvcContext.HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", newRefreshToken);
info.Properties.UpdateTokenValue("access_token", newAccessToken);
info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
await mvcContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
I'm trying to use Google Calendar API v3, but i have problems while running the codes, it always gives me that error :
An exception of type 'System.AggregateException' occurred in mscorlib.ni.dll but was not handled in user code
Additional information: One or more errors occurred.
I don't know why it does, also It should work as well. Here is a screenshot for it :
Also my codes are :
UserCredential credential;
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new Uri("ms-appx:///Assets/client_secrets.json"),
Scopes,
"user",
CancellationToken.None).Result;
// Create Google Calendar API service.
var service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
var calendarService = new CalendarService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = "Windows 10 Calendar sample"
});
var calendarListResource = await calendarService.CalendarList.List().ExecuteAsync();
If you can at least help with calling it through REST API, that would be great too, but you must consider that it's UWP, so it has another way to get it work as well.
As i already tried through REST API, but i always get "Request error code 400".
Thanks for your attention.
The Google API Client Library for .NET does not support UWP by now. So we can't use Google.Apis.Calendar.v3 Client Library in UWP apps now. For more info, please see the similar question: Universal Windows Platform App with google calendar.
To use Google Calendar API in UWP, we can call it through REST API. To use the REST API, we need to authorize requests first. For how to authorize requests, please see Authorizing Requests to the Google Calendar API and Using OAuth 2.0 for Mobile and Desktop Applications.
After we have the access token, we can call Calendar API like following:
var clientId = "{Your Client Id}";
var redirectURI = "pw.oauth2:/oauth2redirect";
var scope = "https://www.googleapis.com/auth/calendar.readonly";
var SpotifyUrl = $"https://accounts.google.com/o/oauth2/auth?client_id={clientId}&redirect_uri={Uri.EscapeDataString(redirectURI)}&response_type=code&scope={Uri.EscapeDataString(scope)}";
var StartUri = new Uri(SpotifyUrl);
var EndUri = new Uri(redirectURI);
// Get Authorization code
WebAuthenticationResult WebAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, StartUri, EndUri);
if (WebAuthenticationResult.ResponseStatus == WebAuthenticationStatus.Success)
{
var decoder = new WwwFormUrlDecoder(new Uri(WebAuthenticationResult.ResponseData).Query);
if (decoder[0].Name != "code")
{
System.Diagnostics.Debug.WriteLine($"OAuth authorization error: {decoder.GetFirstValueByName("error")}.");
return;
}
var autorizationCode = decoder.GetFirstValueByName("code");
//Get Access Token
var pairs = new Dictionary<string, string>();
pairs.Add("code", autorizationCode);
pairs.Add("client_id", clientId);
pairs.Add("redirect_uri", redirectURI);
pairs.Add("grant_type", "authorization_code");
var formContent = new Windows.Web.Http.HttpFormUrlEncodedContent(pairs);
var client = new Windows.Web.Http.HttpClient();
var httpResponseMessage = await client.PostAsync(new Uri("https://www.googleapis.com/oauth2/v4/token"), formContent);
if (!httpResponseMessage.IsSuccessStatusCode)
{
System.Diagnostics.Debug.WriteLine($"OAuth authorization error: {httpResponseMessage.StatusCode}.");
return;
}
string jsonString = await httpResponseMessage.Content.ReadAsStringAsync();
var jsonObject = Windows.Data.Json.JsonObject.Parse(jsonString);
var accessToken = jsonObject["access_token"].GetString();
//Call Google Calendar API
using (var httpRequest = new Windows.Web.Http.HttpRequestMessage())
{
string calendarAPI = "https://www.googleapis.com/calendar/v3/users/me/calendarList";
httpRequest.Method = Windows.Web.Http.HttpMethod.Get;
httpRequest.RequestUri = new Uri(calendarAPI);
httpRequest.Headers.Authorization = new Windows.Web.Http.Headers.HttpCredentialsHeaderValue("Bearer", accessToken);
var response = await client.SendRequestAsync(httpRequest);
if (response.IsSuccessStatusCode)
{
var listString = await response.Content.ReadAsStringAsync();
//TODO
}
}
}
I have the Google .NET Client working in my UWP app. The trick is that you have to put it in a .NET Standard 2.0 Class Library, expose the API services you need, and then reference that library from your UWP app.
Also, you have to handle the getting the auth token yourself. It's not that much work and the Drive APIs and Calendar APIs work just fine (the only ones I've tried). You can see that I pass in a simple class that contains the auth token and other auth details to a method called Initialize.
Here is the single class I used in the .NET Standard 2.0 class library:
namespace GoogleProxy
{
public class GoogleService
{
public CalendarService calendarService { get; private set; }
public DriveService driveService { get; private set; }
public GoogleService()
{
}
public void Initialize(AuthResult authResult)
{
var credential = GetCredentialForApi(authResult);
var baseInitializer = new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = "{your app name here}" };
calendarService = new Google.Apis.Calendar.v3.CalendarService(baseInitializer);
driveService = new Google.Apis.Drive.v3.DriveService(baseInitializer);
}
private UserCredential GetCredentialForApi(AuthResult authResult)
{
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "{your app client id here}",
ClientSecret = "",
},
Scopes = new string[] { "openid", "email", "profile", "https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar.events.readonly", "https://www.googleapis.com/auth/drive.readonly" },
};
var flow = new GoogleAuthorizationCodeFlow(initializer);
var token = new TokenResponse()
{
AccessToken = authResult.AccessToken,
RefreshToken = authResult.RefreshToken,
ExpiresInSeconds = authResult.ExpirationInSeconds,
IdToken = authResult.IdToken,
IssuedUtc = authResult.IssueDateTime,
Scope = "openid email profile https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events.readonly https://www.googleapis.com/auth/drive.readonly",
TokenType = "bearer" };
return new UserCredential(flow, authResult.Id, token);
}
}
}
In order to get the Auth token from google, you have to use custom schemes. Register your app as an 'iOS' app on the google services console and put in a URI scheme (something unique). Then add this scheme to your UWP manifest under Declarations->Protocol. Handle it in your App.xaml.cs:
protected override void OnActivated(IActivatedEventArgs args)
{
base.OnActivated(args);
if (args.Kind == ActivationKind.Protocol)
{
ProtocolActivatedEventArgs protocolArgs = (ProtocolActivatedEventArgs)args;
Uri uri = protocolArgs.Uri;
Debug.WriteLine("Authorization Response: " + uri.AbsoluteUri);
locator.AccountsService.GoogleExternalAuthWait.Set(uri.Query);
}
}
That GoogleExternalAuthWait comes from some magical code I found about how to create an asynchronous ManualResetEvent. https://blogs.msdn.microsoft.com/pfxteam/2012/02/11/building-async-coordination-primitives-part-1-asyncmanualresetevent/ It looks like this (I only converted it to generic).
public class AsyncManualResetEvent<T>
{
private volatile TaskCompletionSource<T> m_tcs = new TaskCompletionSource<T>();
public Task<T> WaitAsync() { return m_tcs.Task; }
public void Set(T TResult) { m_tcs.TrySetResult(TResult); }
public bool IsReset => !m_tcs.Task.IsCompleted;
public void Reset()
{
while (true)
{
var tcs = m_tcs;
if (!tcs.Task.IsCompleted ||
Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<T>(), tcs) == tcs)
return;
}
}
}
This is how you start the Google Authorization. What happens is it launches an external browser to begin the google signing process and then wait (that's what the AsyncManualResetEvent does). When you're done, Google will launch a URI using your custom scheme. You should get a message dialog saying the browser is trying to open an app... click ok and the AsyncManualResetEvent continues and finishes the auth process. You'll need to make a class that contains all the auth info to pass to your class library.
private async Task<AuthResult> AuthenticateGoogleAsync()
{
try
{
var stateGuid = Guid.NewGuid().ToString();
var expiration = DateTimeOffset.Now;
var url = $"{GoogleAuthorizationEndpoint}?client_id={WebUtility.UrlEncode(GoogleAccountClientId)}&redirect_uri={WebUtility.UrlEncode(GoogleRedirectURI)}&state={stateGuid}&scope={WebUtility.UrlEncode(GoogleScopes)}&display=popup&response_type=code";
var success = Windows.System.Launcher.LaunchUriAsync(new Uri(url));
GoogleExternalAuthWait = new AsyncManualResetEvent<string>();
var query = await GoogleExternalAuthWait.WaitAsync();
var dictionary = query.Substring(1).Split('&').ToDictionary(x => x.Split('=')[0], x => Uri.UnescapeDataString(x.Split('=')[1]));
if (dictionary.ContainsKey("error"))
{
return null;
}
if (!dictionary.ContainsKey("code") || !dictionary.ContainsKey("state"))
{
return null;
}
if (dictionary["state"] != stateGuid)
return null;
string tokenRequestBody = $"code={dictionary["code"]}&redirect_uri={Uri.EscapeDataString(GoogleRedirectURI)}&client_id={GoogleAccountClientId}&access_type=offline&scope=&grant_type=authorization_code";
StringContent content = new StringContent(tokenRequestBody, Encoding.UTF8, "application/x-www-form-urlencoded");
// Performs the authorization code exchange.
using (HttpClientHandler handler = new HttpClientHandler())
{
handler.AllowAutoRedirect = true;
using (HttpClient client = new HttpClient(handler))
{
HttpResponseMessage response = await client.PostAsync(GoogleTokenEndpoint, content);
if (response.IsSuccessStatusCode)
{
var stringResponse = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(stringResponse);
var id = DecodeIdFromJWT((string)json["id_token"]);
var oauthToken = new AuthResult()
{
Provider = AccountType.Google,
AccessToken = (string)json["access_token"],
Expiration = DateTimeOffset.Now + TimeSpan.FromSeconds(int.Parse((string)json["expires_in"])),
Id = id,
IdToken = (string)json["id_token"],
ExpirationInSeconds = long.Parse((string)json["expires_in"]),
IssueDateTime = DateTime.Now,
RefreshToken = (string)json["refresh_token"]
};
return oauthToken;
}
else
{
return null;
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return null;
}
}
I've read along the way that Salesforce (I'm extremely new to this 3rd party platform) has a FUEL SDK which one can use instead of the version (using HttpClient -- REST instead of SOAP).
Please correct me if using FUEL SDK is the only way to go about requesting Salesforce's endpoints. Currently I am attempting to hit ExactTargets's API endpoints using HttpClient. These are the tutorials I've been basing my code off of:
https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/messageDefinitionSends.htm
https://developer.salesforce.com/docs/atlas.en-us.mc-getting-started.meta/mc-getting-started/get-access-token.htm
Wanted Result:
To be able to request a Triggered Send email based off a template inside of ExactTarget.
Problem:
The Salesforce endpoint continuously returns a 404. I am able to receive the authorization token successfully. The GetAccessToken method is omitted for brevity
https://www.exacttargetapis.com/messaging/v1/messageDefinitionSends/key:MyExternalKey/send
I do not understand why the 2nd POST request to //www.exacttargetapis.com/..... returns a 404 but the authorization works. This leads me to believe that I do not have to use the FUEL SDK to accomplish triggering a welcome email.
Code:
private const string requestTokenUrl = "https://auth.exacttargetapis.com/v1/requestToken";
private const string messagingSendUrl = "https://www.exacttargetapis.com/messaging/v1/messageDefinitionSends";
private string exactTargetClientId = ConfigurationManager.AppSettings["ExactTargetClientId"];
private string exactTargetClientSecret = ConfigurationManager.AppSettings["ExactTargetClientSecret"];
private string TriggerEmail(User model, string dbName)
{
var etExternalKeyAppSetting = ConfigurationManager.AppSettings.AllKeys.FirstOrDefault(x => x.Equals(dbName));
if (etExternalKeyAppSetting != null)
{
string etExternalKey = ConfigurationManager.AppSettings[etExternalKeyAppSetting];
HttpClient client = new HttpClient
{
BaseAddress = new Uri(string.Format(#"{0}/key:{1}/send", messagingSendUrl, etExternalKey)),
DefaultRequestHeaders =
{
Authorization = new AuthenticationHeaderValue("Bearer", this.GetAccessToken())
}
};
try
{
var postData = this.CreateExactTargetPostData(model.Email, etExternalKey);
var response = client.PostAsync(client.BaseAddress
, new StringContent(JsonConvert.SerializeObject(postData).ToString()
, Encoding.UTF8
, "application/json")).Result;
// get triggered email response
if (response.IsSuccessStatusCode)
{
dynamic result = JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result);
}
}
catch (Exception ex)
{
string message = ex.Message;
}
}
return "testing";
}
private object CreateExactTargetPostData(string email, string extKey)
{
var fromData = new
{
Address = ConfigurationManager.AppSettings["AwsSenderEmail"],
Name = "Test"
};
var subscriberAttributes = new { };
var contactAttributes = new
{
SubscriberAttributes = subscriberAttributes
};
var toData = new
{
Address = email,
//SubscriberKey = extKey,
//ContactAttributes = contactAttributes
};
var postData = new
{
From = fromData,
To = toData
};
return postData;
}
I have also tried using Advanced REST Client using the following:
URL:
https://www.exacttargetapis.com/messaging/v1/messageDefinitionSends/key:MyExternalKey/send
POST
Raw Headers:
Content-Type: application/json
Authorization: Bearer XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Raw Payload:
{
"From": {
"Address": "code#exacttarget.com",
"Name": "Code#"
},
"To": {
"Address": "example#example.com",
"SubscriberKey": "example#example.com",
"ContactAttributes": {
"SubscriberAttributes": {
"Region": "West",
"City": "Indianapolis",
"State": "IN"
}
}
},
"OPTIONS": {
"RequestType": "ASYNC"
}
}
Issue was my App in the AppCenter was pointing to the incorrect login for MarketingCloud =(
I have followed this article to implement an OAuth Authorization server. However when I use post man to get a token, I get an error in the response:
"error": "unsupported_grant_type"
I read somewhere that the data in Postman needs to be posted using Content-type:application/x-www-form-urlencoded. I have prepped the required settings in Postman:
and yet my headers are like this:
Here is my code
public class CustomOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult<object>(null);
}
public override Task MatchEndpoint(OAuthMatchEndpointContext context)
{
if (context.OwinContext.Request.Method == "OPTIONS" && context.IsTokenEndpoint)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Methods", new[] { "POST" });
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "accept", "authorization", "content-type" });
context.OwinContext.Response.StatusCode = 200;
context.RequestCompleted();
return Task.FromResult<object>(null);
}
return base.MatchEndpoint(context);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
string allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Content-Type" });
Models.TheUser user = new Models.TheUser();
user.UserName = context.UserName;
user.FirstName = "Sample first name";
user.LastName = "Dummy Last name";
ClaimsIdentity identity = new ClaimsIdentity("JWT");
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
foreach (string claim in user.Claims)
{
identity.AddClaim(new Claim("Claim", claim));
}
var ticket = new AuthenticationTicket(identity, null);
context.Validated(ticket);
}
}
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly string _issuer = string.Empty;
public CustomJwtFormat(string issuer)
{
_issuer = issuer;
}
public string Protect(AuthenticationTicket data)
{
string audienceId = ConfigurationManager.AppSettings["AudienceId"];
string symmetricKeyAsBase64 = ConfigurationManager.AppSettings["AudienceSecret"];
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.WriteToken(token);
return jwt;
}
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}
In the CustomJWTFormat class above only the breakpoint in the constructor gets hit. In the CustomOauth class, the breakpoint in the GrantResourceOwnerCredentials method never gets hit. The others do.
The Startup class:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
ConfigureOAuthTokenGeneration(app);
ConfigureOAuthTokenConsumption(app);
app.UseWebApi(config);
}
private void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
var OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new CustomOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat(ConfigurationManager.AppSettings["Issuer"])
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
private void ConfigureOAuthTokenConsumption(IAppBuilder app)
{
string issuer = ConfigurationManager.AppSettings["Issuer"];
string audienceId = ConfigurationManager.AppSettings["AudienceId"];
byte[] audienceSecret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["AudienceSecret"]);
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
}
}
Do I need to set up Content-type:application/x-www-form-urlencoded somewhere else in the web api code? What could be wrong? Please help.
The response is a bit late - but in case anyone has the issue in the future...
From the screenshot above - it seems that you are adding the url data (username, password, grant_type) to the header and not to the body element.
Clicking on the body tab, and then select "x-www-form-urlencoded" radio button, there should be a key-value list below that where you can enter the request data
With Postman, select Body tab and choose the raw option and type the following:
grant_type=password&username=yourusername&password=yourpassword
Note the URL: localhost:55828/token (not localhost:55828/API/token)
Note the request data. Its not in json format, its just plain data without double quotes.
userName=xxx#gmail.com&password=Test123$&grant_type=password
Note the content type. Content-Type: 'application/x-www-form-urlencoded' (not Content-Type: 'application/json')
When you use JavaScript to make post request, you may use following:
$http.post("localhost:55828/token",
"userName=" + encodeURIComponent(email) +
"&password=" + encodeURIComponent(password) +
"&grant_type=password",
{headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}
).success(function (data) {//...
See screenshots below from Postman:
If you are using AngularJS you need to pass the body params as string:
factory.getToken = function(person_username) {
console.log('Getting DI Token');
var url = diUrl + "/token";
return $http({
method: 'POST',
url: url,
data: 'grant_type=password&username=myuser#user.com&password=mypass',
responseType:'json',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
};
try to add this in your payload
grant_type=password&username=pippo&password=pluto
I was getting this error too and the reason ended up being wrong call url. I am leaving this answer here, if someone else happens to mix the urls and getting this error. Took me hours to realize I had wrong URL.
Error I got (HTTP code 400):
{
"error": "unsupported_grant_type",
"error_description": "grant type not supported"
}
I was calling:
https://MY_INSTANCE.lightning.force.com
While the correct URL would have been:
https://MY_INSTANCE.cs110.my.salesforce.com
Old Question, but for angular 6, this needs to be done when you are using HttpClient
I am exposing token data publicly here but it would be good if accessed via read-only properties.
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable, of } from 'rxjs';
import { delay, tap } from 'rxjs/operators';
import { Router } from '#angular/router';
#Injectable()
export class AuthService {
isLoggedIn: boolean = false;
url = "token";
tokenData = {};
username = "";
AccessToken = "";
constructor(private http: HttpClient, private router: Router) { }
login(username: string, password: string): Observable<object> {
let model = "username=" + username + "&password=" + password + "&grant_type=" + "password";
return this.http.post(this.url, model).pipe(
tap(
data => {
console.log('Log In succesful')
//console.log(response);
this.isLoggedIn = true;
this.tokenData = data;
this.username = data["username"];
this.AccessToken = data["access_token"];
console.log(this.tokenData);
return true;
},
error => {
console.log(error);
return false;
}
)
);
}
}
Another common cause of this 'unsupported_grant_type' error is calling the API as GET instead of POST.
In Angular 13, this is what I did;
let headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
})
return this.http.post(this.url + 'token', 'grant_type=password&username='+form.username+'&password='+form.password, { headers: headers})
Use grant_type={ Your password}