Build a SPA with ASP.NET Core 2.1, Stripe, and Angular 6
Lee Brandt
Posted on August 20, 2018
Buying things on the Internet has become a daily activity and is a feature many new projects require. In this tutorial, I will show you how to build an app to sell tickets using an Angular 6 single page app (SPA) using an ASP.NET Core 2.1 backend API. You’ll build both the Angular and ASP.NET Core applications and run them from within VS Code. Let’s get to it!
Upgrade to Angular 6
I love to use the latest and greatest when starting a new project. But when you use a project generator (like Angular-CLI, or the DotNetCLI), you may be at the mercy of the latest version the authors of those libraries have added. Right now, the DotNet CLI generates an Angular application with dotnet new angular
gives you an Angular app at about version 4.5, which is about two versions behind the latest. Let me show you how to upgrade the templates and the generated application so that you’re using Angular 6, which is the latest as of the time of this article.
Upgrade the Angular App Template
Update the DotNet command line tools with:
dotnet new --install Microsoft.DotNet.Web.Spa.ProjectTemplates::2.1.0
Then run:
dotnet new --install Microsoft.AspNetCore.SpaTemplates::2.1.0-preview1-final
Generate the ASP.NET Angular App
Now you can scaffold a new project:
dotnet new angular -o ticket-sales-example
Upgrade the Angular App To 6
The closest that gets you is Angular v5.2.0. To update Angular to v6.0.9 (as of this writing) switch to the ClientApp
directory and run:
ng update --all
This will update the package.json
file; then you need to run:
npm install
If you get a message about @angular/cli
you can update it by running:
ng update @angular/cli
You may now see some vulnerabilities in your NPM packages. To fix them run:
npm audit fix
You may have to run this several times as some of the fixes introduce new vulnerabilities. I was only able to get my vulnerability list down to 6. I still have one low and five moderate vulnerabilities. If you want to get to zero vulnerabilities, you would have to hunt them each down and fix them manually.
Create a Stripe Account
One of the easiest ways to take payments on the web is to use Stripe. You can create a free developer account on Stripe’s registration page.
Once you’ve registered, make sure that you go to your dashboard and on the left-hand menu, click the toggle to ensure you are viewing test data. Then click on the Developers menu item and then click API Keys. Copy down the Publishable key to use in your Angular app.
Add Stripe to Your Angular 6 App
In your index.html
file, add a script tag for Stripe’s JavaScript library, right below the app-root
component.
<script type="text/javascript" src="https://js.stripe.com/v2/" />
Also add your publishable key to the Stripe object:
<script type="text/javascript">
Stripe.setPublishableKey('{yourPublishableKey}');
</script>
Make sure that your publishable key starts with
pk_test_
. If it doesn’t, you’re using the production key, and you don’t want to do that yet.
Create the Stripe Ticket Registration Page
You can easily scaffold the base registration component with the Angular CLI. Go to a command line and change directories into the src/app
directory. Then run the command:
ng generate component registration
The shorthand for the CLI is:
ng g c registration
The generate command will generate a folder called registration
, and inside that a registration.compomnent.css
, registration.component.html
, a registration.component.spec.ts
, and a registration.component.ts
file. These are all the basic files for an Angular 6 component. I won’t be covering testing in this tutorial, so you can ignore or delete the registration.component.spec.ts
file.
First, add some basic HTML to your registration.component.html
file for displaying tickets. So the final file contents looks like this:
<h1>Register for SuperDuperConf</h1>
<div class="ticket conf-only">
<span class="title">Conference Only Pass</span>
<span class="price">$295</span>
<button (click)="selectTicket('Conference Only', 295)">Register Now!</button>
</div>
<div class="ticket full">
<span class="title">Full Conference + Workshop Pass</span>
<span class="price">$395</span>
<span class="value">Best Value!</span>
<button (click)="selectTicket('Full Conference + Workshop', 395)">Register Now!</button>
</div>
<div class="ticket work-only">
<span class="title">Workshop Only Pass</span>
<span class="price">$195</span>
<button (click)="selectTicket('Workshop Only', 195)">Register Now!</button>
</div>
<div class="alert alert-success" *ngIf="model.successMessage">{{successMessage}}</div>
<div class="alert alert-danger" *ngIf="model.errorMessage">{{errorMessage}}</div>
<div *ngIf="model.ticket.price">
<form (submit)="purchaseTicket()" class="needs-validation" novalidate #regForm="ngForm">
<div class="form-group">
<label for="firstName">First Name:</label>
<input type="text" class="form-control" name="firstName" id="firstName" [(ngModel)]="model.firstName" required #firstName="ngModel">
<div [hidden]="firstName.valid || firstName.pristine" class="text-danger">First Name is required.</div>
</div>
<div class="form-group">
<label for="lastName">Last Name:</label>
<input type="text" class="form-control" name="lastName" id="lastName" [(ngModel)]="model.lastName" required #lastName="ngModel">
<div [hidden]="lastName.valid || lastName.pristine" class="text-danger">Last Name is required.</div>
</div>
<div class="form-group">
<label for="email">Email Address:</label>
<input type="text" class="form-control" name="email" id="email" [(ngModel)]="model.emailAddress" required #email="ngModel">
<div [hidden]="email.valid || email.pristine" class="text-danger">Email Address is required.</div>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" class="form-control" name="password" id="password" [(ngModel)]="model.password" required #password="ngModel">
<div [hidden]="password.valid || password.pristine" class="text-danger">Password is required.</div>
</div>
<div class="form-group">
<label for="cardNumber">Card Number:</label>
<input type="text" class="form-control" name="cardNumber" id="cardNumber" [(ngModel)]="model.card.number" required>
</div>
<div class="form-group form-inline">
<label for="expiry">Expiry:</label>
<br/>
<input type="text" class="form-control mb-1 mr-sm-1" name="expiryMonth" id="expiryMonth" [(ngModel)]="model.card.exp_month"
required> /
<input type="text" class="form-control" name="expiryYear" id="expiryYear" [(ngModel)]="model.card.exp_year" required>
</div>
<div class="form-group">
<label for="cvc">Security Code:</label>
<input type="text" class="form-control" name="cvc" id="cvc" [(ngModel)]="model.card.cvc" required>
</div>
<button type="submit" class="btn btn-success" [disabled]="!regForm.form.valid">Pay ${{model.ticket.price / 100}}</button>
</form>
</div>
I know it seems like a lot, but there is a lot of repetition here. The first section lists three tickets that a user can buy to register for the “SuperDuperConf”. The second section is just a form that collects the information needed to register an attendee for the conference.
The important thing to take note of here is the [(ngModel)]="model.some.thing"
lines of code. That weird sequence of characters around ngModel
is just parentheses inside of square brackets. The parentheses tell Angular that there is an action associated with this field. You see this a lot for click event handlers. It usually looks something like (click)="someEventHandler()"
. It is the same, in that the ngModel
is the handler of the event when the model changes.
The square brackets are used for updating the DOM when something on the model changes. It is usually seen in something like disabling a button as you did above with [disabled]="!regForm.form.valid"
. It watches the value on the form, and when it is not valid, the button is disabled. Once the form values become valid, the disabled property is removed from the DOM element.
Now that you have all the fields on the page, you will want to style that ticket section up a bit so that it looks like tickets.
.ticket {
text-align: center;
display: inline-block;
width: 31%;
border-radius: 1rem;
color: #fff;
padding: 1rem;
margin: 1rem;
}
.ticket.conf-only,
.ticket.work-only {
background-color: #333;
}
.ticket.full {
background-color: #060;
}
.ticket span {
display: block;
}
.ticket .title {
font-size: 2rem;
}
.ticket .price {
font-size: 2.5rem;
}
.ticket .value {
font-style: italic;
}
.ticket button {
border-radius: 0.5rem;
text-align: center;
font-weight: bold;
color: #333;
margin: 1rem;
}
These are just three basic ticket types I regularly see for conference registrations.
Now the meat of the registration page, the TypeScript component. You will need a few things to make the page work. You will need a model to store the values that the user enters, a way for the user to select a ticket, and a way for the user to pay for the ticket they have selected.
import { Component, ChangeDetectorRef, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html',
styleUrls: ['./registration.component.css']
})
export class RegistrationComponent {
public model: any;
public card: any;
public errorMessage: string;
public successMessage: string;
constructor(
private http: HttpClient,
private changeDetector: ChangeDetectorRef,
@Inject('BASE_URL') private baseUrl: string
) {
this.resetModel();
this.successMessage = this.errorMessage = null;
}
resetModel(): any {
this.model = {
firstName: '',
lastName: '',
emailAddress: '',
password: '',
token: '',
ticket: { ticketType: '', price: 0 }
};
this.card = { number: '', exp_month: '', exp_year: '', cvc: '' };
}
selectTicket(ticketType: string, price: number) {
this.model.ticket = { ticketType, price: price * 100 };
}
purchaseTicket() {
(<any>window).Stripe.card.createToken(
this.card,
(status: number, response: any) => {
if (status === 200) {
this.model.token = response.id;
this.http
.post(this.baseUrl + 'api/registration', this.model)
.subscribe(
result => {
this.resetModel();
this.successMessage = 'Thank you for purchasing a ticket!';
console.log(this.successMessage);
this.changeDetector.detectChanges();
},
error => {
this.errorMessage = 'There was a problem registering you.';
console.error(error);
}
);
} else {
this.errorMessage = 'There was a problem purchasing the ticket.';
console.error(response.error.message);
}
}
);
}
}
Even if you’re familiar with Angular, some of this may look foreign. For instance, the BASE_URL
value that is getting injected into the component. It comes from the main.ts
file that the Angular CLI generated. If you look at that file, right below the imports, there is a function called getBaseUrl()
and below that is a providers
section that provides the value from the getBaseUrl()
function, which is just a simple way to inject constant values into components.
The other thing that might look strange is the purchaseTicket()
function. If you’ve never used Stripe before, the createToken()
method creates a single-use token that you can pass to your server to use in your server-side calls, that way you don’t have to send credit card information to your server, and you can let Stripe handle the security of taking online payments!
Add the ASP.NET Registration Controller
Now that your Angular app can get a token from Stripe, you’ll want to send that token and the user’s information to the server to charge their card for the ticket. Create a controller in the Controllers
folder in the server-side application root. The contents of the file should be:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Okta.Sdk;
using Stripe;
using ticket_sales_example.Models;
namespace ticket_sales_example.Controllers
{
[Produces("application/json")]
[Route("api/[controller]")]
public class RegistrationController : ControllerBase
{
[HttpPost]
public async Task<ActionResult<Registration>> CreateAsync([FromBody] Registration registration)
{
ChargeCard(registration);
var oktaUser = await RegisterUserAsync(registration);
registration.UserId = oktaUser.Id;
return Ok(registration);
}
private async Task<User> RegisterUserAsync(Registration registration)
{
var client = new OktaClient();
var user = await client.Users.CreateUserAsync(
new CreateUserWithPasswordOptions
{
Profile = new UserProfile
{
FirstName = registration.FirstName,
LastName = registration.LastName,
Email = registration.EmailAddress,
Login = registration.EmailAddress,
},
Password = registration.Password,
Activate = true
}
);
var groupName = "";
if (registration.Ticket.TicketType == "Full Conference + Workshop")
{
groupName = "FullAttendees";
}
if (registration.Ticket.TicketType == "Conference Only")
{
groupName = "ConferenceOnlyAttendees";
}
if (registration.Ticket.TicketType == "Workshop Only")
{
groupName = "WorkshopOnlyAttendees";
}
var group = await client.Groups.FirstOrDefault(g => g.Profile.Name == groupName);
if (group != null && user != null)
{
await client.Groups.AddUserToGroupAsync(group.Id, user.Id);
}
return user as User;
}
private StripeCharge ChargeCard(Registration registration)
{
StripeConfiguration.SetApiKey("sk_test_uukFqjqsYGxoHaRTOS6R7nFI");
var options = new StripeChargeCreateOptions
{
Amount = registration.Ticket.Price,
Currency = "usd",
Description = registration.Ticket.TicketType,
SourceTokenOrExistingSourceId = registration.Token,
StatementDescriptor = "SuperDuperConf Ticket"
};
var service = new StripeChargeService();
return service.Create(options);
}
}
}
It seems like there is a bit here, but there is only the HttpPost
method CreateAsync()
that is the API endpoint for a POST
to /api/registration
. The other methods are helpers to the endpoint.
The ChargeCard()
method does just as the name implies, it charges the user’s credit card using the token that the Angular app got from Stripe and sent to the API. Even though I am setting the Stripe API key with a simple string here for demonstration purposes, you might want to store the key in an environment variable, in a configuration file that doesn’t get checked into source control, or in a key management service like Azure’s Key Vault. This will mitigate the chances that you will accidentally check the test key into your source control and have that end up being deployed to production!
The RegisterUserAsync()
method handles registering a user with Okta and putting them into a group that corresponds to the ticket that the user is purchasing. This is done in two steps: by creating the user, then finding the group that corresponds with the ticket purchased, and adding that group’s ID to the newly created Okta user.
Set Up Okta for Your Angular and ASP.NET Core Applications
Dealing with user authentication in web apps is a massive pain for every developer. This is where Okta shines: it helps you secure your web applications with minimal effort.
Why Okta?
At Okta, our goal is to make identity management a lot easier, more secure, and more scalable than what you’re used to. Okta is a cloud service that allows developers to create, edit, and securely store user accounts and user account data, and connect them with one or multiple applications. Our API enables you to:
- Authenticate and authorize your users
- Store data about your users
- Perform password-based and social login
- Secure your application with multi-factor authentication
- And much more! Check out our product documentation
Create an Okta Application
To get started, you’ll need to create an OpenID Connect application in Okta. Sign up for a forever-free developer account (or log in if you already have one).
Once you’ve logged in and landed on the dashboard page, copy down the Org URL pictured below. You will need this later.
Then create a new application by browsing to the Applications tab and clicking Add Application , and from the first page of the wizard choose Single-Page App.
On the settings page, enter the following values:
- Name: TicketSalesApp
- Base URIs: http://localhost:5000
- Login redirect URIs: http://localhost:5000/implicit/callback
You can leave the other values unchanged, and click Done.
Now that your application has been created copy down the Client ID and Client secret values on the following page, you’ll need them soon.
Finally, create a new authentication token. This will allow your app to talk to Okta to retrieve user information, among other things. To do this, click the API tab at the top of the page followed by the Create Token button. Give your token a name, in this case, “Crud API” would be a good name, then click Create Token. Copy down this token value as you will need it soon.
Even though you have a method for registering users, you’ll need to create the groups for the tickets, set up your API to use Okta, and configure it to receive access tokens from users of the Angular app for authorization.
Start by creating a group for each of the three tickets you’ll be selling. From the Okta dashboard hover over the Users menu item until the drop-down appears and choose Groups. From the Groups page, click the Add Group button.
In the Add Group modal that pops up, add a group for each ticket type.
Now, you’ll need to add these newly created groups to the ticket sales application. Click on the Applications menu item, and choose the TicketSalesApp from the list of apps. It should open on the Assignments tab. Click on the Assign button and choose Assign to Groups from the button’s drop-down menu. From here, assign each group you just created to the Ticket Sales app.
Add Groups to the ID Token
Now you just need to add these groups to the token.
- Hover over the API menu item and select Authorization Servers.
- Select the default authorization server (it was created for you when you created your Okta account).
- Choose the Claims tab, and click Add Claim.
- The name of the claim will be “groups”,Select ID Token and Always from the Include in token type setting.
- Choose Groups from the Value Type setting, and Regex from the Filter setting.
- In the text box type
.*
. - Finally, make sure the Disable claim checkbox is unchecked and that the Any scope radio button is selected in the Include in setting.
Add Okta to Your Angular Application
To set up your Angular application to use Okta for authentication, you’ll need to install the Angular SDK and the rxjs
compatibility package.
npm install @okta/okta-angular rxjs-compat@6 --save
Add the components to your app.module.ts
file in src/app
by first importing them:
import {
OktaCallbackComponent,
OktaAuthModule,
OktaAuthGuard
} from '@okta/okta-angular';
Now add a configuration variable right below the import statements:
const config = {
issuer: 'https://{yourOktaDomain}/oauth2/default',
redirectUri: 'http://localhost:5000/implicit/callback',
clientId: '{yourClientId}'
};
Add the callback route to the routes in the imports
section of the @NgModule
declaration:
{ path: 'implicit/callback', component: OktaCallbackComponent }
That’s all for now in the Angular application. Now let’s get the ASP.NET Core app set up.
Add Okta to Your ASP.NET Core API
Now you need to let the API know two things: how to get the user’s identity from an access token (when one is sent) and how to call Okta for user management.
Start by adding the Okta Nuget package:
dotnet add package Okta.Sdk
For the ASP.NET Core application, the best thing to do is set up a file in your home folder to store the configuration. Okta’s SDK will pick the settings up for you, and you’ll never accidentally check them into source control!
In your home directory, create a .okta folder and add a file called okta.yaml. Your home folder will depend on your operating system. For *nix variants like Linux or macOS it is:
~/.okta/okta.yaml
for Windows environments it is:
%userprofile%\.okta\okta.yaml
YAML is just a file format for configuration. The okta.yaml file looks like:
okta:
client:
orgUrl: "https://dev-846291.oktapreview.com/"
token: "{yourApiToken}"
In the ConfigureServices() method before the services.AddMvc() line, add:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://{yourOktaDomain}/oauth2/default";
options.Audience = "api://default";
});
And in the Configure() method before the app.UseMvc() line add:
app.UseAuthentication();
That’s it! Now your ASP.NET Core app will take that bearer token, get the user’s information from Okta add them to the User object so you can get the currently requesting user’s data. It will also use the API token stored in the okta.yaml
file when registering users.
Show the Tickets in Your Angular App
Now that users can purchase a ticket, you’ll want them to be able to log in and see their purchased ticket. To do this, generate a profile component using Angular’s CLI. From the src/app
folder of the client app, run:
ng g c profile
Again, this is just shorthand for ng generate component profile
, which will generate all the base files for the profile component. The profile.component.ts
file should have the following contents:
import { Component, OnInit } from '@angular/core';
import { OktaAuthService } from '@okta/okta-angular';
import 'rxjs/Rx';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
user: any;
ticket: string;
constructor(private oktaAuth: OktaAuthService) {}
async ngOnInit() {
this.user = await this.oktaAuth.getUser();
if (this.user.groups.includes('FullAttendees')) {
this.ticket = 'Full Conference + Workshop';
} else if (this.user.groups.includes('ConferenceOnlyAttendees')) {
this.ticket = 'Conference Only';
} else if (this.user.groups.includes('WorkshopOnlyAttendees')) {
this.ticket = 'Workshop Only';
} else {
this.ticket = 'None';
}
}
}
This does two things: it gets the currently logged in user and translates the group name into a displayable string representation of the ticket type purchased. The profile.component.html
file is straightforward:
<h1>{{user.name}}</h1>
<p>
Your Puchased Ticket: {{ticket}}
</p>
The last thing to do is to add a protected route to the profile page in the app.module.ts
. I added mine right above the callback route:
{
path: 'profile',
component: ProfileComponent,
canActivate: [OktaAuthGuard]
},
You can now sell tickets, and the users can log in and see which ticket they have once they’ve purchased one. You’re ready to hold your event!
Learn More about ASP.NET
Check out our other Angular and .NET posts on the Okta developer blog:
- Ibrahim creates a CRUD app with an ASP.NET Framework 4.x API in his post
- Build a basic CRUD app using Angular and ASP.NET Core
- If you would like to use React instead of Angular for your CRUD app, I’ve got you covered
- Get nitty-gritty on token authentication in ASP.NET Core
- Get your project out into the world by deploying it to Azure, the right way
As always, if you have any comments or questions, feel free to leave a comment below. Don’t forget to follow us on Twitter @oktadev and on Facebook!
Posted on August 20, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.