From b2da1acd0454955d21d9d896c34f5f1b74bb0fc1 Mon Sep 17 00:00:00 2001 From: ByteBlast Date: Sat, 14 Mar 2015 18:39:27 +0000 Subject: [PATCH 1/2] Added SoundCloud middleware --- .../Owin.Security.Providers.csproj | 9 + .../SoundCloud/Constants.cs | 7 + .../ISoundCloudAuthenticationProvider.cs | 29 +++ .../SoundCloudAuthenticatedContext.cs | 64 +++++ .../SoundCloudAuthenticationProvider.cs | 54 ++++ .../SoundCloudReturnEndpointContext.cs | 21 ++ .../SoundCloudAuthenticationExtensions.cs | 29 +++ .../SoundCloudAuthenticationHandler.cs | 238 ++++++++++++++++++ .../SoundCloudAuthenticationMiddleware.cs | 81 ++++++ .../SoundCloudAuthenticationOptions.cs | 93 +++++++ 10 files changed, 625 insertions(+) create mode 100644 Owin.Security.Providers/SoundCloud/Constants.cs create mode 100644 Owin.Security.Providers/SoundCloud/Provider/ISoundCloudAuthenticationProvider.cs create mode 100644 Owin.Security.Providers/SoundCloud/Provider/SoundCloudAuthenticatedContext.cs create mode 100644 Owin.Security.Providers/SoundCloud/Provider/SoundCloudAuthenticationProvider.cs create mode 100644 Owin.Security.Providers/SoundCloud/Provider/SoundCloudReturnEndpointContext.cs create mode 100644 Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationExtensions.cs create mode 100644 Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationHandler.cs create mode 100644 Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationMiddleware.cs create mode 100644 Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationOptions.cs diff --git a/Owin.Security.Providers/Owin.Security.Providers.csproj b/Owin.Security.Providers/Owin.Security.Providers.csproj index 188c5a6..6f5e408 100644 --- a/Owin.Security.Providers/Owin.Security.Providers.csproj +++ b/Owin.Security.Providers/Owin.Security.Providers.csproj @@ -189,6 +189,15 @@ + + + + + + + + + diff --git a/Owin.Security.Providers/SoundCloud/Constants.cs b/Owin.Security.Providers/SoundCloud/Constants.cs new file mode 100644 index 0000000..9fe5154 --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/Constants.cs @@ -0,0 +1,7 @@ +namespace Owin.Security.Providers.SoundCloud +{ + public class Constants + { + public const string DefaultAuthenticationType = "SoundCloud"; + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/SoundCloud/Provider/ISoundCloudAuthenticationProvider.cs b/Owin.Security.Providers/SoundCloud/Provider/ISoundCloudAuthenticationProvider.cs new file mode 100644 index 0000000..276a564 --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/Provider/ISoundCloudAuthenticationProvider.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; + +namespace Owin.Security.Providers.SoundCloud.Provider +{ + /// + /// Specifies callback methods which the invokes to enable + /// developer control over the authentication process. /> + /// + public interface ISoundCloudAuthenticationProvider + { + /// + /// Invoked whenever SoundCloud succesfully authenticates a user + /// + /// + /// Contains information about the login session as well as the user + /// . + /// + /// A representing the completed operation. + Task Authenticated(SoundCloudAuthenticatedContext 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(SoundCloudReturnEndpointContext context); + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/SoundCloud/Provider/SoundCloudAuthenticatedContext.cs b/Owin.Security.Providers/SoundCloud/Provider/SoundCloudAuthenticatedContext.cs new file mode 100644 index 0000000..1b22cfd --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/Provider/SoundCloudAuthenticatedContext.cs @@ -0,0 +1,64 @@ +using System.Security.Claims; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; +using Newtonsoft.Json.Linq; + +namespace Owin.Security.Providers.SoundCloud.Provider +{ + /// + /// Contains information about the login session as well as the user + /// . + /// + public class SoundCloudAuthenticatedContext : BaseContext + { + public SoundCloudAuthenticatedContext(IOwinContext context, JObject user, string accessToken) + : base(context) + { + User = user; + AccessToken = accessToken; + + Id = TryGetValue(user, "id"); + UserName = TryGetValue(user, "username"); + } + + /// + /// Gets the JSON-serialized user + /// + /// + /// Contains the SoundCloud user obtained from the endpoint https://api.soundcloud.com/me + /// + public JObject User { get; private set; } + + /// + /// Gets the SoundCloud access token + /// + public string AccessToken { get; private set; } + + /// + /// Gets the SoundCloud user ID + /// + public string Id { get; set; } + + /// + /// Gets the SoundCloud username + /// + public string UserName { get; 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(JObject user, string propertyName) + { + JToken value; + return user.TryGetValue(propertyName, out value) ? value.ToString() : null; + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/SoundCloud/Provider/SoundCloudAuthenticationProvider.cs b/Owin.Security.Providers/SoundCloud/Provider/SoundCloudAuthenticationProvider.cs new file mode 100644 index 0000000..457ccd1 --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/Provider/SoundCloudAuthenticationProvider.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; + +namespace Owin.Security.Providers.SoundCloud.Provider +{ + /// + /// Default implementation. + /// + public class SoundCloudAuthenticationProvider : ISoundCloudAuthenticationProvider + { + /// + /// Initializes a + /// + public SoundCloudAuthenticationProvider() + { + 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 SoundCloud succesfully authenticates a user + /// + /// + /// Contains information about the login session as well as the user + /// . + /// + /// A representing the completed operation. + public virtual Task Authenticated(SoundCloudAuthenticatedContext 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 Task ReturnEndpoint(SoundCloudReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/SoundCloud/Provider/SoundCloudReturnEndpointContext.cs b/Owin.Security.Providers/SoundCloud/Provider/SoundCloudReturnEndpointContext.cs new file mode 100644 index 0000000..c882e19 --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/Provider/SoundCloudReturnEndpointContext.cs @@ -0,0 +1,21 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; + +namespace Owin.Security.Providers.SoundCloud.Provider +{ + /// + /// Provides context information to middleware providers. + /// + public class SoundCloudReturnEndpointContext : ReturnEndpointContext + { + /// + /// + /// OWIN environment + /// The authentication ticket + public SoundCloudReturnEndpointContext(IOwinContext context, AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationExtensions.cs b/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationExtensions.cs new file mode 100644 index 0000000..3d01dac --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationExtensions.cs @@ -0,0 +1,29 @@ +using System; + +namespace Owin.Security.Providers.SoundCloud +{ + public static class SoundCloudAuthenticationExtensions + { + public static IAppBuilder UseSoundCloudAuthentication(this IAppBuilder app, + SoundCloudAuthenticationOptions options) + { + if (app == null) + throw new ArgumentNullException("app"); + if (options == null) + throw new ArgumentNullException("options"); + + app.Use(typeof (SoundCloudAuthenticationMiddleware), app, options); + + return app; + } + + public static IAppBuilder UseSoundCloudAuthentication(this IAppBuilder app, string clientId, string clientSecret) + { + return app.UseSoundCloudAuthentication(new SoundCloudAuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret + }); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationHandler.cs b/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationHandler.cs new file mode 100644 index 0000000..de799d2 --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationHandler.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +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.SoundCloud.Provider; + +namespace Owin.Security.Providers.SoundCloud +{ + public class SoundCloudAuthenticationHandler : AuthenticationHandler + { + private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; + private const string TokenEndpoint = "https://api.soundcloud.com/oauth2/token"; + private const string ConnectEndpoint = "https://soundcloud.com/connect"; + private const string UserInfoEndpoint = "https://api.soundcloud.com/me.json"; + private readonly HttpClient httpClient; + private readonly ILogger logger; + + public SoundCloudAuthenticationHandler(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; + + var query = Request.Query; + var 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 (!ValidateCorrelationId(properties, logger)) + { + return new AuthenticationTicket(null, properties); + } + + var requestPrefix = Request.Scheme + "://" + Request.Host; + var redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; + + var body = new List> + { + new KeyValuePair("code", code), + new KeyValuePair("redirect_uri", redirectUri), + new KeyValuePair("client_id", Options.ClientId), + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("client_secret", Options.ClientSecret) + }; + + var tokenResponse = await httpClient.PostAsync(TokenEndpoint, new FormUrlEncodedContent(body)); + tokenResponse.EnsureSuccessStatusCode(); + var text = await tokenResponse.Content.ReadAsStringAsync(); + + dynamic response = JsonConvert.DeserializeObject(text); + var accessToken = (string) response.access_token; + + var userRequest = new HttpRequestMessage( + HttpMethod.Get, + UserInfoEndpoint + "?oauth_token=" + Uri.EscapeDataString(accessToken)); + var userResponse = await httpClient.SendAsync(userRequest, Request.CallCancelled); + userResponse.EnsureSuccessStatusCode(); + text = await userResponse.Content.ReadAsStringAsync(); + var user = JObject.Parse(text); + + var context = new SoundCloudAuthenticatedContext(Context, user, accessToken) + { + 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.UserName)) + { + context.Identity.AddClaim(new Claim( + ClaimsIdentity.DefaultNameClaimType, + context.UserName, + XmlSchemaString, + Options.AuthenticationType)); + } + context.Properties = properties; + + await Options.Provider.Authenticated(context); + + return new AuthenticationTicket(context.Identity, context.Properties); + } + catch (Exception exception) + { + logger.WriteError(exception.Message); + } + return new AuthenticationTicket(null, properties); + } + + protected override Task ApplyResponseChallengeAsync() + { + if (Response.StatusCode != 401) + { + return Task.FromResult(null); + } + + var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); + + if (challenge != null) + { + var baseUri = + Request.Scheme + + Uri.SchemeDelimiter + + Request.Host + + Request.PathBase; + + var currentUri = + baseUri + + Request.Path + + Request.QueryString; + + var redirectUri = + baseUri + + Options.CallbackPath; + + var properties = challenge.Properties; + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = currentUri; + } + + // OAuth2 10.12 CSRF + GenerateCorrelationId(properties); + + var state = Options.StateDataFormat.Protect(properties); + + var authorizationEndpoint = + ConnectEndpoint + + "?client_id=" + Uri.EscapeDataString(Options.ClientId) + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + + "&response_type=" + "code" + + "&scope=" + "non-expiring" + + "&display=" + "popup" + + "&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 + + var ticket = await AuthenticateAsync(); + if (ticket == null) + { + logger.WriteWarning("Invalid return state, unable to redirect."); + Response.StatusCode = 500; + return true; + } + + var context = new SoundCloudReturnEndpointContext(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) + { + 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; + } + return false; + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationMiddleware.cs b/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationMiddleware.cs new file mode 100644 index 0000000..cbb2905 --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationMiddleware.cs @@ -0,0 +1,81 @@ +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; +using Owin.Security.Providers.SoundCloud.Provider; + +namespace Owin.Security.Providers.SoundCloud +{ + public class SoundCloudAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly HttpClient httpClient; + private readonly ILogger logger; + + public SoundCloudAuthenticationMiddleware( + OwinMiddleware next, + IAppBuilder app, + SoundCloudAuthenticationOptions 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 SoundCloudAuthenticationProvider(); + + if (Options.StateDataFormat == null) + { + var dataProtector = app.CreateDataProtector( + typeof (SoundCloudAuthenticationMiddleware).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 SoundCloud middleware"); + httpClient.DefaultRequestHeaders.ExpectContinue = false; + } + + protected override AuthenticationHandler CreateHandler() + { + return new SoundCloudAuthenticationHandler(httpClient, logger); + } + + private HttpMessageHandler ResolveHttpMessageHandler(SoundCloudAuthenticationOptions options) + { + var 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/SoundCloud/SoundCloudAuthenticationOptions.cs b/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationOptions.cs new file mode 100644 index 0000000..be0faca --- /dev/null +++ b/Owin.Security.Providers/SoundCloud/SoundCloudAuthenticationOptions.cs @@ -0,0 +1,93 @@ +using System; +using System.Net.Http; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Owin.Security.Providers.SoundCloud.Provider; + +namespace Owin.Security.Providers.SoundCloud +{ + public class SoundCloudAuthenticationOptions : AuthenticationOptions + { + /// + /// Initializes a new + /// + public SoundCloudAuthenticationOptions() + : base("SoundCloud") + { + Caption = Constants.DefaultAuthenticationType; + CallbackPath = new PathString("/signin-soundcloud"); + AuthenticationMode = AuthenticationMode.Passive; + BackchannelTimeout = TimeSpan.FromSeconds(60); + } + + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to SoundCloud. + /// + /// + /// 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 SoundCloud. + /// 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 SoundCloud. + /// + /// + /// 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-soundcloud". + /// + 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 SoundCloud supplied Client ID + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the SoundCloud supplied Client Secret + /// + public string ClientSecret { get; set; } + + /// + /// Gets or sets the used in the authentication events + /// + public ISoundCloudAuthenticationProvider 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; } + } +} \ No newline at end of file From c145fb00a9416a91f058c22b9162eb2f702ecdfd Mon Sep 17 00:00:00 2001 From: ByteBlast Date: Sat, 14 Mar 2015 18:58:40 +0000 Subject: [PATCH 2/2] Added SoundCloud to demo --- OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs b/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs index b5045b9..4a82c19 100755 --- a/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs +++ b/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs @@ -22,6 +22,7 @@ using Owin.Security.Providers.TripIt; using Owin.Security.Providers.Twitch; using Owin.Security.Providers.Yahoo; using Owin.Security.Providers.OpenID; +using Owin.Security.Providers.SoundCloud; using Owin.Security.Providers.Steam; using Owin.Security.Providers.WordPress; @@ -198,6 +199,8 @@ namespace OwinOAuthProvidersDemo // clientSecret: ""); //app.UseAsanaAuthentication("", ""); + + //app.UseSoundCloudAuthentication("", ""); } } } \ No newline at end of file