The applications you work on expect good authentication as a secure foundation. In the past, we treated authentication as binary. You are either authenticated or not. You had to set the same authentication mechanism for access to your application without a standard way to change authentication mechanisms conditionally. Consider the case where sensitive actions warrant verification, such as making a large financial transaction or modifying top-secret data. Those actions require extra scrutiny!
Use Step Up Authentication Challenge to protect resources
Enter the OAuth 2.0 Step Up Authentication Challenge Protocol. This standard, built upon OAuth 2.0, outlines a method to elevate authentication requirements within your application. The standard defines methods to identify authentication rules, including authentication mechanisms and authentication recency. We'll cover more details about this much-needed standard as we go along. If you want to read more about it before jumping into the hands-on coding project, check out Step-up Authentication in Modern Applications.
In this post, you'll add step-up authentication challenge standard to an Angular frontend and NestJS backend. If you want to jump to the completed project, you can find it in the completed
branch of okta-angular-nestjs-stepup-auth-example GitHub repository. Warm up your computer; here we go!
Note
This post is best for developers familiar with web development basics and Angular. If you are an Angular newbie, start by building your first Angular app using the tutorial created by the Angular team.
Prerequisites
For this tutorial, you need the following tools:
- Node.js v18 or greater
- A web browser with good debugging capabilities
- Your favorite IDE. Still searching? I like VS Code and WebStorm because they have integrated terminal windows.
- Terminal window (if you aren't using an IDE with a built-in terminal)
- Git and an optional GitHub account if you want to track your changes using a source control manager
Table of Contents
- Use Step Up Authentication Challenge to protect resources
- Prepare the Angular and NestJS web application
- Set up the Identity Provider to use OAuth 2.0 and OpenID Connect
- Step-up authentication mechanics at a glance
- Set up authenticators
- Guard a route with step-up authentication
- Protect API resources with step-up authentication challenge
- Ensure authentication recency in step-up authentication
- Step-up authentication in Angular and NestJS applications
Prepare the Angular and NestJS web application
You'll start by getting a local copy of the project. I opted to use a starter project instead of building it out within the tutorial because many steps and command line operations detract from the coolness of adding step-up authentication. Open a terminal window and run the following commands to get a local copy of the project in a directory called okta-stepup-auth-project
and install dependencies. Feel free to fork the repo so you can track your changes.
git clone https://github.com/oktadev/okta-angular-nestjs-stepup-auth-example.git okta-stepup-auth-project
cd okta-stepup-auth-project
npm ci
oktadev / okta-angular-nestjs-stepup-auth-example
Example project demonstrating step-up auth implementation in Angular and NestJS
Add Step-up Authentication Using Angular and NestJS Example
This repository contains a working example of adding step-up authentication to protect an Angular route using the Okta Angular SDK. It also contains example code of protecting an API route in NestJS and Angular code required to handle step-up authentication challenge error response. Please read Add Step-up Authentication Using Angular and NestJS for a detailed guide through.
Prerequisites
- Node 18 or greater
- Okta CLI
- Your favorite IDE
- A web browser with good debugging capabilities
- Terminal window
- Git
Okta has Authentication and User Management APIs that reduce development time with instant-on, scalable user infrastructure. Okta's intuitive API and expert support make it easy for developers to authenticate, manage and secure users and roles in any application.
Getting Started
To run this example, run the following commands:
git clone https://github.com/oktadev/okta-angular-nestjs-stepup-auth-example.git stepup-auth
cd stepup-auth
npm ci
Create an OIDC Application
…Open the project up in your favorite IDE. Let's take a quick look at the project organization. The project has an Angular frontend and NestJS API backend housed in a Lerna monorepo. If you are curious about how to recreate the project, check out the repo's README file. I'll include all the npx
commands, CLI commands, and the manual steps used to create the project.
You need to set up an authentication configuration to serve the project. Let's do so now.
Set up the Identity Provider to use OAuth 2.0 and OpenID Connect
You'll use Okta to handle authentication and authorization in this project securely.
Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register
to sign up for a new account. If you already have an account, run okta login
. Then, run okta apps create
. Select the default app name, or change it as you see fit. Choose Single-Page App and press Enter.
Use http://localhost:4200/login/callback for the Redirect URI and set the Logout Redirect URI to http://localhost:4200.
NOTE: You can also use the Okta Admin Console to create your app. See Create an Angular App for more information.What does the Okta CLI do?
The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:4200
. You will see output like the following when it’s finished:
Okta application configuration:
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
Note the Issuer
and the Client ID
. You'll need those values for your authentication configuration, which is coming soon.
There's one manual change to make in the Okta Admin Console. Add the Refresh Token grant type to your Okta Application. Open a browser tab to sign in to your Okta developer account. Navigate to Applications > Applications and find the Okta Application you created. Select the name to edit the application. Find the General Settings section and press the Edit button to add a Grant type. Activate the Refresh Token checkbox and press Save.
I already added Okta Angular and Okta Auth JS libraries to connect our Angular application with Okta authentication. On the API side, I added the Okta JWT Verifier library that you'll use for access token verification later in the post.
In your IDE, open packages/frontend-angular/src/app/app.config.ts
and find the OktaAuthModule.forRoot()
configuration. Replace {yourOktaDomain}
and {yourClientId}
with the values from the Okta CLI.
Start the app by running:
npm start
The command starts both the frontend app and the API. Navigate to localhost:4200
in your browser. What a beautiful app!
Sign in and notice the "profile" route shows your profile information with a warm, friendly greeting using your name, and the "messages" route displays messages from an HTTP call the component makes to your API. Sign out of the application.
We now have a web app with basic authentication capabilities. Let's kick up authentication levels by adding step-up authentication!
Step-up authentication mechanics at a glance
Before we jump into Step Up Authentication, let's take a step back and cover how authentication works in an application. In most circumstances, you'd require authentication before allowing the user to navigate anywhere within the app. You can prevent them from entering a URL:
The Step Up Authentication Challenge idea builds upon the foundation of OAuth 2.0 and OpenID Connect (OIDC). Your app enforces authentication assurances from the user for different actions. Authentication and identity assurance determine the certainty that the user is who they say they are. The specification supports defining authentication strength and recency and responds with a standard error insufficient_user_authentication
. Let's say the authenticated user now wants to make a new transaction. Creating a new transaction is a sensitive action and requires elevated authentication assurances:
Your app needs to verify you have the correct authentication assurances for your requested action. Your frontend app's OIDC library evaluates your ID token for authenticated state and the value of a specific claim called Authentication Context Class Reference (ACR). The acr
claim value contains the client's authentication assurance level. If the ACR claim value doesn't meet the required assurance profile, the client requests the correct level using the acr_values
parameter when communicating with Okta. Let's take another look at the previous diagram, this time incorporating acr
and acr_values
properties:
ACR values can be a standard or use a custom registry. You'll see values such as phr
for phishing-resistant factors, which include FIDO2 + WebAuthn authenticators. You'll also see custom values such as one Okta supports for two-factor authentication, such as urn:okta:loa:2fa:any
. Read more about supported ACR values in Okta's Step Up Authentication documentation.
This is a high-level overview of OAuth 2.0, OpenID Connect, and the Step Up Authentication Challenge spec, and it only covers what we need to know for this tutorial. I'll add links to resources at the end of this post so you can dive deeper into this topic.
It's clearer to see authentication strengths rather than calculate the recency of authentication, so we'll walk through the steps required for authentication strengths in the tutorial. Right now, you only have password authentication. You'll need another authenticator for your app!
Set up authenticators
You'll change the Okta Admin Console to support a different authentication mechanism. We'll change the email authenticator so it's both a recovery and an authentication factor. Sign in to your Okta Developer Edition Account.
Navigate to Security > Authenticators. Find Email, press on the Actions menu, and select Edit. Select the Authentication and recovery radio button under the Used for section.
Your Okta application should allow you to authenticate with a password or email. Sign out of the Okta Admin Console so we can see the entire step-up authentication flow from end-to-end.
Note
You must use the email OTP verification number when authenticating using email. This post doesn't include the steps required to set up email magic link handling. Let me know in the comments below if you want to see a post about this.
Guard a route with step-up authentication
Back to coding! The Okta Angular SDK supports step-up authentication and has a built-in feature so that you can define the required acr_values
for routes right in your route definition. We'll require two-factor authentication for your profile. In your IDE, open packages/frontend-angular/src/app/app.routes.ts
and find the route for "profile." Add route data to route using the okta
object and a property named acrValues
. The property's value is urn:okta:loa:2fa:any
. Your "profile" route definition should look like this:
{
path: 'profile',
component: ProfileComponent,
canActivate: [OktaAuthGuard],
data: {
okta: { acrValues: 'urn:okta:loa:2fa:any' }
}
}
Test this route out. Start the application by running npm start
if it isn't still running. Open the application in the browser, and feel free to open network debugging capabilities so you can see the acr_values
request. Sign in using one factor, such as a password. Then, navigate to the "profile" route. You'll be redirected to Okta to authenticate using your email. Sign in with your email by entering a verification number. Success!
Sign out of the application.
The Okta Angular SDK helps us out, so we don't have to write custom code. Under the covers, the SDK has an Angular guard that:
- Gets the
acr
claim value from the ID token - If the value matches the
acrValues
route data definition, it returns true to allow navigation - Otherwise, it redirects the user to authenticate using the value in the
acrValues
, saves the current route, and returns false to prevent navigation
The code would look something like this:
const stepupGuard: CanActivateFn = async (route, state, oktaAuth = inject(OKTA_AUTH)) => {
const acrValues = route.data['okta']?.['acrValues'];
const acrClaim = return decodeToken(oktaAuth.getIdToken()).payload.acr;
if (acrClaim === acrValues) return true;
oktaAuth.setOriginalUri(state.url);
await oktaAuth.signInWithRedirect({acrValues})
return false;
};
This helps protect application routes, but what else can we do?
Protect API resources with step-up authentication challenge
You can also protect resources by adding step-up authentication handling to the resource server or the API serving resources to your app. You may have API endpoints that require elevated authentication assurances. Also, consider a user making a large financial transaction where some transaction threshold warrants extra scrutiny. Checking the transaction amount requires inspecting the payload before triggering step-up authentication. Both scenarios work with the step-up authentication challenge protocol.
You may wonder why checking the acr
claim in your API is necessary when you have already done so in the web app. Good web application security practices must enforce authentication, identity, and access control in the SPA client and APIs. Someone can bypass the SPA and make direct API calls – always guard all entries into your application system.
The flow works like this:
If the acr
claim value doesn't meet the requirements, the API responds with a standard error:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication",
error_description="A different authentication level is required",
acr_values="elevated"
We'll use this error structure in the API response in NestJS, and when handling the step-up authentication error in the Angular app.
Check the access token's ACR claim in a NestJS middleware
You'll make changes to the API first. I added Okta's jwt-verifier
library to do the heavy lifting on verifying the access token and decoding the payload to get the acr
claim.
The project contains an empty step-up authentication middleware. Open packages/api/src/stepup.middleware.ts
.
In this file, you will:
- Initialize the JWT Verifier instance
- Ensure the
Authorization
header contains the access token - Verify the token
- Get the
acr
claim value and compare it to the required value - Respond with the resource or return 401 HTTP response with the standard header
Let's start by initializing the Okta JWT Verifier instance and changing the use()
method to async
. Add the following code and replace {yourOktaDomain}
with the Okta domain you got from the Okta CLI in a previous step.
import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import OktaJwtVerifier from '@okta/jwt-verifier';
@Injectable()
export class StepupMiddleware implements NestMiddleware {
private jwtVerifier = new OktaJwtVerifier({issuer: 'https://{yourOktaDomain}.okta.com/oauth2/default'});
async use (req: any, res: any, next: () => void) {
next();
}
}
Next, you can use the jwtVerifier
instance to verify the access token... if there is one. 🙀
No need to be dramatic; we can check. Update the use()
method to get the access token from the Authorization
header. Verifying the token using the JWT Verifier library returns the decoded token, so let's save the output in a variable. Update your code to look like this:
async use (req: any, res: any, next: () => void) {
const authHeader = req.headers.authorization || '';
const match:string[]|undefined = authHeader.match(/Bearer (.+)/);
if (!match && match.length >=1 && match[1]) {
return res.status(HttpStatus.UNAUTHORIZED).send();
}
let accessToken:OktaJwtVerifier.Jwt;
try {
accessToken = await this.jwtVerifier.verifyAccessToken(match[1], 'api://default');
} catch (err) {
console.error(err)
return res.status(HttpStatus.UNAUTHORIZED).send(err.message);
}
// add the ACR check
next();
}
Now we can get to the step-up authentication specific code where you'll check the acr
claim and return the standard error. In this example, you'll hardcode the required ACR value in this middleware, but you can define different ACR values per route. After the comment to "add the ACR check" but before the next();
method call, add the following code:
const acr_values = 'urn:okta:loa:2fa:any';
const acr = accessToken.claims['acr'] ?? '';
if (acr === '' || acr !== acr_values) {
res.setHeader('WWW-Authenticate', `Bearer error="insufficient_user_authentication",error_description="A different authentication level is required",acr_values="${acr_values}"`)
return res.status(HttpStatus.UNAUTHORIZED).send();
}
With the middleware implemented, you need to register it in the module. Open packages/api/src/app.module.ts
. Edit the AppModule
to add the StepupMiddleware
to the "messages" route as shown:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(StepupMiddleware).forRoutes('messages');
}
}
💡 Idea 💡
We hardcoded the required
acr_values
in the middleware and applied the middleware to all calls to the "messages" route for this demonstration, but NestJS does provide better mechanisms for scaling to larger projects and defining granular assurance requirements. One option might be to create a NestJS guard to define which HTTP methods of an endpoint require step-up authentication. Furthermore, you can create a custom decorator for step-up authentication and pass in the requiredacr_values
for the HTTP method. That way, a generic guard scales across your API, and you can define a GET call that requires two-factor authentication and a POST call requires phishing-resistant authentication assurance, for example.Let me know in the comments below if you want to see a tutorial about this! 📝
The resource server now handles step-up authentication for the "messages" route. It returns the standard error when user authentication is insufficient, but the Angular frontend needs to respond to this error.
Use an Angular interceptor to catch step-up HTTP error responses
In Angular, we'll first identify the step-up authentication error response, then manipulate the HTTP error response by extracting the information needed for further handling. It starts in an interceptor.
Open packages/frontend-angular/src/app/stepup.interceptor.ts
, an unmodified interceptor file scaffolded by Angular CLI.
import { HttpInterceptorFn } from '@angular/common/http';
export const stepupInterceptor: HttpInterceptorFn = (req, next) => {
return next(req);
};
Intercepting and handling HTTP error responses means we'll catch the error and provide an error handler function. Change your code as shown:
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
const handleError = (httpError: HttpErrorResponse) => {
return throwError(() => httpError);
};
export const stepupInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(catchError(handleError));
};
The error handler needs first to verify this is an error we want to handle. Is this HTTP error a step-up error from our resource server? We can check for this! In the handleError
method, add the following code and change the return value:
const handleError = (httpError: HttpErrorResponse) => {
const allowedOrigins = ['/api'];
if (httpError.status !== HttpStatusCode.Unauthorized || !allowedOrigins.find(origin => httpError.url?.includes(origin))) {
return throwError(() => httpError);
}
let returnError: HttpErrorResponse| {error: string, acr_values: string} = httpError;
const authResponse = httpError.headers.get('WWW-Authenticate') ?? '';
if (!authResponse) {
return throwError(() => returnError);
}
// add code to extract error details and format new error type
return throwError(() => returnError)
};
Now we're at the stage where we know this is most likely an error we should handle, and we'll know for sure by extracting the insuffcient_user_authentication
string from the WWW-Authenticate
header. While parsing the header string, we'll look for the acr_values
. Add the code to support this by replacing the // add code to extract error details and format new error type
with:
const {error, acr_values} = Object.fromEntries((authResponse.replace('Bearer ', '').split(',') ?? []).map(el => el.replaceAll('"', '').split('=')));
if (error === 'insufficient_user_authentication') {
returnError = {error, acr_values};
}
Lastly, add the interceptor to the ApplicationConfig
. Open packages/frontend-angular/src/app/app.config.ts
. Add the stepupInterceptor
to the providers array.
provideHttpClient(withInterceptors([
authInterceptor,
stepupInterceptor
])
You can't redirect the user to authentication from an interceptor. You need to handle this new error format elsewhere, where you initiate redirecting the user to authentication with the required acr_values
before re-requesting the resource.
Handle step-up errors when making HTTP calls in Angular
You can handle catching the error, redirecting the user to authenticate, and re-request the resource within the call to the API, usually within an Angular service. I cheated a bit and wrote the API call directly in the component in this demonstration. Open packages/frontend-angular/src/app/messages/messages.component.ts
.
You need the Angular Router
and OKTA_AUTH
instances so you can save the existing URL then redirect the user to authenticate. Create properties and inject both types within the class.
private oktaAuth = inject(OKTA_AUTH);
private router = inject(Router);
Change messages$
to add a catchError
operator. Within the catchError
function, you'll ensure the error type is insufficient_user_authentication
, set the redirect URI to navigate the user back to this component which will re-request the resource, and finally redirect the user to authenticate using the acr_values
:
public messages$ = this.http.get<Message[]>('/api/messages').pipe(
map(res => res || []),
catchError(error => {
if (error['error'] === 'insufficient_user_authentication') {
const acrValues = error['acr_values'];
const redirectTo = this.router.routerState.snapshot.url;
this.oktaAuth.setOriginalUri(redirectTo);
this.oktaAuth.signInWithRedirect({acrValues});
}
return throwError(() => error);
})
);
This code sure looks similar to the operations in the step-up auth Angular guard!
Try out your work. Ensure you sign out of the application first. Sign in with one factor, then navigate to the "messages" route. You'll redirect to sign in with a second factor. Success!
Ensure authentication recency in step-up authentication
We covered the authentication levels in this post because they are visually visible within the Okta-hosted sign-in page. The step-up authentication challenge protocol also supports authentication recency. The good news is the code and process we covered in the post still apply, but you'll use different claims and properties. In the redirect request, you'll define the required business rules for authentication recency using the max_age
property instead of acr_values
. Calculate authentication recency using the auth_time
claim when evaluating whether a token meets the max_age
requirement. Cool stuff indeed!
Step-up authentication in Angular and NestJS applications
In this post, you walked through adding a step-up authentication challenge to protect resources. You added step-up authentication to protect Angular routes using the Okta Angular SDK, then added step-up authentication to protect resources from the NestJS API and within the Angular app.
There's a lot more we can do, and in a demo, we can't get into all the production-level polish we'd like, but I hope this sparks your imagination about how you can protect resources within your application. You can check out the completed application in the completed
branch of the okta-angular-nestjs-stepup-auth-example GitHub repository. The repository's README also includes instructions on scaffolding the starting project.
Ready to read other interesting posts? Check out the following links.
- Step-up Authentication in Modern Applications
- Step-Up Authentication Examples With Okta
- Flexible Authentication Configurations in Angular Applications Using Okta
- How to Build Micro Frontends Using Module Federation in Angular
Don't forget to follow us on Twitter and subscribe to our YouTube channel for more exciting content. We also want to hear about the tutorials you want to see. Leave us a comment below!
Top comments (0)