ASP.NET Identity Phone Number Token lifespan and SMS limit - c#

I'm building 2 factor registration API using ASP.NET Identity 2.0.
I'd like to give users ability to confirm their phone numer on demand, so even if they didn't confirm they're phone number when registering they always can request new token (making request to my API) that will be send via SMS and enter it on page (also making request to my API).
In method that is responsible for sending token I'm generating token and sending it as shown below:
var token = await UserManager.GeneratePhoneConfirmationTokenAsync(user.Id);
var message = new SmsMessage
{
Id = token,
Recipient = user.PhoneNumber,
Body = string.Format("Your token: {0}", token)
};
await UserManager.SmsService.SendAsync(message);
and inside UserManager:
public virtual async Task<string> GeneratePhoneConfirmationTokenAsync(TKey userId)
{
var number = await GetPhoneNumberAsync(userId);
return await GenerateChangePhoneNumberTokenAsync(userId, number);
}
Each time I call my method I get SMS message that contains token, problem is user can call that metod unlimited number of times and easily can generate costs - each SMS = cost.
I'd like to limit number of requests user can do to that method to one every X minutes.
Also I noticed that when I do multiple requests I get same token, I've tested my method and it looks that this token is valid for 3 minutes, so if I do request in that minutes time window I'll get same token.
Ideally I'd like to have single parameter that would allow me to specify time interval between requests and phone confirmation token lifespan.
I've tried setting token lifespan inside UserManager class using:
appUserManager.UserTokenProvider = new DataProtectorTokenProvider<User,int>(dataProtectionProvider.Create("ASP.NET Identity"))
{
TokenLifespan = new TimeSpan(0,2,0)//2 minutes
};
but this only affects tokens in email confirmation links.
Do I need to add extra field to my user table that will hold token validity date and check it every time I want to generate and send new token or is there easier way?
How can I specify time interval in which ASP.NET Identity will generate same phone number confirmation token?

I'm no expert but i had the same question and found these two threads with a little help from google.
https://forums.asp.net/t/2001843.aspx?Identity+2+0+Two+factor+authentication+using+both+email+and+sms+timeout
https://github.com/aspnet/Identity/issues/465
I'm going to assume you are correct that the default time limit is 3minutes based on the AspNet Identity github discussion.
Hopefully the linked discussions contain the answers you need to configure a new time limit.
Regarding the rate limiting i'm using the following code which is loosely based on this discussions How do I implement rate limiting in an ASP.NET MVC site?
class RateLimitCacheEntry
{
public int RequestsLeft;
public DateTime ExpirationDate;
}
/// <summary>
/// Partially based on
/// https://stackoverflow.com/questions/3082084/how-do-i-implement-rate-limiting-in-an-asp-net-mvc-site
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class RateLimitAttribute : ActionFilterAttribute
{
private static Logger Log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Window to monitor <see cref="RequestCount"/>
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// Maximum amount of requests to allow within the given window of <see cref="Seconds"/>
/// </summary>
public int RequestCount { get; set; }
/// <summary>
/// ctor
/// </summary>
public RateLimitAttribute(int s, int r)
{
Seconds = s;
RequestCount = r;
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var clientIP = RequestHelper.GetClientIp(actionContext.Request);
// Using the IP Address here as part of the key but you could modify
// and use the username if you are going to limit only authenticated users
// filterContext.HttpContext.User.Identity.Name
var key = string.Format("{0}-{1}-{2}",
actionContext.ActionDescriptor.ControllerDescriptor.ControllerName,
actionContext.ActionDescriptor.ActionName,
clientIP
);
var allowExecute = false;
var cacheEntry = (RateLimitCacheEntry)HttpRuntime.Cache[key];
if (cacheEntry == null)
{
var expirationDate = DateTime.Now.AddSeconds(Seconds);
HttpRuntime.Cache.Add(key,
new RateLimitCacheEntry
{
ExpirationDate = expirationDate,
RequestsLeft = RequestCount,
},
null,
expirationDate,
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null);
allowExecute = true;
}
else
{
// Allow and decrement
if (cacheEntry.RequestsLeft > 0)
{
HttpRuntime.Cache.Insert(key,
new RateLimitCacheEntry
{
ExpirationDate = cacheEntry.ExpirationDate,
RequestsLeft = cacheEntry.RequestsLeft - 1,
},
null,
cacheEntry.ExpirationDate,
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null);
allowExecute = true;
}
}
if (!allowExecute)
{
Log.Error("RateLimited request from " + clientIP + " to " + actionContext.Request.RequestUri);
actionContext.Response
= actionContext.Request.CreateResponse(
(HttpStatusCode)429,
string.Format("You can call this {0} time[s] every {1} seconds", RequestCount, Seconds)
);
}
}
catch(Exception ex)
{
Log.Error(ex, "Error in filter attribute");
throw;
}
}
}
public static class RequestHelper
{
/// <summary>
/// Retrieves the client ip address from request
/// </summary>
public static string GetClientIp(HttpRequestMessage request)
{
if (request.Properties.ContainsKey("MS_HttpContext"))
{
return ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
}
if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
{
RemoteEndpointMessageProperty prop;
prop = (RemoteEndpointMessageProperty)request.Properties[RemoteEndpointMessageProperty.Name];
return prop.Address;
}
return null;
}
}
I've also seen this library recommended a few times:
https://github.com/stefanprodan/WebApiThrottle

Related

OpenIddict migration from v2 to v3 rc1 - Login success, Authorize Failed

I have tried to migration my project from v2 to v3 and I think i have updated everything, but for some reason i can login fine and i get an access_token returned, but this access_token fails to authorize a request. Not sure why, but if someone can see anything obvious that would be great. My application is a web Api with a angular2 web application for the client.
In v2 i was using JWT, but i understand that this is the default in v3 so i may have some code that needs to removed for this to work.
To restrict a controllers methods or a single method, i am using [Authorize] attribute.
Extension method to be called in Startup.cs for adding Authentication, done to keep Startup.cs file from getting to big and keeps things in one place
/// <summary>
/// Add authentication
/// </summary>
/// <param name="services"></param>
/// <param name="appSettings"></param>
public static void AddAuthentication(this IServiceCollection services, AppSettings appSettings)
{
// appSettings.ApiUrl = "http://localhost:5000"
// appSettings.WebsiteUrl = "http://localhost:4200"
// the default value for AllowuserNameCharacters is "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+"
// here we have just added some additional characters
services.AddIdentity<User, Role>(options => { options.User.AllowedUserNameCharacters += "'&"; })
.AddDefaultTokenProviders();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
// return unauthorized message instead of url
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return System.Threading.Tasks.Task.CompletedTask;
};
});
// configure all tokens generated from aspnet to expire in 3 days (create password, forget password etc)
services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromDays(3));
// Authentication
var authenticationBuilder = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.Authority = appSettings.ApiUrl;
cfg.Audience = appSettings.ApiUrl;
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
});
// OpenIddict
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFramework(e => e.UseDbContext<ApplicationDbContext>());
})
.AddServer(options =>
{
// For token lifetimes look at the below link
// https://github.com/openiddict/openiddict-core/wiki/Configuration-and-options
// Enable the authorization, logout, token endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token");
// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(Scopes.Email,
Scopes.Profile,
Scopes.OpenId,
Scopes.OfflineAccess,
Scopes.Roles);
// code to allow requests without client_id
options.AcceptAnonymousClients();
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.DisableTransportSecurityRequirement(); // Never use https because we use load balancer
})
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
}
Startup.cs
/// <summary>
/// Configure services
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
// application settings
var appSettings = new AppSettings();
Configuration.Bind(appSettings);
// mvc
services.AddMvc(appSettings);
// identity / saml etc
services.AddAuthentication(appSettings);
}
/// <summary>
/// Configure application
/// </summary>
/// <param name="applicationBuilder"></param>
/// <param name="webHostEnvironment"></param>
/// <param name="loggerFactory"></param>
public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment, ILoggerFactory loggerFactory)
{
// get current appSettings and output to logger
var appSettings = applicationBuilder.ApplicationServices.GetRequiredService<AppSettings>();
var logger = loggerFactory.CreateLogger(appSettings.ApiUrl);
logger.LogInformation(string.Format("{0}\n{1}", appSettings.Environment, appSettings.Data.DefaultConnection.ConnectionString));
// environment
webHostEnvironment.EnvironmentName = appSettings.Environment;
// CORS
applicationBuilder.UseCors("Default");
// static files in root
applicationBuilder.UseStaticFiles();
// routing
applicationBuilder.UseRouting();
// comment this out and you get an error saying
// InvalidOperationException: No authentication handler is configured to handle the scheme: Microsoft.AspNet.Identity.External
applicationBuilder.UseAuthentication();
// for authorization headers
applicationBuilder.UseAuthorization();
// response caching
applicationBuilder.UseResponseCaching();
// routes
applicationBuilder.UseEndpoints(options =>
{
// default goes to Home, and angular will deal with client side routing
options.MapControllerRoute(
name: "default",
pattern: "{*url}",
defaults: new { controller = "home", action = "index" });
});
}
I have updated my AuthorizationController.cs as well
/// <summary>
/// Handles authorization requests
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class AuthorizationController : Controller
{
private IOptions<IdentityOptions> _identityOptions;
private OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> _applicationManager;
private SignInManager<MyUser> _signInManager;
private UserManager<MyUser> _userManager;
private AppSettings _appSettings;
private IEncryptionService _encryptionService;
/// <summary>
/// Create a new authorization controller
/// </summary>
/// <param name="identityOptions"></param>
/// <param name="applicationManager"></param>
/// <param name="signInManager"></param>
/// <param name="userManager"></param>
/// <param name="appSettings"></param>
/// <param name="encryptionService"></param>
public AuthorizationController(
IOptions<IdentityOptions> identityOptions,
OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> applicationManager,
SignInManager<MyUser> signInManager, UserManager<MyUser> userManager,
AppSettings appSettings, IEncryptionService encryptionService)
{
_identityOptions = identityOptions;
_applicationManager = applicationManager;
_signInManager = signInManager;
_userManager = userManager;
_appSettings = appSettings;
_encryptionService = encryptionService;
}
// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:
/// <summary>
/// Authorize an openId request
/// </summary>
/// <param name="connectRequest"></param>
/// <returns></returns>
[Authorize, HttpGet, Route("~/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId, new System.Threading.CancellationToken());
if (application == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidClient,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Details concerning the calling client application cannot be found in the database"
}
)
);
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel
{
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}
/// <summary>
/// Accept an openId request
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the profile of the logged in user
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ServerError,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "An internal error has occurred"
}
)
);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
// returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// Deny an openId request
/// </summary>
/// <returns></returns>
[Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken]
public IActionResult Deny()
{
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// Logout
/// </summary>
/// <returns></returns>
[HttpPost("~/connect/logout")]
public async Task<IActionResult> Logout()
{
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:
/// <summary>
/// Exchange request for valid openId token
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
}
)
);
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
{
await _userManager.AccessFailedAsync(user);
}
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
}
)
);
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/refresh token.
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the refresh token
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid"
}
)
);
}
// Ensure the user is still allowed to sign in
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in"
}
)
);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new NotImplementedException("The specified grant type is not implemented");
}
/// <summary>
/// Creates a principal based on the openId request
/// </summary>
/// <param name="request"></param>
/// <param name="user"></param>
/// <param name="properties"></param>
/// <returns></returns>
private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, MyUser user, AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
if (!request.IsRefreshTokenGrantType())
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
principal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.OfflineAccess,
Scopes.Roles
}.Intersect(request.GetScopes()));
}
// Set resource
principal.SetResources(new string[] { _appSettings.ApiUrl });
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return principal;
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
Also here is my applicationDbContext
/// <summary>
/// Application context to hold our openIddict entities
/// </summary>
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
/// <summary>
/// Create context with connection string
/// </summary>
/// <param name="connectionString"></param>
public ApplicationDbContext(string connectionString) : base(connectionString)
{
}
/// <summary>
/// When creating models
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.UseOpenIddict();
}
/// <summary>
/// Authorizations
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkAuthorization> Authorization { get; set; }
/// <summary>
/// Applications
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkApplication> Application { get; set; }
/// <summary>
/// Tokens
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkToken> Token { get; set; }
/// <summary>
/// Scopes
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkScope> Scope { get; set; }
}
As i said, i am getting a token returned, like below
{
"access_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI5NDVDNjFERjcyRUU1OUNDNUNFQzQ0NzJCRDE0MDg4NDEyNTAyRjM3IiwidHlwIjoiYXQrand0In0.TxmA-Lv60Lwvr5eXdu2fgG95Bx4AUoPvp5XniqSYbBgmpDrVXCUpSjnEv7tWQ5QLPjks6TAy8hGAGfgQPFB1xDM05mZyu2WDPBsoEQ2VtxuabaPhj-uK2KjnWEccjS-n-YAY7OlSDFXrmiDsFCOf3jaPGZ43gUCPyTVl_WVE_KBGVXa19RDZFf9Ger-TsHY3evYbsUchOKMco-CD2IGt6Xg6DttxFcKF3SUPwMXhn6AGdkNesGVIoVSeDd_CnM3MyHtJIbGbkRkmwpMWsbwaWGI38itUJ0XeUqn45B_GsZfaZllytuMHbuqt1GgL0gdOaCfBUxaqy7AaYsY9UKlaEw.LS4FnIOCRESdeo1c5K2_5Q.Ic8fssDScEu7lAomL8d_7JZMVho5hLphg269UIETZui7DUB2gund7YCtGq7imdnDtN-wsEoZaLHJ4UQSJrpuIMCN2pW69J9kKcx-UT1e2Ma-JNj2G5CtxwRf_bszRRsgWC1ia85LU5TPsUFvhd_wDxyUax2GKowb0FVl8EFRoFdZF40h06LaofxDzD2BFdwSYaHaDm3icVNZ0CRpYCoZq-MK_c3Fl98l57zjZl1CNscs1w0trApMDvQ7UeFcez3zelN24H6TCaXqTRP3lixMiv9Rtm3Kqkv67HEUAFD-vXIMyWQTo28oJMMAQz9zQeTp88JsfI4Pv7euUECEkwK7Fe5rbxhD4oicNNa_wmRo2vsjrpm4C7mmRKH2u3cX2CCTwahOsVHxu4kO3zliWmniW-krEAAsaW6BjoWqJiGN3ydoiHLIv3muM78jt85KzKxMIFsalpdf2F9Z8neQBglhzxZQFp9JiEecczxao6Y03WuFNX98kZh8Zr4D21CM_m634u3mf5-Gz-frVlSgnMrE9vgCG2eETI3fOnObf9pQ1XWpK7I9SE0AZvc2PciaKO4H7mKDqXuXFrzdiz6Tx9G9MMkXXbuKsuslZJ6q9wlE3xOl9mzd6shN9GvfSTQNrFoV-PS62xhnfCFKQDfYLzWF1fhQbq_RECp6IVJN960jaMAGIZ0yzIbRInIkbhdFEIbl__h9AYo6FfcZgYv4tfw-iB_unezgHCIPpbBPC0eRMiZh1wqpfwpiJ8zjvcXJXXCfSlu13KlBWD9tHVEHh6aHE258hGdFzApA-MTCkjHwMRjVgmv_-Ed-xwk0hjBpRhiZv-0kNwKI72YJVgZMX9CMpFBg2CO5z5hHSlc0pO9pc5ovuCZ_dYYRiqqcIDUJ5Wl6dDt1JCPI084C4yssC7MC4e94OtvfRtrI096qMI34qrLWi-jn7UOMnWMUunNepQLwQ07DK-ubXsS-m0xvSxPYxtE6XM6QebmvCcXj__vELGscDu8mWmvP1Y0L9SoSpWlErHvlCPpkfVeMnkdP74cKTYgpXSQcGduqcdfU86leI9oUYgnrcPwvMgUq3jwgHGWn_0d4Bo8CsChWatYWmSNKN88h3WfASSl6SyqeVbSSYvIp-0HBGxGOwODgO-YsOlOgeKmm8oSIhnELVNMVEY1uiUZpJ9DGfrUUXKMw7aIz6LK_zH_HxlMiBam0fxgAQwRyYlO0AFmuxFO62KEHFEjdDSEO9pcUP03_RNqDfAX-IAV_EoFT7CwVpOZMUvFLo78S4xkq2ss3CFbkA3J4ioud88T5SUfslnsZmY1dJYtW4HhlGF7SKVMN6GSwckz1YhyaxqlQpMbMRFA0uCbkM6u41K0-_toRQejKDX5juqFwqK8.m0bmPYOuAYYdc6WfjDWP2UneysP1G3FqwlmOzWOrTnM",
"token_type": "Bearer",
"expires_in": 3599,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwMzJBRUJBQjc4NkNCM0ExRkI3Nzk1NjdDQTRGRjU1ODg3OTFCNDEiLCJ4NXQiOiJjREt1dXJlR3l6b2Z0M2xXZktUX1ZZaDVHMEUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxIiwibmFtZSI6InBvd2VydXNlckBtZWFzdXJlMmltcHJvdmUuY29tIiwib2lfYXVfaWQiOiIxMDJjZTE0NC02NzkyLTRlYzAtOTlhNy0xZDBlMzdmNzkyZTYiLCJhdF9oYXNoIjoiV3BibExxNFdNR2dwdHdGVVVSMktRdyIsIm9pX3Rrbl9pZCI6IjQyMzBlYmJkLWUyNzItNDJlZS1iOTZiLTQ1ZGJmMTJlZWJmMiIsImV4cCI6MTYwNjQ3NzgzOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyIsImlhdCI6MTYwNjQ3NjYzOX0.WZkB63ZfZuJfigNVabPegu-B8TvMeS1DmgRQJS151XGR08Pw-fcCldb35oM7ZW9oQenj7059BAZMI1EveHWNVWOEFpabebi7TccGRoR1YKqWSNWTBDwyQgGMyehVmze_TPgsSjAJA0y1f_xtF3-ImfVx5Tzlcjg4XAmAhV3MRd-fEobdGk5540uto5hZJ7ieHrV_7U_FF4NgVT5nSw92bkFNjUokmNgMBpDWelZUEXmsb3MFGDMnQkP0oTGXeIcy0nuuIKpr1Liza_cvv1JfICQnSUKw_u3zdqSbsbXtzGg9GfunEhXf1zSF5dxfbNpr2E4bsnIxGJ-mOYxBsOQDJA",
"refresh_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI5NDVDNjFERjcyRUU1OUNDNUNFQzQ0NzJCRDE0MDg4NDEyNTAyRjM3IiwidHlwIjoib2lfcmVmdCtqd3QifQ.E5xUDcqMK15spCgqoM8YrOCn_32wBX_X_XsG_f1nKOR-TcDI2AKi07vX62uMmbO1bgclGSGNynEUTuPt7KATf9UHXwpytXAK3_BnLxQLz7NbYIlfrja9t5z3gCRIGOZ-5gHbHcU6RdJrDpC73_V7CXYIfkVhfWeeS30_3GtLa-BUu4Nlr9YL7K22KdR0LJQsK6SXLMKobsj6PLrrywHnClaw6dG_O_SDezC5M_eIo_ErdHFqUBwWYhUHyntijJ9ekH4A2SiY0LGaQD6QyvmUNPC7E-LtLVO78saJYAgGW-CkQbwTBF7b_vOoEr_mPrFnpytwLnIvxSQCFVjclH-vzA.RGNxh09nh1RWb6XU6jNZSg.VRwdO_5UrB7tqaMHXwt6qZN8VucSlNXnBNoBxcvisDvjWhBOlhrJxEidGQPBeiIpbrYrWQXyPfSgFrmZAlqYcF-3cXTl0W-xR4oW1ZqRf95eMs-YTA9-bR6n8P2pV7WeYky7wkwa-i3Vs9rzasYjK18LTU6v71xMNVN2K0Z2HRgTXxPg3U4HUgkVQpZp4qetyhozdOhGBV37igAHSZHFUSYg-dlstz3DtEX7IXvi7GQbM_fLKVlDLWYZOtX1yH366C9TIjZbxGP3h-qPqfKKDNP-IhD_x2IpymmiKllfhfLKb62JQzy_ci-ICXGvs0z_ZLZirmzVSEotqrcKmwe1rlqRXbymP8llIXe3AzjLonN3I_35bLeqZz1KQE93pkLOpJGAJuhJ4dc8a7wJ5kxTAs9_CCvb6UBvhRxInUyZ9PQq026RaYJvN9i--x8JxAAtH58h9Y5zgm_M4kFbEoGgZshyGZ8QSQrp4JpPrGeW9ElvkyEmrGvEpm6zmy_Y6tEF3lXFfrGJL6FPGD5_q8m9lGQ162OCmZMGg2Sy46Hne0SPv04TpM43F-m82BTzXxGRbpvOoNye7Pl9dKbdOBno1WEmaesL-R-W_8qogNszW5c-gbhQTUjcQ1J5yuVL4r29qN9xhVDPKqxPYPzInCr30m8SH41NI6WusFVJbNoqVs43wymGfQwuDfJacVCIfuT1Lx2VMdVl3nvCAGOnEwiUDufjqMaA-Nh-Row4QIwTHMGwfzxbFkQjRfwq_NjVU0LZlVDX2BgSETw2ak3KpgAGoeDlVVwmXuQpbvZ3wRHVh4a3k3_JVGf1dOiHbXVcWx984-EARKmW6gLjIhe2t5tzIJ9NQqBrdBrCvMmQXWfBiP00-YH31yDfYRF9tNZoojnkcrkOewHFUsEcFuayBzP5ySR6RgGVVE_zkEXTnBXxeiynzfJn0D3XthpRPosLJTJz91tdmvo6CxPWuaaZoUlXq_EXqFrGCb09f11Hs_mKu7T7pHMbJioTAvj1Jy8jzduMU3Rth-w0A8Md2gdeKmoCXW51lvpuyFsP4R8AdthhXGUbAk6rNJXdkaUkRyygYU8ZQWI2WCyroDnBRpEIaBtjYT1Uf4zXyTUZ3jicKe5Rjr08tmgAQd_3pNu1Wb2dBQgvcoqnwXcnLvoMTp0yk7Osae94Pqh_Oyz45kz_oS_7fR6WvZm-avBPYLmW92eQWiMG6glxPPe_Vnq9ghwjYjS4KEvyFaDqusDWmxBEDwEzLgBTF3p0R2saedTbwpG4Epcey_T18KEuklAWbCwz-fvV3ip-_wGUNe6cuAOeyuXcUHm18Le0knZt6xNS_a8cxMz9RAEpBs3a-tOWQLrVELXxnNY03NL2szkNQuDUzY9JRtTxqngxcPpHVk058dRG1rwFZqiI4-6_yL03X_fbXsIR0ItBig9surYyB_crwyH3C6OZTnGgwxLKUU9qY0LNTzR0gatrrT1l8NrorLAvODzQqhrqqClHgZkVoQXANvz3mNZWIpxjCkOgDkH-YOaaR3egnwLVMk_clo3-gC76UQ-5T-NnZ7MJTv2twFBhUKHABEJsT9a3nR7ra2CbHFzJFNvRRCPHAGOVAY-y-Ek0xZn8mvd3Fvw4wlej4EdQvlnFUcaIH4Cl0MZD9G9t2W3A96KLRZctVfGK6W1yM846DWHwSfeTj3ZPN_6bYDqbWHoXrQxe5BGi9aN8PXfL1Bu9W5jhQpgrvTRvnjhYqwN24ta9r6BbrMzBL3cszf56dG3Ko6aA8gDwUmDSjyNVhk76sdoW.1xLMuQf7NjeYjAXDL5BUxaoFSR0EECEzPh7XxkE8Hj4"
}
Here is the token being passed successfully, but Unauthorized is returned, as you can see the next call fails as well.
Any help much appreciated
I know this is old, but just post my solution in case anyone has the same issue.
Just specificy the schema for the Authorize attribute:
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]

How to generate Body Parameters/ Sample Request for Web API Help Page while having access to raw request body

I need help populating the Body Parameters for the documentation of this endpoint. It shows as empty:
I realize that the reason this is happening is because I am currently reading the request body and then converting the information to PackageUpdate. Since I am reading the raw request body this way, the task has no parameters which will cause the help page to not specify any.
Sample Request:
{
"PackageId": "package/3e585e1c-d6cd-4b6c-aa1c-aa674d11c944",
"SoldDateTime": "2018-08-13 19:57:54.000",
"GuaranteeTermExpiresDate": null
}
PackageUpdate class:
[DataContract]
public class PackageUpdate: BaseRequest
{
/// <summary>
/// Get or Set the package ID of the package to update
/// </summary>
[Required]
[DataMember]
public string PackageId { get; set; }
/// <summary>
/// Get or Set the Sold Date Time field of the package
/// </summary>
[DataMember]
public DateTime? SoldDateTime { get; set; }
/// <summary>
/// Get or Set the Guarantee Term Expires Date field of the package
/// </summary>
[DataMember]
public DateTime? GuaranteeTermExpiresDate { get; set; }
/// <summary>
/// Get or Set the Premium Promised On field of the package
/// </summary>
[DataMember]
public DateTime? PremiumPromisedOn { get; set; }
}
UpdatePackageAsync Method:
/// <summary>
/// Updates a package.
/// </summary>
[Route("update"), Description("Patch a specific package")]
[HttpPatch]
[ResponseType(typeof(Package))]
public async Task<IHttpActionResult> UpdatePackageAsync()
{
string requestBody = await Request.Content.ReadAsStringAsync();
PackageUpdate request = new PackageUpdate();
try
{
JsonSerializerSettings settings = new JsonSerializerSettings{ MissingMemberHandling = MissingMemberHandling.Error };
request = JsonConvert.DeserializeObject<PackageUpdate>(requestBody, settings);
}
catch(Exception e)
{
return BadRequest("Patch Error -> " + e.Message);
}
//Do stuff with request
}
How can I possibly get the Body Parameters field on the documentation to reflect the properties of PackageUpdate, and still have access to the raw request body?
I previously tried the below solution from a different post but it wouldn't allow me access to the request body as a string.
public async Task<IHttpActionResult> UpdatePackageAsync([FromBody] PackageUpdate request)
I ended up trying multiple solutions that I found on the site and then found a solution (here) that allows me to seek to the beginning of the stream, copy to a new stream, and return the body to my Task.
Solution:
using (var stream = new MemoryStream())
{
var context = (HttpContextBase)Request.Properties["MS_HttpContext"];
context.Request.InputStream.Seek(0, SeekOrigin.Begin);
context.Request.InputStream.CopyTo(stream);
string requestBody = Encoding.UTF8.GetString(stream.ToArray());
}
Applicable to my code, I used it in this way:
public async Task<IHttpActionResult> UpdatePackageAsync(PackageUpdate request)
{
string requestBody = ReadInputStream();
// do stuff with request
}
private string ReadInputStream()
{
using (var stream = new MemoryStream())
{
var context = (HttpContextBase)Request.Properties["MS_HttpContext"];
context.Request.InputStream.Seek(0, SeekOrigin.Begin);
context.Request.InputStream.CopyTo(stream);
return Encoding.UTF8.GetString(stream.ToArray());
}
}

c# web api using auth1.0a with OAuthAuthorizationServerProvider

I set up JWT token based authentication using the OAuthAuthorizationServerProvider a while ago. The provider looks like this:
public class OAuthProvider : OAuthAuthorizationServerProvider
{
// Private properties
private readonly IAdvancedEncryptionStandardProvider _helper;
private readonly IUserProvider _userProvider;
// Optional fields
private readonly Lazy<IClientService> _clientService;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="helper"></param>
public OAuthProvider(IAdvancedEncryptionStandardProvider helper, IUserProvider userProvider, Lazy<IClientService> clientService)
{
_helper = helper;
_userProvider = userProvider;
_clientService = clientService;
}
/// <summary>
/// Always validate the client because we are using angular
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Set up our variables
var clientId = string.Empty;
var clientSecret = string.Empty;
Client client = null;
// Try to get our credentials if basic authentication has been used
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
context.TryGetFormCredentials(out clientId, out clientSecret);
// If we have no client id
if (context.ClientId == null)
{
//Remove the comments from the below line context.SetError, and invalidate context
//if you want to force sending clientId/secrects once obtain access tokens.
context.Validated();
//context.SetError("invalid_clientId", "ClientId should be sent.");
return;
}
// Get our client
client = await _clientService.Value.GetAsync(context.ClientId);
// If we have no client, throw an error
if (client == null)
{
context.SetError("invalid_clientId", $"Client '{ context.ClientId }' is not registered in the system.");
return;
}
// Get the application type
if (client.ApplicationType == ApplicationTypes.NativeConfidential)
{
// If we have a client secret
if (string.IsNullOrWhiteSpace(clientSecret))
{
context.SetError("invalid_clientId", "Client secret shoud be sent.");
return;
}
if (client.Secret != _helper.Encrypt(clientSecret))
{
context.SetError("invalid_clientId", "Client secret is invalid.");
return;
}
}
// If the client is inactive, throw an error
if (!client.Active)
{
context.SetError("invalid_clientId", "Client is inactive.");
return;
}
// Set our allowed origin and token expiration
context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());
// Validate our request
context.Validated();
return;
}
/// <summary>
/// Authorize the request
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Set our allowed origin
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
if (string.IsNullOrEmpty(allowedOrigin))
allowedOrigin = "*";
// Add our CORS
context.OwinContext.Response.Headers.Remove("Access-Control-Allow-Origin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
// Find user by username first
var user = await _userProvider.FindByNameAsync(context.UserName);
// If our user actually exists
if (user != null)
{
// Validate the users credentials
var validCredentials = await _userProvider.FindAsync(context.UserName, context.Password);
var lockoutEnabled = await _userProvider.GetLockoutEnabledAsync(user.Id);
// If lockout is enabled
if (lockoutEnabled)
{
// If the user entered invalid credentials
if (validCredentials == null)
{
// Record the failure which also may cause the user to be locked out
await _userProvider.AccessFailedAsync(user);
// Find out how many attempts are left
var accessFailedCount = await _userProvider.GetAccessFailedCountAsync(user.Id);
var attemptsLeft = Convert.ToInt32(ConfigurationManager.AppSettings["MaxFailedAccessAttemptsBeforeLockout"].ToString()) - accessFailedCount;
// Inform the user of the error
context.SetError("invalid_grant", string.Format(Resources.PasswordInvalid, attemptsLeft));
return;
}
// Check to see if the user is already locked out
var lockedOut = await _userProvider.IsLockedOutAsync(user.Id);
// If the user is lockted out
if (lockedOut)
{
// Inform the user
context.SetError("invalid_grant", string.Format(Resources.UserLocked, ConfigurationManager.AppSettings["DefaultAccountLockoutTimeSpan"].ToString()));
return;
}
// If we get this far, reset the access attempts
await _userProvider.ResetAccessFailedCountAsync(validCredentials);
}
// If the user entered the correct details
if (validCredentials != null)
{
// If the user has not confirmed their account
if (!validCredentials.EmailConfirmed)
{
// Inform the user
context.SetError("invalid_grant", Resources.UserHasNotConfirmed);
return;
}
// Generate our identity
var oAuthIdentity = await _userProvider.CreateIdentityAsync(validCredentials, "JWT");
oAuthIdentity.AddClaims(ExtendedClaimsProvider.GetClaims(validCredentials));
// Create our properties
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
{"as:client_id", string.IsNullOrEmpty(context.ClientId) ? string.Empty : context.ClientId},
{"userName", context.UserName}
});
// Create our ticket and authenticate the user
var ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
return;
}
}
// Failsafe
context.SetError("invalid_grant", Resources.UserOrPasswordNotFound);
return;
}
/// <summary>
/// Adds additional properties to the response
/// </summary>
/// <param name="context">The current context</param>
/// <returns></returns>
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
context.AdditionalResponseParameters.Add(property.Key, property.Value);
return Task.FromResult<object>(null);
}
/// <summary>
/// Grants a refresh token for the current context
/// </summary>
/// <param name="context">The current context</param>
/// <returns></returns>
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
// Get our client ids
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId;
// If we are not the same client
if (originalClient != currentClient)
{
// Set the error and exit the function
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
return Task.FromResult<object>(null);
}
// Change auth ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
}
This has worked well and for the most part is fine. My manager how now asked me to put authentication in for other applications using a key and secret.
I would like to use the OAuthAuthorizationServerProvider to do this, but I cannot find any documentation anywhere of how to go about setting this up.
I have read and found a method which can be override: GrantCustomExtension and thought that maybe I could use this to set up the authentication but like I have mentioned, I have no idea how to set it up.
Has anyone had experience with this? If they have, could they help me by providing a code example or giving me a link to a resource that I can read?
Any help would be greatly appreciated.
I recommend to step away from Asp.net Identity and use IdentityServer.
IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core.
and IdentityServer3 is for Asp.Net Classic ( although you can use IdentityServer4 with Asp.net classic )
it's really easy to config and very ongoing project.
it has several features like
Authentication as a Service
Single Sign-on / Sign-out
Access Control for APIs
Federation Gateway
and for the most important part, it's free and open source.

How to transfer file to and from Telegram using POCO (I don't want to use any middleware)

I'm currently working on a Telegram bot, recently i almost finished it's sensitive parts, and now i want to activate the webhook, but the webhook require me to send certificate file to the telegram before it get activated, also later i may want to send a file for our client, or receive their reply as a file (since we want to activate our website features to them through telegram).
Here is a reference to telegram bot APIs:
https://core.telegram.org/bots/api#inputfile
I, myself, done all my api call through HttpClient class, and i wish to continue it as it is.
here is my failed method:
public static Exception SetWebhook(SetWebhook webhook)
{
try
{
using (var hc = new HttpClient())
{
HttpContent requestContent = new ObjectContent(typeof(SetWebhook), webhook,
new JsonMediaTypeFormatter
{
SerializerSettings = new JsonSerializerSettings
{
ContractResolver = new CustomPropertyNamesContractResolver
{
Case = IdentifierCase.UnderscoreSeparator
},
NullValueHandling = NullValueHandling.Ignore
},
SupportedEncodings = {Encoding.UTF8}
}, "multipart/form-data");
var responseMessage =
hc.PostAsync("https://api.telegram.org/bot" + AppSetting.Token + "/setWebhook",
requestContent).Result;
if (responseMessage.IsSuccessStatusCode)
{
return null;
}
else
{
return new Exception("Status Code: " + responseMessage.StatusCode + "\n\nRequest" + responseMessage.RequestMessage.ToString() + "\n\nResponse" + responseMessage.ToString() );
}
}
}
catch (Exception ex)
{
return ex;
}
}
And here are my models:
Since telegram didn't defined the Certificate exact type, i take a look at this: https://github.com/MrRoundRobin/telegram.bot to generate it.
public class SetWebhook
{
/// <summary>
/// Optional<br/>
/// If empty remove webhook
/// </summary>
public string Url { get; set; }
/// <summary>
/// Optional<br/>
/// Upload your public key certificate so that the root certificate in use can be checked. See our self-signed guide for details.
/// </summary>
public InputFile Certificate { get; set; }
}
/// <summary>
/// Represents information for a file to be sent
/// </summary>
public class InputFile
{
/// <summary>
/// Required <b/>
/// Gets or sets the filename.
/// </summary>
public string Filename { get; set; }
/// <summary>
/// Required <b/>
/// Gets or sets the content.
/// </summary>
public Stream Content { get; set; }
public InputFile()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InputFile"/> class.
/// </summary>
/// <param name="filename">The <see cref="Filename"/>.</param>
/// <param name="content">The <see cref="Content"/>.</param>
public InputFile(string filename, Stream content)
{
Filename = filename;
Content = content;
}
}
and here is the way i call it:
public ActionResult SetWebhook()
{
var result = true;
var text = "-";
try
{
WebHook.SetWebhook("http://ravis.ir:444/Data/Message", Server.MapPath("~/Files/ravis.ir-PEM.cer"));
}
catch(Exception ex)
{
result = false;
text = ex.Message;
}
return View(new WebhookResult
{
Result = result,
Text = text
});
}
This last way, errors:
((ex.InnerException).InnerException).Message ->
Timeouts are not supported on this stream.
(ex.InnerException).Message ->
Error getting value from 'ReadTimeout' on 'System.IO.FileStream'.
ex.Message ->
One or more errors occurred.
So how should i send file? how should i receive them? what kind of entity should i define to be more accurate?
In order to get this working you should construct MultipartFormDataContent with StreamContent inside (not an ObjectContent):
using (var httpClient = new HttpClient())
using (var form = new MultipartFormDataContent())
{
var content = new StreamContent(setWebhook.Certificate.Content);
form.Add(content, "certificate", setWebhook.Certificate.Filename);
form.Add(new StringContent(setWebhook.Url, Encoding.UTF8), "url");
var response = await httpClient.PostAsync(uri, form);
}
As for receiving files - first you should get "file info" containing file_path and then it can be downloaded:
var fileId = "fileId";
using (var httpClient = new HttpClient())
{
var responseMessage = await httpClient.PostAsJsonAsync("https://api.telegram.org/bot" + token + "/getFile", new { file_id = fileId });
responseMessage.EnsureSuccessStatusCode();
var fileInfoResponse = await responseMessage.Content.ReadAsAsync<TelegramResponse<FileInfo>>();
var fileUri = new Uri("https://api.telegram.org/file/bot" + token + "/" + fileInfoResponse.result.file_path);
var fileStreamResponse = await httpClient.GetAsync(fileUri, HttpCompletionOption.ResponseHeadersRead);
//and here's downloaded file
var stream = await fileStreamResponse.Content.ReadAsStreamAsync();
}
Here TelegramResponse and FileInfo will look something like this (* not a C# convention, but you can fix this if you want):
class TelegramResponse<T>
{
public bool ok { get; set; }
public T result { get; set; }
}
class FileInfo
{
public string file_path { get; set; }
}

How to notify AngularJS application from EasyNetQ

So I have the following architecture: an Angular SPA (single page application) performs a call to a .NET Web API controller, which publishers a message to a Publisher EasyNetQ window service, which sends an asynchronous request to a second EasyNetQ window service called Subscriber, which calls a backend class to generate a SSRS report, and finally sends an asynchronous response back to Publisher. Here is a diagram of the architecture in question:
So far so good, Subscriber receives the response, generates the report(s), and sends a message(s) back to Publisher. Here is how the Web API controller sends the report data message to the Publisher:
private IHttpActionResult generateReports(int[] incidentIds)
{
try
{
var incident = this.incidentRepository.GetIncident(incidentIds[0]);
var client = this.clientService.GetClient(incident.ClientId_Fk);
using (var messageBus = RabbitHutch.CreateBus("host=localhost"))
{
// Loop through all incidents
foreach (var incidentId in incidentIds)
{
foreach (var format in this.formats)
{
Dictionary<Dictionary<int, Client>, SSRSReportFormat> reportData = new Dictionary
<Dictionary<int, Client>, SSRSReportFormat>()
{
{new Dictionary<int, Client>() {{incidentId, client}}, format}
};
messageBus.Publish(new ReportData
{
clientId = client.Id,
incidentId = incidentId,
clientName = client.Name,
clientNetworkPath = client.NetworkPath,
formatDescription = EnumUtils.GetDescription(format),
reportFormat = format.ToString()
});
}
}
}
return this.Ok();
}
catch (Exception ex)
{
return this.InternalServerError(ex);
}
}
This is how I send a request from Publisher:
public partial class CreateRequestService : ServiceBase
{
private IBus bus = null;
public CreateRequestService()
{
this.InitializeComponent();
}
protected override void OnStart(string[] args)
{
this.bus = RabbitHutch.CreateBus("host=localhost");
this.bus.Subscribe<ReportData>("reportHandling", this.HandleReportData);
}
protected override void OnStop()
{
this.bus.Dispose();
}
private void HandleReportData(ReportData reportData)
{
int clientId = reportData.clientId;
int incidentId = reportData.incidentId;
string clientName = reportData.clientName;
string clientNetworkPath = reportData.clientNetworkPath;
string formatDescription = reportData.formatDescription;
string reportFormat = reportData.reportFormat;
var task = this.bus.RequestAsync<ReportData, TestResponse>(reportData);
task.ContinueWith(response => Library.WriteErrorLog("Got response: '{0}'" + response.Result.Response, "PublisherLogFile"));
}
}
And finally, the code for generating reports and sending responses back from Subscriber:
public partial class RequestResponderService : ServiceBase
{
private IBus bus = null;
public RequestResponderService()
{
this.InitializeComponent();
}
/// <summary>
/// Initialize the Bus to receive and respond to messages through
/// </summary>
/// <param name="args"></param>
protected override void OnStart(string[] args)
{
// Create a group of worker objects
var workers = new BlockingCollection<MyWorker>();
for (int i = 0; i < 10; i++)
{
workers.Add(new MyWorker());
}
workers.CompleteAdding();
// Initialize the bus
this.bus = RabbitHutch.CreateBus("host=localhost");
// Respond to the request asynchronously
this.bus.RespondAsync<ReportData, TestResponse>(request =>
(Task<TestResponse>) Task.Factory.StartNew(() =>
{
var worker = workers.Take();
try
{
return worker.Execute(request);
}
catch (Exception)
{
throw;
}
finally
{
}
}));
}
protected override void OnStop()
{
this.bus.Dispose();
}
}
class MyWorker
{
public TestResponse Execute(ReportData request)
{
int clientId = request.clientId;
int incidentId = request.incidentId;
string clientName = request.clientName;
string clientNetworkPath = request.clientNetworkPath;
string formatDescription = request.formatDescription;
string reportFormat = request.reportFormat;
ReportQuery reportQuery = new ReportQuery();
reportQuery.Get(incidentId, reportFormat, formatDescription, clientName, clientNetworkPath, clientId);
return new TestResponse { Response = " ***** Report generated for client: " + clientName + ", incident Id: " + incidentId + ", and format: " + reportFormat + " ***** " };
}
}
While this all works, I also need some way to notify the Angular SPA that a report has been generated so I can give the user an appropriate feedback. This is where I am a bit lost though. Can EasyNetQ interact with Angular code? Also, once I receive a response in Publisher, i can probably call some method in my Web API controller, but still the problem of alerting the Angular code remains. Any ideas?
First note that you have to store information about report status somewhere. You can store it in two places:
Persistent storage (database, redis cache, whatever).
In memory of web api service (because it's this service which client is communicating to).
When you decided where to store - there are again two options of how to pass this information to a client:
Client (Angular) can poll from time to time (note that it is not what is called "long polling"). If you store your status in database - you can just look it up there in this case.
There is a persistent connection between Angular and your api (web sockets, long polling also falls here). In this case you better store your status in memory of web api (by passing rabbit message with report status from your service to web api, which then stores that in memory and\or directly forwards that to Angular via persistent connection).
If you don't expect clients on different platforms (iOS, pure linux etc) - SignlarR can work fine. It will fallback from websockets to long polling to regular polling depending on user browser's capabilities.

Categories