How can I get channel messages from telegram channels with TLSharp?
The following links haven't helped me:
How can I get messages from a Telegram channel with the Telegram API
How to obtain all messages from my channel in telegram?
You can use this code
public async Task GatherChannelHistory(string channelName, int offset = 0, int maxId = -1, int limit = 50)
{
_resultMessages.Clear();
await _client.ConnectAsync();
var dialogs = (TLDialogsSlice)await _client.GetUserDialogsAsync();
var chat = dialogs.Chats.ToList()
.OfType<TLChannel>()
.FirstOrDefault(c => c.Title == channelName);
if (chat.AccessHash != null)
{
var tlAbsMessages =
await _client.GetHistoryAsync(
new TLInputPeerChannel {ChannelId= chat.Id, AccessHash = chat.AccessHash.Value}, offset,
maxId, limit);
var tlChannelMessages = (TLChannelMessages) tlAbsMessages;
for (var index = 0; index < tlChannelMessages.Messages.Count-1; index++)
{
var tlAbsMessage = tlChannelMessages.Messages.ToList()[index];
var message = (TLMessage) tlAbsMessage;
//Now you have the message and you can do what you need with it
//the code below is an example of messages classification
if (message.media == null)
{
_resultMessages.Add(new ChannelMessage()
{
Id = message.id,
ChannelId = chat.id,
Content = message.message,
Type = EnChannelMessage.Message,
Views = message.views,
});
}
else
{
switch (message.media.GetType().ToString())
{
case "TeleSharp.TL.TLMessageMediaPhoto":
var tLMessageMediaPhoto = (TLMessageMediaPhoto)message.media;
_resultMessages.Add(new ChannelMessage()
{
Id = message.id,
ChannelId = chat.id,
Content = tLMessageMediaPhoto.caption,
Type = EnChannelMessage.MediaPhoto,
Views = message.views ?? 0,
});
break;
case "TeleSharp.TL.TLMessageMediaDocument":
var tLMessageMediaDocument = (TLMessageMediaDocument)message.media;
_resultMessages.Add(new ChannelMessage()
{
Id = message.id,
ChannelId = chat.id,
Content = tLMessageMediaDocument.caption,
Type = EnChannelMessage.MediaDocument,
Views = message.views ?? 0,
});
break;
case "TeleSharp.TL.TLMessageMediaWebPage":
var tLMessageMediaWebPage = (TLMessageMediaWebPage)message.media;
string url = string.Empty;
if (tLMessageMediaWebPage.webpage.GetType().ToString() != "TeleSharp.TL.TLWebPageEmpty")
{
var webPage = (TLWebPage) tLMessageMediaWebPage.webpage;
url = webPage.url;
}
_resultMessages.Add(new ChannelMessage
{
Id = message.id,
ChannelId = chat.id,
Content = message.message + #" : " + url,
Type = EnChannelMessage.WebPage,
Views = message.views ?? 0,
});
break;
}
}
}
}
}
To get channel messages you simply need to be receiving channel updates.
As at TL-schema-52 you could request:
channels.getDialogs#a9d3d249 offset:int limit:int = messages.Dialogs;
however this has been dropped in TL-schema-53.
I'm guessing you can try one of the other channel.* functions,
I have not tried yet on TL-schema-53
What version of the TL-schema is your TLSharp using?
You could simply implement the relevant functions if they are not yet implemented in your TLSharp version
Not sure if this works 100% without missing any messages, but this is what I have used in one of my projects:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TeleSharp.TL;
using TeleSharp.TL.Channels;
using TeleSharp.TL.Messages;
using TLSharp.Core;
using TLSharp.Core.Utils;
namespace NewsArchive.Telegram
{
/// <summary>
/// Created to be used as a workaround of ref/out since they cannot be used in an async method => GetMessagesInternal
/// </summary>
public class RequestOffset
{
/// <summary>
/// Value of the offset
/// </summary>
public int Id { get; set; }
}
public class TelegramNewsClient
{
#region Properties
private TelegramClient _client;
private int _apiId;
private string _apiHash;
private static readonly int RESULT_MAX = 100;
#endregion
/// <summary>
/// Ctor
/// </summary>
/// <param name="apiId"></param>
/// <param name="apiHash"></param>
public TelegramNewsClient(int apiId, string apiHash)
{
_apiId = apiId;
_apiHash = apiHash;
_client = new TelegramClient(_apiId, _apiHash);
_client.ConnectAsync().Wait();
}
/// <summary>
/// Authenticates the user with the phone number
/// </summary>
/// <param name="phone"></param>
/// <returns></returns>
public async Task Authenticate(string phone)
{
var hash = await _client.SendCodeRequestAsync(phone);
var code = "<code_from_telegram>"; // you can change code in debugger
var user = await _client.MakeAuthAsync(phone, hash, code);
}
/// <summary>
/// Gets all messages from a channel
/// </summary>
/// <param name="channelName"></param>
/// <returns></returns>
public async Task<IEnumerable<TLMessage>> GetChannelMessages(string channelName)
{
var messages = new List<TLMessage>();
var channel = await GetChannel(channelName);
if(channel == null)
throw new Exception($"The channel {channelName} was not found!");
var offset = new RequestOffset(){Id = 1};
var internalMessages = new List<TLMessage>();
internalMessages = await GetMessagesInternal(channel.Id, channel.AccessHash.Value, offset);
messages = messages.Concat(internalMessages)
.OrderBy(m => m.Id)
.ToList();
while (internalMessages.Count > 0)
{
/*When you reach the last message, the API will keep returning the same last message over and over again,
that's why we stop making requests and return the result*/
if ((internalMessages.Count == 1 && internalMessages.First().Id == messages.Max(m => m.Id)))
break;
internalMessages = await GetMessagesInternal(channel.Id, channel.AccessHash.Value, offset);
messages = messages.Concat(internalMessages)
.OrderBy(m =>m.Id)
.ToList();
/*if you make too many requests you will be locked out of the API*/
await Task.Delay(TimeSpan.FromSeconds(1));
}
return messages;
}
private async Task<List<TLMessage>> GetMessagesInternal(int channelId, long accessHash, RequestOffset offset)
{
/*Refer to https://core.telegram.org/api/offsets for more info on how to use the offsets.
Here we basically get the last RESULT_MAX (100 in this case) messages newer than the offset.Id aka offsetId*/
var history = await _client.GetHistoryAsync(new TLInputPeerChannel
{
ChannelId = channelId,
AccessHash = accessHash
}, offset.Id, 0, -RESULT_MAX, RESULT_MAX, 0, 0) as TLChannelMessages;
/*Some messages are service messages with no useful content, and if cast to TLMessage it will throw an exception*/
var messages = history.Messages
.Where(m => m is TLMessage)
.Cast<TLMessage>()
.ToList();
/*Get the ID of the last message so it can be used in the next API call*/
offset.Id = messages.Max(m => m.Id);
return messages;
}
private async Task<TLChannel> GetChannel(string channelName)
{
var offset = new RequestOffset() { Id = RESULT_MAX };
var channels = (await _client.GetUserDialogsAsync(0, offset.Id, null, RESULT_MAX) as TLDialogs)
?.Chats
?.Cast<TLChannel>()
?.ToList();
var channel = channels?.FirstOrDefault(c => c.Username.Equals(channelName, StringComparison.OrdinalIgnoreCase));
offset.Id += RESULT_MAX - 1;
while (channels.Count > 0 && channel == null)
{
channels = (await _client.GetUserDialogsAsync(0, offset.Id, null, RESULT_MAX) as TLDialogs)
?.Chats
?.Cast<TLChannel>()
?.ToList();
channel = channels?.FirstOrDefault(c => c.Username.Equals(channelName, StringComparison.OrdinalIgnoreCase));
offset.Id += RESULT_MAX - 1;
/*if you make too many requests you will be locked out of the API*/
await Task.Delay(TimeSpan.FromSeconds(1));
}
return channel;
}
}
}
Related
I am using the QBO API SDK (IppDotNetSdkQuickBooksApiV3) and can't figure out how to allow a user to Connect to QuickBooks and authorize my app. It is currently happening in two (2) steps:
A user clicks "Connect To Intuit" and is sent to Intuit to login
They are redirected back to my app and then have to connect their actual file again
I am clearly missing something but don't know what it is. I am using the ipp:connecttointuit functionality which is built into the app so I don't know how to customize it for the result I am looking for.
My app works with the two steps above however I can't have my app listed in the apps.com site using the process detailed above. They (apps.com) want the user to login using their QBO credentials, authorize the app and then automatically redirect the user back to my site with the app working. They don't want the duplicate authorization (I can't blame them).
Totally stuck. I am an ok programmer but have no experience with OpenId or OAuth.
protected void Page_Load(object sender, EventArgs e)
{
var openIdRelyingParty = new OpenIdRelyingParty();
var openid_identifier = ConfigurationManager.AppSettings["openid_identifier"];
var returnUrl = "~/OpenID/Connect";
var response = openIdRelyingParty.GetResponse();
if (response == null)
{
// Stage 2: user submitting Identifier
Identifier id;
if (Identifier.TryParse(openid_identifier, out id))
{
IAuthenticationRequest request = openIdRelyingParty.CreateRequest(openid_identifier);
FetchRequest fetch = new FetchRequest();
fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email));
fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.FullName));
fetch.Attributes.Add(new AttributeRequest("http://axschema.org/intuit/realmId"));
request.AddExtension(fetch);
request.RedirectToProvider();
}
}
else
{
if (response.FriendlyIdentifierForDisplay == null)
{
Response.Redirect("~/OpenID/Connect");
}
// Stage 3: OpenID Provider sending assertion response
//Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
FetchResponse fetch = response.GetExtension<FetchResponse>();
if (fetch != null)
{
var openIdEmail = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);
var openIdFullName = fetch.GetAttributeValue(WellKnownAttributes.Name.FullName);
var openIdRealmId = fetch.GetAttributeValue("http://axschema.org/intuit/realmId");
string userName = Membership.GetUserNameByEmail(openIdEmail);
//If username is null---------------------------------------------------
if (userName == null)
{
//DG added this---------------------------
String NewPassword = Membership.GeneratePassword(6, 1);
Membership.CreateUser(openIdEmail, NewPassword, openIdEmail);
//DG added this----------------------------
//Membership.CreateUser(openIdEmail, Guid.NewGuid().ToString(), openIdEmail);
FormsAuthentication.SetAuthCookie(openIdEmail, true);
//if (Request.QueryString["Subscribe"] != null)
//{
String csname = "DirectConnectScript";
Type cstype = this.GetType();
ClientScriptManager csm = Page.ClientScript;
// Check to see if the startup script is already registered.
if (!csm.IsStartupScriptRegistered(cstype, csname))
{
StringBuilder cstext = new StringBuilder();
cstext.AppendLine("<script>");
cstext.AppendLine("$(document).ready(function () {");
cstext.AppendLine("intuit.ipp.anywhere.directConnectToIntuit();");
cstext.AppendLine("});");
cstext.AppendLine("</script>");
csm.RegisterStartupScript(cstype, csname, cstext.ToString());
//}
}
}
else if (Request.QueryString["Disconnect"] != null)
{
RestHelper.clearProfile(RestProfile.GetRestProfile());
Response.Redirect("~/ManageConnection");
}
//If username is not null---------------------------------------------------
else if (userName != null)
{
FormsAuthentication.SetAuthCookie(userName, true);
if (!string.IsNullOrEmpty(returnUrl))
{
Response.Redirect("~/ManageConnection");
}
}
}
}
}
I feel for you, it took me a while to get this working myself.
Here is my MVC version of it. I hope it helps.
It starts with QuickBooks/Index. Uses the QB pop up to get the permissions to my app, and then just goes from there. As a bonus, if the user is already logged in to QB they are automatically logged in to the app if they have given permission in the past. This is because I persist the tokens encrypted in the database. (Ignore most of the Session stuff, I just never removed it from the sample that I created the code from).
Anyway here goes, this a lot of a code to post.
Feel free to asks questions in comments if you need any clarification
I'm assuming that you have something like this that receives the OAuthResponse and redirects back to /QuickBooks/Index (in your case the page that you have posted in the question)
public class OauthResponseController : Controller
{
/// <summary>
/// OAuthVerifyer, RealmId, DataSource
/// </summary>
private String _oauthVerifyer, _realmid, _dataSource;
/// <summary>
/// Action Results for Index, OAuthToken, OAuthVerifyer and RealmID is recieved as part of Response
/// and are stored inside Session object for future references
/// NOTE: Session storage is only used for demonstration purpose only.
/// </summary>
/// <returns>View Result.</returns>
public ViewResult Index()
{
if (Request.QueryString.HasKeys())
{
// This value is used to Get Access Token.
_oauthVerifyer = Request.QueryString.GetValues("oauth_verifier").FirstOrDefault().ToString();
if (_oauthVerifyer.Length == 1)
{
_oauthVerifyer = Request.QueryString["oauth_verifier"].ToString();
}
_realmid = Request.QueryString.GetValues("realmId").FirstOrDefault().ToString();
if (_realmid.Length == 1)
{
_realmid = Request.QueryString["realmId"].ToString();
}
Session["Realm"] = _realmid;
//If dataSource is QBO call QuickBooks Online Services, else call QuickBooks Desktop Services
_dataSource = Request.QueryString.GetValues("dataSource").FirstOrDefault().ToString();
if (_dataSource.Length == 1)
{
_dataSource = Request.QueryString["dataSource"].ToString();
}
Session["DataSource"] = _dataSource;
getAccessToken();
//Production applications should securely store the Access Token.
//In this template, encrypted Oauth access token is persisted in OauthAccessTokenStorage.xml
OauthAccessTokenStorageHelper.StoreOauthAccessToken();
// This value is used to redirect to Default.aspx from Cleanup page when user clicks on ConnectToInuit widget.
Session["RedirectToDefault"] = true;
}
else
{
Response.Write("No oauth token was received");
}
return View(); // This will redirect to /QuickBooks/OpenIdIndex which is almost the same as the code that you have posted
}
/// <summary>
/// Gets the OAuth Token
/// </summary>
private void getAccessToken()
{
IOAuthSession clientSession = CreateSession();
try
{
IToken accessToken = clientSession.ExchangeRequestTokenForAccessToken((IToken)Session["requestToken"], _oauthVerifyer);
Session["AccessToken"] = accessToken.Token;
// Add flag to session which tells that accessToken is in session
Session["Flag"] = true;
// Remove the Invalid Access token since we got the new access token
Session.Remove("InvalidAccessToken");
Session["AccessTokenSecret"] = accessToken.TokenSecret;
}
catch (Exception ex)
{
//Handle Exception if token is rejected or exchange of Request Token for Access Token failed.
throw ex;
}
}
/// <summary>
/// Creates User Session
/// </summary>
/// <returns>OAuth Session.</returns>
private IOAuthSession CreateSession()
{
OAuthConsumerContext consumerContext = new OAuthConsumerContext
{
ConsumerKey = ConfigurationManager.AppSettings["consumerKey"].ToString(),
ConsumerSecret = ConfigurationManager.AppSettings["consumerSecret"].ToString(),
SignatureMethod = SignatureMethod.HmacSha1
};
return new OAuthSession(consumerContext,
Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlRequestToken,
Constants.OauthEndPoints.IdFedOAuthBaseUrl,
Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlAccessToken);
}
}
This is my equivalent page to yours.
public class QuickBooksController : Controller
{
private readonly IQueryChannel _queryChannel;
private readonly ICommandChannel _commandChannel;
public QuickBooksController(IQueryChannel queryChannel, ICommandChannel commandChannel)
{
_queryChannel = queryChannel;
_commandChannel = commandChannel;
}
/// <summary>
/// OpenId Relying Party
/// </summary>
private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
public ActionResult Index(string returnurl)
{
QuickBooksAuthStore.Load();
if (QuickBooksContext.AccessToken != null)
{
if (!new Customers().CheckConnection())
{
QuickBooksContext.FriendlyName = null;
OauthAccessTokenStorageHelper.RemoveInvalidOauthAccessToken(QuickBooksContext.FriendlyEmail);
QuickBooksContext.AccessToken = null;
}
}
if (returnurl != null || QuickBooksContext.QuickReturnUrl != null)
{
if (returnurl != null)
{
QuickBooksContext.QuickReturnUrl = returnurl;
}
if (QuickBooksContext.AccessToken != null)
{
var connected = new Customers().CheckConnection();
if (connected)
{
returnurl = QuickBooksContext.QuickReturnUrl;
QuickBooksContext.QuickReturnUrl = null;
return Redirect(returnurl);
}
}
}
return View();
}
public RedirectResult OpenIdIndex()
{
var openid_identifier = ConfigurationManager.AppSettings["openid_identifier"] + SessionContext.CurrentUser.MasterCompanyId;
var response = openid.GetResponse();
if (response == null)
{
// Stage 2: user submitting Identifier
Identifier id;
if (Identifier.TryParse(openid_identifier, out id))
{
try
{
IAuthenticationRequest request = openid.CreateRequest(openid_identifier);
FetchRequest fetch = new FetchRequest();
fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email));
fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.FullName));
request.AddExtension(fetch);
request.RedirectToProvider();
}
catch (ProtocolException ex)
{
throw ex;
}
}
}
else
{
if (response.FriendlyIdentifierForDisplay == null)
{
Response.Redirect("/OpenId");
}
// Stage 3: OpenID Provider sending assertion response, storing the response in Session object is only for demonstration purpose
Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
FetchResponse fetch = response.GetExtension<FetchResponse>();
if (fetch != null)
{
Session["OpenIdResponse"] = "True";
Session["FriendlyEmail"] = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);// emailAddresses.Count > 0 ? emailAddresses[0] : null;
Session["FriendlyName"] = fetch.GetAttributeValue(WellKnownAttributes.Name.FullName);//fullNames.Count > 0 ? fullNames[0] : null;
OauthAccessTokenStorageHelper.GetOauthAccessTokenForUser(Session["FriendlyEmail"].ToString());
QuickBooksAuthStore.UpdateFriendlyId(Session["FriendlyIdentifier"].ToString(), Session["FriendlyName"].ToString());
}
}
string query = Request.Url.Query;
if (!string.IsNullOrWhiteSpace(query) && query.ToLower().Contains("disconnect=true"))
{
Session["AccessToken"] = "dummyAccessToken";
Session["AccessTokenSecret"] = "dummyAccessTokenSecret";
Session["Flag"] = true;
return Redirect("QuickBooks/CleanupOnDisconnect");
}
return Redirect("/QuickBooks/Index");
}
/// <summary>
/// Service response.
/// </summary>
private String txtServiceResponse = "";
/// <summary>
/// Disconnect Flag.
/// </summary>
protected String DisconnectFlg = "";
public ActionResult Disconnect()
{
OAuthConsumerContext consumerContext = new OAuthConsumerContext
{
ConsumerKey = ConfigurationManager.AppSettings["consumerKey"].ToString(),
SignatureMethod = SignatureMethod.HmacSha1,
ConsumerSecret = ConfigurationManager.AppSettings["consumerSecret"].ToString()
};
OAuthSession oSession = new OAuthSession(consumerContext, Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlRequestToken,
Constants.OauthEndPoints.AuthorizeUrl,
Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlAccessToken);
oSession.ConsumerContext.UseHeaderForOAuthParameters = true;
if ((Session["AccessToken"] + "").Length > 0)
{
Session["FriendlyName"] = null;
oSession.AccessToken = new TokenBase
{
Token = Session["AccessToken"].ToString(),
ConsumerKey = ConfigurationManager.AppSettings["consumerKey"].ToString(),
TokenSecret = Session["AccessTokenSecret"].ToString()
};
IConsumerRequest conReq = oSession.Request();
conReq = conReq.Get();
conReq = conReq.ForUrl(Constants.IaEndPoints.DisconnectUrl);
try
{
conReq = conReq.SignWithToken();
}
catch (Exception ex)
{
throw ex;
}
//Used just see the what header contains
string header = conReq.Context.GenerateOAuthParametersForHeader();
//This method will clean up the OAuth Token
txtServiceResponse = conReq.ReadBody();
//Reset All the Session Variables
Session.Remove("oauthToken");
// Dont remove the access token since this is required for Reconnect btn in the Blue dot menu
// Session.Remove("accessToken");
// Add the invalid access token into session for the display of the Disconnect btn
Session["InvalidAccessToken"] = Session["AccessToken"];
// Dont Remove flag since we need to display the blue dot menu for Reconnect btn in the Blue dot menu
// Session.Remove("Flag");
ViewBag.DisconnectFlg = "User is Disconnected from QuickBooks!";
//Remove the Oauth access token from the OauthAccessTokenStorage.xml
OauthAccessTokenStorageHelper.RemoveInvalidOauthAccessToken(Session["FriendlyEmail"].ToString());
}
return RedirectToAction("Index", "QuickBooks");
}
public ActionResult CleanUpOnDisconnect()
{
//perform the clean up here
// Redirect to Home when user clicks on ConenctToIntuit widget.
object value = Session["RedirectToDefault"];
if (value != null)
{
bool isTrue = (bool)value;
if (isTrue)
{
Session.Remove("InvalidAccessToken");
Session.Remove("RedirectToDefault");
return Redirect("/QuickBooks/index");
}
}
return RedirectToAction("Index", "QuickBooks");
}
private String consumerSecret, consumerKey, oauthLink, RequestToken, TokenSecret, oauth_callback_url;
public RedirectResult OAuthGrant()
{
oauth_callback_url = Request.Url.GetLeftPart(UriPartial.Authority) + ConfigurationManager.AppSettings["oauth_callback_url"];
consumerKey = ConfigurationManager.AppSettings["consumerKey"];
consumerSecret = ConfigurationManager.AppSettings["consumerSecret"];
oauthLink = Constants.OauthEndPoints.IdFedOAuthBaseUrl;
IToken token = (IToken)Session["requestToken"];
IOAuthSession session = CreateSession();
IToken requestToken = session.GetRequestToken();
Session["requestToken"] = requestToken;
RequestToken = requestToken.Token;
TokenSecret = requestToken.TokenSecret;
oauthLink = Constants.OauthEndPoints.AuthorizeUrl + "?oauth_token=" + RequestToken + "&oauth_callback=" + UriUtility.UrlEncode(oauth_callback_url);
return Redirect(oauthLink);
}
/// <summary>
/// Gets the Access Token
/// </summary>
/// <returns>Returns OAuth Session</returns>
protected IOAuthSession CreateSession()
{
OAuthConsumerContext consumerContext = new OAuthConsumerContext
{
ConsumerKey = consumerKey,
ConsumerSecret = consumerSecret,
SignatureMethod = SignatureMethod.HmacSha1
};
return new OAuthSession(consumerContext,
Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlRequestToken,
oauthLink,
Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlAccessToken);
}
}
..and here is the cshtml page for QuickBooks/Index
#using System.Configuration
#using PubManager.Domain.WebSession
#{
string MenuProxy = Request.Url.GetLeftPart(UriPartial.Authority) + "/" + System.Configuration.ConfigurationManager.AppSettings["menuProxy"];
string OauthGrant = Request.Url.GetLeftPart(UriPartial.Authority) + "/" + System.Configuration.ConfigurationManager.AppSettings["grantUrl"];
ViewBag.Title = "MagManager - Intuit Anywhere";
string FriendlyName = (string)HttpContext.Current.Session["FriendlyName"] + "";
string FriendlyEmail = (string)HttpContext.Current.Session["FriendlyEmail"];
string FriendlyIdentifier = (string)HttpContext.Current.Session["FriendlyIdentifier"];
string realm = (string)Session["Realm"];
string dataSource = (string)Session["DataSource"];
string accessToken = (string)Session["AccessToken"] + "";
string accessTokenSecret = (string)Session["AccessTokenSecret"];
string invalidAccessToken = (string)Session["InvalidAccessToken"];
}
<h1>Intuit - QuickBooks Online</h1>
<div class="well">
#if (FriendlyName.Length == 0)
{
<script>
window.location.href = "/QuickBooks/OpenIdIndex";
</script>
}
else
{
<div id="IntuitInfo">
<strong>Open Id Information:</strong><br />
Welcome: #FriendlyName<br />
E-mail Address: #FriendlyEmail<br />
<br />
#if (accessToken.Length > 0 && invalidAccessToken == null)
{
<div id="oAuthinfo">
<a onclick="reconcileInvoices()" id="RecInvoices" class="btn btn-primary">Set Invoices Paid</a><br />
<br/>
<a onclick="recTax()" id="RecTax" class="btn btn-primary">Sync Tax Rates</a><br />
<br/>
#if (SessionContext.UseAccountCodes)
{
<a onclick="recProducts()" id="RecProducts" class="btn btn-primary">Sync Products</a><br />
<br />
}
Reconcile Customers<br/>
<br/>
<a href="/QuickBooks/Disconnect" id="Disconnect" class="btn btn-primary">
Disconnect from
QuickBooks
</a>
<br/><br/>
#*Get QuickBooks Customer List<br/>
<br/>*#
#*<br />
Get QuickBooks Invoice List<br />
<br />*#
<br />
</div>
}
else
{
<br />
<ipp:connecttointuit></ipp:connecttointuit>
}
</div>
}
</div>
<script type="text/javascript" src="https://js.appcenter.intuit.com/Content/IA/intuit.ipp.anywhere-1.3.5.js"></script>
<script type="text/javascript">
intuit.ipp.anywhere.setup({
menuProxy: '#MenuProxy',
grantUrl: '#OauthGrant'
});
</script>
tl;dr
Have .NET Core 2.0 application which uses a Data Protection Provider which persists a key file across all of the sites on my domain.
Worked fine, however, application cookie became too big.
Implemented a SessionStore on the cookie using ITicketStore
Cookie size is greatly reduced, however, the key from the DPP no longer persists across my sites.
Is there something I'm supposed to do in my ITicketStore implementation to fix this? I'm assuming so, since this is where the problem arises, however, I could not figure it out.
Some snippets:
Startup.cs --> ConfigureServices()
var keysFolder = $#"c:\temp\_WebAppKeys\{_env.EnvironmentName.ToLower()}";
var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(keysFolder));
var dataProtector = protectionProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
"Cookies",
"v2");
--snip--
services.AddSingleton<ITicketStore, TicketStore>();
--snip--
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetApplicationName("app_auth");
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".XAUTH";
options.Cookie.Domain = ".domain.com";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.LoginPath = "/Account/Login";
options.DataProtectionProvider = protectionProvider;
options.TicketDataFormat = new TicketDataFormat(dataProtector);
options.CookieManager = new ChunkingCookieManager();
options.SessionStore = services.BuildServiceProvider().GetService<ITicketStore>();
});
TicketStore.cs
public class TicketStore : ITicketStore
{
private IMemoryCache _cache;
private const string KeyPrefix = "AuthSessionStore-";
public TicketStore(IMemoryCache cache)
{
_cache = cache;
}
public Task RemoveAsync(string key)
{
_cache.Remove(key);
return Task.FromResult(0);
}
public Task RenewAsync(string key, AuthenticationTicket ticket)
{
var options = new MemoryCacheEntryOptions
{
Priority = CacheItemPriority.NeverRemove
};
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc.HasValue)
{
options.SetAbsoluteExpiration(expiresUtc.Value);
}
options.SetSlidingExpiration(TimeSpan.FromMinutes(60));
_cache.Set(key, ticket, options);
return Task.FromResult(0);
}
public Task<AuthenticationTicket> RetrieveAsync(string key)
{
AuthenticationTicket ticket;
_cache.TryGetValue(key, out ticket);
return Task.FromResult(ticket);
}
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var key = KeyPrefix + Guid.NewGuid();
await RenewAsync(key, ticket);
return key;
}
I also ran into this issue.
The SessionIdClaim value in Microsoft.Owin.Security.Cookies is "Microsoft.Owin.Security.Cookies-SessionId", while the SessionIdClaim value in Microsoft.AspNetCore.Authentication.Cookies is "Microsoft.AspNetCore.Authentication.Cookies-SessionId".
This results in a SessionId Missing error due to this code on the AspNetCore side even when you implemented a distributed session store (using RedisCacheTicketStore for example) as decribed here: https://mikerussellnz.github.io/.NET-Core-Auth-Ticket-Redis/
I was able to re-compile the AspNetKatana project with the new string, and then the SessionID was found on the .NET Core side.
Additionally, it seems the AuthenticationTicket classes are different, so I was able to get this working by implementing a conversion method to convert the
Microsoft.Owin.Security.AuthenticationTicket Ticket to the Microsoft.AspNetCore.Authentication.AuthenticationTicket Ticket and then store the ticket using the AspNetCore serializer (Microsoft.AspNetCore.Authentication.TicketSerializer).
public Microsoft.AspNetCore.Authentication.AuthenticationTicket ConvertTicket(Microsoft.Owin.Security.AuthenticationTicket ticket)
{
Microsoft.AspNetCore.Authentication.AuthenticationProperties netCoreAuthProps = new Microsoft.AspNetCore.Authentication.AuthenticationProperties();
netCoreAuthProps.IssuedUtc = ticket.Properties.IssuedUtc;
netCoreAuthProps.ExpiresUtc = ticket.Properties.ExpiresUtc;
netCoreAuthProps.IsPersistent = ticket.Properties.IsPersistent;
netCoreAuthProps.AllowRefresh = ticket.Properties.AllowRefresh;
netCoreAuthProps.RedirectUri = ticket.Properties.RedirectUri;
ClaimsPrincipal cp = new ClaimsPrincipal(ticket.Identity);
Microsoft.AspNetCore.Authentication.AuthenticationTicket netCoreTicket = new Microsoft.AspNetCore.Authentication.AuthenticationTicket(cp, netCoreAuthProps, "Cookies");
return netCoreTicket;
}
private static Microsoft.AspNetCore.Authentication.TicketSerializer _netCoreSerializer = Microsoft.AspNetCore.Authentication.TicketSerializer.Default;
private static byte[] SerializeToBytesNetCore(Microsoft.AspNetCore.Authentication.AuthenticationTicket source)
{
return _netCoreSerializer.Serialize(source);
}
With these additional methods, the RenwAsync method can be changed to this:
public Task RenewAsync(string key, Microsoft.Owin.Security.AuthenticationTicket ticket)
{
var options = new DistributedCacheEntryOptions();
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc.HasValue)
{
options.SetAbsoluteExpiration(expiresUtc.Value);
}
var netCoreTicket = ConvertTicket(ticket);
// convert to .NET Core format
byte[] netCoreVal = SerializeToBytesNetCore(netCoreTicket);
// serialize ticket using .NET Core Serializer
_cache.Set(key, netCoreVal, options);
return Task.FromResult(0);
}
I am not sure if this is the best approach, but it seems to work on my test project, admittedly I am not using this in production, hopefully this helps.
UPDATE #1: Alternate approach to avoid re-compiling
It looks like this might also work by re-creating the cookie with both SessionId claim values on the OWIN side. This will allow you to use the standard library without re-compiling. I tried it this morning but have not had a chance to thoroughly test it, although on my initial test it does load the claims properly on both sides. Basically, if you modify the authentication ticket to have both SessionId claims, it will find the session in both applications. This code snippet gets the cookie, unprotects it, adds the additional claim, and then replaces the cookie inside the OnValidateIdentity event of the CookieAuthenticationProvider.
string cookieName = "myappname";
string KatanaSessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
string NetCoreSessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";
Microsoft.Owin.Security.Interop.ChunkingCookieManager cookieMgr = new ChunkingCookieManager();
OnValidateIdentity = ctx =>
{
var incomingIdentity = ctx.Identity;
var cookie = cookieMgr.GetRequestCookie(ctx.OwinContext, cookieName);
if (cookie != null)
{
var ticket = TicketDataFormat.Unprotect(cookie);
if (ticket != null)
{
Claim claim = ticket.Identity.Claims.FirstOrDefault(c => c.Type.Equals(KatanaSessionIdClaim));
Claim netCoreSessionClaim = ticket.Identity.Claims.FirstOrDefault(c => c.Type.Equals(NetCoreSessionIdClaim));
if (netCoreSessionClaim == null)
{
// adjust cookie options as needed.
CookieOptions opts = new CookieOptions();
opts.Expires = ticket.Properties.ExpiresUtc == null ?
DateTime.Now.AddDays(14) : ticket.Properties.ExpiresUtc.Value.DateTime;
opts.HttpOnly = true;
opts.Path = "/";
opts.Secure = true;
netCoreSessionClaim = new Claim(NetCoreSessionIdClaim, claim.Value);
ticket.Identity.AddClaim(netCoreSessionClaim);
string newCookieValue = TicketDataFormat.Protect(ticket);
cookieMgr.DeleteCookie(ctx.OwinContext, cookieName, opts);
cookieMgr.AppendResponseCookie(ctx.OwinContext, cookieName, newCookieValue, opts);
}
}
}
}
If there is a better approach I would be curious to know, or a better place to swap out the cookie.
The problem is, as other answers have pointed out, that the Owin cookie's session key claim has another type string than the one expected in ASP.Net Core.
The following implementation of a ticket data format makes sure to add the session key claim for ASP.Net Core when generating the cookie string.
public class AspNetCoreCompatibleTicketDataFormat : ISecureDataFormat<AuthenticationTicket> {
private const string OwinSessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
private const string AspNetCoreSessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";
private readonly ISecureDataFormat<AuthenticationTicket> dataFormat;
public AspNetCoreCompatibleTicketDataFormat(IDataProtector protector) {
this.dataFormat = new AspNetTicketDataFormat(protector);
}
public string Protect(AuthenticationTicket data) {
var sessionClaim = data.Identity.FindFirst(OwinSessionIdClaim);
if (sessionClaim != null) {
data.Identity.AddClaim(new Claim(AspNetCoreSessionIdClaim, sessionClaim.Value));
}
return this.dataFormat.Protect(data);
}
public AuthenticationTicket Unprotect(string protectedText) {
return this.dataFormat.Unprotect(protectedText);
}
}
This code should be added to the ASP.Net Framework project. You use it instead of the AspNetTicketDataFormat in your StartUp.cs code, like this:
app.UseCookieAuthentication(new CookieAuthenticationOptions {
TicketDataFormat = new AspNetCoreCompatibleTicketDataFormat(
new DataProtectorShim(...
The code makes sure that the generated cookie contains a session id claim known to ASP.NET Core. It works for the scenario where you generate the cookie in an ASP.NET Framework OWIN project and consume it in an ASP.NET Core project.
One would have to make sure to always add both to get it working in the opposite circumstance where the cookie is generated in the ASP.NET Core project.
I ended up doing a mix of the above answers, Replacing the ICookieManager implementation on the AspNetCore side that generates the cookies, adding both claims when doing so (as per the relevant part of the answer given by #AnthonyValeri):
public class OwinAspNetCompatibleCookieManager : ICookieManager
{
private const string OwinSessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
private const string AspNetCoreSessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";
private readonly ICookieManager actualCookieManager;
public OwinAspNetCompatibleCookieManager(ICookieManager actualCookieManager) => this.actualCookieManager = actualCookieManager;
// TODO oh this async void is so so bad, i have to find another way
public async void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
{
IAuthenticationHandler handler = await context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>().GetHandlerAsync(context, CookieAuthenticationDefaults.AuthenticationScheme).ConfigureAwait(false);
if (handler is CookieAuthenticationHandler cookieHandler)
{
value = MakeOwinAspNetCoreCompatible(key, value, cookieHandler.Options);
}
actualCookieManager.AppendResponseCookie(context, key, value, options);
}
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
{
actualCookieManager.DeleteCookie(context, key, options);
}
public string GetRequestCookie(HttpContext context, string key)
{
return actualCookieManager.GetRequestCookie(context, key);
}
private string MakeOwinAspNetCoreCompatible(string key, string cookieValue, CookieAuthenticationOptions options)
{
if (key.Equals("MySharedCookieName") && !string.IsNullOrWhiteSpace(cookieValue))
{
AuthenticationTicket ticket = options.TicketDataFormat.Unprotect(cookieValue);
ClaimsPrincipal principal = ticket.Principal;
Claim aspNetCoreClaim = ticket.Principal.Claims.FirstOrDefault(x => x.Type.Equals(AspNetCoreSessionIdClaim));
Claim owinClaim = ticket.Principal.Claims.FirstOrDefault(x => x.Type.Equals(OwinSessionIdClaim));
Claim[] claims = null;
if (aspNetCoreClaim != null && owinClaim == null)
{
claims = new Claim[] { aspNetCoreClaim, new Claim(OwinSessionIdClaim, aspNetCoreClaim.Value) };
}
else if (aspNetCoreClaim == null && owinClaim != null)
{
claims = new Claim[] { owinClaim, new Claim(AspNetCoreSessionIdClaim, owinClaim.Value) };
}
if (claims?.Length > 0)
{
var newIdentity = new ClaimsIdentity(claims, principal.Identity.AuthenticationType);
principal = new ClaimsPrincipal(newIdentity);
ticket = new AuthenticationTicket(principal, ticket.AuthenticationScheme);
cookieValue = options.TicketDataFormat.Protect(ticket);
}
}
return cookieValue;
}
}
And then configuring it on the .AddCookie() call in ConfigureServices:
...
options.CookieManager = new OwinAspNetCompatibleCookieManager(new ChunkingCookieManager());
...
I ran into the same issue and banging my head to resolve this. But thanks to #Anthony Valeri to pointing right at where the issue is. So I came up with the solution below. (I was doing this as part of POC for one of our migration projects and this is not been tested in Production, but worked for POC.)
Created an extended CookieAuthenticationOptions class and added a new property.
public class ExtendedCookieAuthenticationOptions : CookieAuthenticationOptions
{
public string SessionIdClaim { get; set; }
}
Copied CookieAuthenticationHandler class from GitHub Source Code and extended that with above class
public class ExtendedCookieAuthenticationHandler : SignInAuthenticationHandler<ExtendedCookieAuthenticationOptions>
{
private const string HeaderValueNoCache = "no-cache";
private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
private const string SessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";
private bool _shouldRefresh;
private bool _signInCalled;
private bool _signOutCalled;
private DateTimeOffset? _refreshIssuedUtc;
private DateTimeOffset? _refreshExpiresUtc;
private string _sessionKey;
private Task<AuthenticateResult> _readCookieTask;
private AuthenticationTicket _refreshTicket;
public ExtendedCookieAuthenticationHandler(IOptionsMonitor<ExtendedCookieAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
/// <summary>
/// Added this to overwrite default SessionIdClaim value
/// </summary>
public virtual string SessionIdClaimType
{
get { return string.IsNullOrEmpty(Options.SessionIdClaim) ? SessionIdClaim : Options.SessionIdClaim; }
}
/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
/// If it is not provided a default instance is supplied which does nothing when the methods are called.
/// </summary>
protected new CookieAuthenticationEvents Events
{
get { return (CookieAuthenticationEvents)base.Events; }
set { base.Events = value; }
}
protected override Task InitializeHandlerAsync()
{
// Cookies needs to finish the response
Context.Response.OnStarting(FinishResponseAsync);
return Task.CompletedTask;
}
/// <summary>
/// Creates a new instance of the events instance.
/// </summary>
/// <returns>A new instance of the events instance.</returns>
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CookieAuthenticationEvents());
private Task<AuthenticateResult> EnsureCookieTicket()
{
// We only need to read the ticket once
if (_readCookieTask == null)
{
_readCookieTask = ReadCookieTicket();
}
return _readCookieTask;
}
private void CheckForRefresh(AuthenticationTicket ticket)
{
var currentUtc = Clock.UtcNow;
var issuedUtc = ticket.Properties.IssuedUtc;
var expiresUtc = ticket.Properties.ExpiresUtc;
var allowRefresh = ticket.Properties.AllowRefresh ?? true;
if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
var timeRemaining = expiresUtc.Value.Subtract(currentUtc);
if (timeRemaining < timeElapsed)
{
RequestRefresh(ticket);
}
}
}
private void RequestRefresh(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal = null)
{
var issuedUtc = ticket.Properties.IssuedUtc;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (issuedUtc != null && expiresUtc != null)
{
_shouldRefresh = true;
var currentUtc = Clock.UtcNow;
_refreshIssuedUtc = currentUtc;
var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value);
_refreshExpiresUtc = currentUtc.Add(timeSpan);
_refreshTicket = CloneTicket(ticket, replacedPrincipal);
}
}
private AuthenticationTicket CloneTicket(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal)
{
var principal = replacedPrincipal ?? ticket.Principal;
var newPrincipal = new ClaimsPrincipal();
foreach (var identity in principal.Identities)
{
newPrincipal.AddIdentity(identity.Clone());
}
var newProperties = new AuthenticationProperties();
foreach (var item in ticket.Properties.Items)
{
newProperties.Items[item.Key] = item.Value;
}
return new AuthenticationTicket(newPrincipal, newProperties, ticket.AuthenticationScheme);
}
private async Task<AuthenticateResult> ReadCookieTicket()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name);
if (string.IsNullOrEmpty(cookie))
{
return AuthenticateResult.NoResult();
}
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
if (ticket == null)
{
return AuthenticateResult.Fail("Unprotect ticket failed");
}
if (Options.SessionStore != null)
{
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaimType));
if (claim == null)
{
return AuthenticateResult.Fail("SessionId missing");
}
_sessionKey = claim.Value;
ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
if (ticket == null)
{
return AuthenticateResult.Fail("Identity missing in session store");
}
}
var currentUtc = Clock.UtcNow;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
if (Options.SessionStore != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
return AuthenticateResult.Fail("Ticket expired");
}
CheckForRefresh(ticket);
// Finally we have a valid ticket
return AuthenticateResult.Success(ticket);
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var result = await EnsureCookieTicket();
if (!result.Succeeded)
{
return result;
}
var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket);
await Events.ValidatePrincipal(context);
if (context.Principal == null)
{
return AuthenticateResult.Fail("No principal.");
}
if (context.ShouldRenew)
{
RequestRefresh(result.Ticket, context.Principal);
}
return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name));
}
private CookieOptions BuildCookieOptions()
{
var cookieOptions = Options.Cookie.Build(Context);
// ignore the 'Expires' value as this will be computed elsewhere
cookieOptions.Expires = null;
return cookieOptions;
}
protected virtual async Task FinishResponseAsync()
{
// Only renew if requested, and neither sign in or sign out was called
if (!_shouldRefresh || _signInCalled || _signOutCalled)
{
return;
}
var ticket = _refreshTicket;
if (ticket != null)
{
var properties = ticket.Properties;
if (_refreshIssuedUtc.HasValue)
{
properties.IssuedUtc = _refreshIssuedUtc;
}
if (_refreshExpiresUtc.HasValue)
{
properties.ExpiresUtc = _refreshExpiresUtc;
}
if (Options.SessionStore != null && _sessionKey != null)
{
await Options.SessionStore.RenewAsync(_sessionKey, ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaimType, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Scheme.Name));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
}
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
var cookieOptions = BuildCookieOptions();
if (properties.IsPersistent && _refreshExpiresUtc.HasValue)
{
cookieOptions.Expires = _refreshExpiresUtc.Value.ToUniversalTime();
}
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name,
cookieValue,
cookieOptions);
await ApplyHeaders(shouldRedirectToReturnUrl: false, properties: properties);
}
}
protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
properties = properties ?? new AuthenticationProperties();
_signInCalled = true;
// Process the request cookie to initialize members like _sessionKey.
await EnsureCookieTicket();
var cookieOptions = BuildCookieOptions();
var signInContext = new CookieSigningInContext(
Context,
Scheme,
Options,
user,
properties,
cookieOptions);
DateTimeOffset issuedUtc;
if (signInContext.Properties.IssuedUtc.HasValue)
{
issuedUtc = signInContext.Properties.IssuedUtc.Value;
}
else
{
issuedUtc = Clock.UtcNow;
signInContext.Properties.IssuedUtc = issuedUtc;
}
if (!signInContext.Properties.ExpiresUtc.HasValue)
{
signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan);
}
await Events.SigningIn(signInContext);
if (signInContext.Properties.IsPersistent)
{
var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan);
signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime();
}
var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);
if (Options.SessionStore != null)
{
if (_sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
_sessionKey = await Options.SessionStore.StoreAsync(ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaimType, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.ClaimsIssuer));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
}
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name,
cookieValue,
signInContext.CookieOptions);
var signedInContext = new CookieSignedInContext(
Context,
Scheme,
signInContext.Principal,
signInContext.Properties,
Options);
await Events.SignedIn(signedInContext);
// Only redirect on the login path
var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
await ApplyHeaders(shouldRedirect, signedInContext.Properties);
Logger.AuthenticationSchemeSignedIn(Scheme.Name);
}
protected async override Task HandleSignOutAsync(AuthenticationProperties properties)
{
properties = properties ?? new AuthenticationProperties();
_signOutCalled = true;
// Process the request cookie to initialize members like _sessionKey.
await EnsureCookieTicket();
var cookieOptions = BuildCookieOptions();
if (Options.SessionStore != null && _sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
var context = new CookieSigningOutContext(
Context,
Scheme,
Options,
properties,
cookieOptions);
await Events.SigningOut(context);
Options.CookieManager.DeleteCookie(
Context,
Options.Cookie.Name,
context.CookieOptions);
// Only redirect on the logout path
var shouldRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath;
await ApplyHeaders(shouldRedirect, context.Properties);
Logger.AuthenticationSchemeSignedOut(Scheme.Name);
}
private async Task ApplyHeaders(bool shouldRedirectToReturnUrl, AuthenticationProperties properties)
{
Response.Headers[HeaderNames.CacheControl] = HeaderValueNoCache;
Response.Headers[HeaderNames.Pragma] = HeaderValueNoCache;
Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;
if (shouldRedirectToReturnUrl && Response.StatusCode == 200)
{
// set redirect uri in order:
// 1. properties.RedirectUri
// 2. query parameter ReturnUrlParameter
//
// Absolute uri is not allowed if it is from query string as query string is not
// a trusted source.
var redirectUri = properties.RedirectUri;
if (string.IsNullOrEmpty(redirectUri))
{
redirectUri = Request.Query[Options.ReturnUrlParameter];
if (string.IsNullOrEmpty(redirectUri) || !IsHostRelative(redirectUri))
{
redirectUri = null;
}
}
if (redirectUri != null)
{
await Events.RedirectToReturnUrl(
new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, redirectUri));
}
}
}
private static bool IsHostRelative(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
if (path.Length == 1)
{
return path[0] == '/';
}
return path[0] == '/' && path[1] != '/' && path[1] != '\\';
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
var returnUrl = properties.RedirectUri;
if (string.IsNullOrEmpty(returnUrl))
{
returnUrl = OriginalPathBase + OriginalPath + Request.QueryString;
}
var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl);
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri));
await Events.RedirectToAccessDenied(redirectContext);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var redirectUri = properties.RedirectUri;
if (string.IsNullOrEmpty(redirectUri))
{
redirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
}
var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(loginUri));
await Events.RedirectToLogin(redirectContext);
}
private string GetTlsTokenBinding()
{
var binding = Context.Features.Get<ITlsTokenBindingFeature>()?.GetProvidedTokenBindingId();
return binding == null ? null : Convert.ToBase64String(binding);
}
}`
Replaced
private const string SessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";
with new property
/// <summary>
/// Added this to overwrite default SessionIdClaim value
/// </summary>
public virtual string SessionIdClaimType
{
get { return string.IsNullOrEmpty(Options.SessionIdClaim) ? SessionIdClaim : Options.SessionIdClaim; }
}
Added new extension method to use ExtendedCookieAuthenticationHandler.
public static class CookieExtentions
{
public static AuthenticationBuilder AddExtendedCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<ExtendedCookieAuthenticationOptions> configureOptions)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<ExtendedCookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
return builder.AddScheme<ExtendedCookieAuthenticationOptions, ExtendedCookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
}
}
Used new extension method in ConfigureServices method in startup.cs
.AddExtendedCookie("AuthScheme", "DisplayName", options =>
{
options.Cookie.Name = "CookieName";
options.Cookie.Domain = ".domain.com";
options.Cookie.HttpOnly = true;
options.SlidingExpiration = true;
options.Events = new CookieAuthenticationEvents()
{
//Sample how to add additional check for logged in User at Application Level.
OnValidatePrincipal = async context => { await ValidateAsync(context); },
};
options.LoginPath = "/account/login";
options.CookieManager = new ChunkingCookieManager();
options.SessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
options.TicketDataFormat = ticketDataFormat;
//SessionStore is configured in PostConfigureCookieAuthenticationOptions with DI
//options.SessionStore = //From DI
});
I can save a picture in iOS and Android but I can't find a way to save an image in UWP. Any ideas?
Thank you in advance.
I can't find a way to save an image in UWP
No, we can't extract the image data from an Xamarin ImageSource object.
There is StreamImagesourceHandler class implementation in UWP, see here
public sealed class StreamImageSourceHandler : IImageSourceHandler
{
public async Task<Windows.UI.Xaml.Media.ImageSource> LoadImageAsync(ImageSource imagesource, CancellationToken cancellationToken = new CancellationToken())
{
BitmapImage bitmapimage = null;
//Omitted
return bitmapimage;
}
}
So we need to extract data from BitmapImage.
Actually the BitmapImage class is inherited from ImageSource class, while we can't extract the image data from ImageSource, for the reason, please see these two questions:
Convert ImageSource to WriteableBitmap in Metro Windows 8
How save BitmapImage WinRT
The solution here is to use different way for Windows Runtime(W/WP8.1 & UWP) app, extracting image data from System.IO.Stream class is supported in UWP.
We can use DependencyService to access native platform features, firstly, create an interface in PCL:
public interface ISaveImage
{
void SavePictureToDisk(ImageSource imgSrc, string Id, bool OverwriteIfExist = false);
void SavePictureToDiskWINRT(System.IO.Stream imgStream, string Id, bool OverwriteIfExist = false);
}
In the code behind of Xamarin page:
var memoryStream = new MemoryStream(Convert.FromBase64String("iVBOxxxxxxxxxxMVEX/uQOAuwPzUxxxxxxxxxxxx="));
ImageSource imgsource = ImageSource.FromStream(() => memoryStream);
if (Device.OS == TargetPlatform.Windows|| Device.OS == TargetPlatform.WinPhone)
DependencyService.Get<ISaveImage>().SavePictureToDiskWINRT(memoryStream, "1");
else
DependencyService.Get<ISaveImage>().SavePictureToDisk(imgsource, "1");
Implement the interface in UWP platform:
using Xamarin.Forms;
using WorkingWithImages.WinUniversal;
using System.IO;
using System;
using Windows.Storage.Streams;
[assembly: Xamarin.Forms.Dependency(typeof(SaveImageImplementation))]
namespace WorkingWithImages.WinUniversal
{
public class SaveImageImplementation : ISaveImage
{
public SaveImageImplementation() { }
public void SavePictureToDisk(ImageSource imgSrc, string Id, bool OverwriteIfExist = false)
{
throw new NotImplementedException();
}
public async void SavePictureToDiskWINRT(Stream imgStream, string Id, bool OverwriteIfExist = false)
{
var inStream = imgStream.AsRandomAccessStream();
var fileBytes = new byte[inStream.Size];
using (DataReader reader = new DataReader(inStream))
{
await reader.LoadAsync((uint)inStream.Size);
reader.ReadBytes(fileBytes);
}
var file = await Windows.Storage.ApplicationData.Current.LocalFolder.CreateFileAsync(Id+".jpg", Windows.Storage.CreationCollisionOption.ReplaceExisting);
using (var fs = await file.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite))
{
var outStream = fs.GetOutputStreamAt(0);
var dataWriter = new DataWriter(outStream);
dataWriter.WriteBytes(fileBytes);
await dataWriter.StoreAsync();
dataWriter.DetachStream();
await outStream.FlushAsync();
outStream.Dispose();
fs.Dispose();
}
}
}
}
Please check my completed demo in here
About UWP File storage guidance, please see Create, write, and read a file
maybe someone needs a solution for iOS and Android (below). Meanwhile I'm waiting an idea for UWP.
iOS
/// <summary>
/// Saves the picture to disk.
/// </summary>
/// <returns>The picture to disk.</returns>
/// <param name="imgSrc">Image source.</param>
/// <param name="id">Identifier.</param>
/// <param name="overwriteIfExist">if set to <c>true</c> overwrite if exist.</param>
/// <returns>The picture to disk.</returns>
public async void SaveImage(ImageSource imgSrc, string id, bool overwriteIfExist = false)
{
var renderer = new StreamImagesourceHandler();
var photo = await renderer.LoadImageAsync(imgSrc);
string jpgFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), id + ".jpg");
if (File.Exists(jpgFilename))
{
File.Delete(jpgFilename);
}
NSData imgData = photo.AsJPEG();
NSError err;
if (imgData.Save(jpgFilename, false, out err))
{
Console.WriteLine("saved as " + jpgFilename);
}
else
{
Console.WriteLine("NOT saved as " + jpgFilename
+ " because" + err.LocalizedDescription);
}
}
Good to know, when iOS saves an image as jpg, the image header says png.
Android
/// <summary>
/// Saves the picture to disk.
/// </summary>
/// <param name="imgSrc">Image source.</param>
/// <param name="id">The image identifier.</param>
/// <param name="overwriteIfExist">if set to <c>true</c> overwrite if exist.</param>
/// <returns>The picture to disk.</returns>
public async void SaveImage(ImageSource imgSrc, string id,
bool overwriteIfExist = false)
{
var renderer = new StreamImagesourceHandler();
var photo = await renderer.LoadImageAsync(imgSrc, Forms.Context);
string jpgFilename = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), id + ".jpg");
if (File.Exists(jpgFilename))
{
File.Delete(jpgFilename);
}
using (FileStream fs = new FileStream(jpgFilename, FileMode.OpenOrCreate))
{
photo.Compress(Bitmap.CompressFormat.Jpeg, 100, fs);
}
}
As far as i know, i can youse Actions (http://blog.botframework.com/2016/05/13/BotFramework.buttons/) to create inline keyboards in Telegram and other messages.
But what about Custom Keyboards (https://core.telegram.org/bots#keyboards)? How can i add them using Bot Framework?
I read about ChannelData (http://docs.botframework.com/connector/custom-channeldata/#custom-telegram-messages), but i didnt get, how can i pass JSON to CreateReplyMessage method.
Use CreateReplyMessage to create a Message object:
var replyMessage = incomingMessage.CreateReplyMessage("Yo, I heard you.");
Then set the ChannelData
replyMessage.ChannelData = {custom Telegram JSON}
For Bot Framework v4:
{
var reply = context.Context.Activity.CreateReply(messageText);
if (BotDialogHelpers.ExtractMessengerFromDialogContext(context) == BotDialogHelpers.Messengers.Telegram)
{
GenerateReplyMarkupForTelegram(reply);
}
await context.Context.SendActivityAsync(reply, token);
}
/// <summary>
/// https://learn.microsoft.com/en-us/azure/bot-service/dotnet/bot-builder-dotnet-channeldata?view=azure-bot-service-3.0
/// https://core.telegram.org/bots/api#message sendMessage reply_markup
/// </summary>
private void GenerateReplyMarkupForTelegram(IActivity reply)
{
var replyMarkup = new
{
reply_markup = new
{
remove_keyboard = true,
}
};
var channelData = new
{
method = "sendMessage",
parameters = replyMarkup,
};
reply.ChannelData = JObject.FromObject(channelData);
}
want to update a C# Winforms application to use await.
The application calls the MYOB Accountright API via an SDK.
I am using Dot Net Framework 4.5.1
The old code is like this
public void GetItems( CompanyFile companyFile )
{
var itemSvc = new ItemService(MyConfiguration, null, MyOAuthKeyService);
string pageFilter = string.Format("$top={0}&$skip={1}&$orderby=Date desc", PageSize,
PageSize * (_currentPage - 1));
itemSvc.GetRange(MyCompanyFile, pageFilter, MyCredentials, OnComplete, OnError);
}
/// <summary>
/// Method called on Async complete
/// </summary>
/// <param name="statusCode"></param>
/// <param name="items"></param>
/// <remarks></remarks>
private void OnComplete(System.Net.HttpStatusCode statusCode,
PagedCollection<Item> items)
{
myItems = items;
}
/// <summary>
/// Callback if there is an error
/// </summary>
/// <param name="uri"></param>
/// <param name="ex"></param>
/// <remarks></remarks>
private void OnError(Uri uri, Exception ex)
{
Trace.WriteLine("In OnError");
MessageBox.Show(ex.Message);
}
I want code something like this
private async Task FetchItemsAsync()
{
var itemSvc = new ItemService(MyConfiguration, null, MyOAuthKeyService);
string pageFilter = string.Format("$top={0}&$skip={1}&$orderby=Date desc", PageSize,
PageSize * (_currentPage - 1));
itemSvc.GetRange(MyCompanyFile, pageFilter, MyCredentials, OnComplete, OnError);
var totalPages = (int)Math.Ceiling((double)(myItems.Count / PageSize));
while (_currentPage < totalPages)
{
await LoadMore(); // how do I write this?
}
}
How do I do that?
[Update5]
I tried
private const double PageSize = 400;
protected CancellationTokenSource MyCancellationTokenSource;
protected CompanyFile MyCompanyFile;
protected IApiConfiguration MyConfiguration;
protected ICompanyFileCredentials MyCredentials;
protected ItemService MyItemService;
protected IOAuthKeyService MyOAuthKeyService;
private int _currentPage = 1;
private int _totalPages;
public void FetchItems(CompanyFile companyFile, IApiConfiguration configuration, ICompanyFileCredentials credentials)
{
MyCompanyFile = companyFile;
MyConfiguration = configuration;
MyCredentials = credentials;
MyCancellationTokenSource = new CancellationTokenSource();
MyItemService = new ItemService(MyConfiguration, null, MyOAuthKeyService);
FetchAllItemsAsync();
}
private async void FetchAllItemsAsync()
{
try
{
var items = new List<Item>();
int totalPages = 0;
do
{
string pageFilter = string.Format("$top={0}&$skip={1}&$orderby=Date desc", PageSize, PageSize * (_currentPage - 1));
CancellationToken ct = MyCancellationTokenSource.Token;
Log("About to Await GetRange");
Task<PagedCollection<Item>> tpc = MyItemService.GetRangeAsync(MyCompanyFile, pageFilter, MyCredentials, ct, null);
Log("About to Await GetRange B");
PagedCollection<Item> newItems = await tpc; // fails here
Log("Page {0} retrieved {1} items", _currentPage, newItems.Count);
if (totalPages == 0)
{
totalPages = (int)Math.Ceiling((items.Count / PageSize));
}
items.AddRange(newItems.Items.ToArray());
_currentPage++;
}
while (_currentPage < totalPages);
MessageBox.Show(string.Format("Fetched {0} items", items.Count));
}
catch (ApiCommunicationException ex)
{
Log(ex.ToString());
throw;
}
catch (Exception exception)
{
Log(exception.ToString());
throw;
}
}
However I get a ValidationException
{"Encountered a validation error (http://localhost:8080/AccountRight/ab5c1f96-7663-4052-8360-81004cfe8598/Inventory/Item/?$top=400&$skip=0&$orderby=Date desc)"}
[MYOB.AccountRight.SDK.ApiValidationException]: {"Encountered a validation error (http://localhost:8080/AccountRight/ab5c1f96-7663-4052-8360-81004cfe8598/Inventory/Item/?$top=400&$skip=0&$orderby=Date desc)"}
base: {"Encountered a validation error (http://localhost:8080/AccountRight/ab5c1f96-7663-4052-8360-81004cfe8598/Inventory/Item/?$top=400&$skip=0&$orderby=Date desc)"}
ErrorInformation: "Warning, error messages have not been finalised in this release and may change"
Errors: Count = 1
RequestId: "e573dfed-ec68-4aff-ac5e-3ffde1c2f943"
StatusCode: BadRequest
URI: {http://localhost:8080/AccountRight/ab5c1f96-7663-4052-8360-81004cfe8598/Inventory/Item/?$top=400&$skip=0&$orderby=Date desc}
I have cross posted this problem to
MYOB Support
The latest version of the MYOB.AccountRight.API.SDK you are referencing already has overloads for supporting async/await on .NET4, .NET45 and PCL.
The Sample code was created as an example for someone using .NET 3.5 (hence no async/await). Another sample (windows phone) shows async/await in action using the SDK
[Update]
You are probably getting an OData related exception as the Item entity does not have a Date field for which you can filter by (see docs).
When you catch an ApiCommunicationException (of which ApiValidationException is a subclass) there is an Errors property that provides more detail.
There is also a RequestId (and some other properties) which are very useful should you need to talk to the support guys if you have issues talking to the cloud hosted API.
You can use a TaskCompletionSource object that you can resolve with the result or error callback. I'm not sure what the signature of the error callback is so that part probably wont work.
private Task<PagedCollection<Item>> FetchItemsAsync()
{
var taskSource = new TaskCompletionSource<PagedCollection<Item>>();
var itemSvc = new ItemService(MyConfiguration, null, MyOAuthKeyService);
string pageFilter = string.Format("$top={0}&$skip={1}&$orderby=Date desc", PageSize,
PageSize * (_currentPage - 1));
itemSvc.GetRange(
MyCompanyFile,
pageFilter,
MyCredentials,
(statusCode, items) => taskSource.TrySetResult(items),
(error) => taskSource => taskSource.TrySetException(error) // Not sure if this is correct signature
);
return taskSource.Task;
}
You can then return the Task object that it creates which you can use for async things. I'm not really sure about what logic you are trying to implement because your question is not very detailed but you can use the method with the await command because it returns a Task object like the following.
private async void FetchAllItemsAsync()
{
int totalPages;
do
{
items = await FetchItemsAsync()
totalPages = (int)Math.Ceiling((double)(items.Count / PageSize));
_currentPage++;
} while (_currentPage < totalPages)
}