From b3bd46cde5190c8fc0af8e341897b6dae1ea79f6 Mon Sep 17 00:00:00 2001 From: Anthony Ruffino Date: Wed, 4 Feb 2015 16:42:13 -0500 Subject: [PATCH] added Asana. Using /signin-asana for path of redirect_uri --- .../Asana/AsanaAuthenticationExtensions.cs | 29 ++ .../Asana/AsanaAuthenticationHandler.cs | 247 ++++++++++++++++++ .../Asana/AsanaAuthenticationMiddleware.cs | 87 ++++++ .../Asana/AsanaAuthenticationOptions.cs | 136 ++++++++++ Owin.Security.Providers/Asana/Constants.cs | 7 + .../Provider/AsanaAuthenticatedContext.cs | 90 +++++++ .../Provider/AsanaAuthenticationProvider.cs | 50 ++++ .../Provider/AsanaReturnEndpointContext.cs | 26 ++ .../Provider/IAsanaAuthenticationProvider.cs | 24 ++ .../Owin.Security.Providers.csproj | 9 + ...-OwinOAuthProvidersDemo-20131113093838.mdf | Bin 3211264 -> 3211264 bytes ...nOAuthProvidersDemo-20131113093838_log.ldf | Bin 1048576 -> 1048576 bytes .../App_Start/Startup.Auth.cs | 3 + 13 files changed, 708 insertions(+) create mode 100644 Owin.Security.Providers/Asana/AsanaAuthenticationExtensions.cs create mode 100644 Owin.Security.Providers/Asana/AsanaAuthenticationHandler.cs create mode 100644 Owin.Security.Providers/Asana/AsanaAuthenticationMiddleware.cs create mode 100644 Owin.Security.Providers/Asana/AsanaAuthenticationOptions.cs create mode 100644 Owin.Security.Providers/Asana/Constants.cs create mode 100644 Owin.Security.Providers/Asana/Provider/AsanaAuthenticatedContext.cs create mode 100644 Owin.Security.Providers/Asana/Provider/AsanaAuthenticationProvider.cs create mode 100644 Owin.Security.Providers/Asana/Provider/AsanaReturnEndpointContext.cs create mode 100644 Owin.Security.Providers/Asana/Provider/IAsanaAuthenticationProvider.cs diff --git a/Owin.Security.Providers/Asana/AsanaAuthenticationExtensions.cs b/Owin.Security.Providers/Asana/AsanaAuthenticationExtensions.cs new file mode 100644 index 0000000..bbb8dbd --- /dev/null +++ b/Owin.Security.Providers/Asana/AsanaAuthenticationExtensions.cs @@ -0,0 +1,29 @@ +using System; + +namespace Owin.Security.Providers.Asana +{ + public static class AsanaAuthenticationExtensions + { + public static IAppBuilder UseAsanaAuthentication(this IAppBuilder app, + AsanaAuthenticationOptions options) + { + if (app == null) + throw new ArgumentNullException("app"); + if (options == null) + throw new ArgumentNullException("options"); + + app.Use(typeof(AsanaAuthenticationMiddleware), app, options); + + return app; + } + + public static IAppBuilder UseAsanaAuthentication(this IAppBuilder app, string clientId, string clientSecret) + { + return app.UseAsanaAuthentication(new AsanaAuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret + }); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Asana/AsanaAuthenticationHandler.cs b/Owin.Security.Providers/Asana/AsanaAuthenticationHandler.cs new file mode 100644 index 0000000..04d8b9c --- /dev/null +++ b/Owin.Security.Providers/Asana/AsanaAuthenticationHandler.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Infrastructure; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Infrastructure; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Owin.Security.Providers.Asana +{ + public class AsanaAuthenticationHandler : AuthenticationHandler + { + private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; + + private readonly ILogger logger; + private readonly HttpClient httpClient; + + public AsanaAuthenticationHandler(HttpClient httpClient, ILogger logger) + { + this.httpClient = httpClient; + this.logger = logger; + } + + protected override async Task AuthenticateCoreAsync() + { + AuthenticationProperties properties = null; + + try + { + string code = null; + string state = null; + + IReadableStringCollection query = Request.Query; + IList values = query.GetValues("code"); + if (values != null && values.Count == 1) + { + code = values[0]; + } + values = query.GetValues("state"); + if (values != null && values.Count == 1) + { + state = values[0]; + } + + properties = Options.StateDataFormat.Unprotect(state); + if (properties == null) + { + return null; + } + + // OAuth2 10.12 CSRF + if (!ValidateCorrelationId(properties, logger)) + { + return new AuthenticationTicket(null, properties); + } + + string requestPrefix = Request.Scheme + "://" + Request.Host; + string redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; + + // Build up the body for the token request + var body = new List>(); + body.Add(new KeyValuePair("grant_type", "authorization_code")); + body.Add(new KeyValuePair("client_id", Options.ClientId)); + body.Add(new KeyValuePair("client_secret", Options.ClientSecret)); + body.Add(new KeyValuePair("redirect_uri", redirectUri)); + body.Add(new KeyValuePair("code", code)); + + /*Your app makes a POST request to https://app.asana.com/-/oauth_token, passing the parameters as part of a standard form-encoded post body. + grant_type - required Must be authorization_code + client_id - required The Client ID uniquely identifies the application making the request. + client_secret - required The Client Secret belonging to the app, found in the details pane of the developer view + redirect_uri - required Must match the redirect_uri specified in the original request + code - required The code you are exchanging for an auth token + */ + + // Request the token + var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.Endpoints.TokenEndpoint); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + requestMessage.Content = new FormUrlEncodedContent(body); + HttpResponseMessage tokenResponse = await httpClient.SendAsync(requestMessage); + tokenResponse.EnsureSuccessStatusCode(); + string text = await tokenResponse.Content.ReadAsStringAsync(); + + // Deserializes the token response + dynamic response = JsonConvert.DeserializeObject(text); + string accessToken = (string)response.access_token; + + /* + * In the response, you will receive a JSON payload with the following parameters: + access_token - The token to use in future requests against the API + expires_in - The number of seconds the token is valid, typically 3600 (one hour) + token_type - The type of token, in our case, bearer + refresh_token - If exchanging a code, a long-lived token that can be used to get new access tokens when old ones expire. + data - A JSON object encoding a few key fields about the logged-in user, currently id, name, and email. + */ + + // Get the Asana user + + var context = new AsanaAuthenticatedContext(Context, response.data, accessToken); + context.Identity = new ClaimsIdentity( + Options.AuthenticationType, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + if (!string.IsNullOrEmpty(context.Id)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Email)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Name)) + { + context.Identity.AddClaim(new Claim("urn:asana:name", context.Name, XmlSchemaString, Options.AuthenticationType)); + } + context.Properties = properties; + + await Options.Provider.Authenticated(context); + + return new AuthenticationTicket(context.Identity, context.Properties); + } + catch (Exception ex) + { + logger.WriteError(ex.Message); + } + return new AuthenticationTicket(null, properties); + } + + protected override Task ApplyResponseChallengeAsync() + { + if (Response.StatusCode != 401) + { + return Task.FromResult(null); + } + + AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); + + if (challenge != null) + { + string baseUri = + Request.Scheme + + Uri.SchemeDelimiter + + Request.Host + + Request.PathBase; + + string currentUri = + baseUri + + Request.Path + + Request.QueryString; + + string redirectUri = + baseUri + + Options.CallbackPath; + + AuthenticationProperties properties = challenge.Properties; + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = currentUri; + } + + // OAuth2 10.12 CSRF + GenerateCorrelationId(properties); + + string state = Options.StateDataFormat.Protect(properties); + string authorizationEndpoint = + Options.Endpoints.AuthorizationEndpoint + + "?response_type=code" + + "&client_id=" + Uri.EscapeDataString(Options.ClientId) + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + + "&state=" + Uri.EscapeDataString(state) + ; + + + /*Your app redirects the user to https://app.asana.com/-/oauth_authorize, passing parameters along as a standard query string: + + client_id - required The Client ID uniquely identifies the application making the request. + redirect_uri - required The URI to redirect to on success or error. This must match the Redirect URL specified in the application settings. + response_type - required Must be one of either code (if using the Authorization Code Grant flow) or token (if using the Implicit Grant flow). Other flows are currently not supported. + state - optional Encodes state of the app, which will be returned verbatim in the response and can be used to match the response up to a given request. + */ + + Response.Redirect(authorizationEndpoint); + } + + return Task.FromResult(null); + } + + public override async Task InvokeAsync() + { + return await InvokeReplyPathAsync(); + } + + private async Task InvokeReplyPathAsync() + { + if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) + { + // TODO: error responses + + AuthenticationTicket ticket = await AuthenticateAsync(); + if (ticket == null) + { + logger.WriteWarning("Invalid return state, unable to redirect."); + Response.StatusCode = 500; + return true; + } + + var context = new AsanaReturnEndpointContext(Context, ticket); + context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType; + context.RedirectUri = ticket.Properties.RedirectUri; + + await Options.Provider.ReturnEndpoint(context); + + if (context.SignInAsAuthenticationType != null && + context.Identity != null) + { + ClaimsIdentity 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) + { + string 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; + } + return false; + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Asana/AsanaAuthenticationMiddleware.cs b/Owin.Security.Providers/Asana/AsanaAuthenticationMiddleware.cs new file mode 100644 index 0000000..8e7a95e --- /dev/null +++ b/Owin.Security.Providers/Asana/AsanaAuthenticationMiddleware.cs @@ -0,0 +1,87 @@ +using System; +using System.Globalization; +using System.Net.Http; +using Microsoft.Owin; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.DataHandler; +using Microsoft.Owin.Security.DataProtection; +using Microsoft.Owin.Security.Infrastructure; +using Owin.Security.Providers.Properties; + +namespace Owin.Security.Providers.Asana +{ + public class AsanaAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly HttpClient httpClient; + private readonly ILogger logger; + + public AsanaAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, + AsanaAuthenticationOptions options) + : base(next, options) + { + if (String.IsNullOrWhiteSpace(Options.ClientId)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "ClientId")); + if (String.IsNullOrWhiteSpace(Options.ClientSecret)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "ClientSecret")); + + logger = app.CreateLogger(); + + if (Options.Provider == null) + Options.Provider = new AsanaAuthenticationProvider(); + + if (Options.StateDataFormat == null) + { + IDataProtector dataProtector = app.CreateDataProtector( + typeof (AsanaAuthenticationMiddleware).FullName, + Options.AuthenticationType, "v1"); + Options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (String.IsNullOrEmpty(Options.SignInAsAuthenticationType)) + Options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + + httpClient = new HttpClient(ResolveHttpMessageHandler(Options)) + { + Timeout = Options.BackchannelTimeout, + MaxResponseContentBufferSize = 1024*1024*10, + }; + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft Owin Asana middleware"); + httpClient.DefaultRequestHeaders.ExpectContinue = false; + } + + /// + /// Provides the object for processing + /// authentication-related requests. + /// + /// + /// An configured with the + /// supplied to the constructor. + /// + protected override AuthenticationHandler CreateHandler() + { + return new AsanaAuthenticationHandler(httpClient, logger); + } + + private HttpMessageHandler ResolveHttpMessageHandler(AsanaAuthenticationOptions options) + { + HttpMessageHandler handler = options.BackchannelHttpHandler ?? new WebRequestHandler(); + + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator != null) + { + // Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (webRequestHandler == null) + { + throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch); + } + webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; + } + + return handler; + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Asana/AsanaAuthenticationOptions.cs b/Owin.Security.Providers/Asana/AsanaAuthenticationOptions.cs new file mode 100644 index 0000000..92ea3a6 --- /dev/null +++ b/Owin.Security.Providers/Asana/AsanaAuthenticationOptions.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Owin; +using Microsoft.Owin.Security; + +namespace Owin.Security.Providers.Asana +{ + public class AsanaAuthenticationOptions : AuthenticationOptions + { + public class AsanaAuthenticationEndpoints + { + /// + /// Endpoint which is used to redirect users to request Asana access + /// + /// + /// Defaults to https://app.asana.com/-/oauth_authorize + /// + public string AuthorizationEndpoint { get; set; } + + /// + /// Endpoint which is used to exchange code for access token + /// + /// + /// Defaults to https://app.asana.com/-/oauth_token + /// + public string TokenEndpoint { get; set; } + + /// + /// Endpoint which is used to obtain user information after authentication + /// + /// + /// Defaults to https://asana.com/1/OAuthGetRequestToken + /// + public string UserInfoEndpoint { get; set; } + } + + private const string AuthorizationEndPoint = "https://app.asana.com/-/oauth_authorize"; + private const string TokenEndpoint = "https://app.asana.com/-/oauth_token"; + + + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to Asana. + /// + /// + /// The pinned certificate validator. + /// + /// + /// If this property is null then the default certificate checks are performed, + /// validating the subject name and if the signing chain is a trusted party. + /// + public ICertificateValidator BackchannelCertificateValidator { get; set; } + + /// + /// The HttpMessageHandler used to communicate with Asana. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Asana. + /// + /// + /// The back channel timeout in milliseconds. + /// + public TimeSpan BackchannelTimeout { get; set; } + + /// + /// The request path within the application's base path where the user-agent will be returned. + /// The middleware will process this request when it arrives. + /// Default value is "/signin-asana". + /// + public PathString CallbackPath { get; set; } + + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + public string Caption + { + get { return Description.Caption; } + set { Description.Caption = value; } + } + + /// + /// Gets or sets the Asana supplied Client ID + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the Asana supplied Client Secret + /// + public string ClientSecret { get; set; } + + /// + /// Gets the sets of OAuth endpoints used to authenticate against Asana. Overriding these endpoints allows you to use Asana Enterprise for + /// authentication. + /// + public AsanaAuthenticationEndpoints Endpoints { get; set; } + + /// + /// Gets or sets the used in the authentication events + /// + public IAsanaAuthenticationProvider Provider { get; set; } + + + /// + /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user + /// . + /// + public string SignInAsAuthenticationType { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Initializes a new + /// + public AsanaAuthenticationOptions() + : base("Asana") + { + Caption = Constants.DefaultAuthenticationType; + CallbackPath = new PathString("/signin-asana"); + AuthenticationMode = AuthenticationMode.Passive; + BackchannelTimeout = TimeSpan.FromSeconds(60); + Endpoints = new AsanaAuthenticationEndpoints + { + AuthorizationEndpoint = AuthorizationEndPoint, + TokenEndpoint = TokenEndpoint + }; + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Asana/Constants.cs b/Owin.Security.Providers/Asana/Constants.cs new file mode 100644 index 0000000..7a00a4f --- /dev/null +++ b/Owin.Security.Providers/Asana/Constants.cs @@ -0,0 +1,7 @@ +namespace Owin.Security.Providers.Asana +{ + internal static class Constants + { + public const string DefaultAuthenticationType = "Asana"; + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Asana/Provider/AsanaAuthenticatedContext.cs b/Owin.Security.Providers/Asana/Provider/AsanaAuthenticatedContext.cs new file mode 100644 index 0000000..c2a1a90 --- /dev/null +++ b/Owin.Security.Providers/Asana/Provider/AsanaAuthenticatedContext.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Security.Claims; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; +using Newtonsoft.Json.Linq; + +namespace Owin.Security.Providers.Asana +{ + /// + /// Contains information about the login session as well as the user . + /// + public class AsanaAuthenticatedContext : BaseContext + { + /// + /// Initializes a + /// + /// The OWIN environment + /// The JSON-serialized user + /// Asana Access token + public AsanaAuthenticatedContext(IOwinContext context, dynamic user, string accessToken) + : base(context) + { + User = user; + AccessToken = accessToken; + + Id = TryGetValue(user, "id"); + Name = TryGetValue(user, "name"); + Email = TryGetValue(user, "email"); + } + + /// + /// Gets the JSON-serialized user + /// + /// + /// Contains the Asana user obtained from the User Info endpoint. By default this is https://asana.com/1/OAuthGetRequestToken but it can be + /// overridden in the options + /// + public JObject User { get; private set; } + + /// + /// Gets the Asana access token + /// + public string AccessToken { get; private set; } + + /// + /// Gets the Asana user ID + /// + public string Id { get; private set; } + + /// + /// Gets the user's name + /// + public string Name { get; private set; } + + /// + /// Gets the Asana email + /// + public string Email { get; private set; } + + /// + /// Gets the representing the user + /// + public ClaimsIdentity Identity { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties + /// + public AuthenticationProperties Properties { get; set; } + + private static string TryGetValue(dynamic user, string propertyName) + { + string value = null; + + try + { + value = (string)user[propertyName]; + } + catch + { + + } + + return value; + } + } +} diff --git a/Owin.Security.Providers/Asana/Provider/AsanaAuthenticationProvider.cs b/Owin.Security.Providers/Asana/Provider/AsanaAuthenticationProvider.cs new file mode 100644 index 0000000..b0f2a3c --- /dev/null +++ b/Owin.Security.Providers/Asana/Provider/AsanaAuthenticationProvider.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; + +namespace Owin.Security.Providers.Asana +{ + /// + /// Default implementation. + /// + public class AsanaAuthenticationProvider : IAsanaAuthenticationProvider + { + /// + /// Initializes a + /// + public AsanaAuthenticationProvider() + { + OnAuthenticated = context => Task.FromResult(null); + OnReturnEndpoint = context => Task.FromResult(null); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. + /// + public Func OnReturnEndpoint { get; set; } + + /// + /// Invoked whenever Asana succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(AsanaAuthenticatedContext context) + { + return OnAuthenticated(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + public virtual Task ReturnEndpoint(AsanaReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Asana/Provider/AsanaReturnEndpointContext.cs b/Owin.Security.Providers/Asana/Provider/AsanaReturnEndpointContext.cs new file mode 100644 index 0000000..19d0f29 --- /dev/null +++ b/Owin.Security.Providers/Asana/Provider/AsanaReturnEndpointContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; + +namespace Owin.Security.Providers.Asana +{ + /// + /// Provides context information to middleware providers. + /// + public class AsanaReturnEndpointContext : ReturnEndpointContext + { + /// + /// + /// + /// OWIN environment + /// The authentication ticket + public AsanaReturnEndpointContext( + IOwinContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/Owin.Security.Providers/Asana/Provider/IAsanaAuthenticationProvider.cs b/Owin.Security.Providers/Asana/Provider/IAsanaAuthenticationProvider.cs new file mode 100644 index 0000000..418f121 --- /dev/null +++ b/Owin.Security.Providers/Asana/Provider/IAsanaAuthenticationProvider.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Owin.Security.Providers.Asana +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> + /// + public interface IAsanaAuthenticationProvider + { + /// + /// Invoked whenever Asana succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(AsanaAuthenticatedContext context); + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + Task ReturnEndpoint(AsanaReturnEndpointContext context); + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Owin.Security.Providers.csproj b/Owin.Security.Providers/Owin.Security.Providers.csproj index f5eb564..188c5a6 100644 --- a/Owin.Security.Providers/Owin.Security.Providers.csproj +++ b/Owin.Security.Providers/Owin.Security.Providers.csproj @@ -67,6 +67,15 @@ + + + + + + + + + diff --git a/OwinOAuthProvidersDemo/App_Data/aspnet-OwinOAuthProvidersDemo-20131113093838.mdf b/OwinOAuthProvidersDemo/App_Data/aspnet-OwinOAuthProvidersDemo-20131113093838.mdf index 20d9ae72f052ff7e3ac1a6f6a0f22c7323595fe8..205a9e81f4239a5662b93b892b1b80cb732e4a77 100644 GIT binary patch delta 2510 zcmb_dU2GIp6ux(Mb~_!aw4HXhbeDG8Ev3KGZA+nrgsoyUnwYi}Frkv!+BOk|Hj5Y} zm~LX-)kYrx;YLhwSv44qNuiZ7+5{Sj5)(^#MzC1)I}xNJaB@^ z0pTvm%f`ZjdBCnzNfJ!_@h8NWt~o7nUJ8z4mRBvc(qh&*BzV}qol=2ejvcNxv<$Ag zO%hqWS}K7VdR_LgvAr&l&6|vWxH`N!0QDzoq;i`cG3m2)l80%@6(W0a!1idiM!IE- zC2OS~i;i{INkF#VT_^u|%t#FHwIblE)mQJ=e65&?xhPM%se?`bEc;n-Ms`yt+c_h* zVmo4PW#?osJ2fxovW8jN#r~a%M6Z`0L24f65D{s#q5x*5Q5)CTRU_mrk7!A6kK~FST z6b-J528*LX@A;swO7qxPpbxpWpGlX49r=NlF)i*ZWuGQ|GJ7NL3!9YlwC^URtnIX~ zf)yovJ1LC~C49hYpadudRG>^BN|Yb}Z&R;PQ`pY4>{D_G)K?|4uo~623(jYQ)3>eJ zc=CGNd%WOq!ap@^sao5RWh?j9*KQhDW)7)2>mtt^?>HGJ-;qWVz}rh&jlCZ|6$0Ie z8?v4>gXU-T;0%xrqy^LfP8vbA0s%+_NS|?=2FQBw2b%G7175Dw0`(?CsToHbA;66{ z!$bg7pP}4utQ+x*%l1L$?n}EJS9u86cZjG~779f@zUb)5(r?A3lWxSdj-J2hcdBja z4D$T&M<63uCrTctrIs!)V;tZkV`h+&t>wE|XSL!v(jnu@@@sb`@g?-9Kq|-!_i$~w zvIwtC-wHBf?D04>awV2Z`GE>Om0IJ{HYMzCG3p8}N+MX5(u1@qBUaum{ARGbWg$N? zWp;~JIefg!pFfN?a*$O>{43;6s(FPQjGkz)>fB-!YCTH3tg-Sxwaj|fc^o&$4Lvnn z=~C}Z#GQIZ5zA;vn6A%=@gwkD=@=;)Y`FTSH4&AE?=GehZa3MD*;=kZ?wK|voi})B mJelD(^3whmOI@f0)&f;PHBbZ80(C$=@DQ+0PhDuB!T$kM1u|Ly delta 1730 zcma)+e@q)?7{~9uUd!>0v6k!FTU+{*l~T$8jeoGsa8nbFQ8P9oF$M3UBl*3SwF}fp=0_X?Vn-ZEg*M_4Pwz0a)2N#$)4uUYf?h8= zD%lbk5={Lr`60Y2In4n(CK*_KK=i|SFK-UOOhOb<;+NmBeC?{eR9=Cl3>jPvVjGd6 zBh7o^#V*lp$iZNPG0Vt?4~ha58pI&VkZaTn<2?=m7R|(;Y&x!{52rtz6r)x=Y{nm^ zL@%iN76CHPS}U4QinpvXy-_?=iu0W*5vkNVQ*?H8eQ$!e%5O?LedUA4B=f~g(g_dY z;qrGh2Fe`u5F2#O(KdXXG#`<3)DNc@sS{e}sRRC=qaqGs)(e4o8nxob%@`;7;q7@E zGzI)HW5!Rt%1iGo(72VpZ-EBkfg-A=7ibc*$FEYac5Z=EVy#AB#MSVy?<_jCd`sQi z8nFS6unrh}zIBNrTaYSbD^iVYL!4UclIss)y>)i(tk#Rye0Q10Qg>;pUNg~Nr~9gF z-(}i}bwt?_7%BaX27VrXj!l7IcS#=pEe}jC()WuREWBCEZEktEl9LHxp9ue{0Gg4|VgL zn+%kx9z*@3O!X*K-z#~D2p8khPp+}D5@z2Im2v5f&Bn{x>Q2ey)uQ`qZt}vI?|__B zq@3c;Dcf_3C#QIG3TAfX6kks9f20Hws@M8{M6FkV&%xX%oV-FJpcVol__Pq9@OmN8 zYex1nft#?>##6ZeOu!FvG0;W0e7YDwYLOrkLc&NyOBbW3{@D|?zI3jV3*iPZ`a0N5 zYH_pJezA2FFkrvI+s5f8+-uz(v&Md@h1e8L7?lJ^wZf|HP=$7@Aim;-wNT8b$&;~_ z(C&2`Cde+r4dcBfG{c3A$s0Dz!Ls>`>jPKDhVAMxwPeFaH*Drjn-g-~33=xQIlj`p z(USEIBwQ9XU7;23?mN7;mU;Dm>3rX8NsSMcylk7iKCxQL##cfcHXAMFP|h9tk-O&( w8(L=bs58@CdUNr?k`2GfX2P_|%ad`W9!Ve#ND^s8Qb-fB3u(qT-10;Fe229cQb0&Q=K&4Mj(Bp}0{z zC|(pFN*+o+%827H{;qOjf!PMySr@S z4GW9Klt+nT+|owHuI!CQF&a&&O7{)5!mX;^A)F4c+IRNIl5eee{wdGypR4-oDLXEY z@T-8oOvL1$`3seV383(}pDTIfwA|e54lcw^ECZs!wuT%# z=(-7Dq48@t_M()cWCtVqys3w!Mil&uEq9GSQ0QL=U>@Z8xl`YTRjcAQF)VSrGvd_B z##62;5lc0$4i>c1ofM@uT0>i?HkEw5_L^F%N6kesYKPDYD?HTX%Q`^2DeC^_-8Y`^ z072ydU9(NYHCmQKC#Imo0Y<|Ch!#Nfjnqo*w2-z#nYDB~Z9v_fRQIt;Y*IEdyu%Pm zGx1$ZMFhY+whyHmC5~c@%{B04agG|uL+9|povNFSLARl7piK~FBd*$D(-yi30&Sr= zw3Sv;3oLxO;A3sD1qkihhq*;|f83LvwhmZmLaLL-7+}{16`P z5HuGkVU%hVwv#(k!yG0bJ}#6}a0Jbz<)jHstjCA%O1YV!8_5`$p)jzE^1Ht=yZ*j3 z(PG@MN4a1F&!F=RmAr66b=6}mvMH=*6n2RSIIIM_0YB>OImVo9IfhrTV|axZYJCF7 zp!xI_I7%DMPd)JXsE>D%)O#W@FGA;kK8<@MKi)lf%&uIn;mTObat)ze!?KhP%NfqX zXQrWDqeFBUuEC~7Q`0l9S6$dOTGQ*FkKyTGi<+;Zd>YsI1kca}!!?ZQm4A8;$VoLZ zbdxn{DP>b%T0KfMVumgA4R}v@;;x7;<8@l!6Nsha*l)Fex~qfrTO;mX8c)3uidyl} zisq-@I=Wd!pQ|uRLb+NDSB9(Mf(}x`oFw_qpcl2E>Vtq*llt*bMwjd41!p44VOVjd z*$k)Dn-7$ydVAW@vN(0^X>Q5E(rS4fuFYRawM_5vrKazmbFf3yryhQ0booSrysGsa zoa<0b!Bp#k?LMKWx_|j#D)zLee3EL*Mq%C9R`#|)7L;jwrK2rNltck3-1fcpwzZD7 z9yGG!#`gJyAGil<3X~13@v{bGSe|Pze6lAF)ox%zpg9j~HD@&~SIso<-4lj2ddD{? z<^m;0HLlqq#-n7%F!B4hhlPigG`lQRYFT6C57h-@IJz`Bv>h>2m&D$%2-!wi5DhH{h7HX6LCdv{AN+Gy|E#rW|4gZQvjM6hV zsjiDPL#Ad2S`m1;(Af8xD%dEdOt(5vcz(`>V$ADKl%)=okr#lH$AW-Zv$BIe@kL&A z+AO}zfmn=|x#VKEVX|Ai#eot8Hx~+f5>rTacWIrehMqFhP|F=qC1{chiam_UF3@cb z6ked`LSbiPQYdqcpMHe{g;&|RP}td+ctoZoCXgOevxEb!4BT93>}*Wx88%T`9Vq;u z=Rz?S13;1cU(na3c0V)Dgj(rMd=`ZbOoydodegNJYsI}?*#`n3*}N^om4Z7;0{JnU2F&TBPKh_9S)Ql zaC4!sA2Bh?O=kSJqQnLV+GS{!3yuAW$u84I2g+D*bD^-0G1*bt94MEAo8%^nj3{i~ zS!eb!COgU|2g()T=8}bdi%FrF&rrJqWgNJJP~?N#^l}y6r@^GLTiUDAV@_34Wps36 zRBy$J@_}$994||j_J#UO0wu9vdvSW?sQ?9{MTx@Rf)oCM5s?w`{A6CAuiqQ+#ysur zwCj{kdQ?k@UbUqmvCRD0u?{g%kV@}yjSrG~=J-VhD^7t|L-j%);e$b@_)$Zy;%2q_LdS3qrW}Et588_lN$e=^iYQEZ9E+*(jHH8W~brlqBD&m=G z_;uDESlDz%pc{Uj9e$n7dIMkC=|Au~d+AL&SgdZ61G6-5C6+rLdQ{KmAJQ-ePc^-x zUHjB#8NUVHjAD^SB`OI~{>VmS74+@w>P@vNk(=AyL3v-lxU$fhZjwJu=oK=tQV(>c zF$VS@a5Of~)9A{Zg(WV=(6mZ$SPb60<;7j+RSf+zX zP<1uKG7F?D)3WKd!LAyOMP6n|d9dZ)3Z*i(n}f@L-*#$j93ZUHthVvVuYK=OlB4ne7qt*h-jl$@LPYxoGwF%EK}8W+&UrX0z9If3TxF;5ByNo8=VHE2<%lYGBX5p4 zV$R!BDXJn~)C^)%5r&#Mf=#0f4@qv?z$f=PMf8n z61L=V%&N@kP*Y`nic*<~+gmClX@Xi#pJi3vg&U~s-=_3!qT3M1Y|&}(&$S%tUU+2B zWjMobEDsa(O-8lwE%P^vU(cFv)LCK7>5$zNHl5p24SFd`nrG{DQa1iUlzC6-{aT-P zLf%rR-HEr=(dEP0Jj+7nOWbfa&oYR4zGSm`mu|RnSJ58Ct4jKNh$5Iz)DBD^?8#5( zhxa@=8#^$m`o5B~0yCh9%=?I%u~YL`hfKaPV;j5b)-zO0Y=uW&a5P)jZED+?O` z49YX-y*#XXD^jz1s)}Yh{)$j7pN(rT6);i~g`=8FPFScNm$3y}z|H?wsF8~nm`zw@ zPB828`&;$nvVWl#$m1p|u_J#J*TxB5R&UY1na6N;&b3goMusl2!xJRLU>{i87<7bm7%qx;xmnzY^sBMjFh3hir3I$>rpJ< zYEiY=eS1)*muvovX}t2;<=Q+MxmUj{Q|FO8Z?oB2C6489y7fu&-6rkpMO!)es;I>y zmdeW$+9Vm+t{*Jg=HQgc_n6baU5_BW9Yb!TLO$H8)nvvgktbWV>ty79eQ%~NBoD0A zUMjPGIV)|2fk1Wgot^s8B46fD87x4BoU&SbY*Io=)QJi!*zmt%c5X9PS=`&87d5P~ z@IOw76q(T++Ewyg@Jw;Ic+SYqz~Q3T3w?#t3)cA`8u4=e`+4K@nti*x&w0+c!|pk* zPW`a2gUzTaD^KjnNvV;-6tvl=dS>`=?0cp_CvRo2*&v92K znbfME|KQn&FqKJ5MtCkbfx^%A;wLU^R*$co^+(L-lcS=dNMOyMT}5ldVaf=4|GQy| z_iGQfiG2BkZao_T9OLHK z;!P}I7GYh@JQD3Z5{y911jNih%mT!$K+Fcj>_E%`#GF9P1;pIjc_etggtsN|wk7Z_ ROW