From 43ee292d0cc8068cee0459390966c4bc19c9e1da Mon Sep 17 00:00:00 2001 From: Joseph Pisano Date: Fri, 3 Nov 2017 00:40:41 -0400 Subject: [PATCH] I added an auth provider for the Podbean API. (#218) * Added the podbean auth provider. * Updated Readme to include podbean * Fixed Owin nuget reference * Removed regions from around using statements, corrected the VS version in the solution file and changed the Startup.Auth in the demo project to use spaces instead of tabs. * Added the OAuth refresh token to the PodbeanAuthenticatedContext. --- OwinOAuthProviders.sln | 9 + .../App_Start/Startup.Auth.cs | 7 + .../OwinOAuthProvidersDemo.csproj | 5 + README.md | 1 + .../Constants.cs | 7 + .../Owin.Security.Providers.Podbean.csproj | 98 +++++++ .../PodbeanAuthenticationExtensions.cs | 33 +++ .../PodbeanAuthenticationHandler.cs | 246 ++++++++++++++++++ .../PodbeanAuthenticationMiddleware.cs | 84 ++++++ .../PodbeanAuthenticationOptions.cs | 110 ++++++++ .../Properties/AssemblyInfo.cs | 15 ++ .../IPodbeanAuthenticationProvider.cs | 33 +++ .../Provider/PodbeanAuthenticatedContext.cs | 130 +++++++++ .../Provider/PodbeanAuthenticationProvider.cs | 58 +++++ .../Provider/PodbeanReturnEndpointContext.cs | 23 ++ .../Resources.Designer.cs | 81 ++++++ .../Resources.resx | 126 +++++++++ .../packages.config | 7 + 18 files changed, 1073 insertions(+) create mode 100644 src/Owin.Security.Providers.Podbean/Constants.cs create mode 100644 src/Owin.Security.Providers.Podbean/Owin.Security.Providers.Podbean.csproj create mode 100644 src/Owin.Security.Providers.Podbean/PodbeanAuthenticationExtensions.cs create mode 100644 src/Owin.Security.Providers.Podbean/PodbeanAuthenticationHandler.cs create mode 100644 src/Owin.Security.Providers.Podbean/PodbeanAuthenticationMiddleware.cs create mode 100644 src/Owin.Security.Providers.Podbean/PodbeanAuthenticationOptions.cs create mode 100644 src/Owin.Security.Providers.Podbean/Properties/AssemblyInfo.cs create mode 100644 src/Owin.Security.Providers.Podbean/Provider/IPodbeanAuthenticationProvider.cs create mode 100644 src/Owin.Security.Providers.Podbean/Provider/PodbeanAuthenticatedContext.cs create mode 100644 src/Owin.Security.Providers.Podbean/Provider/PodbeanAuthenticationProvider.cs create mode 100644 src/Owin.Security.Providers.Podbean/Provider/PodbeanReturnEndpointContext.cs create mode 100644 src/Owin.Security.Providers.Podbean/Resources.Designer.cs create mode 100644 src/Owin.Security.Providers.Podbean/Resources.resx create mode 100644 src/Owin.Security.Providers.Podbean/packages.config diff --git a/OwinOAuthProviders.sln b/OwinOAuthProviders.sln index b984d9ed..1d462ceb 100644 --- a/OwinOAuthProviders.sln +++ b/OwinOAuthProviders.sln @@ -108,6 +108,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.Security.Providers.Eve EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.Security.Providers.WSO2", "src\Owin.Security.Providers.WSO2\Owin.Security.Providers.WSO2.csproj", "{8FD3A9CB-E684-42C0-A8BF-7746FDD3D43C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.Security.Providers.Podbean", "src\Owin.Security.Providers.Podbean\Owin.Security.Providers.Podbean.csproj", "{A7B95FD4-08AD-499F-B574-07560CC2A63F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -326,8 +328,15 @@ Global {8FD3A9CB-E684-42C0-A8BF-7746FDD3D43C}.Debug|Any CPU.Build.0 = Debug|Any CPU {8FD3A9CB-E684-42C0-A8BF-7746FDD3D43C}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FD3A9CB-E684-42C0-A8BF-7746FDD3D43C}.Release|Any CPU.Build.0 = Release|Any CPU + {A7B95FD4-08AD-499F-B574-07560CC2A63F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7B95FD4-08AD-499F-B574-07560CC2A63F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7B95FD4-08AD-499F-B574-07560CC2A63F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7B95FD4-08AD-499F-B574-07560CC2A63F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EDFDB942-6583-4AD9-A868-B354B5BF07FC} + EndGlobalSection EndGlobal diff --git a/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs b/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs index 0a98ee80..b1bf7d84 100755 --- a/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs +++ b/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs @@ -307,6 +307,13 @@ public void ConfigureAuth(IAppBuilder app) // AppKey = "", // AppSecret = "" //}); + + //app.UsePodbeanAuthentication(new PodbeanAuthenticationOptions + //{ + // AppId = "", + // AppSecret = "", + // DebugUsingRequestHeadersToBuildBaseUri = true + //}); } } } \ No newline at end of file diff --git a/OwinOAuthProvidersDemo/OwinOAuthProvidersDemo.csproj b/OwinOAuthProvidersDemo/OwinOAuthProvidersDemo.csproj index ee56195b..969ad495 100644 --- a/OwinOAuthProvidersDemo/OwinOAuthProvidersDemo.csproj +++ b/OwinOAuthProvidersDemo/OwinOAuthProvidersDemo.csproj @@ -22,6 +22,7 @@ false + true @@ -380,6 +381,10 @@ {f7129064-3db7-4b79-81d3-80130d664e45} Owin.Security.Providers.PayPal + + {a7b95fd4-08ad-499f-b574-07560cc2a63f} + Owin.Security.Providers.Podbean + {d0cd86c8-a6f9-4c6c-9bf0-eaa461e7fbad} Owin.Security.Providers.Reddit diff --git a/README.md b/README.md index 2bf8d6de..9bcbf4d9 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Provides a set of extra authentication providers for OWIN ([Project Katana](http - Onshape - ORCID - PayPal + - Podbean - Reddit - Salesforce - Shopify diff --git a/src/Owin.Security.Providers.Podbean/Constants.cs b/src/Owin.Security.Providers.Podbean/Constants.cs new file mode 100644 index 00000000..9be8f40f --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Constants.cs @@ -0,0 +1,7 @@ +namespace Owin.Security.Providers.Podbean +{ + internal static class Constants + { + public const string DefaultAuthenticationType = "Podbean"; + } +} \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/Owin.Security.Providers.Podbean.csproj b/src/Owin.Security.Providers.Podbean/Owin.Security.Providers.Podbean.csproj new file mode 100644 index 00000000..ae986db4 --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Owin.Security.Providers.Podbean.csproj @@ -0,0 +1,98 @@ + + + + + Debug + AnyCPU + {A7B95FD4-08AD-499F-B574-07560CC2A63F} + Library + Properties + Owin.Security.Providers.Podbean + Owin.Security.Providers.Podbean + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + + + ..\..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll + + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\Owin.1.0\lib\net40\Owin.dll + + + + + + + + + + + + + + + + + + + + + + + + Resources.resx + True + True + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + + + + + + + + + + $(PostBuildEventDependsOn); + PostBuildMacros; + + + \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationExtensions.cs b/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationExtensions.cs new file mode 100644 index 00000000..1c035563 --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationExtensions.cs @@ -0,0 +1,33 @@ +#region + +using System; + +#endregion + +namespace Owin.Security.Providers.Podbean +{ + public static class PodbeanAuthenticationExtensions + { + public static IAppBuilder UsePodbeanAuthentication(this IAppBuilder app, + PodbeanAuthenticationOptions options) + { + if (app == null) + throw new ArgumentNullException(nameof(app)); + if (options == null) + throw new ArgumentNullException(nameof(options)); + + app.Use(typeof(PodbeanAuthenticationMiddleware), app, options); + + return app; + } + + public static IAppBuilder UsePodbeanAuthentication(this IAppBuilder app, string appId, string appSecret) + { + return app.UsePodbeanAuthentication(new PodbeanAuthenticationOptions + { + AppId = appId, + AppSecret = appSecret + }); + } + } +} \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationHandler.cs b/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationHandler.cs new file mode 100644 index 00000000..495ac8f1 --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationHandler.cs @@ -0,0 +1,246 @@ +#region + +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 Microsoft.Owin; +using System.Net.Http.Headers; + +#endregion + +namespace Owin.Security.Providers.Podbean +{ + public class PodbeanAuthenticationHandler : AuthenticationHandler + { + private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; + private const string TokenEndpoint = "https://api.podbean.com/v1/oauth/token"; + private const string PodcastIdEndpoint = "https://api.podbean.com/v1/oauth/debugToken"; + private const string PodcastEndpoint = "https://api.podbean.com/v1/podcast"; + private readonly HttpClient _httpClient; + + private readonly ILogger _logger; + + public PodbeanAuthenticationHandler(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _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; + + // OAuth2 10.12 CSRF + if (!ValidateCorrelationId(properties, _logger)) + return new AuthenticationTicket(null, properties); + + var requestPrefix = GetBaseUri(Request); + var redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; + + // Build up the body for the token request + var body = new List> + { + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("code", code), + new KeyValuePair("redirect_uri", redirectUri) + }; + + // Request the token + var tokenRequest = new HttpRequestMessage(HttpMethod.Post, TokenEndpoint); + tokenRequest.Headers.Authorization = + new AuthenticationHeaderValue("Basic", Base64Encode($"{Options.AppId}:{Options.AppSecret}")); + tokenRequest.Content = new FormUrlEncodedContent(body); + + var tokenResponse = await _httpClient.SendAsync(tokenRequest, Request.CallCancelled); + tokenResponse.EnsureSuccessStatusCode(); + var text = await tokenResponse.Content.ReadAsStringAsync(); + + // Deserializes the token response + dynamic token = JsonConvert.DeserializeObject(text); + var accessToken = (string)token.access_token; + var refreshToken = (string)token.refresh_token; + var expires = (string)token.expires_in; + + // Get the Podbean podcast + var podcastResponse = await _httpClient.GetAsync( + $"{PodcastEndpoint}?access_token={Uri.EscapeDataString(accessToken)}", Request.CallCancelled); + podcastResponse.EnsureSuccessStatusCode(); + text = await podcastResponse.Content.ReadAsStringAsync(); + var podcast = JObject.Parse(text)["podcast"].ToObject(); + + // Get the Podbean podcast id + var podcastIdRequest = new HttpRequestMessage(HttpMethod.Get, $"{PodcastIdEndpoint}?access_token={Uri.EscapeDataString(accessToken)}"); + podcastIdRequest.Headers.Authorization = + new AuthenticationHeaderValue("Basic", Base64Encode($"{Options.AppId}:{Options.AppSecret}")); + var podcastIdResponse = await _httpClient.SendAsync(podcastIdRequest, Request.CallCancelled); + podcastIdResponse.EnsureSuccessStatusCode(); + text = await podcastIdResponse.Content.ReadAsStringAsync(); + var podcastId = JsonConvert.DeserializeObject(text); + podcast.Id = (string)podcastId.podcast_id; + + var context = new PodbeanAuthenticatedContext(Context, podcast, accessToken, refreshToken, expires) + { + 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.Name)) + context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, 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); + + var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); + + if (challenge == null) return Task.FromResult(null); + var baseUri = GetBaseUri(Request); + + 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 scope = string.Join(" ", Options.Scope); + + var authorizationEndpoint = + "https://api.podbean.com/v1/dialog/oauth" + + "?response_type=code" + + "&client_id=" + Uri.EscapeDataString(Options.AppId) + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + + "&scope=" + Uri.EscapeDataString(scope) + + "&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) 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 PodbeanReturnEndpointContext(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) + redirectUri = WebUtilities.AddQueryString(redirectUri, "error", "access_denied"); + Response.Redirect(redirectUri); + context.RequestCompleted(); + + return context.IsRequestCompleted; + } + + private static string Base64Encode(string plainText) + { + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); + return System.Convert.ToBase64String(plainTextBytes); + } + + private string GetBaseUri(IOwinRequest request) + { + if (Options.DebugUsingRequestHeadersToBuildBaseUri && + request.Headers["X-Original-Host"] != null && + request.Headers["X-Forwarded-Proto"] != null) + { + return request.Headers["X-Forwarded-Proto"] + Uri.SchemeDelimiter + request.Headers["X-Original-Host"]; + } + + var baseUri = + request.Scheme + + Uri.SchemeDelimiter + + request.Host + + request.PathBase; + + return baseUri; + } + } +} \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationMiddleware.cs b/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationMiddleware.cs new file mode 100644 index 00000000..4b4c1d9e --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationMiddleware.cs @@ -0,0 +1,84 @@ +#region + +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; + +#endregion + +namespace Owin.Security.Providers.Podbean +{ + public class PodbeanAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public PodbeanAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, + PodbeanAuthenticationOptions options) + : base(next, options) + { + if (string.IsNullOrWhiteSpace(Options.AppId)) + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "AppId")); + if (string.IsNullOrWhiteSpace(Options.AppSecret)) + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "AppSecret")); + + _logger = app.CreateLogger(); + + if (Options.Provider == null) + Options.Provider = new PodbeanAuthenticationProvider(); + + if (Options.StateDataFormat == null) + { + var dataProtector = app.CreateDataProtector( + typeof(PodbeanAuthenticationMiddleware).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 + }; + } + + /// + /// Provides the object for processing + /// authentication-related requests. + /// + /// + /// An configured with the + /// supplied to the constructor. + /// + protected override AuthenticationHandler CreateHandler() + { + return new PodbeanAuthenticationHandler(_httpClient, _logger); + } + + private static HttpMessageHandler ResolveHttpMessageHandler(PodbeanAuthenticationOptions options) + { + var handler = options.BackchannelHttpHandler ?? new WebRequestHandler(); + + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator == null) return handler; + // 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/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationOptions.cs b/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationOptions.cs new file mode 100644 index 00000000..55f4045d --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/PodbeanAuthenticationOptions.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Owin; +using Microsoft.Owin.Security; + +namespace Owin.Security.Providers.Podbean +{ + public class PodbeanAuthenticationOptions : AuthenticationOptions + { + /// + /// Initializes a new + /// + public PodbeanAuthenticationOptions() + : base("Podbean") + { + Caption = Constants.DefaultAuthenticationType; + CallbackPath = new PathString("/signin-podbean"); + AuthenticationMode = AuthenticationMode.Passive; + Scope = new List + { + "podcast_read" + }; + 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 Podbean + /// + /// + /// 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 Podbean. + /// 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 Podbean. + /// + /// + /// 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-Podbean". + /// + 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 Podbean supplied App ID + /// + public string AppId { get; set; } + + /// + /// Gets or sets the Podbean supplied App Secret + /// + public string AppSecret { get; set; } + + /// + /// Set this value to true to debug locally using a service such as https://ngrok.io. + /// Podbean doesn't allow you to redirect to localhost. Ngrok and similar services + /// set the X-Original-Host and X-Forwarded-Proto headers to build the base Uri for + /// redirects back to localhost. + /// + public bool DebugUsingRequestHeadersToBuildBaseUri { get; set; } + + /// + /// A list of permissions to request. + /// + public IList Scope { get; private set; } + + /// + /// Gets or sets the used in the authentication events + /// + public IPodbeanAuthenticationProvider 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 diff --git a/src/Owin.Security.Providers.Podbean/Properties/AssemblyInfo.cs b/src/Owin.Security.Providers.Podbean/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..f02f872c --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Owin.Security.Providers.Podbean")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Owin.Security.Providers.Podbean")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("38815307-3360-4f54-9204-03fae50160bc")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/Provider/IPodbeanAuthenticationProvider.cs b/src/Owin.Security.Providers.Podbean/Provider/IPodbeanAuthenticationProvider.cs new file mode 100644 index 00000000..47198e6d --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Provider/IPodbeanAuthenticationProvider.cs @@ -0,0 +1,33 @@ +#region + +using System.Threading.Tasks; + +#endregion + +namespace Owin.Security.Providers.Podbean +{ + /// + /// Specifies callback methods which the invokes to enable developer + /// control over the authentication process. /> + /// + public interface IPodbeanAuthenticationProvider + { + /// + /// Invoked whenever Podbean successfully authenticates a user + /// + /// + /// Contains information about the login session as well as the user + /// . + /// + /// A representing the completed operation. + Task Authenticated(PodbeanAuthenticatedContext 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(PodbeanReturnEndpointContext context); + } +} \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/Provider/PodbeanAuthenticatedContext.cs b/src/Owin.Security.Providers.Podbean/Provider/PodbeanAuthenticatedContext.cs new file mode 100644 index 00000000..16adcb6d --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Provider/PodbeanAuthenticatedContext.cs @@ -0,0 +1,130 @@ +#region + +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; +using Newtonsoft.Json; + +#endregion + +namespace Owin.Security.Providers.Podbean +{ + /// + /// Contains information about the login session as well as the user + /// . + /// + public class PodbeanAuthenticatedContext : BaseContext + { + /// + /// Initializes a + /// + /// The OWIN environment + /// The + /// Podbean Access token + /// Podbean Refresh token + /// Seconds until expiration + public PodbeanAuthenticatedContext(IOwinContext context, Podcast podcast, string accessToken, string refreshToken, string expires) + : base(context) + { + Podcast = podcast; + Id = podcast.Id; + Name = podcast.Title; + AccessToken = accessToken; + RefreshToken = refreshToken; + + int expiresValue; + if (int.TryParse(expires, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresValue)) + ExpiresIn = TimeSpan.FromSeconds(expiresValue); + } + + /// + /// Gets the JSON-serialized user + /// + /// + /// Contains the Podbean user obtained from the endpoint https://api.Podbeanapp.com/1/user.json + /// + public Podcast Podcast { get; } + + /// + /// Gets the Podbean OAuth access token + /// + public string AccessToken { get; } + + /// + /// Gets the Podbean OAuth refresh token + /// + public string RefreshToken { get; } + + /// + /// Gets the Podbean access token expiration time + /// + public TimeSpan? ExpiresIn { get; set; } + + /// + /// Gets the Podbean Podcast ID + /// + public string Id { get; } + + /// + /// The name of the user + /// + public string Name { get; } + + /// + /// 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; } + } + + public class Podcast + { + /// + /// The Id of the + /// + public string Id { get; set; } + + /// + /// The title of the + /// + public string Title { get; set; } + + /// + /// The description of the + /// + [JsonProperty(PropertyName = "desc")] + public string Description { get; set; } + + /// + /// A url to an image representing the logo of the + /// + public string Logo { get; set; } + + /// + /// A url to the 's website + /// + public string Website { get; set; } + + /// + /// The name of the category of the + /// + [JsonProperty(PropertyName = "category_name")] + public string CategoryName { get; set; } + + /// + /// The episode types of the . + /// The possible value is a combination of public, premium or private + /// + [JsonProperty(PropertyName = "allow_episode_type")] + public string[] AllowEpisodeTypes { get; set; } + } + +} \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/Provider/PodbeanAuthenticationProvider.cs b/src/Owin.Security.Providers.Podbean/Provider/PodbeanAuthenticationProvider.cs new file mode 100644 index 00000000..d2ce70a9 --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Provider/PodbeanAuthenticationProvider.cs @@ -0,0 +1,58 @@ +#region + +using System; +using System.Threading.Tasks; + +#endregion + +namespace Owin.Security.Providers.Podbean +{ + /// + /// Default implementation. + /// + public class PodbeanAuthenticationProvider : IPodbeanAuthenticationProvider + { + /// + /// Initializes a + /// + public PodbeanAuthenticationProvider() + { + 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 Podbean successfully authenticates a user + /// + /// + /// Contains information about the login session as well as the user + /// . + /// + /// A representing the completed operation. + public virtual Task Authenticated(PodbeanAuthenticatedContext 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(PodbeanReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + } +} \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/Provider/PodbeanReturnEndpointContext.cs b/src/Owin.Security.Providers.Podbean/Provider/PodbeanReturnEndpointContext.cs new file mode 100644 index 00000000..97098f1e --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Provider/PodbeanReturnEndpointContext.cs @@ -0,0 +1,23 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; + +namespace Owin.Security.Providers.Podbean +{ + /// + /// Provides context information to middleware providers. + /// + public class PodbeanReturnEndpointContext : ReturnEndpointContext + { + /// + /// + /// OWIN environment + /// The authentication ticket + public PodbeanReturnEndpointContext( + IOwinContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/Resources.Designer.cs b/src/Owin.Security.Providers.Podbean/Resources.Designer.cs new file mode 100644 index 00000000..a3bcdb85 --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Owin.Security.Providers.Podbean { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Owin.Security.Providers.Podbean.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided { + get { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. + /// + internal static string Exception_ValidatorHandlerMismatch { + get { + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); + } + } + } +} diff --git a/src/Owin.Security.Providers.Podbean/Resources.resx b/src/Owin.Security.Providers.Podbean/Resources.resx new file mode 100644 index 00000000..2a19bea9 --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Owin.Security.Providers.Podbean/packages.config b/src/Owin.Security.Providers.Podbean/packages.config new file mode 100644 index 00000000..cbfe6a2a --- /dev/null +++ b/src/Owin.Security.Providers.Podbean/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file