* Evernote provider is now available. Based on Evernote SDK for .NET and the obsolete doc (ie. https://dev.evernote.com/doc/articles/authentication.php) (Step 3 is POST, not GET) * Fix SyncrhonizationContext deadlock caused by ASP.NET site * Evernote provider now working trought Xamarin OAuthAuthenticator and Identity Server 3 * Add claims for notestoreuri and accesstoken * Evernote OK, before cleanup * Cleanup * Remove my credentials in demo project. * Change the default URL to lower case
379 lines
14 KiB
C#
379 lines
14 KiB
C#
using EvernoteSDK;
|
|
using Microsoft.Owin;
|
|
using Microsoft.Owin.Infrastructure;
|
|
using Microsoft.Owin.Logging;
|
|
using Microsoft.Owin.Security;
|
|
using Microsoft.Owin.Security.Infrastructure;
|
|
using Owin.Security.Providers.Evernote.Messages;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Net.Http;
|
|
using System.Security.Claims;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Web;
|
|
|
|
namespace Owin.Security.Providers.Evernote
|
|
{
|
|
public class EvernoteAuthenticationHandler : AuthenticationHandler<EvernoteAuthenticationOptions>
|
|
{
|
|
private const string StateCookie = "_EvernoteState";
|
|
private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";
|
|
|
|
private const string RequestTokenEndpoint = "/oauth";
|
|
private const string AuthenticationEndpoint = "/OAuth.action?oauth_token=";
|
|
private const string AccessTokenEndpoint = "/oauth";
|
|
|
|
private const string ProductionBase = "https://www.evernote.com";
|
|
private const string SandBoxBase = "https://sandbox.evernote.com";
|
|
|
|
public string BaseUri => Options.IsSandBox ? SandBoxBase : ProductionBase;
|
|
|
|
private readonly ILogger _logger;
|
|
private readonly HttpClient _httpClient;
|
|
|
|
public EvernoteAuthenticationHandler(HttpClient httpClient, ILogger logger)
|
|
{
|
|
_httpClient = httpClient;
|
|
_logger = logger;
|
|
|
|
_logger.WriteInformation("Authentication Handler initialized");
|
|
}
|
|
|
|
public override async Task<bool> InvokeAsync()
|
|
{
|
|
if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
|
|
{
|
|
return await InvokeReturnPathAsync();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
|
|
{
|
|
AuthenticationProperties properties = null;
|
|
|
|
try
|
|
{
|
|
var query = Request.Query;
|
|
var protectedRequestToken = Request.Cookies[StateCookie];
|
|
|
|
var requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken);
|
|
|
|
if (requestToken == null)
|
|
{
|
|
_logger.WriteWarning("Invalid state");
|
|
return null;
|
|
}
|
|
|
|
properties = requestToken.Properties;
|
|
|
|
var returnedToken = query.Get("oauth_token");
|
|
if (string.IsNullOrWhiteSpace(returnedToken))
|
|
{
|
|
_logger.WriteWarning("Missing oauth_token");
|
|
return new AuthenticationTicket(null, properties);
|
|
}
|
|
|
|
if (returnedToken != requestToken.Token)
|
|
{
|
|
_logger.WriteWarning("Unmatched token");
|
|
return new AuthenticationTicket(null, properties);
|
|
}
|
|
|
|
var oauthVerifier = query.Get("oauth_verifier");
|
|
if (string.IsNullOrWhiteSpace(oauthVerifier))
|
|
{
|
|
_logger.WriteWarning("Missing or blank oauth_verifier");
|
|
return new AuthenticationTicket(null, properties);
|
|
}
|
|
|
|
// Retrieve access token
|
|
var accessToken = await ObtainAccessTokenAsync(Options.AppKey, Options.AppSecret, requestToken, oauthVerifier);
|
|
|
|
// Retrieve user infos
|
|
var userName = GetUserInfos(accessToken);
|
|
|
|
var context = new EvernoteAuthenticatedContext(Context, accessToken)
|
|
{
|
|
Identity = new ClaimsIdentity(
|
|
Options.AuthenticationType,
|
|
ClaimsIdentity.DefaultNameClaimType,
|
|
ClaimsIdentity.DefaultRoleClaimType)
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(context.UserId))
|
|
{
|
|
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.UserId, XmlSchemaString, Options.AuthenticationType));
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(userName))
|
|
{
|
|
context.Identity.AddClaim(new Claim(ClaimTypes.Name, userName, XmlSchemaString, Options.AuthenticationType));
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(context.NoteStoreUrl))
|
|
{
|
|
context.Identity.AddClaim(new Claim("urn:evernote:notestoreuri", context.NoteStoreUrl, XmlSchemaString, Options.AuthenticationType));
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(context.AccessToken))
|
|
{
|
|
context.Identity.AddClaim(new Claim("urn:evernote:accesstoken", context.AccessToken, XmlSchemaString, Options.AuthenticationType));
|
|
}
|
|
|
|
context.Properties = requestToken.Properties;
|
|
|
|
Response.Cookies.Delete(StateCookie);
|
|
|
|
await Options.Provider.Authenticated(context);
|
|
|
|
return new AuthenticationTicket(context.Identity, context.Properties);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.WriteError("Authentication failed", ex);
|
|
return new AuthenticationTicket(null, properties);
|
|
}
|
|
}
|
|
|
|
private string GetUserInfos(AccessToken accessToken)
|
|
{
|
|
ENSession.SetSharedSessionDeveloperToken(accessToken.Token, accessToken.NoteStoreUrl);
|
|
var session = new ENSession();
|
|
session.PerformPostAuthentication();
|
|
return session.UserDisplayName;
|
|
}
|
|
|
|
protected override async Task ApplyResponseChallengeAsync()
|
|
{
|
|
_logger.WriteInformation("Applying response challenge");
|
|
|
|
if (Response.StatusCode != 401)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
|
|
|
|
if (challenge == null) return;
|
|
|
|
var baseUri =
|
|
Request.Scheme +
|
|
Uri.SchemeDelimiter +
|
|
Request.Host +
|
|
Request.PathBase;
|
|
|
|
var callBackUri = baseUri + Options.CallbackPath;
|
|
|
|
var properties = challenge.Properties;
|
|
if (string.IsNullOrEmpty(properties.RedirectUri))
|
|
{
|
|
properties.RedirectUri = baseUri + Request.Path + Request.QueryString;
|
|
}
|
|
|
|
_logger.WriteInformation("Requesting Token...");
|
|
|
|
var requestToken = await ObtainRequestTokenAsync(Options.AppKey, Options.AppSecret, callBackUri, properties);
|
|
|
|
_logger.WriteInformation("Token request successfull. Token: " + requestToken.Token);
|
|
|
|
var authorizationEndpoint = BaseUri + AuthenticationEndpoint + requestToken.Token;
|
|
|
|
if (requestToken.CallbackConfirmed)
|
|
{
|
|
var cookieOptions = new CookieOptions
|
|
{
|
|
HttpOnly = true,
|
|
Secure = Request.IsSecure
|
|
};
|
|
|
|
Response.StatusCode = 302;
|
|
Response.Cookies.Append(StateCookie, Options.StateDataFormat.Protect(requestToken), cookieOptions);
|
|
Response.Headers.Set("Location", authorizationEndpoint);
|
|
}
|
|
else
|
|
{
|
|
_logger.WriteError("requestToken CallbackConfirmed!=true");
|
|
}
|
|
}
|
|
|
|
private async Task<RequestToken> ObtainRequestTokenAsync(string appKey, string appSecret, string callBackUri, AuthenticationProperties properties)
|
|
{
|
|
string normalizedUrl;
|
|
string normalizedRequestParameters;
|
|
|
|
var oauthBase = new OAuthBase();
|
|
var url = new Uri(BaseUri + RequestTokenEndpoint);
|
|
var signature = oauthBase.GenerateSignature(
|
|
url,
|
|
appKey,
|
|
appSecret,
|
|
null,
|
|
null,
|
|
"GET",
|
|
oauthBase.GenerateTimeStamp(),
|
|
oauthBase.GenerateNonce(),
|
|
callBackUri, out normalizedUrl, out normalizedRequestParameters);
|
|
|
|
normalizedRequestParameters = normalizedRequestParameters + "&oauth_signature=" + HttpUtility.UrlEncode(signature);
|
|
|
|
if (normalizedRequestParameters.Length > 0)
|
|
normalizedUrl += "?";
|
|
|
|
var authorizationEndpoint = normalizedUrl + normalizedRequestParameters;
|
|
|
|
_logger.WriteInformation("Url =" + authorizationEndpoint);
|
|
|
|
string query = await WebRequestAsync(HttpMethod.Get, authorizationEndpoint, null);
|
|
|
|
if (query?.Length > 0)
|
|
{
|
|
NameValueCollection queryString = HttpUtility.ParseQueryString(query);
|
|
if (queryString["oauth_token"] != null)
|
|
{
|
|
_logger.WriteInformation("Retrieving data...");
|
|
|
|
return new RequestToken
|
|
{
|
|
Token = Uri.UnescapeDataString(queryString["oauth_token"]),
|
|
CallbackConfirmed = true,
|
|
Properties = properties
|
|
};
|
|
}
|
|
}
|
|
|
|
return new RequestToken();
|
|
}
|
|
|
|
private async Task<bool> InvokeReturnPathAsync()
|
|
{
|
|
if (!Options.CallbackPath.HasValue || Options.CallbackPath != Request.Path) return false;
|
|
// TODO: error responses
|
|
|
|
var ticket = await AuthenticateAsync();
|
|
if (ticket == null)
|
|
{
|
|
_logger.WriteWarning("Invalid return state, unable to redirect.");
|
|
Response.StatusCode = 500;
|
|
return true;
|
|
}
|
|
|
|
var context = new EvernoteReturnEndpointContext(Context, ticket)
|
|
{
|
|
SignInAsAuthenticationType = Options.SignInAsAuthenticationType,
|
|
RedirectUri = ticket.Properties.RedirectUri
|
|
};
|
|
|
|
await Options.Provider.ReturnEndpoint(context);
|
|
|
|
if (context.SignInAsAuthenticationType != null && context.Identity != null)
|
|
{
|
|
var grantIdentity = context.Identity;
|
|
if (!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
|
|
{
|
|
grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType);
|
|
}
|
|
Context.Authentication.SignIn(context.Properties, grantIdentity);
|
|
}
|
|
|
|
if (context.IsRequestCompleted || context.RedirectUri == null) return context.IsRequestCompleted;
|
|
|
|
var redirectUri = context.RedirectUri;
|
|
|
|
if (context.Identity == null)
|
|
{
|
|
// add a redirect hint that sign-in failed in some way
|
|
redirectUri = WebUtilities.AddQueryString(redirectUri, "error", "access_denied");
|
|
}
|
|
Response.Redirect(redirectUri);
|
|
context.RequestCompleted();
|
|
|
|
return context.IsRequestCompleted;
|
|
}
|
|
|
|
private async Task<AccessToken> ObtainAccessTokenAsync(string appKey, string appSecret, RequestToken token, string verifier)
|
|
{
|
|
string normalizedUrl;
|
|
string normalizedRequestParameters;
|
|
|
|
var oauthBase = new OAuthBase();
|
|
var url = new Uri(BaseUri + AccessTokenEndpoint);
|
|
var signature = oauthBase.GenerateSignature(
|
|
url,
|
|
appKey,
|
|
appSecret,
|
|
token.Token,
|
|
verifier,
|
|
"POST",
|
|
oauthBase.GenerateTimeStamp(),
|
|
oauthBase.GenerateNonce(),
|
|
"",
|
|
out normalizedUrl, out normalizedRequestParameters);
|
|
|
|
var postData = normalizedRequestParameters + "&oauth_signature=" + HttpUtility.UrlEncode(signature);
|
|
var authorizationParts = new SortedDictionary<string, string>();
|
|
|
|
foreach (var key in postData.Split('&'))
|
|
{
|
|
authorizationParts.Add(key.Split('=')[0], key.Split('=')[1]);
|
|
}
|
|
|
|
var authorizationHeaderBuilder = new StringBuilder();
|
|
authorizationHeaderBuilder.Append("OAuth ");
|
|
foreach (var authorizationPart in authorizationParts)
|
|
{
|
|
authorizationHeaderBuilder.AppendFormat(
|
|
"{0}=\"{1}\", ", authorizationPart.Key, Uri.EscapeDataString(authorizationPart.Value));
|
|
}
|
|
authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2;
|
|
|
|
string query = await WebRequestAsync(HttpMethod.Post, normalizedUrl, authorizationHeaderBuilder.ToString());
|
|
|
|
if (query.Length > 0)
|
|
{
|
|
var responseParameters = HttpUtility.ParseQueryString(query);
|
|
if (responseParameters["oauth_token"] != null)
|
|
{
|
|
return new AccessToken
|
|
{
|
|
Token = Uri.UnescapeDataString(responseParameters["oauth_token"]),
|
|
Shard = Uri.UnescapeDataString(responseParameters["edam_shard"]),
|
|
UserId = Uri.UnescapeDataString(responseParameters["edam_userId"]),
|
|
NoteStoreUrl = Uri.UnescapeDataString(responseParameters["edam_noteStoreUrl"]),
|
|
WebApiUrlPrefix = Uri.UnescapeDataString(responseParameters["edam_webApiUrlPrefix"]),
|
|
};
|
|
}
|
|
}
|
|
|
|
return new AccessToken();
|
|
}
|
|
|
|
private Task<string> WebRequestAsync(HttpMethod method, string url, string postData)
|
|
{
|
|
try
|
|
{
|
|
var request = new HttpRequestMessage(method, url);
|
|
|
|
if (method == HttpMethod.Post)
|
|
request.Headers.Add("Authorization", postData);
|
|
|
|
_logger.WriteInformation("Send request...");
|
|
|
|
var response = _httpClient.SendAsync(request, Request.CallCancelled).Result;
|
|
|
|
_logger.WriteInformation("Request ended");
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
return response.Content.ReadAsStringAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.WriteError(ex.Message, ex);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
} |