Is there a way in C# to send a notification to a user (personal message) from a bot? I only found solutions to send messages to a channel/group where my bot is added with Admin rights.
I want to send notifications - if allowed - from my system to the user when logged in / system notifications.
Later on I want to enable the user to send commands to this bot to action some key functionalities in the system - such as, but not limited to, adding new users. With the necessary administrator rights of course.
I am posting this answer as I solved my own question after several hours of researching and testing.
First off, I created a EventHandler class to handle all incoming messages to my Telegram bot:
public class TelegramBotEvents
{
private readonly TelegramBotClient _telegramBotClient;
private readonly ODWorkflowDBContext _context;
public TelegramBotEvents(
ODWorkflowDBContext context,
TelegramBotClient telegramBotClient
)
{
this._context = context ?? throw new ArgumentNullException(nameof(context));
this._telegramBotClient = telegramBotClient ?? throw new ArgumentNullException(nameof(telegramBotClient));
#pragma warning disable CS0618 // Type or member is obsolete. TODO: Implement Polling
this._telegramBotClient.StartReceiving();
this._telegramBotClient.OnMessage += (sender, eventArg) =>
{
if (eventArg.Message != null)
{
MessageReceive(eventArg.Message);
}
};
#pragma warning restore CS0618 // Type or member is obsolete. TODO: Implement Polling
}
public void MessageReceive(Message message)
{
try
{
if (message.Type == MessageType.Text)
{
if (message.Text.ToLower() == "/register")
{
var userThirdPartyNotification = this._context.UserThirdPartyNotifications.FirstOrDefault(e =>
e.UserThirdPartyNotificationActive &&
e.UserThirdPartyNotificationUserName == message.From.Username);
userThirdPartyNotification.UserThirdPartyNotificationChatId = message.From.Id;
this._context.UserThirdPartyNotifications.Update(userThirdPartyNotification);
this._context.SaveChanges();
this._telegramBotClient.SendTextMessageAsync(message.From.Id, "You have successfully opted in to receive notifications via Telegram.");
}
}
else
{
this._telegramBotClient.SendTextMessageAsync(message.From.Id, $"\"{message.Text}\" was not found. Please try again.");
}
}
catch (Exception ex)
{
this._telegramBotClient.SendTextMessageAsync(message.From.Id, $"Failure processing your message: {ex.Message}");
}
}
}
After the above I Injected this class in my Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddSession();
#region Dependency_Injections
services.AddScoped<IPasswordEncrypt, PasswordEncrypt>();
services.AddScoped<IHelperFunctions, HelperFunctions>();
services.AddScoped<IEncryptor, Encryptor>();
services.AddTransient(typeof(ILogging<>), typeof(Logging<>));
services.AddSingleton(new TelegramBotClient(Configuration["ArtificialConfigs:TelegramBotToken"]));
services.AddScoped(p =>
new TelegramBotEvents(
(DBContext)p.GetService(typeof(DBContext)),
(TelegramBotClient)p.GetService(typeof(TelegramBotClient))));
#endregion
services.AddControllersWithViews();
services.AddRazorPages();
}
After these two steps I could capture the client id from the user that sent any message to my bot.
Once any user logins - with permissions granted inside the web system - they will receive personal messages from my bot inside Telegram.
Related
I am new to both MassTransit and Azure Service Bus. I am attempting to use an architecture where either RabbitMq or Azure Service Bus is used in a .NET Core 3.1 API. I have the RabbitMq portion working and just started on the Azure Service Bus. I have an API that will take an incoming payload and publish it to a queue. When I attempt to publish via the Azure Service Bus approach, I get an error "SubCode=40000. Cannot operate on type Topic because the namespace 'servicehubqa' is using 'Basic' tier.
I am attempting to use a queue approach and am hoping to create the queue as messages are published. Currently, the service bus is using a Basic pricing tier as the documentation says that I can play with queues at that level. I am not sure if I need to manually create the queue (I had to do this approach using RabbitMq since no queue would be created if no consumer exists). Is topic the default approach if nothing is specified? How do I specify queue vs topic?
My code is as follows below.
Startup - ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(Configuration);
services.AddScoped<IMassTransitRabbitMqTransport, MassTransitRabbitMqTransport>();
services.AddScoped<IMassTransitAzureServiceBusTransport, MassTransitAzureServiceBusTransport>();
var messageProvider = ConfigProvider.GetConfig("MessageService", "Messaging_Service");
switch (messageProvider)
{
case "AzureServiceBus":
services.AddScoped<IMessagingService, MassTransitAzureServiceBusMessagingService>();
break;
case "RabbitMq":
services.AddScoped<IMessagingService, MassTransitRabbitMqMessagingService>();
break;
default:
throw new ArgumentException("Invalid message service");
};
services.AddControllers();
}
Controller
public class ListenerController : ControllerBase
{
readonly ILogger<ListenerController> logger;
readonly IMessagingService messenger;
public ListenerController(
ILogger<ListenerController> logger,
IMessagingService messenger)
{
this.logger = logger;
this.messenger = messenger;
}
[HttpPost]
public async Task<IActionResult> Post()
{
var payload = new
{
...
};
await messenger.Publish(payload);
return Ok();
}
}
IMessagingService
public interface IMessagingService
{
Task Publish(object payload);
}
IMassTransitTransport
public interface IMassTransitTransport
{
IBusControl BusControl { get; }
}
public interface IMassTransitRabbitMqTransport : IMassTransitTransport { }
public interface IMassTransitAzureServiceBusTransport : IMassTransitTransport { }
MassTransitAzureServiceBusTransport
public sealed class MassTransitAzureServiceBusTransport : IMassTransitAzureServiceBusTransport
{
public IBusControl BusControl { get; }
public MassTransitAzureServiceBusTransport()
{
BusControl = ConfigureBus();
BusControl.StartAsync();
}
IBusControl ConfigureBus()
{
return Bus.Factory.CreateUsingAzureServiceBus(config => {
var host = config.Host(ConfigProvider.GetConfig("AzureServiceBus", "AzureServiceBus_ConnStr"), host => { });
});
}
}
MassTransitAzureServiceBusMessagingService
public class MassTransitAzureServiceBusMessagingService : IMessagingService
{
readonly IMassTransitAzureServiceBusTransport massTransitTransport;
public MassTransitAzureServiceBusMessagingService(IMassTransitAzureServiceBusTransport massTransitTransport)
{
//transport bus config already happens in massTransitTransport constructor
this.massTransitTransport = massTransitTransport;
}
public async Task Publish(object payload)
{
var jsn = Newtonsoft.Json.JsonConvert.SerializeObject(payload);
var cmd = JObject.Parse(jsn)["Command"];
switch (cmd.ToString())
{
case "UPDATESTATUS":
//IRegisterCommandUpdateStatus is an interface specifying the properties needed
await massTransitTransport.BusControl.Publish<IRegisterCommandUpdateStatus>(payload);
break;
default: break;
}
}
}
The Azure Service Bus basic tier does not allow the use of topics. So you would not be able to use publish. That said, MassTransit doesn't really work with the basic tier, despite attempts in the past that may have been successful.
The MassTransit documentation does state that if you want to use a Topic (i.e. the ability to publish to multiple subscriptions at the same time), you use the publish.
If you want to send a message to a queue (the message is routed to a specific location), you use the send and provide the correct information.
Topics require standard pricing and Queues can use basic pricing.
With this information, the MassTransitAzureServiceBusMessagingService would be modified as follows:
Basic Pricing - Queues
public async Task Publish(object payload)
{
var jsn = Newtonsoft.Json.JsonConvert.SerializeObject(payload);
var cmd = JObject.Parse(jsn)["Command"];
switch (cmd.ToString())
{
case "UPDATESTATUS":
var queueUri = new Uri(massTransitTransport.BusControl.Address, "registration.updatestatus");
var endpoint = await massTransitTransport.BusControl.GetSendEndpoint(queueUri);
await endpoint.Send<IRegisterCommandUpdateStatus>(payload);
break;
default: break;
}
}
Standard Pricing - Topics/Subscriptions
public async Task Publish(object payload)
{
var jsn = Newtonsoft.Json.JsonConvert.SerializeObject(payload);
var cmd = JObject.Parse(jsn)["Command"];
switch (cmd.ToString())
{
case "UPDATESTATUS":
await massTransitTransport.BusControl.Publish<IRegisterCommandUpdateStatus>(payload);
break;
default: break;
}
}
var acceptAction = UNNotificationAction.FromIdentifier("AcceptAction", "Accept", UNNotificationActionOptions.None);
var declineAction = UNNotificationAction.FromIdentifier("DeclineAction", "Decline", UNNotificationActionOptions.None);
// Create category
var meetingInviteCategory = UNNotificationCategory.FromIdentifier("MeetingInvitation",
new UNNotificationAction[] { acceptAction, declineAction }, new string[] { }, UNNotificationCategoryOptions.CustomDismissAction);
// Register category
var categories = new UNNotificationCategory[] { meetingInviteCategory };
UNUserNotificationCenter.Current.SetNotificationCategories(new NSSet<UNNotificationCategory>(categories));
how can you receive a custom actionable push notification and where need to put the above code in which file?
Before an iOS app can send notifications to the user the app must be registered with the system and, because a notification is an interruption to the user, an app must explicitly request permission before sending them.
Notification permission should be requested as soon as the app launches by adding the following code to the FinishedLaunching method of the AppDelegate and setting the desired notification type (UNAuthorizationOptions):
...
using UserNotifications;
...
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
....
//after iOS 10
if(UIDevice.CurrentDevice.CheckSystemVersion(10,0))
{
UNUserNotificationCenter center = UNUserNotificationCenter.Current;
center.RequestAuthorization(UNAuthorizationOptions.Alert | UNAuthorizationOptions.Sound | UNAuthorizationOptions.UNAuthorizationOptions.Badge, (bool arg1, NSError arg2) =>
{
});
center.Delegate = new NotificationDelegate();
}
else if(UIDevice.CurrentDevice.CheckSystemVersion(8, 0))
{
var settings = UIUserNotificationSettings.GetSettingsForTypes(UIUserNotificationType.Alert| UIUserNotificationType.Badge| UIUserNotificationType.Sound,new NSSet());
UIApplication.SharedApplication.RegisterUserNotificationSettings(settings);
}
return true;
}
New to iOS 10, an app can handle Notifications differently when it is in the foreground and a Notification is triggered. By providing aUNUserNotificationCenterDelegate and implementing theUserNotificationCentermethod, the app can take over responsibility for displaying the Notification. For example:
using System;
using ObjCRuntime;
using UserNotifications;
namespace xxx
{
public class NotificationDelegate:UNUserNotificationCenterDelegate
{
public NotificationDelegate()
{
}
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
// Do something with the notification
Console.WriteLine("Active Notification: {0}", notification);
// Tell system to display the notification anyway or use
// `None` to say we have handled the display locally.
completionHandler(UNNotificationPresentationOptions.Alert|UNNotificationPresentationOptions.Sound);
}
public override void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler)
{
// Take action based on Action ID
switch (response.ActionIdentifier)
{
case "reply":
// Do something
break;
default:
// Take action based on identifier
if (response.IsDefaultAction)
{
// Handle default action...
}
else if (response.IsDismissAction)
{
// Handle dismiss action
}
break;
}
// Inform caller it has been handled
completionHandler();
}
}
}
This code results in the device receiving a test notification, but there's no call to RegisterNativeAsync unless there's an error. Thus, how does the hub know about the device?
[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
SBNotificationHub Hub { get; set; }
public const string ConnectionString = "Endpoint=xxx";
public const string NotificationHubPath = "xxx";
public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions)
{
var settings = UIUserNotificationSettings.GetSettingsForTypes(UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound, new NSSet());
UIApplication.SharedApplication.RegisterUserNotificationSettings(settings);
UIApplication.SharedApplication.RegisterForRemoteNotifications();
global::Xamarin.Forms.Forms.Init();
LoadApplication(new App());
return base.FinishedLaunching(uiApplication, launchOptions);
}
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
// Create a new notification hub with the connection string and hub path
Hub = new SBNotificationHub(ConnectionString, NotificationHubPath);
// Unregister any previous instances using the device token
Hub.UnregisterAllAsync(deviceToken, (error) =>
{
if (error != null)
{
// Error unregistering
return;
}
// Register this device with the notification hub
Hub.RegisterNativeAsync(deviceToken, null, (registerError) =>
{
if (registerError != null)
{
// Error registering
}
});
});
}
}
According to RegisteredForRemoteNotifications - Xamarin, this method has nothing to do with registering itself.1 As far a I can tell from RegisteredForRemoteNotifications never triggered — Xamarin Forums, applications are supposed to override it, and it serves as a handler that is invoked after the user allows the application to receive push notifications.
In fact, the code you've given is your code, not library's.
1And examining the source code of UnregisterAllAsync in ILSpy decompilation of Xamarin.Azure.NotificationHubs.iOS.dll confirms as such.
I want to make use of Azure notification hub service. In all their examples. The clients directly register with azure to give the service their device token.
I want to change this model slightly in order to gain central control and also due to compatibility with existing clients.
I want all my clients to register with GCM or APNS and obtain their Token. I then want to send that token off to my own api. ASP.NET Web API. The api will then fire off a request to the Azure notification service and register on behalf of the device.
Can I achieve this? and how would I go about registering a device from the asp.net api.
In their Documentation examples it contains code to send the actual push notifications from an asp.net app. But not how to register a device (being of any type) from asp.net
Unless im being daft and missing something...
You can find this in the official documentation: Registering from your App Backend.
public class RegisterController : ApiController
{
private NotificationHubClient hub;
public RegisterController()
{
hub = NotificationHubClient.CreateClientFromConnectionString("Endpoint=sb://buildhub-ns.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=DuWV4SQ08poV6HZly8O/KQNWv3YRTZlExJxu3pNCjGU=", "build2014_2");
}
public class DeviceRegistration
{
public string Platform { get; set; }
public string Handle { get; set; }
public string[] Tags { get; set; }
}
// POST api/register
// This creates a registration id
public async Task<string> Post()
{
return await hub.CreateRegistrationIdAsync();
}
// PUT api/register/5
// This creates or updates a registration (with provided PNS handle) at the specified id
public async void Put(string id, DeviceRegistration deviceUpdate)
{
// IMPORTANT: add logic to make sure that caller is allowed to register for the provided tags
RegistrationDescription registration = null;
switch (deviceUpdate.Platform)
{
case "mpns":
registration = new MpnsRegistrationDescription(deviceUpdate.Handle);
break;
case "wns":
registration = new WindowsRegistrationDescription(deviceUpdate.Handle);
break;
case "apns":
registration = new AppleRegistrationDescription(deviceUpdate.Handle);
break;
case "gcm":
registration = new GcmRegistrationDescription(deviceUpdate.Handle);
break;
default:
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
registration.RegistrationId = id;
registration.Tags = new HashSet<string>(deviceUpdate.Tags);
try
{
await hub.CreateOrUpdateRegistrationAsync(registration);
} catch (MessagingException e) {
ReturnGoneIfHubResponseIsGone(e);
}
}
// DELETE api/register/5
public async void Delete(string id)
{
await hub.DeleteRegistrationAsync(id);
}
private static void ReturnGoneIfHubResponseIsGone(MessagingException e)
{
var webex = e.InnerException as WebException;
if (webex.Status == WebExceptionStatus.ProtocolError)
{
var response = (HttpWebResponse)webex.Response;
if (response.StatusCode == HttpStatusCode.Gone)
throw new HttpRequestException(HttpStatusCode.Gone.ToString());
}
}
}
I'm using ExchangeUserCredentialForToken function to get the token from the Authorization server. It's working fine when my user exists in my databas, but when the credentials are incorect I would like to send back a message to the client. I'm using the following 2 lines of code to set the error message:
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
But on the client side I'm getting only protocol error (error 400). Can you help me how can I get the error message set on the server side on the authorization server?
The full app config from the Authorization server:
using Constants;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using AuthorizationServer.Entities;
using AuthorizationServer.Entities.Infrastructure.Abstract;
using AuthorizationServer.Entities.Infrastructure.Concrete;
namespace AuthorizationServer
{
public partial class Startup
{
private IEmployeeRepository Repository;
public void ConfigureAuth(IAppBuilder app)
{
//instanciate the repository
Repository = new EmployeeRepository();
// Enable Application Sign In Cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Application",
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString(Paths.LoginPath),
LogoutPath = new PathString(Paths.LogoutPath),
});
// Enable External Sign In Cookie
app.SetDefaultSignInAsAuthenticationType("External");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "External",
AuthenticationMode = AuthenticationMode.Passive,
CookieName = CookieAuthenticationDefaults.CookiePrefix + "External",
ExpireTimeSpan = TimeSpan.FromMinutes(5),
});
// Enable google authentication
app.UseGoogleAuthentication();
// Setup Authorization Server
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
AuthorizeEndpointPath = new PathString(Paths.AuthorizePath),
TokenEndpointPath = new PathString(Paths.TokenPath),
ApplicationCanDisplayErrors = true,
#if DEBUG
AllowInsecureHttp = true,
#endif
// Authorization server provider which controls the lifecycle of Authorization Server
Provider = new OAuthAuthorizationServerProvider
{
OnValidateClientRedirectUri = ValidateClientRedirectUri,
OnValidateClientAuthentication = ValidateClientAuthentication,
OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials,
OnGrantClientCredentials = GrantClientCredetails
},
// Authorization code provider which creates and receives authorization code
AuthorizationCodeProvider = new AuthenticationTokenProvider
{
OnCreate = CreateAuthenticationCode,
OnReceive = ReceiveAuthenticationCode,
},
// Refresh token provider which creates and receives referesh token
RefreshTokenProvider = new AuthenticationTokenProvider
{
OnCreate = CreateRefreshToken,
OnReceive = ReceiveRefreshToken,
}
});
// indicate our intent to use bearer authentication
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AuthenticationType = "Bearer",
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active
});
}
private Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == Clients.Client1.Id)
{
context.Validated(Clients.Client1.RedirectUrl);
}
else if (context.ClientId == Clients.Client2.Id)
{
context.Validated(Clients.Client2.RedirectUrl);
}
return Task.FromResult(0);
}
private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientname;
string clientpassword;
if (context.TryGetBasicCredentials(out clientname, out clientpassword) ||
context.TryGetFormCredentials(out clientname, out clientpassword))
{
employee Employee = Repository.GetEmployee(clientname, clientpassword);
if (Employee != null)
{
context.Validated();
}
else
{
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
}
}
return Task.FromResult(0);
}
private Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private readonly ConcurrentDictionary<string, string> _authenticationCodes =
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authenticationCodes[context.Token] = context.SerializeTicket();
}
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
string value;
if (_authenticationCodes.TryRemove(context.Token, out value))
{
context.DeserializeTicket(value);
}
}
private void CreateRefreshToken(AuthenticationTokenCreateContext context)
{
context.SetToken(context.SerializeTicket());
}
private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
}
}
After hours of searching the web and reading blobs, and the owin documentation, I have found a way to return a 401 for a failed login attempt.
I realize adding the header below is a bit of a hack, but I could not find any way to read the IOwinContext.Response.Body stream to look for the error message.
First of all, In the OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials I used SetError() and added a Headers to the response
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Response.Headers.Add("AuthorizationResponse", new[] { "Failed" });
Now, you have a way to differentiate between a 400 error for a failed athentication request, and a 400 error caused by something else.
The next step is to create a class that inherits OwinMiddleware. This class checks the outgoing response and if the StatusCode == 400 and the Header above is present, it changes the StatucCode to 401.
public class InvalidAuthenticationMiddleware : OwinMiddleware
{
public InvalidAuthenticationMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
await Next.Invoke(context);
if (context.Response.StatusCode == 400 && context.Response.Headers.ContainsKey("AuthorizationResponse"))
{
context.Response.Headers.Remove("AuthorizationResponse");
context.Response.StatusCode = 401;
}
}
}
The last thing to do is in your Startup.Configuration method, register the class you just created. I registered it before I did anything else in the method.
app.Use<InvalidAuthenticationMiddleware>();
Here is a full solution, using Jeff's concepts in conjunction with my original post.
1) Setting the error message in the context
If you call context.Rejected() after you have set the error message, then the error message is removed (see example below):
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
context.Rejected();
You will want to remove the context.Rejected() from your Task. Please note the definitions of the Rejected and SetError methods are:
Rejected:
Marks this context as not validated by the application. IsValidated and HasError become false as a result of calling.
SetError:
Marks this context as not validated by the application and assigns various error information properties. HasError becomes true and IsValidated becomes false as a result of calling.
Again, by calling the Rejected method after you set the error, the context will be marked as not having an error and the error message will be removed.
2) Setting the status code of the response: Using Jeff's example, with a bit of a spin on it.
Instead of using a magic string, I would create a global property for setting the tag for the status code. In your static global class, create a property for flagging the status code (I used X-Challenge, but you of course could use whatever you choose.) This will be used to flag the header property that is added in the response.
public static class ServerGlobalVariables
{
//Your other properties...
public const string OwinChallengeFlag = "X-Challenge";
}
Then in the various tasks of your OAuthAuthorizationServerProvider, you will add the tag as the key to a new header value in the response. Using the HttpStatusCode enum in conjunction with you global flag, you will have access to all of the various status codes and you avoid a magic string.
//Set the error message
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
//Add your flag to the header of the response
context.Response.Headers.Add(ServerGlobalVariables.OwinChallengeFlag,
new[] { ((int)HttpStatusCode.Unauthorized).ToString() });
In the customer OwinMiddleware, you can search for the flag in the header using the global variable:
//This class handles all the OwinMiddleware responses, so the name should
//not just focus on invalid authentication
public class CustomAuthenticationMiddleware : OwinMiddleware
{
public CustomAuthenticationMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
await Next.Invoke(context);
if (context.Response.StatusCode == 400
&& context.Response.Headers.ContainsKey(
ServerGlobalVariables.OwinChallengeFlag))
{
var headerValues = context.Response.Headers.GetValues
(ServerGlobalVariables.OwinChallengeFlag);
context.Response.StatusCode =
Convert.ToInt16(headerValues.FirstOrDefault());
context.Response.Headers.Remove(
ServerGlobalVariables.OwinChallengeFlag);
}
}
}
Finally, as Jeff pointed out, you have to register this custom OwinMiddleware in your Startup.Configuration or Startup.ConfigureAuth method:
app.Use<CustomAuthenticationMiddleware>();
Using the above solution, you can now set the status codes and a custom error message, like the ones shown below:
Invalid user name or password
This account has exceeded the maximum number of attempts
The email account has not been confirmed
3) Extracting the error message from the ProtocolException
In the client application, a ProtocolException will need to be caught and processed. Something like this will give you the answer:
//Need to create a class to deserialize the Json
//Create this somewhere in your application
public class OAuthErrorMsg
{
public string error { get; set; }
public string error_description { get; set; }
public string error_uri { get; set; }
}
//Need to make sure to include Newtonsoft.Json
using Newtonsoft.Json;
//Code for your object....
private void login()
{
try
{
var state = _webServerClient.ExchangeUserCredentialForToken(
this.emailTextBox.Text,
this.passwordBox.Password.Trim(),
scopes: new string[] { "PublicProfile" });
_accessToken = state.AccessToken;
_refreshToken = state.RefreshToken;
}
catch (ProtocolException ex)
{
var webException = ex.InnerException as WebException;
OAuthErrorMsg error =
JsonConvert.DeserializeObject<OAuthErrorMsg>(
ExtractResponseString(webException));
var errorMessage = error.error_description;
//Now it's up to you how you process the errorMessage
}
}
public static string ExtractResponseString(WebException webException)
{
if (webException == null || webException.Response == null)
return null;
var responseStream =
webException.Response.GetResponseStream() as MemoryStream;
if (responseStream == null)
return null;
var responseBytes = responseStream.ToArray();
var responseString = Encoding.UTF8.GetString(responseBytes);
return responseString;
}
I have tested this and it works perfectly in VS2013 Pro with 4.5!!
(please note, I did not include all the necessary namespaces or the additional code since this will vary depending on the application: WPF, MVC, or Winform. Also, I didn't discuss error handling, so you will want to make sure to implement proper error handling throughout your solution.)
Jeff's solution does not work for me, but when I use OnSendingHeaders it works fine:
public class InvalidAuthenticationMiddleware : OwinMiddleware
{
public InvalidAuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
context.Response.OnSendingHeaders(state =>
{
var response = (OwinResponse)state;
if (!response.Headers.ContainsKey("AuthorizationResponse") && response.StatusCode != 400) return;
response.Headers.Remove("AuthorizationResponse");
response.StatusCode = 401;
}, context.Response);
await Next.Invoke(context);
}
}