Creating a “freemium” or paid access website is easier than ever with Stripe and Stormpath to manage the workflow of your paid membership content. As the owner of a subscription site, you need to concentrate creating content rather than all the plumbing that goes along with getting paid for that content.
In this tutorial, we will create an ASP.NET Core web application integrated with Stormpath to manage authentication and authorization. Then, we are going to add a Premium User section which will only be available to users who belong to the “Premium” group. This group will be created and managed through Stormpath.
Users will be able to upgrade their accounts to “Premium”. For this, we are going to use the Stripe API to create a payment form for our freemium site.
Let’s get started!
Create a Stormpath account
Visit the Stormpath website and sign-up to create your free developer account in Stormpath.
Get an API key pair
In order for your application to communicate with Stormpath, you’ll need a API key pair which will function as your credentials for the Stormpath API.
Getting these keys is simple. Log in to the Admin Console and click on the “Create API Key” button on the right side of the page to generate and download an API key file called apiKey.Properties
.
Note: As a best practice we recommend you to save your keys as Environment Variables. Check out the Stormpath documentation to learn more.
Open the apiKey.Properties
file and set environment variables by running these commands in the Command Line:
setx STORMPATH_CLIENT_APIKEY_ID "value_from_properties_file"
setx STORMPATH_CLIENT_APIKEY_SECRET "value_from_properties_file"
If you get an “Additional information: API key ID is required” error restart Visual Studio to pick up the environment variables from your OS. This is a pretty common error.
Create a new ASP.NET Core Project in Visual Studio
To create a new ASP.NET Core project, first click on File > New Project. Then, under Visual C# – .NET Core, pick the ASP.NET Core Web Application (.NET Core) template.
In the New ASP.NET Core Project dialog, pick the Web Application template. Click Change Authentication to No Authentication. (You’ll be adding it yourself!)
Install the Stormpath package
The Stormpath.AspNetCore NuGet package contains everything you need to use Stormpath from your application. Install it using the NuGet Package Manager, or from the Package Manager Console:
PM> install-package Stormpath.AspNetCore
Initialize the Stormpath ASP.NET Core Middleware
In the Startup.cs
file, include the Stormpath namespace by adding this line at the beginning of it:
1 2 |
using Stormpath.AspNetCore; |
In the same file, find the ConfigureServices
method and add the Stormpath service in the service container:
1 2 3 4 5 6 7 8 |
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddStormpath(); // Add framework services. services.AddMvc(); } |
Make sure that the Stormpath middleware is added before any middleware that needs to be protected, such as MVC.
Then, find the Configure
method and add Stormpath to your middleware pipeline.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseStormpath(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } |
With these simple steps, Stormpath attaches the Default Features to your application, such as the login and registration routes.
Update your layout
When a user logs in, the Stormpath middleware will set the Context.User
automatically.
You’ll want to change the _Layout.cshtml
located in Views/Shared
to show different content based on the IsAuthenticated
property, which returns true when the user has logged in to your application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<br /><br /><div class="navbar-collapse collapse"> @if (Context.User.Identity.IsAuthenticated) { <form action="/logout" method="post" id="logout_form"> <ul class="nav navbar-nav navbar-right"> <li><p class="navbar-text">Hello, <b>@Context.User.Identity.Name</b></p></li> <li> <a onclick="document.getElementById('logout_form').submit();" style="cursor: pointer;">Log Out</a> </li> </ul> </form> } else { <ul class="nav navbar-nav navbar-right"> <li><a href="/register">Register</a></li> <li><a href="/login">Log In</a></li> </ul> } </div> |
Then, customize your home page by removing the boilerplate “Welcome to ASP.NET Core” in the Home/Index.cshtml
and add, you know, your actual site. I have added a super basic “Welcome to Stormpath World” message.
Run your application
After running your application, you will see the “Log in” and “Register” options in your application nav bar. When you click on any of these links you will be redirected to the corresponding Stormpath default view.
Once, you get registered and log in to the application, you will see the “Hello your_username” and “Log Out” options.
Yay! Users can now securely register and log into your application with Stormpath.
Create a new Group in Stormpath
Log in to the Admin Console and click on the “Groups” tab. Then, click on “Create Group” and complete the form:
The “Premium” group is ready to be used. Super easy!
Get a Stripe API key pair
Visit Stripe, and create your Stripe account if you don’t have one already. Once you’re logged in, go to account > account settings > API Keys
and save your test API keys to be used later on.
Store your Stripe API key pair using ASP.NET Core User Secrets
You should always avoid storing sensitive data in your source code. This can be done by using the environment variables or the Secret Manager.
The Secret Manager tool provides a mechanism to store sensitive data for development work outside of your project tree, and you can access to the secret keys by using Configuration
. Let’s give it a try!
Create a class in Model
folder called PaymentSettings
. This class will be used to store the Stripe keys.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace stormpathWebAppDemo.Model { public class PaymentSettings { public string StripePublicKey { get; set; } public string StripePrivateKey { get; set; } } } |
Install the User Secrets package by running the following command in the Package Manager Console:
PM > Install-Package Microsoft.Extensions.Configuration.UserSecrets
In Visual Studio, right-click on your web application and then click on “Manage User Secrets”.
A JSON file called secrets.json
will be open for editing. This file is located in the user profile directory.
Add these two lines:
1 2 3 |
"PaymentSettings:StripePublicKey":"your_stripe_public_key" "PaymentSettings:StripePrivateKey":"your_stripe_private_key" |
In the Startup
class, add the AddUserSecrets()
line in the constructor to load settings from the User Secrets.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables(); if (env.IsDevelopment()) { builder.AddUserSecrets(); } Configuration = builder.Build(); } |
In order to bind the PaymentSettings
class to your application you need to add them to the ConfigureServices
method of Startup.cs
.
1 2 3 4 5 |
public void ConfigureServices(IServiceCollection services) { services.Configure<PaymentSettings>(Configuration.GetSection("PaymentSettings")); } |
Then, when you need to access the values of PaymentSettings
you just need to inject an instance of an IOptions<PaymentSettings>
class into the constructor of your client class, in this case, the PaymentService
class. We will see this in action in the following steps.
Create a “Premium User” section
Now let’s create a view on our application that will only be accessible to Premium users. You will then create a way for users to upgrade their accounts to Premium.
Create a new folder called Services
and add a new class called “AccountManager”. This class will be responsible for managing everything related to user accounts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
using Stormpath.SDK; using Stormpath.SDK.Account; using Stormpath.SDK.Application; using System.Linq; using System.Security.Principal; using System.Threading.Tasks; namespace stormpathWebAppDemo.Services { public class AccountManager { private readonly IApplication stormpathApplication; private readonly string PREMIUM_GROUP = "Premium"; private async Task<IAccount> GetUserAccount(IIdentity userIdentity) { return await stormpathApplication.GetAccounts().Where(x => x.Email == userIdentity.Name).FirstOrDefaultAsync(); } public AccountManager(IApplication stormpathApplication) { this.stormpathApplication = stormpathApplication; } public async Task AddUserToPremiumGroup(IIdentity userIdentity) { var premiumGroup = await stormpathApplication.GetGroups().Where(g => g.Name == PREMIUM_GROUP).FirstOrDefaultAsync(); var account = await GetUserAccount(userIdentity); if (premiumGroup != null && account != null) { await premiumGroup.AddAccountAsync(account); } } public async Task<bool> IsPremiumUser(IIdentity userIdentity) { var isPremium = false; if (userIdentity != null) { var account = await GetUserAccount(userIdentity); if (account != null) { isPremium = await account.GetGroups().Where(g => g.Name == PREMIUM_GROUP).AnyAsync(); } } return isPremium; } } } |
When the Stormpath middleware is added to your ASP.NET Core application pipeline, these types will be available for each request:
- Stormpath Client (
IClient
) - Stormpath Application (
IApplication
) - Current user’s Stormpath Account (
IAccount
)
In this class we used IApplication
; you learn more about using the other objects in the documentation.
Now, create a new Controller called PremiumContentController
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using stormpathWebAppDemo.Services; namespace stormpathWebAppDemo.Controllers { public class PremiumContentController : Controller { private readonly AccountManager accountManager; public PremiumContentController(AccountManager accountManager) { this.accountManager = accountManager; } // GET: /<controller>/ public async Task<IActionResult> Index() { bool isPremiumUser = await accountManager.IsPremiumUser(HttpContext.User.Identity); if (isPremiumUser) { return View(); } else { return Redirect("~/Home/BePremium"); } } } } |
This controller tells the ASP.NET Core dependency injection mechanism to inject the AccountManager service. To do this, you need to register the service in the ConfigureService
method in the Startup
class:
services.AddTransient<AccountManager>();
Create a new folder called “PremiumContent” under Views
and then add a new Index
view. This view will contain all your site data related to premium users. I have added a simple message:
1 2 3 4 5 6 |
<br /><br /><div class="row"> <h1>Lots of cool stuff here</h1> </div> |
On the _Layout
, add a link to the “PremiumContent” Index View if the user is already Premium:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
if (await AccountManager.IsPremiumUser(Context.User.Identity)) { <ul class="nav navbar-nav navbar-right"> <li><a asp-controller="PremiumContent" asp-action="Index">Hello Premium folks!</a></li> </ul> } else { <ul class="nav navbar-nav navbar-right"> <li><a asp-controller="Home" asp-action="BePremium">Be Premium!</a></li> </ul> } |
In order to inject the AccountManager on the view, add this line on top of the file:
1 2 |
@inject AccountManager AccountManager |
The Index
action checks whether the user is allowed to access it, that is if the user is Premium. If not, it redirects them to a section where he can upgrade their account.
Use Stripe to charge users for Premium accounts
We will now use Stripe to create a form and accept payment for a Premium account.
Go to your HomeController
and create the BePremium
action. This action will render the Stripe charge form:
1 2 3 4 5 |
public IActionResult BePremium() { return View("PremiumPayment"); } |
Now, create the PremiumPayment view under Views/Home:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
@using stormpathWebAppDemo.Model @using Microsoft.Extensions.Options @inject IOptions<PaymentSettings> PaymentSettings @section Scripts{ <script type="text/javascript" src="https://js.stripe.com/v2/"></script> <script type="text/javascript"> Stripe.setPublishableKey('@PaymentSettings.Value.StripePublicKey'); </script> <script type="text/javascript"> $(function () { var $form = $('#payment-form'); $form.submit(function (event) { // Disable the submit button to prevent repeated clicks: $form.find('.submit').prop('disabled', true); // Request a token from Stripe: Stripe.card.createToken($form, stripeResponseHandler); // Prevent the form from being submitted: return false; }); }); function stripeResponseHandler(status, response) { // Grab the form: var $form = $('#payment-form'); if (response.error) { // Problem! // Show the errors on the form: $form.find('.payment-errors').text(response.error.message); $form.find('.submit').prop('disabled', false); // Re-enable submission } else { // Token was created! // Get the token ID: var token = response.id; // Insert the token ID into the form so it gets submitted to the server: $form.append($('<input type="hidden" name="Token">').val(token)); // Submit the form: $form.get(0).submit(); } }; </script> } <div class="row"> <div class="col-md-12 form-column"> <div class="form-container"> <form asp-controller="home" asp-action="processpayment" method="POST" id="payment-form"> <span class="payment-errors"></span> <div class="form-group"> <h3>Membership Amount: USD 50</h3> </div> <div class="form-group"> <label for="cardNumber">Card Number</label> <input class="form-control form-input" id="cardNumber" type="text" size="20" data-stripe="number"> </div> <div class="form-group"> <label>Expiration (MM/YY)</label> <div> <input class="form-control form-input date-input" type="text" size="2" data-stripe="exp_month"> <input class="form-control form-input date-input" type="text" size="2" data-stripe="exp_year"> </div> </div> <div class="form-group"> <label for="cvc">CVC</label> <input class="form-control form-input" id="cvc" type="text" size="4" data-stripe="cvc"> </div> <input class="btn btn-default" type="submit" class="submit" value="Submit Payment"> </form> </div> </div> </div> |
In this view, we are including the Stripe javascript library (which will do all the client-side magic for us).
1 2 |
<script type="text/javascript" src="https://js.stripe.com/v2/"></script> |
We set our Public API Key (Be sure it’s the public one! You should NEVER use private keys on client-side code), and as you can see, we are not using the Stripe key directly. Instead, we are injecting the PaymentSettings
by adding this line:
1 2 |
@inject IOptions<PaymentSettings> PaymentSettings |
And accessing the Stripe keys by the PaymentSettings
.
1 2 3 4 5 6 |
<br /><br /><script type="text/javascript"> Stripe.setPublishableKey('@PaymentSettings.Value.StripePublicKey'); </script> |
After that, we’ve added another script block with custom code. We’ve defined a form submit hook, which will prevent the form submission and obtain a token for the user’s credit card information (this token will then be used on the back-end to process the charge). If this action is successful, the handler will be executed, and will post the data to the back-end to process:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<br /><br /><script type="text/javascript"> $(function () { var $form = $('#payment-form'); $form.submit(function (event) { // Disable the submit button to prevent repeated clicks: $form.find('.submit').prop('disabled', true); // Request a token from Stripe: Stripe.card.createToken($form, stripeResponseHandler); // Prevent the form from being submitted: return false; }); }); function stripeResponseHandler(status, response) { // Grab the form: var $form = $('#payment-form'); if (response.error) { // Problem! // Show the errors on the form: $form.find('.payment-errors').text(response.error.message); $form.find('.submit').prop('disabled', false); // Re-enable submission } else { // Token was created! // Get the token ID: var token = response.id; // Insert the token ID into the form so it gets submitted to the server: $form.append($('<input type="hidden" name="Token">').val(token)); // Submit the form: $form.get(0).submit(); } }; </script> |
And obviously, there is a simple charge form to capture the user credit card information.
As you may have already noticed, the form will post to the ProcessPayment
action of the HomeController
. Before that though, create a folder called “Models” on the root of your project. Inside it, add a class to hold the form data that will be posted to the back-end:
1 2 3 4 5 6 7 8 |
namespace stormpathWebAppDemo.Model { public class PaymentFormData { public string Token { get; set; } } } |
Install Stripe.NET
using the NuGet Package Manager, or from the Package Manager Console to implement our PaymentService
:
1 2 |
PM> Install-Package Stripe.net |
Then, add a new service class to your “Services” folder. This class will be responsible for handling and processing the payment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using Microsoft.Extensions.Options; using stormpathWebAppDemo.Model; using Stripe; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace stormpathWebAppDemo.Services { public class PaymentService { private readonly PaymentSettings paymentSettings; private readonly StripeChargeService stripeChargeService; public PaymentService(IOptions<PaymentSettings> paymentSettings, StripeChargeService stripeChargeService) { this.paymentSettings = paymentSettings.Value; this.stripeChargeService = stripeChargeService; } public bool ProcessPayment(string token) { var myCharge = new StripeChargeCreateOptions(); // Always set these properties myCharge.Amount = 50; myCharge.Currency = "usd"; myCharge.Description = "Premium membership"; myCharge.SourceTokenOrExistingSourceId = token; // (not required) set this to false if you don't want to capture the charge yet - requires you call capture later myCharge.Capture = true; stripeChargeService.ApiKey = paymentSettings.StripePrivateKey; StripeCharge stripeCharge = stripeChargeService.Create(myCharge); if (String.IsNullOrEmpty(stripeCharge.FailureCode) && String.IsNullOrEmpty(stripeCharge.FailureMessage)) { return true; } else { return false; } } } } |
As you did for the AccountManager
service, register the PaymentService
and the StripeChargeService
in the Startup class to be injected by the framework
1 2 3 |
services.AddTransient<StripeChargeService>(); services.AddTransient<PaymentService>(); |
Notice that the PaymentSettings
was injected in the service, and used to get the Stripe private key.
In order to process the payment, the ProcessPayment
method creates a StripeChargeCreateOptions
options with all the charge data (amount, currency, etc). Then, it creates the charge (passing this object as argument) with the StripeChargeService
Now, go to your HomeController
and create the ProcessPayment
action as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[HttpPost] public async Task<IActionResult> ProcessPayment(PaymentFormData formData) { if (paymentService.ProcessPayment(formData.Token)) { await accountManager.AddUserToPremiumGroup(HttpContext.User.Identity); return Redirect("Index"); } else { // Handle errors return Redirect("Error"); } } |
In order to use these services, inject them in the HomeController
constructor:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class HomeController : Controller { private readonly PaymentService paymentService; private readonly AccountManager accountManager; public HomeController(PaymentService paymentService, AccountManager accountManager) { this.paymentService = paymentService; this.accountManager = accountManager; } } |
When the user submits the form, your back-end will charge them a fixed amount. The user will also be added to the Premium group in Stormpath and be granted access to your premium content!
Keep learning!
As you’ve seen, managing your “freemium” ASP.NET Core website can be simplified with Stripe for payments and Stormpath for user management, including registration, login, authentication, and authorization concerns.
If you are interested in learning more about Stormpath and ASP.NET Core, check out these resources: