From 27b5d0d133f9276aa0e606699720a1e098937fa6 Mon Sep 17 00:00:00 2001 From: ravind Date: Sat, 22 Nov 2014 18:30:24 -0500 Subject: [PATCH] Added Yammer --- .../Owin.Security.Providers.csproj | 9 + Owin.Security.Providers/Yammer/Constants.cs | 8 + .../Provider/IYammerAuthenticationProvider.cs | 21 ++ .../Provider/YammerAuthenticatedContext.cs | 87 ++++++ .../Provider/YammerAuthenticationProvider.cs | 50 ++++ .../Provider/YammerReturnEndpointContext.cs | 24 ++ .../Yammer/YammerAuthenticationExtensions.cs | 28 ++ .../Yammer/YammerAuthenticationHandler.cs | 250 ++++++++++++++++++ .../Yammer/YammerAuthenticationMiddleware.cs | 71 +++++ .../Yammer/YammerAuthenticationOptions.cs | 104 ++++++++ 10 files changed, 652 insertions(+) create mode 100644 Owin.Security.Providers/Yammer/Constants.cs create mode 100644 Owin.Security.Providers/Yammer/Provider/IYammerAuthenticationProvider.cs create mode 100644 Owin.Security.Providers/Yammer/Provider/YammerAuthenticatedContext.cs create mode 100644 Owin.Security.Providers/Yammer/Provider/YammerAuthenticationProvider.cs create mode 100644 Owin.Security.Providers/Yammer/Provider/YammerReturnEndpointContext.cs create mode 100644 Owin.Security.Providers/Yammer/YammerAuthenticationExtensions.cs create mode 100644 Owin.Security.Providers/Yammer/YammerAuthenticationHandler.cs create mode 100644 Owin.Security.Providers/Yammer/YammerAuthenticationMiddleware.cs create mode 100644 Owin.Security.Providers/Yammer/YammerAuthenticationOptions.cs diff --git a/Owin.Security.Providers/Owin.Security.Providers.csproj b/Owin.Security.Providers/Owin.Security.Providers.csproj index d761043..85bd92a 100644 --- a/Owin.Security.Providers/Owin.Security.Providers.csproj +++ b/Owin.Security.Providers/Owin.Security.Providers.csproj @@ -219,6 +219,15 @@ + + + + + + + + + diff --git a/Owin.Security.Providers/Yammer/Constants.cs b/Owin.Security.Providers/Yammer/Constants.cs new file mode 100644 index 0000000..bbdfb29 --- /dev/null +++ b/Owin.Security.Providers/Yammer/Constants.cs @@ -0,0 +1,8 @@ + +namespace Owin.Security.Providers.Yammer +{ + internal static class Constants + { + public const string DefaultAuthenticationType = "Yammer"; + } +} diff --git a/Owin.Security.Providers/Yammer/Provider/IYammerAuthenticationProvider.cs b/Owin.Security.Providers/Yammer/Provider/IYammerAuthenticationProvider.cs new file mode 100644 index 0000000..00ac549 --- /dev/null +++ b/Owin.Security.Providers/Yammer/Provider/IYammerAuthenticationProvider.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace Owin.Security.Providers.Yammer.Provider +{ + public interface IYammerAuthenticationProvider + { + /// + /// Invoked whenever Yammer succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(YammerAuthenticatedContext 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(YammerReturnEndpointContext context); + } +} diff --git a/Owin.Security.Providers/Yammer/Provider/YammerAuthenticatedContext.cs b/Owin.Security.Providers/Yammer/Provider/YammerAuthenticatedContext.cs new file mode 100644 index 0000000..508962b --- /dev/null +++ b/Owin.Security.Providers/Yammer/Provider/YammerAuthenticatedContext.cs @@ -0,0 +1,87 @@ +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.Yammer.Provider +{ + /// + /// Contains information about the login session as well as the user . + /// + public class YammerAuthenticatedContext : BaseContext + { + /// + /// Initializes a + /// + /// The OWIN environment + /// The JSON-serialized user + /// Yammer Access token + public YammerAuthenticatedContext(IOwinContext context, dynamic user, string accessToken) : base(context) + { + User = user; + AccessToken = accessToken; + Id = user.id; + Name = user.full_name; + Url = user.url; + Network = user.network_name; + if (user.contact.email_addresses != null) + { + foreach (var eml in user.contact.email_addresses) + { + if (eml.type == "primary") PrimaryEmail = eml.address; + } + } + } + + /// + /// Gets the JSON-serialized user + /// + /// + /// Contains the Yammer user + /// + public dynamic User { get; private set; } + + /// + /// Gets the Yammer access token + /// + public string AccessToken { get; private set; } + + /// + /// Gets the Yammer user ID + /// + public string Id { get; private set; } + + /// + /// Gets the Yammer full_name + /// + public string Name { get; private set; } + + /// + /// Gets the Yammer url + /// + public string Url { get; private set; } + + /// + /// Gets the Yammer Primary Email + /// + public string PrimaryEmail { get; private set; } + + /// + /// Gets the yammer network_name + /// + public string Network { 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; } + } +} diff --git a/Owin.Security.Providers/Yammer/Provider/YammerAuthenticationProvider.cs b/Owin.Security.Providers/Yammer/Provider/YammerAuthenticationProvider.cs new file mode 100644 index 0000000..73ea506 --- /dev/null +++ b/Owin.Security.Providers/Yammer/Provider/YammerAuthenticationProvider.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; + +namespace Owin.Security.Providers.Yammer.Provider +{ + /// + /// Default implementation. + /// + public class YammerAuthenticationProvider : IYammerAuthenticationProvider + { + /// + /// Initializes a + /// + public YammerAuthenticationProvider() + { + 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 Yammer succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(YammerAuthenticatedContext 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(YammerReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Yammer/Provider/YammerReturnEndpointContext.cs b/Owin.Security.Providers/Yammer/Provider/YammerReturnEndpointContext.cs new file mode 100644 index 0000000..1cd3dc1 --- /dev/null +++ b/Owin.Security.Providers/Yammer/Provider/YammerReturnEndpointContext.cs @@ -0,0 +1,24 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; + +namespace Owin.Security.Providers.Yammer.Provider +{ + /// + /// Provides context information to middleware providers. + /// + public class YammerReturnEndpointContext : ReturnEndpointContext + { + /// + /// + /// + /// OWIN environment + /// The authentication ticket + public YammerReturnEndpointContext( + IOwinContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/Owin.Security.Providers/Yammer/YammerAuthenticationExtensions.cs b/Owin.Security.Providers/Yammer/YammerAuthenticationExtensions.cs new file mode 100644 index 0000000..aa31c9b --- /dev/null +++ b/Owin.Security.Providers/Yammer/YammerAuthenticationExtensions.cs @@ -0,0 +1,28 @@ +using System; + +namespace Owin.Security.Providers.Yammer +{ + public static class YammerAuthenticationExtensions + { + public static IAppBuilder UseYammerAuthentication(this IAppBuilder app, YammerAuthenticationOptions options) + { + if (app == null) + throw new ArgumentNullException("app"); + if (options == null) + throw new ArgumentNullException("options"); + + app.Use(typeof(YammerAuthenticationMiddleware), app, options); + + return app; + } + + public static IAppBuilder UseYammerAuthentication(this IAppBuilder app, string clientId, string clientSecret) + { + return app.UseYammerAuthentication(new YammerAuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret + }); + } + } +} diff --git a/Owin.Security.Providers/Yammer/YammerAuthenticationHandler.cs b/Owin.Security.Providers/Yammer/YammerAuthenticationHandler.cs new file mode 100644 index 0000000..84e23e4 --- /dev/null +++ b/Owin.Security.Providers/Yammer/YammerAuthenticationHandler.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Net; +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; +using Owin.Security.Providers.Yammer.Provider; + +namespace Owin.Security.Providers.Yammer +{ + public class YammerAuthenticationHandler : AuthenticationHandler + { + private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; + private const string TokenEndpoint = "https://www.yammer.com/oauth2/access_token.json"; + private const string UserAuthenticationEndpoint = "https://www.yammer.com/dialog/oauth"; + + private readonly ILogger logger; + private readonly HttpClient httpClient; + + public YammerAuthenticationHandler(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; + } + if (code == null) + { + throw new Exception(query["error"] + " - " + query["error_description"]); + } + + // 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; + + string endPoint = + TokenEndpoint + + "?client_id=" + Uri.EscapeDataString(Options.ClientId) + + "&client_secret=" + Uri.EscapeDataString(Options.ClientSecret) + + "&code=" + Uri.EscapeDataString(code); + + // Request the token + HttpResponseMessage tokenResponse = await httpClient.GetAsync(endPoint); + tokenResponse.EnsureSuccessStatusCode(); + string text = await tokenResponse.Content.ReadAsStringAsync(); + + // Deserializes the token response + dynamic response = JsonConvert.DeserializeObject(text); + string accessToken = (string)response.access_token.token; + + // Get the Yammer user + dynamic user = response.user; + var context = new YammerAuthenticatedContext(Context, user, accessToken); + context.Identity = new ClaimsIdentity( + Options.AuthenticationType, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + EnsureAcceptedNetwork(Options.AcceptedNetworks, context.Network); + + if (!string.IsNullOrEmpty(context.Id)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Name)) + { + context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.Name, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.PrimaryEmail)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.PrimaryEmail, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Url)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.Uri, context.Url, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.AccessToken)) + { + context.Identity.AddClaim(new Claim("urn:Yammer:accesstoken", context.AccessToken, 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 = + UserAuthenticationEndpoint + + "?client_id=" + Uri.EscapeDataString(Options.ClientId) + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + + "&state=" + Uri.EscapeDataString(state); + + 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 YammerReturnEndpointContext(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; + } + + private void EnsureAcceptedNetwork(string[] validNetworks, string userNetwork) + { + if (validNetworks != null && validNetworks.Length > 0) + { + bool isValid = false; + foreach (string network in validNetworks) + { + if (userNetwork == network) + { + isValid = true; + break; + } + } + if (!isValid) throw new Exception("User is not in list of accepted networks"); + } + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Yammer/YammerAuthenticationMiddleware.cs b/Owin.Security.Providers/Yammer/YammerAuthenticationMiddleware.cs new file mode 100644 index 0000000..cd5a110 --- /dev/null +++ b/Owin.Security.Providers/Yammer/YammerAuthenticationMiddleware.cs @@ -0,0 +1,71 @@ +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.Yammer.Provider; +using System; +using System.Globalization; +using System.Net; +using System.Net.Http; + +namespace Owin.Security.Providers.Yammer +{ + public class YammerAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly HttpClient httpClient; + private readonly ILogger logger; + + public YammerAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, YammerAuthenticationOptions options) : base(next, options) + { + if (String.IsNullOrWhiteSpace(Options.ClientId)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "Option must be provided {0}", "ClientId")); + if (String.IsNullOrWhiteSpace(Options.ClientSecret)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "Option must be provided {0}", "ClientSecret")); + + logger = app.CreateLogger(); + + if (Options.Provider == null) + Options.Provider = new YammerAuthenticationProvider(); + + if (Options.StateDataFormat == null) + { + IDataProtector dataProtector = app.CreateDataProtector( + typeof(YammerAuthenticationMiddleware).FullName, + Options.AuthenticationType, "v1"); + Options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (String.IsNullOrEmpty(Options.SignInAsAuthenticationType)) + Options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + + httpClient = new HttpClient(ResolveHttpMessageHandler()) + { + Timeout = Options.BackchannelTimeout, + MaxResponseContentBufferSize = 1024 * 1024 * 10 + }; + } + + /// + /// Provides the object for processing + /// authentication-related requests. + /// + /// + /// An configured with the + /// supplied to the constructor. + /// + protected override AuthenticationHandler CreateHandler() + { + return new YammerAuthenticationHandler(httpClient, logger); + } + + private HttpClientHandler ResolveHttpMessageHandler() + { + return new HttpClientHandler + { + Credentials = new NetworkCredential(Options.ClientId, Options.ClientSecret) + }; + } + } +} diff --git a/Owin.Security.Providers/Yammer/YammerAuthenticationOptions.cs b/Owin.Security.Providers/Yammer/YammerAuthenticationOptions.cs new file mode 100644 index 0000000..e9a10ab --- /dev/null +++ b/Owin.Security.Providers/Yammer/YammerAuthenticationOptions.cs @@ -0,0 +1,104 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Owin.Security.Providers.Yammer.Provider; +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace Owin.Security.Providers.Yammer +{ + public class YammerAuthenticationOptions : AuthenticationOptions + { + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to Yammer. + /// + /// + /// 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 Yammer. + /// 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 Yammer. + /// + /// + /// 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-Yammer". + /// + 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 Yammer supplied Client ID + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the Yammer supplied Client Secret + /// + public string ClientSecret { get; set; } + + /// + /// Gets or sets the used in the authentication events + /// + public IYammerAuthenticationProvider Provider { get; set; } + + /// + /// A list of permissions to request. + /// + public IList Scope { get; private 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; } + + /// + /// Will only allow logins from users belonging to one of these networks. Leave blank to allow all. + /// + public string[] AcceptedNetworks { get; set; } + + /// + /// Initializes a new + /// + public YammerAuthenticationOptions() + : base("Yammer") + { + Caption = Constants.DefaultAuthenticationType; + CallbackPath = new PathString("/signin-Yammer"); + AuthenticationMode = AuthenticationMode.Passive; + BackchannelTimeout = TimeSpan.FromSeconds(60); + } + } +}