What do you want to learn?
Skip to main content
Securing Angular Apps with OpenID and OAuth2
by Brian Noyes
OpenID Connect and OAuth 2 allow your apps to use modern security protocols and to participate in a Single Sign-On (SSO) experience across multiple apps. This course will show you how to authenticate users and authorize access in your Angular apps.
Resume CourseBookmarkAdd to Channel
Table of contents
Hi, this is Brian Noyes, and welcome to my course, Securing Angular Apps with OpenID Connect and OAuth2. I'm a software architect with over 20 years of experience and have been working with Angular and other single-page application frameworks for over 5 years. I've seen many approaches to security, some good and some really bad. It's never been more important to make sure the apps you build and the data they manage are securer than it is today. There's also a growing expectation from users that apps will participate in a single sign-on experience for the user so they don't have to remember and use separate accounts for every app they work with. In this course I'll show you how you can use OpenID Connect and OAuth2 protocols to secure your Angular apps with the most modern, interoperable, standardized, and secure protocols, and ones that allow your app to be part of a single sign-on ecosystem of apps for your users. You'll see how to handle the authentication process, getting your users logged in, and authorization, or access control, to make sure users can only see and do what they're supposed to within your app and the back-end APIs that your app talks to. I'm really looking forward to showing you how easy it can be to do the right thing with security in your Angular apps.
Angular App Security Big Picture
Hi, this is Brian Noyes, and welcome to my Pluralsight course, Securing Angular Apps with OpenID Connect and OAuth2. OpenID Connect and OAuth2 are the most modern and standardized approaches for authenticating users and authorizing their access to your apps and data. These protocols let users of your app participate in a single sign-on experience across one or more applications in the federated ecosystem of applications. Implementing your security with these protocols helps guide you to follow best practices on security and available libraries and platform support for the protocols limit how much you need to know about the inner workings of the protocols to get them right. So let's talk about what you're going to learn in this course. In this course you'll learn how to handle everything on the client side in your Angular app related to authentication and authorization. You'll first learn the high-level security concepts you need to know about security in general, and the OpenID Connect and OAuth2 protocols. Then you'll learn how to get your users logged in with your identity provider and obtain the identity and access tokens that you'll need for the rest of your security flows. Next you'll learn how to pass the tokens to your back-end APIs to authenticate and authorize your calls to those APIs. After that, I'll wrap up by showing how to keep your login session alive by using a silent renew pattern for your access tokens. Then I'll cover how to use the security context of the user and their login with the identity provider to manage the user experience and do things like control what pages in app they can see and hiding and showing or disabling parts of the UI based on their permissions. This course is designed for a developer who has already been exposed to the fundamentals of building applications with Angular. Some exposure to the Angular CLI, Angular data binding, routing and dependency injection will be required to fully follow along with the demos in this course. Additionally, since I'll be showing some back-end code for APIs and for IdentityServer4 as an SDS in this course, some exposure to ASP.NET Core would be good as well. You can find a number of courses to get you up to speed on the fundamentals of Angular and ASP.NET Core in the Pluralsight library. In this first module I'm going to make sure you're familiar with the overall security landscape for Angular apps. I'll cover the high-level concepts you really need to know when focusing on the security of your application. You'll learn about the OpenID Connect and OAuth2 protocols at a high level so you can understand what's going on behind the scenes without knowing all the gory details or needing to implement those details yourself. You'll also learn about some of the identity providers that exist that you can use with these protocols and what client libraries are available to help you implement them in your apps.
Security Design Considerations
There are a number of security considerations that you should make sure are covered in your application design and architecture. The first two are authentication and authorization, which will be the primary focus of this course. Other things you should make sure are part of your design include transport protection, cross-origin resource sharing, or CORS, cross-site request forgery, or CSRF, and cross-site scripting, typically abbreviated XSS so it doesn't get confused with CSS styling. I'll be going into a lot more depth on authentication and authorization throughout the course, but let's talk briefly about the latter four. Transport protection refers to how you protect the requests and responses that are made between the browser and back-end resources that your application talks to. This also applies to any back-end to back-end communications between services. This one is pretty simple, just use HTTPS everywhere. The underlying HTTPS protocol is often referred to as SSL, for Secure Sockets Layer, which is actually a somewhat outdated term. The protocols used in modern day HTTPS are more correctly referred to as TLS, for Transport Layer Security, a newer version of the SSL security protocol. Despite that, the term SSL is still commonly used when describing the certificates that you use to implement TLS. The bottom line is that you should be using HTTPS for any calls in your application that could contain sensitive information, and the only way to really protect yourself there is to just use it for everything because you don't know how the data flows of your application are going to evolve over time. So most apps simply protect the entire site and all API calls with HTTPS, and that's generally the right thing to do. You don't have to do anything explicit in your client-side Angular code for this, other than making sure the URLs you are using to call your backend APIs are prefixed with HTTPS instead of just HTTP. The next security consideration is cross-origin resource sharing, or CORS for short. Since the early days of the web, browsers have restricted client-side code from being able to make HTTP requests to any site other than the site that the client code came from. There are specific security threats this mechanism was designed for, but as applications evolve from just being static content rendered by the server side to more and more sophisticated interactive applications on the client side, this became a limitation that had to be worked around. Modern browsers have standardized on the CORS protocol for how to explicitly manage what sites your code can call out to. I'll cover CORS briefly in the module on authorization. Again, you don't have to do anything explicit in your Angular code because the Angular HTTP client service will take care of sending and receiving the right headers for you based on the CORS protocol. The next security consideration is cross-site request forgery, or CSRF for short. CSRF is a vulnerability that exists when using cookies to maintain a secure session with a back-end web server. If you've ever used the mechanism typically referred to as forms authentication then you have used cookies to maintain secure sessions. The vulnerability exists because of the fact that the browser will automatically send any cookies associated with a given site anytime a request is made to that site, even if the request originates from a separate browser tab or window than the one that originally connected and got the cookies back in a response. So let's say you log in to your bank in one browser tab and it returns a cookie of your authorized session. Then you go to another site in a different tab. If that site's page code sends a request to your bank site the cookie that authorizes your calls from the original tab to the bank will be sent along with the request from the other tab, potentially authorizing that site's code to do privileged operations that were never intended to be exposed to the other site's code, such as transferring money from your account. The good news is that using token-based protocols, including OpenID Connect and OAuth2, makes it so you don't have to worry about CSRF because these protocols require your client-side code to explicitly put the tokens into authorization headers of any HTTP request to your APIs. So there is nothing automatic that is going to cause those tokens to be sent. Code running in other tabs in the browser will never have access to those tokens, nor be able to send them in a request to your APIs. The last consideration is cross-site scripting, or XSS for short. The cross-site scripting vulnerability occurs when you take direct input from a user, say a string in a text box, and then inject that directly into the DOM. If that input contains certain expressions, such as a script block, it can be executed as soon as it's inserted into the DOM, or it can set up event handlers that execute later. This could allow a user to execute scripts that cause your app to do things they were never intended to do. The good news is, Angular has your back here. Angular treats values that are put into the DOM through data binding as untrusted and will sanitize or escape parts of the values that contain something like a script block that could be executed. You still have to make sure that the end user input doesn't ever get turned into executable code directly, but that would be a dangerous thing to do in most cases anyway. So for the most part, you don't have to do anything explicit to be protected from cross-site scripting in your Angular apps. For more in-depth coverage of these security considerations I suggest you check out the Pluralsight course, AngularJS Security Fundamentals by Troy Hunt. Even though it uses AngularJS, meaning version 1.x, the coverage of these vulnerabilities there is still completely relevant to Angular 2 and later applications.
Client vs. Server Security
One very important concept to make sure you always remember, you can't really secure any code that runs in the browser. Anyone with any knowledge of web development can simply open the developer tools in the browser, access the code, and make whatever changes they want. For production code you should be minifying or uglifying your code, which does make a hackers job harder, but that's done more for shrinking the size of the code files the browser needs to download than it is to make your code hard to hack, but that is a good side effect of doing so. So if you think you will secure your app by doing things like filtering what data the user sees on the client side, or restricting what pages they can get to you through Angular routing, or just allowing certain actions based on who the user is, you need to realize that it won't. You should still use features like Angular route guards to tailor the user experience and prevent the user from getting to pages they should not be able to use. You can also use data binding to security context data to hide or disable actions that the user should not use. But just realize that's not really securing anything, you're just setting up a better user experience. All of your true security protections need to happen on the server side. There you can filter data so it never gets to the client side if the user should not have access to it. You can also refuse requests to API endpoints that the current user is not authorized for. So even if, let's say, a hacker goes and enables a button to delete a customer's order history that they're not supposed to be able to do, when the request gets to the server side it should fail.
Angular App Security Architecture
Another thing to understand about OpenID Connect and OAuth2 is how it affects the architecture of your Angular app and the back-end for that app. Most web applications are designed to handle both authentication and authorization internally within the host application site, so your host site would render out the root page for your Angular app and the user would have to log in either prior to getting to that page or from inside the Angular app itself. And then once that user is logged in you could secure the calls that come back down to the APIs of your app based on either cookies or some other form of security token that would work only with that site. The host for your Angular app and the APIs could be in separate sites, and if so you typically would not need the user to log in just to render the Angular app because the structure of your pages is rarely what you want to protect, but then as soon as the Angular app started accessing any sensitive data you would want to have the user log in and then start calling the authenticated and access control-protected APIs. Once you start using OpenID Connect you will typically have a separate site for your identity provider. It's possible to integrate it into a single site along with your pages and APIs, but you should generally avoid doing so. Part of the benefit of using OpenID Connect and OAuth2 is that you can separate the concerns of authentication and high-level access control into the identity provider, then your app doesn't have to worry about collecting credentials from the user invalidating them it just needs an access token from the identity provider that tells it this is an authorized user for your app. And separating out these responsibilities like this opens the door for enabling single sign-on across a collection of applications that all use the same identity provider.
Authentication and Authorization
Now let me briefly cover the difference between authentication and authorization, just to make sure you're clear on the difference. Authentication is focused entirely on determining the identity of the user or client application. Keep in mind these security protocols don't just apply to end users logging into your client applications, they also apply to calls from one back-end system or service to another. The first part of authentication is requiring the user or client to present their credentials. This can be triggered by redirecting the user to a login screen or it could be triggered by a 401 status code returned from an API call indicating that authentication is required. Then the user or client application needs to present an acceptable form of credentials. For an end user this can be a username and password, a biometric device scan, or presentation of a client certificate with something like a smart card. For a client application that is not being controlled by an end user this is typically going to be a shared secret. In simplest form, this is just an arbitrary, and hopefully unique enough string of characters, that it's difficult to guess. Usually the shared secrets you'll use will be based on a cryptographic hashing algorithm that makes it both unique enough and hard to guess. The identity provider will need to be able to verify the credentials and then it will issue a temporary, meaning time-limited, credential that can be used to authenticate and be allowed access to your application or APIs. Anyone who's been in the military or larger business buildings has probably experienced something very similar outside the realm of computers. You want to gain access to some facility, but first you must go to a guard station at the periphery of the facility. You present a set of credentials, such as your driver's license, passport, or company ID, then the guard issues you're a badge that you must wear while in the facility, and maybe use that badge to gain access to specific buildings, rooms, or floors within the facility through an electronic badge scanner. This is essentially what you're doing during OpenID Connect authentication. The guard station is the identity provider, and the initial set of credentials is typically your username and password. Then the issued ID and access tokens from the identity provider are the temporary credential for a limited-time session of access to the requested resource. Authorization has to follow authentication because it relies on the knowing the identity of the user or client application that's trying to gain access to a resource. Authorization might include looking up the roles the user is associated with and using the membership in those roles to authorize access to operations or data. It could also be based on more fine-grained permissions that are application-specific, such as which user can edit which entity. In this case the identity provider should not be the one managing those permissions because they are application-specific, but we'll get into that more in the authorization module. Authorization could also be based on a security policy that takes into consideration both roles and fine-grained permissions. Ultimately the authorization code in the resources being accessed needs to validate user actions or data access against what they should be allowed to do.
There's a fair bit of terminology surrounding the OpenID Connect and OAuth2 protocols you should be aware of. First, there is the identity provider. As shown in the architecture diagrams earlier in the module, this is almost always a separate site from the application a user is interacting with. There are a bunch of other names sometimes used for an identity provider. These include authentication server, authorization server, single sign-on server, and STS for security token service. Using the term identity provider or STS are the most accurate terms because authentication, authorization, and single sign-on are all aspects of what the identity provider does, but not the whole picture. Next, there is the user agent. I think you know what a user is, but a user agent is basically the piece of software the user is directly interacting with. This might be your client application in the browser, or even the operating system, if using built-in mechanisms for native apps to collect credentials outside of your app. Next, there is the client. Again, this represents a piece of software, not the user, and in a lot of cases it is the application that the user is interacting with. But just remember it could also be a back-end API or batch process running with no interactive user. Ultimately it is the software that is trying to access a resource outside of its own scope of control, so the client needs to authenticate and be authorized by that resource's identity provider. Next there is the resource. This is the thing that a user or client is trying to access. This could be the website itself, or in the context of an Angular app, the resources are typically the APIs that your app calls out to through HTTP. But outside the context of Angular it could be a website, database, or a file store that a user or client app needs to access. Scope is another term you'll want to be familiar with. Scopes are a part of the configuration of an identity provider and represent the individual resources that that identity provider protects. When a client connects to the identity provider to authenticate it tells the identity provider what scopes it is requesting, then the identity provider checks the configuration for that client to see if it's intended to access the associated resource. And an end user will experience scopes when using external identity providers like Google or Facebook in the form of consent screens that confirm what the user that they want to allow the application access to the resources requested. The last term you'll hear me use a number of times in the course is JWT. This is how people in security disciplines refer to JSON Web Tokens. JSON Web Tokens are the format used to encode information about the authenticated user in an identity token and claims about what they access in the form of scopes in access tokens. These are a standard format used for both OpenID Connect and OAuth2 protocols.
OpenID Connect And OAuth 2 Protocols
So now let me focus on the high-level details of the two protocols this course centers on. First, let's talk about OAuth2 since it came out before OpenID Connect. The first version of OAuth started around 2006 and focused on delegating access control responsibility for APIs, specifically the Twitter APIs. OAuth1 became an approved standard in 2010, but the spec for OAuth2 was already underway. OAuth2 broadened the focus for the protocol to include websites, mobile apps, desktop apps, and APIs. OAuth2 became an approved standard in 2012. The only problem with OAuth is that it's actually a protocol for authorization only, there was no standard for how you authenticate someone first so that you could then make the decisions about what to allow them to do. So it was up to the individual identity providers to do their own thing. So that led to needing a standard for how the authentication process happens, which became OpenID Connect. OpenID Connect is a derivative standard from OAuth2. OAuth2 had already standardized on a token format, JWT, that OpenID Connect could use as well. OpenID Connect presumes you will be using OAuth for the authorization piece, so part of the authentication process is to obtain, not only an identity token representing the identity of the end user or client application, but also an access token that specifies what the holder of that token can do with respect to the resources it's requesting access to. OpenID Connect was approved as a standard in 2014 and has been gaining wide-spread adoption ever since. This protocol includes specifying the flow for how a user gets redirected to a login page at the identity provider or how credentials are sent directly in the case of non-end user clients, as well as how control gets returned to the application and the tokens that are issued. You'll learn more about OpenID Connect in the next module and OAuth2 in the module that follows that, including the important part, how to use them in your Angular apps.
Identity Provider Options
But before we get there, let me quickly cover the options you have for identity providers and client libraries to help you implement all this stuff. There are a lot of choices for identity providers out there, and what you choose will depend largely on who your target audience is. The first set of choices that most developers will already be familiar with are Google, Facebook, Twitter, and other social media providers. If your app will be a consumer-facing app, and you want them to be able to sign in with identities they already have with those social media platforms then you can use one or several of those as your identity provider. You've probably seen this before with web or mobile apps you use. When you first go to register you might have the option to select one of these identity providers to sign in with; however, most apps don't want to force you to go establish an identity with one of those identity providers if you don't already have one. Also, many users don't want to use one of their social identities for application access for fear that the application may try to access more of their social platform profile or capabilities than they want to, for example, posting to their Facebook or Twitter feed without them realizing it. I'll talk more about that in the module on authorization, but the bottom line is that it is usually insufficient to just rely on one of those providers. You'll usually want to support having user accounts that you can manage through an identity provider that you set up and control, then you can support single sign-on from other identity providers using the OpenID Connect and OAuth2 protocols. So if you're setting up your own identity provider there are a number of Security as a Service providers that give you cloud or on-premises options for setting up and managing that provider. The first I'll mention is the hardest one to understand because it has several different variants, each with different sets of capabilities. The first of these variants is just named Azure Active Directory with no stated suffix, which usually means the first version of that service. Azure Active Directory is called AAD for short. The version 1 service does not support OpenID Connect and was more focused on establishing a cloud-based domain for your users and optionally federating that domain with on-premises Active Directory domains or other AAD domains. Azure Active Directory v2, which is a newer versions of the AAD APIs does support OpenID Connect, but only for Microsoft organizational accounts, which are also called work or school accounts, and Microsoft accounts, ones that you have registered with an email address that can be used for things like Office 365, Xbox, Microsoft developer networks, or other cloud-based Microsoft services. And finally, the third variant is Azure Active Directory Business to Consumer, or B2C for short, which allows you to use any Microsoft account, as well as external social identity providers, and even users that you just register directly with your AAD B2C accounts, using their email. All of these variants are pay-as-you-go cloud services. Then there are a number of Identity as a Service providers. These include Auth0, Okta, Ping Identity, and many others. One of the value propositions of these is that they provide a fully-managed service that is compliant with the standard protocols, so there's no setup, hosting, patching, or management of those services at a low level. All you have to do is set up an account or tenant with them, do some configuration through their user interface, or expose management APIs, and you're ready to go. But of course these all have a recurring subscription cost that are typically based on the number of users you have and/or how many security transactions, such as login and logout your application has. The last one I want to mention, and will be using for the samples in this course, is IdentityServer4. IdentityServer is an open-source framework for building your own protocol-compliant identity provider. It is open source, so free to adopt, but being a framework and not a pay-as-you-go service like the other options, you have to do some development to get it set up and ready to go. And you will have to host your identity provider somewhere yourself. This could be in the cloud through something like Azure Web Apps, or a cloud virtual machine, or you could host it on-premises in your own web servers. IdentityServer has the most flexibility in terms of integrating with other identity providers and supports integrating with external providers for single sign-on including Active Directory federation services for on-premises or cloud hybrid deployments, Azure Active Directory, or any other OpenID Connect and OAuth2 protocol-compliant identity provider. IdentityServer4 is certified by the OpenID foundation as being protocol-compliant. You can see what other providers are compliant on the page shown below.
Client Library Options
A Tour Through the Demo Application
Before we wrap up this module let me give you a quick tour of the application we'll be working with for the rest of this course. What I'm showing is the completed version of the app with all authentication and authorization in place. First, as you can see, the scenario is a simple project management app. Note that the site is running on port 4200 on localhost. You can see that the first thing you'll want to do is log in to the app. The demo app includes several built-in accounts I'll cover later. In a lot of cases where you might be using OpenID Connect and OAuth you may not allow self-registration. A lot of times new users have to be provisioned by an admin creating an account for the user and sending them an invite that triggers the provisioning of the user. How these things are done is going to vary a lot from one app to another and from one identity provider to another. User registration and multifactor authentication are not part of the OpenID Connect specification, so how you set that up and how it flows into the authentication process happens outside the scope of the client app and will vary from provider to provider. If I start by clicking on Login we get started into the OpenID Connect implicit flow, which I'll talk about more in the next module. You can see we're redirected to the identity provider and asked to log in there. I'll log in with the user email@example.com. The password for all sample accounts is Test, with a capital T, 123, and then three exclamation marks. Once I enter my credentials and log in we get redirected back to our application. Now you can see the menu bar has updated with actions to go to our projects or log out. If I log out with that account I actually get quickly redirected back to the identity provider because the validity and expiration of tokens is managed by the identity provider, not our app. So we need to go to the identity provider and log out there and then get redirected back to our app after logging out. But it happens so fast the user will never even realize that's what's happening. Now if I log back in as the admin we get redirected back to the app, and you can see we have an additional menu for managing projects. I can go into that Manage Projects link, and in there I can add a new project. Once I've added the project I can click this button to go to a page that lets me manage permissions for that project. You can see in here I can add a user to the project, selecting Alice in this case, and say whether they have View or Edit permissions on that project. I'll give her Edit permissions on this one. I can also go to existing projects and change the permissions for existing users, remove a user, or add a new one to the project. Now, if I log out as admin, back in as Alice, go to My Projects, we can see that now she has those projects in her list of projects. If I click through to the detail page for that project, basically all a project has in this simple app is a list of milestones. Because Alice has Edit permission she can add a new milestone using the button at the bottom, and then once it's added she can also go edit the milestone to change its status or delete the milestone. If I back out to the project listing and go into the other project that Alice has access to, but only with View permission, you can see that she can see the milestones, but cannot edit or delete milestones, nor add new ones. So that is the basic functionality of the app. As we get into the next few modules I'll go into more detail about the stuff going on behind the scenes, some of the options you have, and how the access control restrictions are enforced, both server-side for true security and client-side for tailoring the user experience. Starting with the code in the next module I'll actually be starting with the same functionality, but will start with none of the security wired up, then I'll walk you through implementing each piece of the login process, access control, and authorization through the rest of the modules of this course. So join me in the next module to dive deeper into how to make the authentication process work with an OpenID Connect provider.
Authenticating with OpenID Connect
Hi, this is Brian Noyes. In this module we're going to start drilling down into authentication with OpenID Connect. I'll cover a few more details of the protocols than I did in the first module and then we'll dive into starting to hook things up in the sample app. So first up I will review some of the reasons for adopting OpenID Connect for your authentication protocol. Next, I'll review the different authentication flows that are defined as part of the OpenID Connect protocol and we'll talk about which kinds of applications should use which. Then we'll dive into coding for the rest of the module, starting with the sample app you saw in the first module, but stripped down to just the raw functionality with no security hooked up yet. In this module we'll add the code to log on with an OpenID Connect identity provider and then later modules will expand on that with authorization code, token management, and managing the user experience based on the security context of the user. For brevity throughout the rest of the course I will often refer to the OpenID Connect Identity Provider as simply the IdP, for short, or STS for short, both terms are essentially equivalent. Likewise, I'll use OIDC as an abbreviation and quicker way of saying OpenID Connect.
Why Use OpenID Connect?
As discussed in the first module, adopting OpenID Connect implies a little different architecture than you may be used to if you've only worked on building single websites that handle their own authentication. OpenID Connect is based on the idea that you will have an identity provider, also called IdP or STS for short, that is responsible for authenticating your application users and issuing them ID and access tokens for authenticating and authorizing access to your back-end APIs. Even though this implies a slightly more complicated architecture and setup for your application, doing so has a number of important benefits as well. First off, you end up with a more decoupled system, since all the authentication, logging in, collecting and validating credentials, and issuing access control tokens happens outside of your application code that lets you focus more on the business functionality of your app instead of needing to weave in a bunch of plumbing code for security. As mentioned in the last module, most apps will still have some degree of access control or permissions that are specific to the app and therefore should be handled by the application code itself, not the identity provider. But the lines of separation there end up much more clean than when the app has to handle everything itself. Another big benefit, and one of the primary motivators for a lot of companies, is that you enable single sign-on scenarios where one to many apps can share the same identity provider, and that means as a user moves from app to app, not only don't they have to keep track of a bunch of separate sets of credentials, they only need to log in once and their authentication session can flow from app to app as they move between them. Finally, because you end up having most of the authentication-related functionality in the STS, that means you will also have centralized credential management, so your STS becomes your one-stop shop for provisioning users and managing their accounts in high-level access control.
JWT Tokens and OpenID Connect Flows
Now let me quickly cover a few more details of the OpenID Connect protocol that you should be aware of. First off, there are some aspects of the protocol that are the same regardless of what kind of app you're building. This includes the use of JSON Web Tokens, or JWT, format for the tokens. JWT tokens encode a collection of claims, which are just key value pairs, pieces of information about the user, the client app, the identity provider, the protected resource, and the protocol itself. JWT tokens are digitally signed using cryptography so that they cannot be tampered with after they have been created by the identity provider. But they are not encrypted. Anyone who can get their hands on the token can view its encoded contents. The two tokens that will be issued as part of the OpenID Connect authentication are an ID token, which contains information about the authenticated, secure session for a user or client app, and an access token, which is based on the OAuth2 protocol. Part of the process of generating tokens and validating those tokens at the relying party requires cryptographic keys to digitally sign the tokens and validate that signature. So storing these keys appropriately is one of the considerations in choosing which protocol flow to use for your app. OIDC defines three different flows, which are basically sequences of operations and messaging. For any given app you will only be using a single flow, but realize that each app that is connected to a given OpenID Connect provider can be using a different flow from the other apps. The first flow of the OpenID Connect protocol is called authorization code flow. This flow is designed for server-rendered web apps, or native apps, that have a way to store a shared secret securely. These secrets are crypto keys that are used in the various flows of OpenID Connect. Another flow in the OIDC protocol is hybrid flow. This flow starts the same as the authorization code flow, but also involves obtaining a token for the end user on the back-end for calling out to downstream services or APIs on behalf of the user. Neither authorization code flow or hybrid flow are applicable to Angular apps because of the requirement to store a shared secret to support the flows. As you should already know, Angular code runs in the browser, and anyone with some development or hacking experience knows that you can open up the browser development tools and expect and modify the code however you like. So there's not really any way to store shared secrets within your Angular app code that would truly be secure. The flow we will focus on this course is called implicit flow. Implicit flow is designed for applications who do not have secure storage in the client app for storing secrets. So implicit flow is designed with this in mind and relies only on end-user authentication and the tokens issued are returned directly to the browser client app for use when calling out to APIs from the browser.
A Word About the oidc-client Library
Getting Started with the App Code
So now that you know the big-picture concepts of OpenID Connect, now let's get started putting it to use in our sample application. As a starting point for the demos in the rest of the course, I'll be using the same sample application I showed at the end of last module, but without any of the security-related code hooked up yet. Over the rest of the course I'll add that code in incrementally explaining each bit as we go and showing some of the options you have to do things a little differently depending on your requirements along the way. If I fire up the app at this point we land on the home page as before, but I can go into the My Projects view and see all projects, regardless of who is using the app, because at this point, the app can't know who is using it without authentication and the back-end cannot filter the list of projects it returns because it has no idea who the caller is either. If I select one of the projects I can go into its detail view, which shows a list of milestones. Again, we will constrain whether the user can edit or view the project later in the course, but at this point, anyone can add, edit, or delete milestones. If I go into the Manage Projects page, this is the one we will want to constrain to only admins where they can create or delete projects, and manage which users have access to which projects, and what permissions they have in terms of being able to edit or view the projects. But again, at this point, anyone using the app can go exercise this admin functionality. So that's the baseline functionality that you saw before, now let's talk about getting it up and running on your machine.
Getting the Client App Running
The first step before starting to modify the code is to make sure you can run the client application on your local machine. I've created the Angular app for this course using the Angular CLI, so it has everything you need to run and debug your code. You just have to make sure you have Node.js installed on your machine. If you do not, go to Node.js.org and download and install the LTS version of Node for your machine. You will also need the Angular CLI installed on your machine. To do that, open a Command Prompt, or PowerShell window on a Windows machine, or Terminal on a Mac, and run npm install -g @angular/cli. Once that's installed you'll need to restore the packages for the client app. So at the Command line in the root folder for the client project run npm install with no arguments. This will install all the packages referenced in the package.json file along with all of their dependencies into the node_modules folder. If you see some warning, don't worry, that's normal. Once that has run to completion you can start debugging for the app by running ng, space, serve. This will build the client app, start up the web pack development server, and serve up the code on port 4200 by default. It will also set up a file system watcher to rebuild the app any time it sees one of the source code files change. Once it's finished building you can open a browser and navigate to localhost 4200 to see the app running. The client code is configured to use cloud-hosted instances of the back-end API and the STS so that you don't need to set up and run the back-end to work with the sample code if you don't want to. But if you do want to get everything running locally let me cover getting the back-end code running locally as well.
Getting the Back End Code Running
The code provided with the course includes both the client-side code that we will be mostly focusing on, but also includes the back-end APIs for the app, which are implemented with ASP.NET Core. It also includes an IdentityServer4 project for running the STS locally. I've deployed a version of this code to the cloud and the client code is set to point to those cloud services so you don't need to set up any of this back-end stuff on your machine if you don't want. But in case you do want to follow along and run everything locally, let me show what you need to do to set up the back-end API and STS. If you are completely new to Visual Studio, ASP.NET, and/or SQL Server, just stick to the code running in the cloud, but let me run through the steps quickly for those who are familiar with .NET development. To do this you'll need a version of Visual Studio on your machine, that's different from Visual Studio Code. You can use the Community, Pro, or Enterprise versions. The first step is to get the databases in place. For that I've included a SQL script named InitData.sql that is in the root folder for the demo code. The connection strings in the back-end code are configured to use the LocalDB instance of SQL Server that gets installed with Visual Studio. You'll also need SQL Server Management Studio on your machine or know how to run SQL scripts from a Command Prompt. Again, if this is all sounding foreign to you just stick to running with the cloud-hosted versions. This course is not meant to teach you how to do the back-end stuff and there are plenty of other courses in the Pluralsight library for getting up to speed on ASP.NET Core development and working with SQL Server. So you can open the InitialData.sql script in SQL Management Studio and connect to the localdb\MSSQLLocalDB instance with Windows Authentication. Then execute that script and it will create the two databases needed, one for the application API the other for the IdentityServer STS and it will populate them with some sample data. Once you've run that script you should open the SecuringAngularApps.API solution in Visual Studio. To minimize configuration issues I recommend running the projects as console apps, which I'll show how to do here. First, select the API project in Solution Explorer, right-click on it, and select Set as StartUp Project. Up in the Debug toolbar drop down the arrow next to IIS Express and select the API project name. This will configure that project to run as a console app on your machine. Then repeat those steps to set the STS project to run as a console app as well. Once that is done, let's get things set to run both projects when you start debugging. To do that, right-click on the root Solution node and select Set StartUp Projects. In the dialog that pops up select Multiple startup projects and then select Start next to each of the projects. Click OK to complete that and then you can click on the Debug button to get both up and running and attached to the debugger. Now you should be ready to run against the locally running API and STS. So the last step would be to drop into the constants file in the client application, comment out the lines that configure it to connect to the cloud versions of the services, and uncomment the lines that replace those with localhost configuration.
Adding oidc-client and an Authorization Service
Adding Login to the App
Now that we have a UserManager configured to work with we can set up our first log in. UserManager makes this easy for us. We don't have to know exactly what the requests to the STS need to be, we just ask UserManager to do the right thing for us. That involves getting redirected to the STS to log in and then getting redirected back to our app with the tokens. So all we need to do cause the first redirect is to ask UserManager to do that for us. So I'll add a login method here in the auth.service that can be called from a view and call userManager.signinRedirect. That method returns a promise, but since we were going to be redirected to a different site for the STS we will completely unload the context of our Angular app in the process. So there's not a whole lot you might do between when you ask for the redirect and when your Angular code is no longer executing. To finish the initial definition of the auth.service we need to go add it to the providers of the modules. By the way, I'm using an extension to VS Code here called TypeScript Hero that automatically adds the import statements to the top of the file for you the first time you try to use a type from the app or its dependencies. So see here it added the import statement when I tabbed to accept the AuthService type name from the IntelliSense list. To get things going quickly for now let me go to the app component that sets up the menu for the app and add a login link. To get that hooked up I'll add a login method to the component class and in there we need to call the login method on the AuthService. So first we need to inject that into the constructor to make it available and then just call the login method on that service when Login is clicked in the view. Now let's fire this thing up and take it for a drive. To run an Angular CLI project all you need to do is drop to the Command Line in the project root folder and run ng, space, serve. Now if I load localhost 4200 in the browser and click on the Login button, after a moment we end up on the Login page for the identity provider. There are actually several requests that happened behind the scenes of UserManager to get us there, let's take a look at those in Fiddler. If you're not familiar with Fiddler it's a great wire-level tracing tool that lets you inspect and create raw HTTP requests and responses that are happening from your machine. When you call sign-in redirect on the UserManager it makes a request to this .well-known/openid-configuration address on the STS. That is part of the OIDC protocol, specifically in the endpoint that you can get back metadata about the STS itself from. You can go hit that address in the browser directly and you can see that it's just returning some JSON that contains metadata about the public settings of this server for its support of the OpenID Connect protocol. After the UserManager makes that request you can see it makes a request to the authorization endpoint of the STS, passing as query string parameters the configuration that was set for the client, including client ID, callback URLs, the requested scopes, and a few other pieces of data that are part of the protocol. If any of the parameters passed here do not match up with the client configuration or registration of the STS you'll get an error here. For example, if I change the client ID to something different than what has been configured on the STS then when I get redirected over to that authorization endpoint it shows an error, telling you basically you cannot proceed with trying to log in, unless you are coming from a client application that is registered with the STS. So I'll change that back, hit Login again, and we end up at the Login page for the STS. So let me log in there with an existing account and we can look at the completion of the login process. The DB script that comes with the sample code provisions the database with four users, firstname.lastname@example.org., email@example.com, firstname.lastname@example.org. All of these have the same password set for demo purposes, and that password is capital T-e-s-t, so Test123!!!. I'll log in using the admin account here. As soon as we click Login there the STS validates the entered credentials and then redirects back to the authorization endpoint again. Part of that login process that the STS establishes an authentication session with the STS itself. So the redirect back to the authorization endpoint will succeed this time based on that session being in place. Then that call completes with a redirect back to the URL that you configured as your client callback URL. And in that URL as URL parameters are the ID token and access token. Now at this point we don't have that redirect page in place in the Angular app, so let's add that next. But before we do, let me show the beginnings of single sign-on capabilities you get with an OpenID Connect provider. Note that if I click Login again after already being logged in and having a valid sessions with the STS, I get briefly redirected to the STS, but get immediately redirected back to the client application. If we look at what's going on at a wire level, again with Fiddler, you can see the get request to the authorization endpoint of the STS and it immediately redirects back to the post login callback URL with the tokens in the URL as parameters again. That's because the STS recognizes that this client app already has an existing authentication session with the STS through a cookie that was issued when the user first logged in there. So it does not need to collect credentials before redirecting back to the client app with the same tokens it sent before. So as a result, even if I completely leave my Angular app, and go to some other site, and then come back and get a new instance of my Angular app firing up, when I click Login again I'm already logged in and will be ready to go. Of course, we need to get our redirect page in place to finish the login process, so let's do that now.
Adding the Post-login Redirect Page
As mentioned earlier, the cleanest and safest way to handle the redirect back to your application is to do it in a separate page from the one hosting your Angular app. There are a couple of reasons for this. The first is that is can be a little complicated tapping into the routing system as your app is just starting up to harvest the tokens from the redirect from the STS. The other is that you really want to check and see if the login process completed successfully before even relaunching your Angular app and paying the overhead of getting your app to fully load and run again. So we will just have a separate HTML file that we'll get redirected to after the completion of the login with the provider. We'll want that page to harvest the tokens returned from the provider, store them so the Angular app has access to them as well, and then redirect to the Angular app once we know the tokens are ready to go. So let me add an HTML file to the assets folder so that it gets copied over in the build process as a static resource that will be part of the site hosting of your Angular app. What I need to put in that page is a script block that uses an instance of the UserManager to collect the tokens from the redirect URL and store them. Since this is just a simple, static HTML page, I'll just add a script tag that pulls in oidc-client from a CDN. Then I can create an instance of the UserManager. For this instance of the UserManager we won't ever be trying to use it to log in, so I can just call the constructor with no config object. Then I call signinRedirectCallback on the UserManager. This call will pull the tokens out of the redirect URL and store them in session storage by default. That way any other instance of UserManager that is created in a page from the same site can access and use those tokens, such as the UserManager we encapsulated down in our AuthService. If you want the tokens to stick around even longer you can set a configuration parameter on the UserManager named user store to change that storage to localStorage instead of local session. Then the tokens can be used again even if the user closes the browser and then comes back to the app. So let me change that here and I'll also do so in the auth.service UserManager configuration. Lastly, I want to effectively redirect to the root of my Angular app, but if the user hits the back button I don't want them to go back to the redirect page because that's only there for the transitory processing of the redirect from the STS. So that is what this code here is doing using the window object. So if I save those changes and go back to the browser I'll go to the STS and log out, just to have a fresh session for this run. Now I can go to the Angular app, click Login, we get redirected to the STS, I log in there, and then we're redirected back to the static HTML page with the script we just looked at. Then it redirects on the client side to the root of our Angular app. All that happened pretty quickly, much quicker than I can say it. At this point, the UserManager in our auth.service can start using the tokens that were returned, so let's take a look at that next.
Creating the User Object and Checking Logged In Status
Now let's start getting things set up in the app so it can use the tokens returned from the STS. I'll go into the auth.service and add to the constructor a call to UserManager.getUser. That returns a promise, and in the promise completion I'll put the user object into a variable within the service so that other methods can access it. If you are not logged in that method will return null. While we are here let me add a logout method as well. That method just needs to call signoutRedirect on the UserManager, which will result in a redirect back to the STS to log out. I'll go add a logout button to the menu bar in the root app component, and in that method just call the corresponding method on the authService, like we did for login. Finally, there will typically be a number of places in your application where you need to check whether the user is logged in or not, such as to hide and show these login and logout buttons based on that state. So let me add an isLoggedIn method in the auth.service that retunes a Boolean. And to come up with that value I'll make sure we have a user object that the user object has an access token, and that the token is not expired. While I'm in here I'll also add a method for later named getAccessToken that returns the access token from the user object if there is one. Now I can go back into the root app component and add an ngIf statement to each of the buttons to only have either the login or logout button show based on the login state. I'll again need to wire that through the authService. Now, if I go back to the app I can log in and when I come back into the app the Login button is hidden and the Logout button shows. Then, if I click Logout, we're taken for a moment to the STS and then it redirects back to the client app after killing the authenticated session between the client and the STS. I have IdentityServer configured to silently redirect back to the client as soon as logout is complete. You can also leave the user on a page at the STS and let them decide from there if they want to go back to the app after logging out. There is one last thing we should do after logout, and that is to make sure there is no residual token state stored in either local session or local storage, depending on which we're using. To do that we need the post logout URL that is configured with the STS to have something in the URL that we can easily detect to tell us we are entering the app as a result of a redirect from logging out at the STS. So I'll add a query string parameter to the URL both in the IdentityServer configuration and in the auth.service UserManager configuration. Now I can go into the app component since that is the first that will load in the startup of the app and add some code that checks for that query string in the URL. If it sees it there it can call a signoutRedirectCallback method on the authService. In that method I call signoutRedirectCallback on the UserManager. Doing so will have the UserManager clear out the items in storage that are created during the login process. Then back in the app component when the promise completes I can use the router to strip off the query string parameter that we were using for post-logout detection. I'll save those changes, go back to the app, log in, and if I go look in the debug tools for local storage we can see the login info is there. Then I can go back to the app, log out, and look in Local Storage again, and that data goes away. So now we have login and logout all hooked up and working. Let's take a quick look at how you can inspect the JWT tokens returned from the STS for troubleshooting and diagnostic purposes.
Inspecting JWT Tokens
One thing you may want to do from time to time for troubleshooting purposes is inspect the JWT tokens that are being issued and passed as part of your authentication and authorization flows. One way to grab the ID and access tokens is to comment out the client-side redirect from our post login redirect page handling code. If I do that and go through the login process you can see we stay on that blank page, and in the URL that was redirected to there are URL parameters for the tokens. So if I copy the whole URL to the clipboard, then it paste it into a new file, and then start chopping it up to find the ID token and the access token parameters I can get the value of each individually. Even though this looks like it might be ciphertext it's really just a digitally signed and encoded value. I can copy the value for the ID token and go to jwt.io in the browser and paste that text into this box, and voila, on the right side you can see the contents of the token. Some of the important claims that you see here include the issuer, which is an identifier for the identity provider, subject identifier, which is an end user unique identifier at the STS, audience, which is the client ID of the OAuth2 relying party, or client application, not before, or nbf, which is the time the token was issued, expiration, which tells you when the token will expire, and a nonce, n-o-n-c-e, which is a security session identifier for your session with the STS. If I do the same for the access token, which is an OAuth2 access token, you can see a similar structure, but this contains the info that the back-end resources you call from your client will need. Note that the audience now is not the client app, the audience is an array that includes the resources that this access token can be used with. That includes our project API resource and it includes a resource's endpoint on the STS where you can determine what other resources are available to this client. You can see that this too includes claims for not before, expiration, and issuer, as well as the client ID. It also includes subject, which again, is the unique identifier of the end user within the STS, and, the scopes that are granted to this client and this user. So if you're ever having trouble getting the communications between a client app, back-end resource, and your STS set up correctly, this is a useful tool to know exactly what is ending up in the tokens issued.
Switching to Auth0 and Setting up Tenants, Apps, Users, and APIs
Changing the Client Configuration for Auth0
If we compare this to the configuration in IdentityServer, since it's a framework for building your own IdP the configuration settings for clients, APIs, and their associated settings can just be set from code. IdentityServer does support storing your configuration in a database, and you can consult their documentation for that. But there are no nice, clean UIs to go through and set up your configuration like there is with Autho0, but then again, IdentityServer is a free, open-source framework, and Auth0 is a paid service that can be a bit expensive depending on the size of your organization and what features you need to support. So if I try to do the minimal changes you might expect to get things working, I'll go and add the client ID to my constants file so it's easy to switch there like the URLs. And I'll have the value I was using with IdentityServer and add the one provided by Auth0 as my current value, which I again just copy out of their tenant portal. Then I need to go change the authority URL, and that gets set to softinsight.auth0.com the way they set up a subdomain for your tenant URL. With those changes in place I can go fire up the app, click Login, and I get redirected to Auth0's login interface. I'll log in with the user account I had created there, and everything seems to go smoothly and I'm redirected back into my app and it registers that I'm logged in and shows the Logout button. If I go look at Local Storage I can see an entry for my domain at Auth0. So everything has worked fine, but take a look at the access_token, it's just a short string compared to the very long string that is the id_token that we know is a JWT. So it turns out Auth0 does not give you a JWT for your access_token by default. Also, look at the scopes. In my configuration I had a project's API scope that I need for my API, but that's not here in the granted token. That's because I need to configure an API with Auth0 for them to issue that scope. Finally, if I try to log out nothing happens, and if I look in the debugger console there is an error from the oidc-client saying that there is no end session endpoint. So let's fix all of those issues next.
Addressing Configuration Differences Between Auth0 and IdentityServer4
So we've come a long way in this module. We talked through some more details of the OpenID Connect protocol itself, including getting a better understanding of JWT tokens and talking through the protocol flows that are available, even though you only really need to know about one for Angular apps, the implicit flow. Then we start getting our hands dirty and we started adding authentication into our app using IdentityServer at first, stepping through all the client configuration with the oidc-client, and getting it so that we could get redirected to the identity provider for login and back to the identity provider for logout as well. Then we switched over and did the same for Auth0 as an identity provider and saw some of the differences that result in configuration. Next up we're going to focus on authorization and access control. So I'll talk to you in the next module.
Authorizing Calls to Your Backend APIs with OAuth2
Hi this is Brian Noyes. In this module we're going to focus on authorization with the OAuth2 protocol. I'll again start with some conceptual coverage of the protocol without getting too much in the weeds and then we'll spend most of the module working through demos showing you how to leverage the capabilities of OAuth2 in your apps. First, I'll cover a few more details about the OAuth2 protocol itself. You'll see that there's a lot of overlap with the OpenID Connect protocol details that I covered in the last module, which his not surprising since OpenID Connect is a derivative spec from OAuth2, which preceded it. Then I'll step through showing you how to start using the OAuth2 access token to authorize calls to your back-end, which also enables those services to filter data or restrict access to certain APIs or functionality based on the identity of the caller. Although this course is mostly focused on what you need to do in your front-end Angular app, you can't talk about authorization and access control without getting into some specifics about whatever back-end technology you're using for your APIs. So I'll be showing how to authorize calls, filter data, and enforce access control in the ASP.NET Core APIs that I'm using for the back-end of this course's sample app. But don't worry if you're going to be using something else on the back-end, the concepts and general approach are very similar regardless of what API stack you are using. Just the specific framework mechanisms and code will vary a bit.
OAuth 2 Terminology/Roles
Like OpenID Connect there are some terms you should be familiar with when focusing on OAuth2. What I'm going to cover here are referred to as roles in the OAuth2 protocol documentation, but don't confuse that with the concept of role-based access control, that is a different and somewhat obsolete thing. The first term to be aware of is the resource owner. In the context of an Angular application this represents the end user, someone who has access or ownership of the rights to access some back-end resource. The next term is a resource server. This is the thing that is being protected by the OAuth2-based access control and can represent a website, a set of APIs, or even a remotely-accessible data store. Next is the client, or client application. Like I discussed for OpenID Connect, this is a piece of software that will be making calls to the resource server. So in the context of an Angular app it is generally the Angular app itself, but it could also be a back-end component that is going to make calls to a downstream resource. Lastly, there is the authorization server. As I talked about earlier in the course, this is pretty much synonymous with STS or identity provider, IdP. It is the server application that manages login and the issuing of ID and access control tokens.
OAuth 2 Grant Types
Similar to the flows I covered for OpenID Connect, OAuth2 has a set of grant types, which really represent a flow of how a user and client application obtain an access token to use for authorization. Like with OpenID Connect flows, there's only one grant type that really matters to an Angular app, and that is the implicit grant type, which represents the same implicit flow I covered for OpenID Connect. So that is the one we'll be using when I get into the demos. There are three other grant types to be aware of if you work on other projects using other client or back-end technologies. The first is the authorization code grant type, which aligns with the authorization code flow of OpenID Connect. Using this grant type, again, requires your client app to be able to securely store a secret that is used in the message flows. So it's not appropriate for an Angular app, but it involves having the user first log in at the STS and obtain an authorization code, that code is then used by your back-end to go back to the STS and obtain an access token that can then be used to call out to other resource servers protected by that STS. Another grant type is the resource owner password credential grant type. This grant type is designed for applications that have a highly-trusted relationship with the STS, in particular, desktop or mobile apps that can use the operating system mechanisms to authenticate the user and obtain the access control token instead of the client application managing that flow itself. The last is a client credential grant type, and this one is designed for service-to-service communication where there is no end user in the loop. So, for example, a back-end app could be triggered by a timer or incoming data from an event source, and then needs to call another service to process that event. The authentication and authorization for this kind of scenario would use a client credential grant type.
OAuth 2 Token Types
There are two main token types that are part of the OAuth2 protocol, access tokens and refresh tokens. Access tokens are the primary type and we have already had a brief look at them earlier in the course. If you recall, after successfully authenticating the user at the STS, the STS returns an ID token and an access token for the user based on the client application they're using and the resources they need access to. An access token is, again, typically a JWT, but could be just an opaque string that represents an authorization session. I'm going to focus on JWTs for access tokens, since they encode important information about the user and the session in them that can be used to manage the secured communications with your back-end APIs. An OAuth2 JWT access token will contain encoded data that includes a client ID representing the client application identity with the STS, a subject identifier that represents the end user who is using the access token, information about which identity provider issued the access token, when the token was issued, and when it expires. Then you'll also have a set of claims identifying the scopes that this access token can be used to gain access to. Again, scopes are just named identifiers that represent a resource server, and individual API on a resource server, or a remotely-callable data store. Finally, access tokens can contain an arbitrary number of additional claims that are up to the identity provider to define and populate. Even though all this information is encoded into the OAuth2 access tokens, this information is not intended for consumption on the client side in an Angular app because again, this is an insecure environment. So the information in a JWT access token is really meant to be consumed by the resource server the token corresponds to. It is the resource server's responsibility to decode the token, validate the signature, and then make authorization and access control decisions about the caller based on the information in the token. As I'll show towards the end of the course, if you want to have access to any of this information directly in your Angular client, you'd need to implement an API that the client can call to retrieve this information from the back-end after it has been decoded and validated by the API. The other kind of token that is part of the OAuth2 protocol is a refresh token. Refresh tokens are not applicable to Angular apps using implicit flow because they require secure storage of that refresh token once obtained from the STS. There is an ability to do a silent refresh with an access token in an Angular app that I'll cover later in the course, but it does not involve using the refresh token approach that is part of the OAuth2 protocol. So for the purposes of this course you can ignore refresh tokens and how to use them in OAuth2.
Demo Overview and Current Status
So now that you understand the basic concepts of OAuth2 let's get back to coding and leveraging OAuth2 access control in our sample application. At this point I'm starting with the sample application as I left it at the end of the last module, where we had finished the process of getting logged in at the STS, getting redirected back to the Angular app, and harvesting the tokens returned from the STS into the UserManager and user objects of the oidc-client. I'm going to use the version that was using IdentityServer4 as the STS so that you can run the whole thing standalone on your machine if you want, and so that you don't have to go establish an account at Auth0 or some other STS just to try out the code. But just keep in mind that what I'll be showing on the client side should not be very different regardless of which STS you're using. In order to show how the access token is used on the back-end I'll be showing how to enable authorization and leverage the information that gets passed in the access token in ASP.NET Core. I'll show first how to require authentication on your APIs so that an access token is required. Then I'll also show you how you can use the identity information about the user that flows into the code from the access token to do things like filter what data you return for that user or to block access to operations the user is not authorized for. If we go into the app as it stands right now we can trigger the redirect to the STS to log in as shown in the last module. Once we complete the login the access tokens are returned to the app in the redirect and we are using oidc-client's UserManager to harvest the tokens from the response and store them in local session, or in our case, localStorage since we modified from the default. The type provided by oidc-client that encapsulates the information that was encoded in the ID token is the user type, but through that user type we can get the access token that was returned as well. Now at this point my APIs are not enforcing any kind of access control. To show that I could simply go to one of the APIs that handles get requests and issue the request through the browser or through a tool like Fiddler with no tokens at all. Here you can see the API can still be called and will return the data regardless of who the caller is. That's obviously insufficient for most business scenarios. So we need to first have the back-end require authentication and authorization and then modify the client to provide the access token when it makes the calls.
Adding Authentication to the API
The first step to adding authentication into an ASP.NET Core application is to add the appropriate NuGet packages. Since we're using IdentityServer4 they have a helper library that makes this very easy. First, you just add the IdentityServer4.AccessTokenValidation library to your API project. That will bring in some dependencies that include the Microsoft authentication middleware libraries. Next you need to hook up the authentication middleware to handle looking for and validating access tokens when requests come in. You do that by going into the Startup class, then into the ConfigureServices method. There you can use the AddAuthentication extensions method on the services object and pass it an authentication scheme you get from the JwtBearerDefaults class. Next you chain a call to the AddJwtBearer method and in that call you configure the options for the middleware to tell it who your STS is and what the API resource identifier is for this resource. To hook up to our local machine IdentityServer4 STS I need to set the Authority URL to the root URL for the STS site. I can set RequireHttpsMetadata to false so I can debug locally without setting up SSL certificates. But you would definitely want to leave that in its default, which is true, in production sites. And then I need to set the audience property to the name of the API resource for this API. That should already be configured as an API resource in the STS. Next, I go down to the configure method in the Startup class and call UseAuthentication on the builder object. Now that the ASP.NET Core infrastructure is in place to look for access tokens and validate them when presented, now I just have to indicate which APIs are protected by that authentication. To do that I go into the individual controller classes and add an Authorize attribute to each. You can of course do this at the individual method level as well. When you use the attribute on its own like this you're simply indicating that the caller needs to be an authenticated user for that app, but we're not enforcing any specific access control permissions in the process yet. If I were to run that and switch back to my client application I can go through the login process again, and then when I try to go to the My Projects list nothing happens in the app, and if we go look in the browser debugger Windows console we see an error, specifically a 401 Unauthorized response. If we look at the request in Fiddler you can also see it returned a header of www-Authenticate: Bearer, which tells the caller what the API's expectation is of how to get authorized, specifically that it is expecting a bearer token to be presented. A bearer token is just a generalized term for an authorization token that could be satisfied with a number of different security protocols. So the API rejects the call with a 401 status code, which basically means, I'm sorry, but authentication is required with a bearer token in order to be authorized to call that API.
Adding Access Tokens Manually to HTTP Requests
To fix the 401 error code we are getting we just need to pass our access token to the API following the OAuth2 spec, which specifies that you should pass that token in an authorization header with a prefix of bearer. In Angular there are several ways we could go about doing this. Let's start with the simplest way to understand, specifically manually adding the header to the outgoing HTTP request when you make it through the Angular HTTP client service. To show that I'll drop down into my project.service here, this is where the first request that API gets called when I go to the My Projects view. To manually set the header I'll need to create a HttpHeaders object that comes out of the same module as the HttpClient. So I'll add that to my import statement up here. Next, I'll need to have a value for my access token available in that getProjects method where the request is being sent. So I'll add the authService to the constructor here to get injected through dependency injection. Then in the getProjects method I'll call the getAccessToken method I added in the last module to get that access token. Now I need to create an instance of the HttpHeaders object and set the authorization header in that collection to a value of Bearer, space, and the value of the accessToken, so I just a template string here to provide that. Then I need to modify the call to the get method of the HttpClient to pass an options object that sets the headers using the headers object I just created and populated. with that code in place I can save, go back to the app, code to the My Project page, and behold, we are now getting our projects again.
Adding Access Tokens Automatically with an HTTP Interceptor
So at this point we could go to each of the individual service methods, or an HTTP call to our API that's being made, and add the access token to the headers manually like I just showed. The problem with that is it would obviously add a lot of duplicate code and be hard to maintain. One of the reasons the Angular team deprecated the original HTTP service in Angular and replaced it with the HttpClient in Angular 4.3 and later is because the original service did not support an interception model. When making HTTP calls from a client you often need to have some custom code that gets called right before a request goes out or right after a response comes back in, regardless where those calls are being made in the app. In our case, at this point, what we want to do is add the access token to the authorization header right before any call is made to our API. The HttpClient service enabled this using what is called an HTTP interceptor, so let's create one of those and add our access token to the request with it. I'll add a class to the core module here name auth.interceptor. In there I'll again just use a code snippet to stub out the boilerplate code that is needed to create an interceptor class that sets headers on the requests. So I'll type a- to get the list of snippets from John Papa's Angular snippets extension, and then select the one that indicates it will create an HTTP interceptor for headers. I'll name the class AuthInterceptor. In here we'll basically be doing the same thing I showed in the previous demo to manually add the header, but by doing it here it will be done automatically for all requests. So, I'll first inject a reference to the authService in the constructor. Then in the intercept method I'll get the accessToken as I did before. Then I just set the Authorization header to Bearer, space, and my accessToken as I did before. Notice the structure of this interceptor. It is part of a handler pipeline architecture used by the HttpClient, so when you're intercept method gets invoked by the HttpClient when a request is being made, the request is passed in as an object that you can inspect and/or modify before it actually goes out on the wire. You also get passed this HttpHandler parameter, which represents the next piece of middleware in the pipeline. The expectation is that you will do what you need to do and then invoke the next handler, and that's what your interceptor is designed to do is block the request from going out based on some criteria. You can also use the interceptor to look at the response that comes back after invoking the next handler. I'll get into that later for detecting 401 or 403 status codes and using those to drive renewing your access token or sending the user to an appropriate page. While I'm here you should consider whether it's appropriate to blindly add the access token to any request from your app. I probably can't anticipate how this app is going to evolve over time, and it's certainly possible or likely that there may be calls going out to other APIs besides my own. So I probably don't want to blindly add my access token to all calls that are going out, only those that I know are going to my API. So I'll add a check here to see if the request is going to one of my APIs by using the URL that's in my Constants and only add the access token if they are. Otherwise, I just invoke the next handler. Once your interceptor is defined you need to hook it up to the Angular HttpClient infrastructure. The way you do that is you go into the NgModule where your interceptor is defined and first import your interceptor class, as well as the HTTP_INTERCEPTORS provider from Angular. Then, down in the provider section of the module we add a provider that sets the provider property to that HTTP_INTERCEPTORS object, the use class property to your interceptor class, and the multi-property to true so that you could hook up other interceptors elsewhere in your app. With that code in place, let me go back to the app and go to one of the other routes that makes HTTP requests other than the My Projects view that we already hooked up the header manually in. And you can see those calls are now being allowed through as well. And if we go look in Fiddler we can see that the authorization header was being set with the our bearer token.
Adding Access Tokens Automatically with an HTTP Interceptor
Now that the calls are being authenticated in the API there is a security context that will be available to your code in the back-end to make decisions about what the caller can access. The way this manifests itself is going to be very dependent on what technology or framework you're using for your back-end. In .net apps, whether full framework or .NET Core, what happens is that a security principle is established based on the contents of the access token. This principle is called a claims principle and will contain all of the claims that came in through the access token. To get to it you can use the user object that is available on a controller base class or you can use a static reference using ClaimsPrincipal.Current. So one scenario you can use this in is to filter data returned by the back-end based on who the caller is. To see what we have available to us let me dump all the claims and their values out to the Console window when a call comes into the GetProjects method. After adding that code I'll go back to the client app, load My Projects view again, and then go look at the console that's running the API site. There you can see a bunch of claims dumped out, all of which we've already talked about. Note that the name of some of the claims is actually a URI, including the nameidentifier claim. This is how the user ID gets passed through to your API with OAuth2. So now, if I want to filter the projects based on that I just need a relationship in the data model between the user ID and a project. It turns out I already have that implemented as part of the model. In the model there is a user permission class that links a project to a user and also has a value to specify what level of access that user has to the project, which I'll use a bit later. So in my GetProjects method all I need to do is get the value of the name identifier claim from my claims principle, get the list of permissions for the User ID, and then filter the project based on those permissions. Once I add that code, if I fire things up again, go log out as administrator and log back in as Alice, I see a few projects she has access to. If I log out again and log back in as Bob, you can see he has a different set of projects he has access to.
Filtering Data in the API Based on Caller Identity
The user permissions objects that I used in the last demo to filter data are an application-specific control mechanism, they link a particular model object for this application, a project, to a user, and are additionally specifying a level of access to that resource, whether the user has View or Edit permission. You can see that here in the admin part of the app, where the admin can go assign those permissions to a user for a given project. Now it turns out that, at the moment, any user can get in there and make those assignments. So they could just grant themselves access to everything. Obviously that's not secure, I'll address that in a little bit. But when a user is viewing their projects and goes into the details for a project, they should be restricted to not add, delete, or edit milestones unless they have edit permission on the project. As discussed before, the only secure place to enforce that kind of access control is on the back-end. So let me go into the controller methods where those operations occur and add some code to check and make sure the user is authorized to perform those operations. Down here at the bottom of the ProjectsController are the methods for adding, updating, and deleting a milestone. So I just need to add an access controlled guard into those methods and return an appropriate error if the caller is not authorized for those operations. The code to do that is going to depend the app, the framework, and the permissions mechanism you're using. In this case, I just have that simple user permissions mapping object as part of the data model. So I'll add a helper method here to go look up the permission for the user and the project based on the milestone that is being edited, and return a Boolean flag to indicate whether they do or do not have permission. Now, I'll add a call to that method in each of the add, delete, and update methods returning a 403 forbidden response if not. Likewise, some of the other operations in this controller should be restricted based on the user permissions as well. So I'll also add a helper method to check permissions for project level operations. And then I'll go insert a call to that with a return of forbidden if they do not have Edit permission in the method for updating a project and View permission in the GetProjects method. We'll come back to the other methods because those should be restricted to an admin. Now let me fire up the app, I'll sign out and sign back in as Alice, and she only has View permissions on this first project. So if I go do edit the milestone and try to save, we can look at the error in the debugger and see that, in fact, we get a 403 response back. So now our access control is working as desired, obviously having the app crash if you do get a 403 error back is not a good idea, and we should make sure that our user can never intentionally get to data or actions they don't have permissions for in the first place. But just remember that you still need to always assume someone may try to call your APIs without using your client code unmodified. So you always have to have the protections like we just added on the server side.
Adding Access Control Checks Based on Permissions
One thing you might want to add to the client is detection of authorization errors being returned to the client, and send the users somewhere appropriate in the app if they do occur. A simple way to do this is to leverage our AuthInterceptor again and look at responses coming back from the server in addition to modifying the requests as they go out, like we have already done for the authorization header. One question you'll have to decide on is what is appropriate to show the user if an authorization error does occur? If you've added client-side authorization code like I will show later in the course, and are keeping that in sync with the authorization roles on the back-end, theoretically the user should never see an authorization error, unless they're trying to hack the app by modifying the code. But keep in mind there can always be a latency between what the client knows and what the back-end is enforcing, even in a nearly-perfect system. So what I'm going to do for now is add a simple view that we can route the user to that tells them they had an authorization problem, and in that page I can give them the option to log out and log back in with another account. So I'll go into my home folder here and add a view component named unauthorized.component. I'll also add its HTML template file, and in there I'll simply show an error message saying they were unauthorized and a link to give them the opportunity to log out and log back in with another account. I'll go into the component code and inject the AuthService in the constructor, and then add the logout method and call logout on the authService. Now I need to add the component to my app module declarations, and I need to go add a route to get to it. Next, I want to trigger that navigation automatically if a 401 or 403 status code is returned from an HTTP call. So I'll go into my auth.interceptor and add a little code. First I need to pull in an additional operator from the rxjs library named do. So I'll add that import here. I'll use the do method on the observable returned from calling next. I don't want to do anything in the normal case, only if we get a 401 or a 403 response back, which will be treated as an error. So in the error handler for the do method I'll check and see if it was an HttpErrorResponse, and if so I'll check the status code and see if it's a 401 or 403. If it is I want to route to the unauthorized view I just created. So I'll inject the router into the constructor and then check the status code and call navigate to the unauthorized view if appropriate. Now, if I save those changes, go back and make sure I'm logged in as Alice, then go to the first project, and first milestone, try to edit it, and save, and boom, I end up in my crude unauthorized view, and can click to log out if I want.
Handling Authorization Errors in the Client App
Another thing you might want to based authorization decisions on is a role claim or other custom claim. Each identity provider will be a little different on how you can add custom claims into the access tokens that are issued by the STS. I've modified the IdentityServer4 code that I'm using for the STS to add a role claim of admin if the admin user is logged in. And I'm doing that in a simple and crude fashion here for demo purposes by simply looking for the admin email on login. We can use that for access control or data filtering, similar to how we used the identity of the user. If I go into the GetProjects method in my ProjectsController this is being called both to display the list of projects for an individual user when they go to My Projects, but it's also being used for when an admin goes into manage projects. So what I can do here is simply modify the code to check for a role claim of admin, and if present, return all projects. An admin has no projects explicitly mapped to them through user permissions, since they should be able to see and edit everything. So I'll add a check here for User.IsInRole Admin. And if that is true I'll return all projects, if not I'll filter based on user permissions. I'll also go to the method GetProjectUsers, which should only be called by admins when going to the manage projects page. So I'll add a role check with an authorized attribute. This basically prevents the caller from executing the method, unless they are in that role. I'll do the same on the PostProject method and DeleteProject method, as well as the whole user permissions and the count controller classes, which will apply to all of the methods in that class, unless overridden at a method level. If I run that and go to the client app, sign in as admin, and go to my projects, or Manage Projects, you can see I'm now getting back an entire list. If I log out, log back in as Alice, then go to Manage Projects, and try to delete a project, you can see she gets an unauthorized response back and is directed to the unauthorized page at this point. Later I'll make it so she could not even get to that page without having the code. Even though role-based access control like this has been popular in the past, it's important to realize that the claims issued by the identity provider should be linked to the user's identity, not application-specific roles or permissions. As mentioned before, introducing application-specific information into the STS will defeat a lot of the decoupling that is intended with having a separate identity provider. So instead of using a role claim issued by your STS you should consider using an authorization system on the resource server side to look up what permissions a user has just for that resource. In ASP.NET Core the mechanism for doing this is called authorization policies. I won't go into that in this course, but you can check out the ASP.NET Core documentation or other ASP.NET Core Pluralsight courses for more info.
So that wraps up this module on authorization with OAuth2. You've seen how to pass your access tokens in the authorization header as a bearer token and have the back-end enforce that you need that. You've seen how to manually add that header, or better yet, use an HTTP interceptor to add it automatically for you, as well as how to use the interceptor to watch for authorization-related status codes and take some action based on that. You saw that because the identity of the user is passed as a claim in access tokens from the STS, you can then use that name identifier to either filter data or enforce access control rules in your API logic. And finally, you saw that if it makes sense for your app and your STS to include custom identity claims in the issued access tokens, you can easily look for those claims on the server side to make authorization decisions based on them. Next up we'll look at refining the user experience on the client side, both in terms of managing the tokens and using client-side knowledge of the security context of the user to do things like hide or disable UI elements and block navigation to pages that a user should not see.
Enhancing the User Experience with Client Security Context
Hi, this is Brian Noyes. In this module I'm going to shift the focus back to the client side and show you how you can round out the user experience of your app with respect to security. That will include showing a technique for renewing your authorization tokens when they're due to expire. I'll also show how to provide the client side with enough security information about the logged-in user to tailor their experience in the app based on their identity, what roles they're associated with, or what permissions they have. So first up, I'm going to revisit token management. If you recall from earlier in the course I briefly covered the topic of refresh tokens as part of the OAuth2 spec, but said they don't apply to Angular app because they're not supported in implicit flow apps. However, there is a technique you can use depending on your identity provider to get new authorization tokens when your current one is about to expire. Next up, I've emphasized several times throughout the course that none of the code running within your Angular app is really secure, it can be easily changed by simply accessing the browser debugging tools. However, that doesn't mean you should give up on enforcing anything on the client side, while doing so may not be truly securing anything, you should still provide a sensible and coherent user experience that aligns with what the user is allowed to do in the application as a whole, which includes both the front-end and the back-end APIs. So I'll show you an approach for letting the client retrieve what I'll call a client-side security context that informs the client code about what roles and permissions the user has. Using that information you can leverage features in Angular, like data binding, to hide, show, or disable UI elements like menus, as well as using Angular route cards to block navigation to views that the current user should not be able to access. Finally, I'll demonstrate that you don't have to do anything special beyond what I've covered in this course for multiple apps that use the same STS to have a single sign-on experience across those apps. So let's dive right in.
Silent Renewal of Access Tokens
One challenge with OAuth access tokens is that they have a fixed expiration time. That means that if your user just finished filling out a long form of data that took 2 hours to complete, and your access token expiration is 1 hour, the call to the server to save the data is going to result in an unauthorized response from the server, which means at that point you need to redirect to the user to the STS to log in again and get a new token. But the redirect to the STS means your Angular app is fully unloaded from memory, and unless your Angular app persists that unsaved data locally before it unloads, the user could lose all their work. Not the way to have happy users. So we really need a way to logically refresh an authorization token, but we can't use the refresh tokens of OAuth because they can't be used securely from an implicit flow app. So what can we do? Well, when you log in to an identity provider with implicit flow you're just doing a form post of a login form, just like using forms authentication for a single web application site prior to standards like OpenID Connect and OAuth2. As a result, the identity provider will typically establish a cookie-based secure session between the user's browser and the identity provider site itself. That means any time a request is sent from the browser to the identity provider site the browser sill send the authentication session cookie along with the request. This includes requests to obtain a new OAuth2 authorization token. So, if the expiration of the login session with the identity provider is longer than the expiration of the authorization token, the identity provider can support issuing a new authorization token as long as the session with the STS is still active. This allows you to get the same effect as a sliding expiration of the authorization token, even though doing so is not part of the protocol. Since we're talking about security here, you should probably be asking yourself whether this is a good idea. After all, it basically sounds like you're trying to hack your way around the protocol, right? Well, to answer that you have to understand how cookie-based authentication sessions work for your back-end. The reasons that many new systems are moving away from cookie-based authentication is not because of any inherent weakness or vulnerability in the approach, it's due to a number of factors including, cookie-based authentication requires server-side processing and rendering that may not otherwise be part of your architecture if you're building a front-end web app, or a spa, mobile app, or desktop app. Cookies are only sent to the same site they were issued from, so do not support a single sign-on model on their own. And cookies require additional attention and protection against cross-site request forgery, or CSRF, and cross-site scripting, XSS, that's not required for tokens. But for single-site access cookies work great and are secure when properly implemented and used. They also support a sliding expiration in ways that tokens generally cannot. And for implicit flow client applications, if the session with the STS is alive it means the user is still running the browser. They may be working in another tab or have even navigated to a different site, but there is a direct tie to an interactive user session. With tokens there's not, because remember that OAuth tokens can be used with non-interactive background server to server communications as well. Authentication session cookies are also issued from the server as HTTP-only cookies. This makes it harder for a hacker to hijack that cookie than it is for them to hijack an OAuth token. So it's reasonable and safe to allow cookie-based sessions to have longer expirations than OAuth tokens. So what we can do to leverage that fact is set up some code that will go back to our STS periodically and get a new authorization token, as long as our cookie-based session with the STS is still alive. The way this is done is to use a hidden iFrame in your app to issue a request to the authorization endpoint for your STS. That request can and should include a parameter that's part of the OpenID Connect protocol named prompt and set to false. That tells the STS to try to authenticate the user without prompting or trying to show any kind of UI, which it can succeed in doing if the session cookie with the STS is still valid. If that call succeeds, the STS will return a new authorization token, just like it did when the user first logged in and got the original one. Then you just throw away the old access token and start using the new one for subsequent requests to your APIs. Now this all probably sounds a little complicated to implement, right? And it is. The good news is, you don't have to, oidc-client has already done so for you. With oidc-client all you have to do is tell it you want to enable silent renew and it will keep an eye on the expiration of the tokens it has obtained for you, and it will create the hidden iFrame for you, and issue the authorization request, and if that succeeds it will replace your existing access token inside a user instance once it gets the new one. So let's go add that and see it working in the demo.
Enabling Silent Renew in Your Angular App
In this demo I want to show you how to enable silent renewal of your access tokens from the STS. First, let's take a look at what's happening with our access tokens right now before I make any changes. If I go and run the app, and log in as a user, I can go set a breakpoint and take a look at what the expiration of our access tokens are by default with IdentityServer4 as our STS. To do that I'll go into our auth.interceptor that's adding the access token each time we make a call to the back-end, and I'll set a breakpoint there. Then I'll go back into the app and select My Projects to get the app to make a call. Then I can go in the debugger and grab the ciphertext of the token and copy to the clipboard. Now I'll go to jwt.io in the browser that I showed earlier and paste in the token. In the decoded token claims on the right I can hover over the exp, or expiration claim, which is a JSON timestamp value, and you can see the expiration time. Now, if you compare it to the nbf, or not before value, which is when the token was issued, you can probably do the math there to realize the default expiration is 1 hour. That's going to be a little long to wait in our demo to get an expiration app. So I'm going to go into my IdentityServer4 STS code and change the configuration for our client app to set the lifetime of the identity and access tokens to 30 seconds. With that change, I'll go and log out and log back in to get a new token, and can go hit my breakpoint, copy the access token again, and go look on jwt.io again to confirm the expiration is now 30 seconds, which you can see again by looking at the exp time versus the nbf claim time. If you remember, in a previous module I had used ngIf statements in the main menu of our app component so that the login and logout buttons would be shown based on whether the user is logged in or not. In particular it's bound to a method in the auth.service that checks that we have a user object, that the user object has an access token, and that the access token is not expired. So, as a result, you can see that after 30 seconds the Logout button changed to be Login because the token expired. Now let's add the code to get a new access token before our current one actually expires. the first step is to set another property on the configuration object that we pass in to the oidc-client UserManager. So I'll set the automaticSilentRenew property to true. Then, hand in hand with that we can provide another redirect URL for where the STS redirects to in the iFrame after issuing a new token through silent renew. Like our other redirect I'm going to handle that in a separate page so that our whole app doesn't have to load up to capture the new token. So let me set the silent_redirect_uri property here to the URL for the page I'm about to create. Now I'll add that page under assets like I did for the other redirect page, and similar to the code we put in that one, I'll create an instance of the UserManager in that page and call silent sign in callback in that page, which will capture the token. And like our other redirect handling page, I'll add the config option to store the token in local storage so it will be stored even if the user closes the browser. If I save those changes and go back to the browser I can log in, I can access pages and their data using the access token with the API, but if I watch the Logout button in the upper-right it's still changing to the Login after 30 seconds, so we're still missing something. To figure out what that is let me enable logging in oidc-client. This is an important hook to be aware of for troubleshooting problems where you think oidc-client's not doing something it should. In oidc-client there is a Log class that we can import, and then on that Log class there is a static logger property that we can point to a handler to output log messages. The simplest option here is just to point it to the console object, and then it will output its log statements to the debugger console. Now if I go get a fresh login session, and take a look in the debugger console, we can see an error's being thrown there that's complaining about the content security policy and a frame-ancestors value. Whether you could go try and set that, but it turns out that error is a distractor. Whenever something's not working right with communication with the STS the first place to look is in the server logs, which I'm already logging out to the console that's running the STS. If I go review the errors there we can see some validation errors, and if I scroll up some more the specific cause becomes apparent, it's not accepting the redirect URL I've provided for silent redirects. To fix that, all I need to do is go add that URL to the allowed redirect URLs for the client configuration in the STS. Once I do that, if I go back to the app and refresh, you can see the error in the console goes away, and we see successful sign-in requests happening. We can also see this if we fire up Fiddler. You can see the repeating calls to the authorize endpoint and they have a query string parameter of prompt=none to tell the STS to only handle the request if it can do so without prompting the user. At this point, it looks like the requests are happening continuously. The reason for that is that by default oidc-client has a 60-second window that if you're within 60 seconds of the token expiring it will go ahead and issue the request to get a new one. Since our expiration is currently set to 30 seconds for demo purposes that means it's always within that 60-second window. So let me go change the token expiration to 120 seconds to get it outside that window. Now the other issue is that currently the auth.service goes and loads up the user object once as the auth.service is being initialized. But it turns out that when silent renew happens the token held by that user instance is the old one, not the new one just obtained through silent refresh. So to fix that we just need to add some code that will reload the user object we're pointing to when that object has been refreshed by the UserManager. You can see in the log output that a user loaded event is firing each time the silent sign in happens. UserManager exposes a number of events for situations like this. So I'm going to tap into that user loaded event, and to hook that up UserManager has an events object with the all the events on it, and helper methods on that events object to subscribe to its events. So let me add this little chunk of code, which will listen for a user loaded event and reload the user object held onto the by the auth.service whenever a new one is loaded. With that change in place, if we went back to the app and we left it running for long enough, we would see the events continue to fire and 60 seconds before the expiration of the access token the silent renew happens and gets us a new token. And since we then reload the user object that the UI is using to determine if we're logged in, the Logout button remains the one shown because we can now stay logged in indefinitely as long as the browser stays open.
Providing a User Security Context to the Client App
Now let's start putting the finishing touches on our app with respect to security. To do so we need to make the client code aware of a security context for the logged in user. Using that context we can do things like hide or show things to the user based on their permissions or role and can also block client-side navigation to pages that the user should never see. So first I'm going to go into the server API project and add an endpoint that the client can call after login to get the security context for the user. To do so, first I need to some data structures to return the information I want the client to be aware of. I'm going to create a class here named AuthContext, and we'll add properties for the user profile and for a collection of simple claims. I'll declare this class SimpleClaim with Type and Value properties and that's what we'll put in our claims collection that we return via the API. Even though there is a claim class built into the framework you can't easily serialize that class, so I'll just project the Type and Value properties of each claim into my SimpleClaim class. Now I'll go to the AccountController and add a new get method that will return an instance of that security context. The code for that method will populate the user profile and populate the child permissions collection as part of the query. And then it will populate the claims from the user principle on the controller base class. And because this method is going to have a different security requirement than the method that was already in here I need to move the authorize attribute that restricts to admin down onto the individual method, otherwise it would override the one at the method level for this new method. Now let me switch to the client side and start hooking up this security context. First, I'll go to the model folder and add types for AuthContext and SimpleClaim that match the server-side definitions. Next, I'll go into my auth.service and add a method to retrieve that security context and store it in a public property on the auth.service. Then I'll add calls to that method, both when the user object is first loaded as the app is initializing and also when the silent renew happens. Now let's run and make sure all is working okay. I can set a breakpoint here where the call completes and take a look at the security context to make sure it contains what we expect. Next up, let's start adding some restrictions and customize the UI based on that context.
Hide, Show, or Disable UI Based on Security Context
Now that there is a security context available to the client side let's go hook it up to make sure the user experience matches what the user is allowed to do in the app. First, let me go to the main menu in our app.component and I'll add an ngIf to the Manage Projects menu item that checks whether the user is admin or not. Additionally, I'll make the My Projects button check to make sure the user is logged in. For the isAdmin check I'll go create that method in the app.component. To implement it I simply need to check whether a security context exists on the authService, and if so if there are claims, and finally, if we can find a claim with the role type, which happens to be created with a URL in the ASP.NET identity framework. So I'll check to see that the role claim is set to admin. With that in place I can fire up the app and sign out. Now I'll sign in as an admin and see that the Manage Projects menu is there, but then sign out again, and sign back in as Alice, and she no longer sees the Manage Projects menu. Next, I'll go the project view that lists the milestones for a project. If a user does not have Edit permissions for this project the Edit, Delete, and Add buttons should be disabled, and maybe have a tooltip that tells the user why they're disabled. To do so, first I need to inject the authService into this component. Then I'll go add a method called canEditProject. In there I'll make sure I have a project, a security context, a user profile on that security context, and a permissions collection on the profile. Then I'll check to see if the permissions contain an Edit permission of this project, or they can be an admin who has permission for everything implicitly. So I return a result based on them having Edit permission or being an admin. Now I can go into the view markup for the project.component and set the disabled property based on a call to canEditProject. I can also modify the title to be dynamically set based on whether they have Edit permission or not. I'll repeat that on the delete and add buttons as well. If I run with that code in place and do a fresh login as Alice, and then go down into a project's milestones, we can see the buttons get disabled and their tooltip set appropriately. Next let me log out as Alice and log back in as admin and try to go to the project page. Turns out we have a bug lingering here from when I added the authorization code to the APIs. Down here in the milestone and project access checks I'm checking for permissions, but if the user is an admin they don't need any permissions assigned so this code ends up returning a false value resulting in a 403 unauthorized for admins. Let me fix that quick. I'll just add a pre-check to both methods that if the user is an admin return true. Now if I run with that, we can go into My Projects as admin and see and edit all projects. Next up, let's add a route card to restrain navigation to routes that the user should not be able to go to.
Block Navigation to Unauthorized Routes with Route Guards
If I'm logged in as an non-admin account we hit the Manage Projects menu item by data binding to the security context info. But right now that doesn't prevent a user from putting an address into the address bar of the browser for the admin page. And you can see it will take them there. But then if we've done our job putting authorization checks on the back-end they should get a 403 response if they try to take any actions in the page. But of course it would be better if they simply could not get to that page in the first place. The best way to do this in the Angular is with route guards. A route guard is a class that you wire up to a router set of routes, and it will be consulted to decide if navigation should be allowed. So let's go add a route guard for admin routes. To do so, I'll go into my core module and add a new file. In there I will select the code snippet for a route guard using CanActivate. In there I'll inject the authService, and in the CanActivate method return false if the user is not in the admin role. Now I'll add that to the providers of the CoreModule, and then I can go into the admin.module, add an import of the CoreModule, and then go to the routes for the admin.module and add a CanActivate check for the routes that are using my new route guard. If I run now and log in as non-admin, and try putting the /admin address in the address bar, you can see it just redirects back to the root URL and does not even activate the Manage Projects component. But if I sign out and sign back in with the admin account, I can see the Manage Projects menu, and can click there or put the admin address in the address bar and they will still go to those pages as admin.
Single Sign-On Across Multiple Apps
So that gets us pretty close to done. One other capability I wanted to demo since it's a key reason for going with OpenID Connect and OAuth in your apps is single sign-on. To demonstrate that I need one or more other sites that are set up to use the same STS. In the sample code for this module you'll find a separate project for an ASP.NET MVC client app. That app has been configured to use OpenID Connect for its security and that configuration points to our IdentityServer4 STS, same as our Angular app is using. The about page in that app is secured and requires sign in. So here you can see I can go and hit the About page, and I get redirected to the STS to log in, just like for our Angular app. Notice that for this app the STS presents a consent screen as well. Consent screens are part of the OAuth protocol and allow the STS to prompt the user for the scopes that are being requested for a client application. In IdentityServer4 this is just a flag on the client configuration saying whether you require consent or not. Once I do, I can see the contents of the about page. Then I can go into my Angular app on port 4200 and click Login, and you can see that we didn't get prompted for login because the browser sent the cookie that was already established for login on the other site. And that results in the STS immediately returning a new token to the Angular client and it's logged in. As long as those tokens don't expire you can bounce back and forth between the sites and you are logged into both. If you were to do a third site that used that same STS the result would be the same. As soon as an authorization request is sent to the STS for the new site it would detect a session already in progress and could log in the third app silently as well.
So that brings us to the end of this course. In this course you've learned about the big picture security concepts that apply to an Angular app. You learned how to use OpenID Connect to get your users logged in through an OpenID Connect identity provider, or STS. You also learned how to hook up authorization based on the access tokens issued as part of the OpenID Connect process. You've seen how to use IdentityServer4 as your STS, as well as Auth0. Other identity providers that support these same protocols would have similar configuration options, you just have to track down how to get those things configured for that provider. You also learned how to authorize API calls based on OAuth2 access tokens. And then use that authorization to grant specific access to APIs or functionality on the back-end, as well as the filter data based on who the use is. As you've seen, the oidc-client library does a nice job of insulating your code from needing to know too much about OpenID Connect and OAuth protocols. It encapsulates the process of getting redirected to the identity provider and collecting the tokens returned and making them accessible to your code. It even makes it easy to set up silent renew to keep your secure session going as long as your session with the STS is current, even if your access tokens have shorter lifetimes. Finally, you saw how to pull some data back to the client about the security context of the user, and then use Angular features like data binding and route cards to enforce security constraints on the client side for user experience reasons, even if they're not truly securing your application in any way on the client side. I hope you've enjoyed this course. Please feel free to ask questions in the discussion forum for this course on the Pluralsight site. Thanks for watching.
Brian Noyes is CTO and Architect at Solliance, an expert technology solutions development company. Brian is a Microsoft Regional Director and MVP, and specializes in rich client technologies...
Released27 Jul 2018