SignalR callbacks not firing in .NET Core integration Test - c#

I have trying to write integration tests for my SignalR chat application.
But the connection.On() callbacks never gets fired.
While debugging I have made sure InvokeAsync works perfectly fine but not the callbacks
Also I have tested my hub with Angular client and it works fine.
Test.cs
public sealed class ChatHubTest : ControllerTests
{
private IHost? host_;
[Test]
public async Task ReplyWithTheSameMessageWhenInvokeSendMethod()
{
//var message = "Integration Testing in Microsoft AspNetCore SignalR";
string echo = "";
var hostBuilder = new HostBuilder()
.ConfigureWebHost(webHost => _ = webHost
.UseTestServer()
.UseStartup<Startup>()
.UseEnvironment("IntegrationTest"));
host_?.Dispose();
host_ = await hostBuilder.StartAsync();
TestServer server = host_.GetTestServer();
var connection = new HubConnectionBuilder()
.WithUrl(
"http://localhost:5000/hub/chat?username=21671", options =>
{
options.AccessTokenProvider = () => Task.FromResult("hello");
options.HttpMessageHandlerFactory = _ => server.CreateHandler();
})
.Build();
using IDisposable handler = connection.On<(int, string)>(WebSocketActions.GroupAction, msg =>
{
(_, echo) = msg;
_ = echo.Should().Be("Typing");
});
await connection.StartAsync();
await connection.InvokeAsync("JoinGroup", "group1");
await connection.InvokeAsync("SendToGroup", "group1", 123, "Typing");
_ = echo.Should().Be("Typing");
}
}
Hub.cs
public sealed class ChatHub : Hub
{
////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Join a socket group </summary>
///
/// <param name="group"> Identifier for the group. </param>
///
/// <returns> . </returns>
//[Authorize(Policy = "CustomHubAuthorizatioPolicy")]
public async Task JoinGroup(string group)
{
await Groups.AddToGroupAsync(Context.ConnectionId, group);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Send action to group </summary>
///
/// <param name="group"> Identifier for the group. </param>
/// <param name="userId"> Identifier for the user. </param>
/// <param name="action"> Action to send </param>
///
/// <returns> . </returns>
//[Authorize(Policy = "CustomHubAuthorizatioPolicy")]
public async Task SendToGroup(string group, int userId, string action)
{
await Clients.Group(group).SendAsync(WebSocketActions.GroupAction, userId, action);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary> Leave group </summary>
///
/// <param name="group"> Identifier for the group. </param>
///
/// <returns> . </returns>
//[Authorize(Policy = "CustomHubAuthorizatioPolicy")]
public async Task LeaveGroup(string group)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, group);
}
}
The hub works with angular client.
Thanks In Advance.

Related

Azure Function: Binder Dependency Injection into class constructor

I am using Http Trigger Azure Function, after receiving request message I am uploading that message into Azure blob storage.
Can I inject Binder in the class constructor?
Below is my Http Trigger Azure Function
namespace Notifications.Receiver.Publishers
{
/// <summary>
/// InvoiceMessageWebHook - Http Trigger Azure function
/// </summary>
public class InvoiceMessageWebHook
{
#region private fields
private readonly ILogger<InvoiceMessageWebHook> logger;
private readonly IEventValidator validator;
private readonly IEventHandler<InvoiceResultDto, InvoiceMessageEvent> handler;
#endregion
#region ctors
public InvoiceMessageWebHook(ILogger<InvoiceMessageWebHook> logger,
IEventValidator validator,
IEventHandler<InvoiceResultDto, InvoiceMessageEvent> handler)
{
this.logger = logger;
this.validator = validator;
this.handler = handler;
}
#endregion
#region function
/// <summary>
/// InvoiceMessageWebHook - Http Trigger Azure function
/// </summary>
/// <param name="req"></param>
/// <param name="binder"></param>
/// <returns></returns>
[FunctionName(nameof(InvoiceMessageWebHook))]
public async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
Binder binder)
{
try
{
IActionResult actionResult = null;
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
logger.LogInformation($"Invoice message request received on {nameof(InvoiceMessageWebHook)} request body:{requestBody.AsJson()}");
var #event = requestBody.AsPoco<InvoiceMessageEvent>();
#event.ReceivedDate = $"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff}";
if (validator.Validate(#event, new InvoiceMessageEventValidator(), logger, ref actionResult))
{
var response = await handler.HandleAsync(#event, binder);
actionResult = new OkObjectResult(response);
}
return actionResult;
}
catch (Exception ex)
{
logger.LogError($"Exception while processing {nameof(InvoiceMessageWebHook)}," +
$" Exception Message:{ex.Message}, StackTrace:{ex.StackTrace}");
throw;
}
}
#endregion
}
}
Below is my handler:
namespace Notifications.Receiver.Events.Handlers
{
/// <summary>
/// Handler
/// </summary>
public class InvoiceMessageEventHandler : IEventHandler<InvoiceResultDto, InvoiceMessageEvent>
{
#region private fields
public const string ServiceBusTopicNotificationMonitoring = "ServiceBusTopicNotificationMonitoring";
public const string ServiceBusConnectionString = "ServiceBusConnectionString";
private readonly ILogger<InvoiceMessageEventHandler> logger;
private readonly IMessageBusFactory messageBusFactory;
#endregion
#region ctors
public InvoiceMessageEventHandler(ILogger<InvoiceMessageEventHandler> logger,
IMessageBusFactory messageBusFactory)
{
this.logger = logger;
this.messageBusFactory = messageBusFactory;
}
#endregion
#region Handler
/// <summary>
/// Upload invoice message into Blob storage
/// </summary>
/// <param name="event"></param>
/// <param name="binder"></param>
/// <param name="logger"></param>
/// <returns></returns>
public async Task<InvoiceResultDto> HandleAsync(InvoiceMessageEvent #event, Binder binder)
{
#event.RequestID = Guid.NewGuid().ToString();
#event.MonitoringType = MonitoringType.EventReceiver.ToString();
var blockBlob = await GetCloudBlockBlob(#event, binder);
logger.LogInformation($"Start uploading invoice message into blob storage." +
$" requestID:{#event.RequestID}");
await blockBlob.UploadTextAsync(#event.AsJson());
logger.LogInformation($"Successfully uploaded invoice message into blob storage." +
$" requestID:{#event.RequestID}");
//Publishing message into Service Bus Topic
await PublishMessageIntoNotificationMonitoringAsync(#event, logger);
return new InvoiceResultDto
{
RequestId = #event.RequestID,
OverallStatus = ResultType.Success.ToString().ToUpper()
};
}
#endregion
#region private methods
/// <summary>
/// GetCloudBlockBlob
/// </summary>
/// <param name="binder"></param>
/// <param name="fileName"></param>
/// <returns></returns>
private async Task<CloudBlockBlob> GetCloudBlockBlob(InvoiceMessageEvent #event, Binder binder)
{
var container = $"{BlobStorage.ContainerName}";
var attributes = new Attribute[]
{
new BlobAttribute($"{container}", FileAccess.ReadWrite),
new StorageAccountAttribute(StorageConnectionString.Name)
};
var outputContainer = await binder.BindAsync<CloudBlobContainer>(attributes);
await outputContainer.CreateIfNotExistsAsync();
var blockBlob = outputContainer.GetBlockBlobReference($"messages/{#event.RequestID}.json");
return blockBlob;
}
/// <summary>
/// Publish message into NotificationMonitoring Service Bus Topic
/// </summary>
/// <param name="event"></param>
/// <returns></returns>
private async Task PublishMessageIntoNotificationMonitoringAsync(InvoiceMessageEvent #event, ILogger logger)
{
var serviceBusTopicNotificationMonitoring = Environment.GetEnvironmentVariable(ServiceBusTopicNotificationMonitoring);
Ensure.ConditionIsMet(serviceBusTopicNotificationMonitoring.IsNotNullOrEmpty(),
() => throw new ArgumentNullException($"{nameof(ServiceBusTopicNotificationMonitoring)} not configured"));
logger.LogInformation($"Start publishing the invoice message with requestID: {#event.RequestID} " +
$"into service bus topic: {serviceBusTopicNotificationMonitoring}");
var serviceBusClient = messageBusFactory.GetClient(serviceBusTopicNotificationMonitoring);
await serviceBusClient.PublishMessageAsync<InvoiceMessageEvent>(#event, #event.RequestID, nameof(#event.MonitoringType), #event.MonitoringType);
logger.LogInformation($"Successfully published the invoice message with requestID: {#event.RequestID} " +
$"into service bus topic: {serviceBusTopicNotificationMonitoring}");
}
#endregion
}
}

Custom auth "The oauth state was missing or invalid"

My team and I created a custom OAuth to be used for external SSO. It works on localhost but as soon as we take it up to our staging environment we get an "The oauth state was missing or invalid." error.
We used "https://auth0.com/" for testing.
To try and troubleshoot this we overrode the following built in methods and via breakpoints can see that Query state comes back null.
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri);
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync();
I need some help figuring out why this is a problem on staging and not on local as we are a bit stumped. A theory we have is that the decoder used inside these methods change on var properties = Options.StateDataFormat.Unprotect(state); and thus because they aren't the same they can't decode each others states. I will put our implementation below, if its required I can paste the built in methods as well but I can't fathom the problem lying with the built in functions.
Startup:
foreach (var customAuthItem in customAuthList)
{
services.AddAuthentication().AddCustom(customAuthItem.CampaignId, options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.AuthorizationEndpoint = customAuthItem.AuthEndpoint;
options.TokenEndpoint = customAuthItem.TokenEndpoint;
options.UserInformationEndpoint = customAuthItem.UserInfoEndpoint;
options.ClientId = customAuthItem.ClientId;
options.ClientSecret = customAuthItem.ClientSecret;
});
}
Options:
public class CustomAuthenticationOptions : OAuthOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomAuthenticationOptions"/> class.
/// </summary>
public CustomAuthenticationOptions()
{
ClaimsIssuer = CustomAuthenticationDefaults.Issuer;
CallbackPath = CustomAuthenticationDefaults.CallbackPath;
AuthorizationEndpoint = CustomAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = CustomAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = CustomAuthenticationDefaults.UserInformationEndpoint;
Scope.Add("openid");
Scope.Add("profile");
Scope.Add("email");
ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
}
/// Gets the list of fields to retrieve from the user information endpoint.
/// </summary>
public ISet<string> Fields { get; } = new HashSet<string>
{
"email",
"name",
"sub"
};
Defaults:
public static class CustomAuthenticationDefaults
{
/// <summary>
/// Default value for <see cref="AuthenticationScheme.Name"/>.
/// </summary>
public const string AuthenticationScheme = "Custom";
/// <summary>
/// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
/// </summary>
public static readonly string DisplayName = "Custom";
/// <summary>
/// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
/// </summary>
public static readonly string Issuer = "Custom";
/// <summary>
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
/// </summary>
public static readonly string CallbackPath = "/signin-custom";
/// <summary>
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
/// </summary>
public static readonly string AuthorizationEndpoint = "https://dev-egd511ku.us.auth0.com/authorize";
/// <summary>
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
/// </summary>
public static readonly string TokenEndpoint = "https://dev-egd511ku.us.auth0.com/oauth/token";
/// <summary>
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
public static readonly string UserInformationEndpoint = "https://dev-egd511ku.us.auth0.com/userinfo";
}
Handler:
protected override async Task<AuthenticationTicket> CreateTicketAsync(
[NotNull] ClaimsIdentity identity,
[NotNull] AuthenticationProperties properties,
[NotNull] OAuthTokenResponse tokens)
{
Serilog.Log.Debug("CustomAuthenticationHandler.CreateTicketAsync: STARTED!");
string endpoint = Options.UserInformationEndpoint;
if (Options.Fields.Count > 0)
{
endpoint = QueryHelpers.AddQueryString(endpoint, "fields", string.Join(',', Options.Fields));
}
using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
Serilog.Log.Debug("CustomAuthenticationHandler.CreateTicketAsync: ABOUT TO SEND REQUEST!");
using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
Serilog.Log.Debug($"CustomAuthenticationHandler.CreateTicketAsync: FAILED REQUEST: {response.ReasonPhrase}");
await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted);
throw new HttpRequestException("An error occurred while retrieving the user profile from Custom.");
}
var payloadString = await response.Content.ReadAsStringAsync();
Serilog.Log.Debug($"CustomAuthenticationHandler.CreateTicketAsync: PAYLOAD: {payloadString}");
using var payload = JsonDocument.Parse(payloadString);// Context.RequestAborted));
var principal = new ClaimsPrincipal(identity);
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
context.RunClaimActions();
await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
}
EDIT: The error is received after successful login and after being redirected back to our site. I can see through sentry breadcrumbs that the states are correct so it seems to be a decryption issue.
It turns out the problem is because I AddAuthentication() twice, it ignores the follow-up registration of the auth methods, resulting in only one OAuth working. This is a bit of a problem because we want to support multiple SSO options for our clients, but might need to figure out a different approach. I am just glad I finally know where the problem is.

Odata supported endpoint cannot output count?

I am currently having an API endpoint, which using EF fetches data from the database as such:
Controller:
/// <summary>
/// The View Controller Class
/// </summary>
[ApiController]
[Authorize]
[Route(ApiEndpointConfiguration.External)]
public class ViewController : ControllerBase
{
private readonly IViewService viewService;
private readonly ILogger logger;
private readonly IUserContext userContext;
/// <summary>
/// Constructs ViewController
/// </summary>
/// <param name="viewService">IViewService</param>
/// <param name="userContext">IUserContext</param>
/// <param name="loggerFactory">ILoggerFactory</param>
/// <param name="options">LoggerOptions</param>
public ViewController(
IViewService viewService,
IUserContext userContext,
ILoggerFactory loggerFactory,
LoggerOptions options)
{
this.userContext = userContext;
this.viewService = viewService;
logger = loggerFactory.CreateSystemLogger<ViewController>("Log", options);
}
/// <summary>
/// Retrieve TableViews you can view
/// OData is enabled to allow for building queries
/// </summary>
/// <returns>TableView</returns>
/// <response code="200">TableView</response>
/// <response code="401">Unauthorized</response>
/// <response code="403">Forbidden</response>
/// <response code="404">Not Found</response>
/// <response code="500">Internal Server Error</response>
[HttpGet]
[EnableQuery()]
public ActionResult Get()
{
logger.Information($"A GET request was performed by {HttpContext.Connection.Id}");
var userIdParameter = HttpContext.Request.Query["userId"];
Guid userId = string.IsNullOrEmpty(userIdParameter) ? userContext.UserId : Guid.Parse(userIdParameter);
bool isAllowed = userContext.CanExecute("AdminView");
if (!isAllowed && userContext.UserId != userId)
{
throw new InvalidPermissionsException(userContext.UserId,
"User does not have permission to get the view");
}
var views = viewService
.Get()
.Where(x => x.UserId == userId || x.UserId == null)
.ToList();
return Ok(views.ToViewModel());
}
I am able to filter the result using url as
https://localhost:5001/external/api/View?$filter=targetSystem%20eq%20%27frontpage%27%20and%20targetName%20eq%20%27car%27
But when I add count I don't see the count value anywhere?
why?
https://localhost:5001/external/api/View?$filter=targetSystem%20eq%20%27frontpage%27%20and%20targetName%20eq%20%27car%27&$count=true
in my startup.cs I have configured it as such
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection();
routeBuilder.Expand().Select().Count().OrderBy().Filter();
});
so count should be included? why is it not being parsed in my output?
I am interested in knowing the sql generated by odata when count is being queried, but it seems like count does not respond at all..
This is a sample OData request and response using count:
Request
https://localhost:5000/api/v1/HomepageBanner?%24filter=id%20eq%202&%24count=true
Response body
{
"#odata.context": "https://localhost:5000/api/v1/$metadata#HomepageBanner",
"#odata.count": 0,
"value": []
}
As you can see from the response, the one you are looking at is "#odata.count"
Edit:
To be honest, I can't see why you could filter but can't get the count, you could do filter means that the OData endpoint was working.
Maybe you could compare your solution with what I'm doing on my github repo?
https://github.com/martinussuherman/odata-test

DesignAutomationClient() object creation fails with System.TypeLoadException

I'm thinking I must be missing something obvious, but im trying to create a new DesignAutomationClient object like follows:
private void runDAButton_Click(object sender, EventArgs e)
{
createWorkItem();
}
private async Task createWorkItem()
{
var forgeConfig = new Autodesk.Forge.Core.ForgeConfiguration();
forgeConfig.ClientId = clientID;
forgeConfig.ClientSecret = clientSecret;
var apiInstance = new DesignAutomationClient();
// Code to create work item will go here
}
but when I do, the following error appears in my Visual Studio Debug/Immediate window after trying to execute the var apiInstance = new DesignAutomationClient(); line:
Exception thrown: 'System.TypeLoadException' in mscorlib.dll
Am i missing something obvious? The design automation client was downloaded using NuGet so i should have all the required depencies, but searches of forums for this kind of error all say it means I'm either missing a DLL file, or the type I'm looking for doesn't exist within a DLL, neither of which I believe are true.
This code is in a simple windows form application written in C#
There are no web servers or ASP.NET involved.
The user clicks a button on the form which runs the runDAButton_Click function(which in turn runs the createWorkItem() function). That function should create an instance of the API, and then use it to create my work item.
Can anyone help?
We need more information to troubleshoot, is it a ASP .NET core? how you are handling DI
but if your app is .NET core console app from the code as it appears.
The right way to do is.
dotnet new console
dotnet add package Autodesk.Forge.DesignAutomation --version 3.0.3
Code:
namespace daconsole
{
using Autodesk.Forge.Core;
using Autodesk.Forge.DesignAutomation;
using Autodesk.Forge.DesignAutomation.Model;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Defines the <see cref="ConsoleHost" />.
/// </summary>
class ConsoleHost : IHostedService
{
/// <summary>
/// The StartAsync.
/// </summary>
/// <param name="cancellationToken">The cancellationToken<see cref="CancellationToken"/>.</param>
/// <returns>The <see cref="Task"/>.</returns>
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// The StopAsync.
/// </summary>
/// <param name="cancellationToken">The cancellationToken<see cref="CancellationToken"/>.</param>
/// <returns>The <see cref="Task"/>.</returns>
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
/// <summary>
/// Defines the <see cref="App" />.
/// </summary>
internal class App
{
/// <summary>
/// Defines the api.
/// </summary>
public DesignAutomationClient api;
/// <summary>
/// Defines the config.
/// </summary>
public ForgeConfiguration config;
/// <summary>
/// Initializes a new instance of the <see cref="App"/> class.
/// </summary>
/// <param name="api">The api<see cref="DesignAutomationClient"/>.</param>
/// <param name="config">The config<see cref="IOptions{ForgeConfiguration}"/>.</param>
public App(DesignAutomationClient api, IOptions<ForgeConfiguration> config)
{
this.api = api;
this.config = config.Value;
}
/// <summary>
/// The CreateWorkItem.
/// </summary>
/// <returns>The <see cref="Task"/>.</returns>
private async Task CreateWorkItem()
{
//step1:
var forgeEnginesApi = api.EnginesApi;
ApiResponse<Page<string>> engines = await forgeEnginesApi.GetEnginesAsync();
if (engines.HttpResponse.IsSuccessStatusCode)
{
Console.WriteLine(JsonConvert.SerializeObject(engines.Content, Formatting.Indented));
}
//step2:
Console.WriteLine("\nActiviy Start");
var activitiesApi = api.ActivitiesApi;
ApiResponse<Page<string>> activitiesResp = await activitiesApi.GetActivitiesAsync();
List<string> listOfActivities = new List<string>();
string activityName = null;
if (activitiesResp.HttpResponse.IsSuccessStatusCode)
{
var page = activitiesResp.Content.PaginationToken;
activitiesResp.Content.Data.ForEach(e => listOfActivities.Add(e));
while (page != null)
{
activitiesResp = await activitiesApi.GetActivitiesAsync(page);
page = activitiesResp.Content.PaginationToken;
activitiesResp.Content.Data.ForEach(e => listOfActivities.Add(e));
}
var activities = listOfActivities.Where(a => a.Contains("PlotToPDF")).Select(a => a);
if (activities.Count() > 0)
{
activityName = activities.FirstOrDefault();
}
}
//step3:
Console.WriteLine("\nWorkItem Start...");
var workItemsApi = api.WorkItemsApi;
ApiResponse<WorkItemStatus> workItemStatus = await workItemsApi.CreateWorkItemAsync(new Autodesk.Forge.DesignAutomation.Model.WorkItem()
{
ActivityId = activityName,
Arguments = new Dictionary<string, IArgument>() {
{
"HostDwg",
new XrefTreeArgument() {
Url = "http://download.autodesk.com/us/samplefiles/acad/blocks_and_tables_-_metric.dwg",
Verb = Verb.Get
}
}, {
"Result",
new XrefTreeArgument() {
Verb = Verb.Put, Url = "azure blob storage url",
Headers = new Dictionary<string,string>()
{
{ "Content-Type","application/octet-stream" },
{ "x-ms-blob-type","BlockBlob" }
}
}
}
}
});
Console.Write("\tPolling status");
while (!workItemStatus.Content.Status.IsDone())
{
await Task.Delay(TimeSpan.FromSeconds(2));
workItemStatus = await workItemsApi.GetWorkitemStatusAsync(workItemStatus.Content.Id);
Console.Write(".");
}
Console.WriteLine(JsonConvert.SerializeObject(workItemStatus.Content, Formatting.Indented));
}
/// <summary>
/// The RunAsync.
/// </summary>
/// <returns>The <see cref="Task"/>.</returns>
public async Task RunAsync()
{
await CreateWorkItem();
}
}
/// <summary>
/// Defines the <see cref="Program" />.
/// </summary>
internal class Program
{
/// <summary>
/// The Main.
/// </summary>
/// <param name="args">The args<see cref="string[]"/>.</param>
/// <returns>The <see cref="Task"/>.</returns>
static async Task Main(string[] args)
{
var host = new HostBuilder()
.ConfigureAppConfiguration(builder =>
{
builder.AddEnvironmentVariables();
builder.AddForgeAlternativeEnvironmentVariables();
}).ConfigureServices((hostContext, services) =>
{ // add our no-op host (required by the HostBuilder)
services.AddHostedService<ConsoleHost>();
// our own app where all the real stuff happens
services.AddSingleton<App>();
// add and configure DESIGN AUTOMATION
services.AddDesignAutomation(hostContext.Configuration);
services.AddOptions();
})
.UseConsoleLifetime()
.Build();
using (host)
{
await host.StartAsync();
// Get a reference to our App and run it
var app = host.Services.GetRequiredService<App>();
await app.RunAsync();
await host.StopAsync();
}
}
}
}
add Forge Env to your launchSettings.json
{
"profiles": {
"daconsole": {
"commandName": "Project",
"environmentVariables": {
"FORGE_CLIENT_SECRET": "",
"FORGE_CLIENT_ID": ""
}
}
}
}
To run:
dotnet run --launch-profile daconsole

2-legged OAuth with DotNetOpenAuth and Twitter. Getting a 401 error

I'm trying to get 2-legged oauth with twitter working to be able to retrieve tweets to display on websites. For this i want to use DotNetOpenAuth. To get me started i used the OAuthConsumer example that comes with DotNetOpenAuth.
Below is the codebehind code i have now. Here i use either the 3-legged part or the 2-legged part. The 3-legged part works fine (this is based on Twitter.aspx from the sample). The 2-legged part (based on GoogleApps2Legged.aspx) however gives a 401 error on twitter.RequestNewClientAccount();
private string AccessToken
{
get { return (string)Session["TwitterAccessToken"]; }
set { Session["TwitterAccessToken"] = value; }
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// 3 legged
var twitter = new WebConsumer(TwitterConsumer.ServiceDescription, TwitterConsumer.ShortTermUserSessionTokenManager);
//// Is Twitter calling back with authorization?
var accessTokenResponse = twitter.ProcessUserAuthorization();
if (accessTokenResponse != null)
{
this.AccessToken = accessTokenResponse.AccessToken;
}
else if (this.AccessToken == null)
{
// If we don't yet have access, immediately request it.
twitter.Channel.Send(twitter.PrepareRequestUserAuthorization());
}
var response = TwitterConsumer.GetUserTimeLine(twitter, this.AccessToken);
// 2 legged
var twitter = new WebConsumer(TwitterConsumer.ServiceDescription, TwitterConsumer.ShortTermUserSessionTokenManager);
this.AccessToken = twitter.RequestNewClientAccount();
var response = twitter.PrepareAuthorizedRequestAndSend(TwitterConsumer.GetUserTimelineStatusEndpoint, this.AccessToken);
}
}
TwitterConsumer.cs
//-----------------------------------------------------------------------
// <copyright file="TwitterConsumer.cs" company="Outercurve Foundation">
// Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.ApplicationBlock {
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Net;
using System.Web;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth;
using DotNetOpenAuth.OAuth.ChannelElements;
/// <summary>
/// A consumer capable of communicating with Twitter.
/// </summary>
public static class TwitterConsumer {
/// <summary>
/// The description of Twitter's OAuth protocol URIs for use with actually reading/writing
/// a user's private Twitter data.
/// </summary>
public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription {
RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/access_token", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
};
/// <summary>
/// The description of Twitter's OAuth protocol URIs for use with their "Sign in with Twitter" feature.
/// </summary>
public static readonly ServiceProviderDescription SignInWithTwitterServiceDescription = new ServiceProviderDescription {
RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/access_token", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
};
/// <summary>
/// The URI to get a user's favorites.
/// </summary>
private static readonly MessageReceivingEndpoint GetFavoritesEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/favorites.xml", HttpDeliveryMethods.GetRequest);
/// <summary>
/// The URI to get the data on the user's home page.
/// </summary>
public static readonly MessageReceivingEndpoint GetFriendTimelineStatusEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/statuses/friends_timeline.xml", HttpDeliveryMethods.GetRequest);
public static readonly MessageReceivingEndpoint UpdateProfileBackgroundImageEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/account/update_profile_background_image.xml", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest);
public static readonly MessageReceivingEndpoint UpdateProfileImageEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/account/update_profile_image.xml", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest);
public static readonly MessageReceivingEndpoint VerifyCredentialsEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/account/verify_credentials.xml", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest);
public static readonly MessageReceivingEndpoint GetUserTimelineStatusEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=concepts2go", HttpDeliveryMethods.GetRequest);
/// <summary>
/// The consumer used for the Sign in to Twitter feature.
/// </summary>
private static WebConsumer signInConsumer;
/// <summary>
/// The lock acquired to initialize the <see cref="signInConsumer"/> field.
/// </summary>
private static object signInConsumerInitLock = new object();
/// <summary>
/// Initializes static members of the <see cref="TwitterConsumer"/> class.
/// </summary>
static TwitterConsumer() {
// Twitter can't handle the Expect 100 Continue HTTP header.
ServicePointManager.FindServicePoint(GetFavoritesEndpoint.Location).Expect100Continue = false;
}
/// <summary>
/// Gets a value indicating whether the Twitter consumer key and secret are set in the web.config file.
/// </summary>
public static bool IsTwitterConsumerConfigured {
get {
return true;
}
}
/// <summary>
/// Gets the consumer to use for the Sign in to Twitter feature.
/// </summary>
/// <value>The twitter sign in.</value>
private static WebConsumer TwitterSignIn {
get {
if (signInConsumer == null) {
lock (signInConsumerInitLock) {
if (signInConsumer == null) {
signInConsumer = new WebConsumer(SignInWithTwitterServiceDescription, ShortTermUserSessionTokenManager);
}
}
}
return signInConsumer;
}
}
public static InMemoryTokenManager ShortTermUserSessionTokenManager {
get {
var store = HttpContext.Current.Session;
var tokenManager = (InMemoryTokenManager)store["TwitterShortTermUserSessionTokenManager"];
if (tokenManager == null) {
string consumerKey = "dgnwPLz40rcvmGKn0SugfA";
string consumerSecret = "IvaV0cA3N146mXTXPH5EaEHX8XbULch1vwBYpeI4Xc";
if (IsTwitterConsumerConfigured) {
tokenManager = new InMemoryTokenManager(consumerKey, consumerSecret);
store["TwitterShortTermUserSessionTokenManager"] = tokenManager;
} else {
throw new InvalidOperationException("No Twitter OAuth consumer key and secret could be found in web.config AppSettings.");
}
}
return tokenManager;
}
}
public static XDocument GetUpdates(ConsumerBase twitter, string accessToken) {
IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetFriendTimelineStatusEndpoint, accessToken);
return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
public static IncomingWebResponse GetUserTimeLine(ConsumerBase twitter, string accessToken)
{
IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetUserTimelineStatusEndpoint, accessToken);
return response;
//return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
public static XDocument GetFavorites(ConsumerBase twitter, string accessToken) {
IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetFavoritesEndpoint, accessToken);
return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
public static XDocument UpdateProfileBackgroundImage(ConsumerBase twitter, string accessToken, string image, bool tile) {
var parts = new[] {
MultipartPostPart.CreateFormFilePart("image", image, "image/" + Path.GetExtension(image).Substring(1).ToLowerInvariant()),
MultipartPostPart.CreateFormPart("tile", tile.ToString().ToLowerInvariant()),
};
HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileBackgroundImageEndpoint, accessToken, parts);
request.ServicePoint.Expect100Continue = false;
IncomingWebResponse response = twitter.Channel.WebRequestHandler.GetResponse(request);
string responseString = response.GetResponseReader().ReadToEnd();
return XDocument.Parse(responseString);
}
public static XDocument UpdateProfileImage(ConsumerBase twitter, string accessToken, string pathToImage) {
string contentType = "image/" + Path.GetExtension(pathToImage).Substring(1).ToLowerInvariant();
return UpdateProfileImage(twitter, accessToken, File.OpenRead(pathToImage), contentType);
}
public static XDocument UpdateProfileImage(ConsumerBase twitter, string accessToken, Stream image, string contentType) {
var parts = new[] {
MultipartPostPart.CreateFormFilePart("image", "twitterPhoto", contentType, image),
};
HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileImageEndpoint, accessToken, parts);
IncomingWebResponse response = twitter.Channel.WebRequestHandler.GetResponse(request);
string responseString = response.GetResponseReader().ReadToEnd();
return XDocument.Parse(responseString);
}
public static XDocument VerifyCredentials(ConsumerBase twitter, string accessToken) {
IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(VerifyCredentialsEndpoint, accessToken);
return XDocument.Load(XmlReader.Create(response.GetResponseReader()));
}
public static string GetUsername(ConsumerBase twitter, string accessToken) {
XDocument xml = VerifyCredentials(twitter, accessToken);
XPathNavigator nav = xml.CreateNavigator();
return nav.SelectSingleNode("/user/screen_name").Value;
}
/// <summary>
/// Prepares a redirect that will send the user to Twitter to sign in.
/// </summary>
/// <param name="forceNewLogin">if set to <c>true</c> the user will be required to re-enter their Twitter credentials even if already logged in to Twitter.</param>
/// <returns>The redirect message.</returns>
/// <remarks>
/// Call <see cref="OutgoingWebResponse.Send"/> or
/// <c>return StartSignInWithTwitter().<see cref="MessagingUtilities.AsActionResult">AsActionResult()</see></c>
/// to actually perform the redirect.
/// </remarks>
public static OutgoingWebResponse StartSignInWithTwitter(bool forceNewLogin) {
var redirectParameters = new Dictionary<string, string>();
if (forceNewLogin) {
redirectParameters["force_login"] = "true";
}
Uri callback = MessagingUtilities.GetRequestUrlFromContext().StripQueryArgumentsWithPrefix("oauth_");
var request = TwitterSignIn.PrepareRequestUserAuthorization(callback, null, redirectParameters);
return TwitterSignIn.Channel.PrepareResponse(request);
}
/// <summary>
/// Checks the incoming web request to see if it carries a Twitter authentication response,
/// and provides the user's Twitter screen name and unique id if available.
/// </summary>
/// <param name="screenName">The user's Twitter screen name.</param>
/// <param name="userId">The user's Twitter unique user ID.</param>
/// <returns>
/// A value indicating whether Twitter authentication was successful;
/// otherwise <c>false</c> to indicate that no Twitter response was present.
/// </returns>
public static bool TryFinishSignInWithTwitter(out string screenName, out int userId) {
screenName = null;
userId = 0;
var response = TwitterSignIn.ProcessUserAuthorization();
if (response == null) {
return false;
}
screenName = response.ExtraData["screen_name"];
userId = int.Parse(response.ExtraData["user_id"]);
// If we were going to make this LOOK like OpenID even though it isn't,
// this seems like a reasonable, secure claimed id to allow the user to assume.
OpenId.Identifier fake_claimed_id = string.Format(CultureInfo.InvariantCulture, "http://twitter.com/{0}#{1}", screenName, userId);
return true;
}
}
}
InMemoryTokenManager.cs
//-----------------------------------------------------------------------
// <copyright file="InMemoryTokenManager.cs" company="Outercurve Foundation">
// Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.ApplicationBlock {
using System;
using System.Collections.Generic;
using System.Diagnostics;
using DotNetOpenAuth.OAuth.ChannelElements;
using DotNetOpenAuth.OAuth.Messages;
using DotNetOpenAuth.OpenId.Extensions.OAuth;
/// <summary>
/// A token manager that only retains tokens in memory.
/// Meant for SHORT TERM USE TOKENS ONLY.
/// </summary>
/// <remarks>
/// A likely application of this class is for "Sign In With Twitter",
/// where the user only signs in without providing any authorization to access
/// Twitter APIs except to authenticate, since that access token is only useful once.
/// </remarks>
public class InMemoryTokenManager : IConsumerTokenManager, IOpenIdOAuthTokenManager {
private Dictionary<string, string> tokensAndSecrets = new Dictionary<string, string>();
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryTokenManager"/> class.
/// </summary>
/// <param name="consumerKey">The consumer key.</param>
/// <param name="consumerSecret">The consumer secret.</param>
public InMemoryTokenManager(string consumerKey, string consumerSecret) {
if (string.IsNullOrEmpty(consumerKey)) {
throw new ArgumentNullException("consumerKey");
}
this.ConsumerKey = consumerKey;
this.ConsumerSecret = consumerSecret;
}
/// <summary>
/// Gets the consumer key.
/// </summary>
/// <value>The consumer key.</value>
public string ConsumerKey { get; private set; }
/// <summary>
/// Gets the consumer secret.
/// </summary>
/// <value>The consumer secret.</value>
public string ConsumerSecret { get; private set; }
#region ITokenManager Members
/// <summary>
/// Gets the Token Secret given a request or access token.
/// </summary>
/// <param name="token">The request or access token.</param>
/// <returns>
/// The secret associated with the given token.
/// </returns>
/// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception>
public string GetTokenSecret(string token) {
return this.tokensAndSecrets[token];
}
/// <summary>
/// Stores a newly generated unauthorized request token, secret, and optional
/// application-specific parameters for later recall.
/// </summary>
/// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param>
/// <param name="response">The response message that includes the unauthorized request token.</param>
/// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception>
/// <remarks>
/// Request tokens stored by this method SHOULD NOT associate any user account with this token.
/// It usually opens up security holes in your application to do so. Instead, you associate a user
/// account with access tokens (not request tokens) in the <see cref="ExpireRequestTokenAndStoreNewAccessToken"/>
/// method.
/// </remarks>
public void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response) {
this.tokensAndSecrets[response.Token] = response.TokenSecret;
}
/// <summary>
/// Deletes a request token and its associated secret and stores a new access token and secret.
/// </summary>
/// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param>
/// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param>
/// <param name="accessToken">The new access token that is being issued to the Consumer.</param>
/// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param>
/// <remarks>
/// <para>
/// Any scope of granted privileges associated with the request token from the
/// original call to <see cref="StoreNewRequestToken"/> should be carried over
/// to the new Access Token.
/// </para>
/// <para>
/// To associate a user account with the new access token,
/// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be
/// useful in an ASP.NET web application within the implementation of this method.
/// Alternatively you may store the access token here without associating with a user account,
/// and wait until <see cref="WebConsumer.ProcessUserAuthorization()"/> or
/// <see cref="DesktopConsumer.ProcessUserAuthorization(string, string)"/> return the access
/// token to associate the access token with a user account at that point.
/// </para>
/// </remarks>
public void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) {
this.tokensAndSecrets.Remove(requestToken);
this.tokensAndSecrets[accessToken] = accessTokenSecret;
}
/// <summary>
/// Classifies a token as a request token or an access token.
/// </summary>
/// <param name="token">The token to classify.</param>
/// <returns>Request or Access token, or invalid if the token is not recognized.</returns>
public TokenType GetTokenType(string token) {
throw new NotImplementedException();
}
#endregion
#region IOpenIdOAuthTokenManager Members
/// <summary>
/// Stores a new request token obtained over an OpenID request.
/// </summary>
/// <param name="consumerKey">The consumer key.</param>
/// <param name="authorization">The authorization message carrying the request token and authorized access scope.</param>
/// <remarks>
/// <para>The token secret is the empty string.</para>
/// <para>Tokens stored by this method should be short-lived to mitigate
/// possible security threats. Their lifetime should be sufficient for the
/// relying party to receive the positive authentication assertion and immediately
/// send a follow-up request for the access token.</para>
/// </remarks>
public void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization) {
this.tokensAndSecrets[authorization.RequestToken] = string.Empty;
}
#endregion
}
}
Twitter doesn't really support what I call 2-legged OAuth. It only does "0-legged OAuth". Since the ConsumerBase.RequestNewClientAccount method that you're calling is for 2-legged OAuth, it's failing.
For 0-legged OAuth, you need to create an InMemoryTokenManager that is prefilled with your consumer key, secret and access token and secret. Then pass that token manager into your ConsumerBase-derived type (WebConsumer or DesktopConsumer) and begin making authorized calls.
Or much more simply, you can download the DotNetOpenAuth v4.3 preview that includes an DelegatingHandler where you can completely skip the above steps and just inject your key, token and secrets into one simple method and start making calls with HttpClient that are automatically OAuth 1 signed.

Categories

Resources