© Yvonne Wilson, Abhishek Hingnikar  2019
Y. Wilson, A. HingnikarSolving Identity Management in Modern Applicationshttps://doi.org/10.1007/978-1-4842-5095-2_9

9. Sample Application with Custom API

Yvonne Wilson1  and Abhishek Hingnikar2
(1)
San Francisco, CA, USA
(2)
London, UK
 

It’s not just what it looks like and feels like. Design is how it works.

—Steve Jobs, founder of Apple Computers, as quoted in “The Guts of a New Machine,” New York Times Magazine

The past chapters covered how certain identity protocols provide a solution for authentication, API authorization, and application authorization. Now we can bring together the topics from the past few chapters and describe a sample program that solves typical identity challenges using these protocols, specifically OIDC and OAuth 2.0.

In the past 3 decades, we have seen a lot of transformation in how applications are designed and implemented. We went from traditional web applications rendering user interfaces from the server and on-premise desktop applications to scalable cloud services designed around APIs that talk to multiple user and machine facing applications. A machine-first approach for the back end and experience-driven front end is the modern standard for designing applications. From photo-browsing applications like Imgur to banks like Monzo, we have seen the rise of highly scalable, stateless, back-end APIs serving a user-centric front end.

We created a sample program to accompany the text and which uses OAuth 2.0 for API authorization and OIDC to authenticate users. The following sections explain the approach used to design the application and the rationale for identity-related design decisions.

Background

In order to keep the example simple, we chose to build a simple document-writing app. This type of application allowed us to cover a decent set of the problems faced by many applications today. The application involves a stateless back-end API serving a single-page application (SPA) and native app. Discussing every detail of how to build and deploy modern applications is far beyond the scope of this chapter, and thus we will focus here solely on the identity-related aspects of the application.

The application offers the following features and services:
  • Allows the user to write an article in Markdown.

  • Renders the final text from the Markdown.

  • Articles are owned by a single user.

  • An article owner can share an article with others.

  • Sharing is based on a simple attribute-based access control scheme.

  • There are two levels of access to an article, reader and editor:
    • Readers can view articles.

    • Editors can create and update articles.

  • When a new version of the article is created, it is published as a versioned new document, to keep the program as simple as possible.

The sample application is similar to popular code-sharing web sites like JSFiddle or Codepen. We made this choice so the application functionality would be familiar to most developers which allowed us to keep the focus on demonstrating and explaining identity management concepts.

Several measures have been taken to simplify the application. For example, the application doesn’t support multiple users editing the same article at the same time. Furthermore, each saved edit results in a new version. Thus if Jon created an article and Jessie were to edit it, Jessie would get her own copy of the document. We kept the user interface quite simple, and validation and error checking are also minimal for the sake of simplicity. Now that we’ve covered the obligatory caveats about what isn’t included, we can discuss what the application will do!

Application Requirements

In Chapter 1, we suggested several questions to ask about your project that would help you better understand your identity management requirements. Let us now revisit those questions for this sample application. We share the following answers to clarify the application requirements.

Who Are Your Users: Employees or Consumers?

For the sake of simplicity, our design assumes a consumer type of user, who may wish to enable login via a social authentication provider to access their content. The sample application wasn’t designed for typical enterprise needs, but if it were, employee-facing or business-to-business scenarios could use enterprise authentication providers in a similar fashion, instead of social authentication providers. We’ll want to attract consumer users to our application and make it as easy as possible for them to start using our application. This question helps us understand where users will come from which helps us sort through the provisioning options discussed in Chapter 4.

How Will Users Log In?

To demonstrate some common use cases, the application allows users to sign up for a local account and then subsequently add the ability to link a social authentication provider account to the local account. This enables users to log in via a social provider account to access their documents. The application will maintain a local profile for users and allow users to change their name, email address, and picture in the local user profile. We want to use an industry standard protocol for authentication rather than write custom code for every provider, so we’ll focus on OIDC for authentication, as described in Chapter 6, and use our identity service to broker requests to social identity providers as some aren’t compliant with this standard, and we don’t want to write custom code for each one. This question helps us think through the authentication protocols our application will need to support and that we’ll need a local repository of user profile information.

Can Your App Be Used Anonymously?

Users can start out anonymously and create an anonymous document. To keep the application simple, if a user subsequently signs in, their anonymous documents are not converted to named-owner documents and are not visible to the named account. If an anonymous document is edited, the application creates another copy of the document.

Web-Based or Native App Format or Both?

The sample application will provide a web-based single-page application as well as a native mobile application. This question helps us understand the scope of the project and argues for creating an API that encapsulates the back-end business logic needed by both front-end applications.

Does Your Application Call APIs?

Our application will call our own custom, first-party API that provides access to users’ articles. There is no use of third-party APIs at this time. This leads us to realize we need a scheme to authorize the applications to call the API. The use of OAuth 2.0, as described in Chapter 5, will allow us to do this in a standard way that could also be used with third-party clients in the future, if needed. Thinking about consent, our API is a first-party API to our front-end applications which means we don’t need to obtain user consent to call the API. The front end and API are logically part of the same entity. If we needed to call a third-party API on the user’s behalf, our API authorization would need to obtain user consent, but for our own API that is logically a part of the application, this is not necessary.

Does Your Application Store Sensitive Data?

The sample application assumes that the data in a user’s article might be sensitive. As a result, the web-based application will implement a session timeout, and both applications will provide the ability for users to enable multi-factor authentication to better protect access to their account.

What Access Control Requirements Exist?

We first considered the capabilities of the two applications and decided that both would offer the same functionality. Then we dove into the details of what users could do within the applications.

The goal of our application is to allow quick access to markdown documents with features like pastebin. In order to do so, we would like to enable anonymous authors with the ability to write and share documents. Obviously, this feature would be available only for public documents, and such documents could be edited by the document author or cloned and edited by someone else to create another public document. In order to provide the ability to edit private articles, however, we’d need the user to sign up for an account and log in to access private content.

Once logged in, users can edit their own private documents and share their documents with others by specifying an email address domain. Each individual user can access the documents they created as well as any documents which have been shared with them individually or to a group to which they belong (based on email domain for simplicity). Basing document sharing on a user’s email address’s Internet domain name allows us to demonstrate a simple implementation of attribute-based access control without much added complexity. A person with whom a document was shared can open the document. If they edit it, they create another copy, of which they are the owner.

How Long Should a User Session Last?

In order to protect users who are careless about leaving open sessions, we decided the single-page application should have a 2-hour idle timeout and an 8-hour maximum session timeout. After session timeout, a user needs to reauthenticate. The native application does not implement a session timeout as we assumed it is run on a device that is usually with the user, and we want to make it convenient for quick edits. Both applications provide a means for a user to log out.

Will Users Need Single Sign-On (If More Than One Application)?

In our sample scenario, there is only one application, so single sign-on is not needed. If we had more than one application, or wanted single sign-on across the native and single-page versions of our application, we could use the same identity provider for both to provide single sign-on for users.

What Should Happen When a User Logs Out?

When a user logs out, their application session should be terminated, and the user should be returned to a home page from which they could log in again, if desired.

Are There Any Compliance Requirements?

To keep the example simple, we assume no compliance requirements. The sample does not include a privacy notice, nor does it support any privacy requirements such as the right to erasure. We can only do this because it’s a sample. Real applications have to consider privacy requirements!

Platform, Framework, and Identity Provider

In order to keep the applications simple and easy to understand, they are implemented using the popular React Framework for the front end and Express.js on the back end. All the code is written in JavaScript. This decision was made as the learnings here translate to many frameworks in many different languages. The application components can be easily deployed to a hosting platform such as Heroku1 or Ziet’s Now.2

The application was designed using a modern, API-first approach, so it has a custom API for its back end. If an application delegates authentication to an OpenID Provider, as described in Chapter 6, the access token it receives is for the OpenID Provider’s API, specifically the UserInfo endpoint. To obtain an access token for our application’s custom API, we need an authorization server we can configure to protect our own API. We selected an identity provider service that supports OIDC and OAuth 2.0 so our application can delegate authentication to it with OIDC and request authorization from it using OAuth 2.0 to obtain access tokens for our custom API.

The identity provider service we chose can also function as an authentication broker. Our application will delegate authentication to it, and it can in turn delegate authentication to a variety of authentication services and return the results to our application. Figure 7-4 in Chapter 7 shows this scenario. This allows us to efficiently support SAML 2.0 identity providers, without having to implement SAML 2.0 directly in our application. Our application can use the same provider to authenticate users via OIDC and get access tokens for our custom API with OAuth 2.0.

API

In the old days, when starting to design an app, some people liked to start with the data model and then build the app around it. One of the reasons to do so was to create proper separation between the data model and control logic. The principle of abstraction is still valuable today for good software architecture. Modern applications aim for strong abstractions between the application front end and back end. One benefit of this is that once the API contract has been somewhat established, it is possible to start developing the back-end API and user-facing front end(s) independently. An API Server that implements key business logic provides a back end that can serve multiple applications on different platforms like iOS and Android. It could also facilitate integrations with partners and external or internal developers writing agents and additional clients on top of the API. With these ideas in mind, we started with the API and iterated from there.

With business logic implemented to be stateless and on a server, it can be protected and scaled easily. We recognize that API Gateways are often used in modern application deployment environments, but again, for the sake of simplicity, we decided not to include such an element in the sample.

There are two major concerns for a back-end service:
  • What actions need to be performed?

  • For whom?

The former is focused on the business purpose and logic of the application. In our case, this revolves around creating and viewing documents. As a starting point, we usually like to look at our application specification and check if everything makes sense without a user being in the picture. It’s not that we’re suggesting not having a user model. The exercise of being able to write the application with as little application as possible can help us align with the stateless RESTful principle and reduce the data required in requests to the bare essentials.

Our API centers on one resource, namely, a user’s documents, and CRUD (Create, Read, Update, Delete) operations on the documents. In keeping with the RESTful model, our API has an endpoint for articles and supports standard HTTP “verbs” that map to CRUD functions.

The application is fully functional without the notion of a user, as it allows creating documents anonymously. This might entice users to try out the app who might otherwise balk at having to sign up for an account first. However, if we enable a user to create documents anonymously and the user later signs in, documents created before logging in will stay anonymous and public. The trade-off we made is that allowing a user to start anonymously means that if they upgrade to a full user later, we miss out on the ability to integrate the information about the user with content created anonymously. If we required authentication from the beginning, this would not be an issue, but might dissuade users from trying the app. We decided to allow anonymous use to encourage users to try out the application with as little effort as possible.

Protect the API

With the API designed, we needed a mechanism to protect the API so that it could only be called by our application. The client application will use OAuth 2.0, described in Chapter 5, to obtain an access token to call our custom API. This provided an industry standard model that could support API authorization for our current applications and could easily be expanded in the future to support the use of our API by external, third-party partner applications if desired.

Both our single-page application and our native application use the OAuth 2.0 authorization code grant with PKCE for API authorization, as described in Chapter 5. Our applications and API are owned by the same entity, so the applications are first-party clients for the API and don’t need the user’s consent for the API call. However, our API needs information in order to enforce access policy, including information about the user. This brings us to the users of our applications.

We elected to use OIDC to delegate user authentication to the same authorization server that protects our API. The front-end application can redirect the user to the OpenID Provider and get back an ID Token with claims about the user. The application needs information such as an identifier for the authenticated user for display purposes and their email address for notification purposes. It can use the scope parameter in the authentication request to specify what user profile claims it needs. In this case, it will use “oidc profile email” to get basic profile attributes and email, as described in Chapter 6.

By using the same OpenID Provider for user authentication and API authorization for our custom API, we can have the provider issue an access token with custom claims to provide information about the user to our API. The custom API is a relying party to the authorization server, and the custom claims in the access token will provide trusted information to the API that it can use to enforce access policy for the users’ articles. This brings us to the remaining scopes for the access token.

OAuth 2.0 Scopes – for API Authorization

OAuth 2.0 defines scopes as a means for an application to indicate the specific privileges it requests for an API call. The single-page application and the native application enable the same access and will therefore require the same scopes. We defined access scopes for the applications around the API endpoints and functions the applications would perform. This resulted in the following scopes that the applications can request:
  • get:article

  • post:article

  • patch:article

  • patch:profile

  • get:author

Note that these are the privileges that the applications will use with the API, and not privileges for users, which we’ll discuss in the next section. We registered our API in the authorization server and defined these scopes for it. This takes care of the applications’ privileges.

User Authorization

Once we have built the application’s business logic and enabled the applications to call the API, the next major concern is “for whom” will the API actions be done. This is a multifaceted question, as this includes “who is the user?”, “are they allowed to perform this action?”, “at this time?”. Attributes needed to answer these questions must be present in the API request in order for the API to know if it should perform the requested action. Since all of this has to be present in the request, it is lucrative to standardize and reuse the logic that drives this.

OIDC can deliver to our application information about an authenticated user, and OAuth 2.0 can be used to obtain an access token for the API. The access token can contain additional claims to augment the information provided to an API. We’ll use additional claims to provide the API information about the user, so it can enforce access policy at the level of individual users and ensure that a user can only access the articles they’re allowed to view. The additional information could be put into additional, custom claims in the access token, or it could be bundled into an existing claim. User privileges are outside the scope of the OAuth 2.0 specification, so different OAuth 2.0 providers may offer different ways of handling this. We chose to use as our provider Auth0,3 which uses the scope claim to represent the integration of delegated and granted permissions. In any case, the API simply needs the logic to retrieve the necessary information from the various claims in the access tokens it will receive. Different OAuth 2.0 providers have used different access token formats, so a helper function in the API can be used to extract the information from claims in the token or a token introspection endpoint at the provider, depending on the format used. This helps us encapsulate all vendor-specific logic for validating tokens and extracting or querying claims in one place.

API Implementation

Going back to our original requirements, we ideally want the access token to have information about the user requesting an article, such as a user identifier and permissions. In order to keep things as flexible as possible, we added to the API two helper functions.
function getUserId(token) {} - Takes a token and extracts the user_id
function hasPermission(token, permission) {} - Given a token and a set of permissions, checks if the token has all the permissions

The first function helps the API identify the user, by retrieving the identity of the user from the “sub” claim in the token. As recommended in Chapter 4, we use an internal, application-specific identifier for a user in all application and API logic and use separate attributes for display and notification. This enables a user to change attributes such as their display name or notification email address without impacting articles tied to their identity. To keep the program simple, we didn’t implement functionality to let them actually make such changes.

The second helper function is to check if the application and specifically the user have the permissions to perform the requested action.

Defining and abstracting these two functions has the advantage that it encapsulates in one place the logic to deal with any changes in token formats or information over time or if we have to change providers for some reason.

In addition, logic is needed to validate any tokens before using them. When building your application or API, we recommend using standard libraries for your language, framework, platform, or provider to implement these protocols as well as validate and consume these tokens. This simplifies your application and may help isolate your code from vendor changes.

Processing Requests

When handling requests, we chose to have the API follow a middleware approach, which is to handle the logic of processing a request in multiple parts. In our demo application, we do this in the following order:
  • IP-based rate limiting

  • Check token’s validity

  • Extract token information

  • Extract request data

  • Handle request

If using a reverse proxy or an API Gateway, you may find that the top 3 are done for you. If not, you’ll have to implement the steps. Your OIDC/OAuth 2.0 provider may provide libraries to help with validating and consuming tokens. For the remaining tasks, you may have noticed that the middleware that checks for the token’s validity is early in the list, allowing us to reject an unauthorized response as early as possible. For example, if a user does not have a subscription that allows big files, the application can simply respond with an error very early in the process.

It is essential for an API to validate an OAuth 2.0 access token received with a request, before processing the request. The mechanism to validate an access token will vary by provider and is influenced by the token format used by a provider. An authorization server may provide a token introspection endpoint that can be used for this purpose. Alternatively, an authorization server may use a JWT, CWT, or IronToken access token format that supports independent validation by the resource server. The documentation for an authorization server should indicate how an API should validate access tokens it receives. This should include checking the issuer matches its chosen authorization server, the expiration has not passed, and that the API is the intended audience (recipient) for the access tokens.

There are cases where applications or APIs need meta information about a token such as its validity, the scopes for which it was authorized, and possibly the context surrounding issuance which includes the subject (user) who granted authorization and the client to which the token is issued. The original OAuth 2.0 specification did not provide a means for this, but two approaches have been used, namely, a token introspection endpoint and JWT-formatted tokens.

The OAuth 2.0 Token Introspection specificationi defines an Introspection endpoint for authorization servers which resource servers can use to obtain token metadata. This approach requires a call to the authorization server. Some providers have taken an alternative approach using JWT-formatted access tokens. With a JWT access token format, the resource server can obtain metadata about the token from claims within the JWT payload, after validating the token. This eliminates the need to call the issuer to validate each token. Our authorization server uses JWT-formatted access tokens so our program can extract claims information from the access token.

Extensibility and Adding Custom Claims

Somewhere along the line, you may have wondered how the custom claims will get into the access token. This is made possible because the authorization server we chose, Auth0, offers an extensibility feature which allows us to write custom code to tailor the authentication and authorization process to the needs of our application. Other providers offer similar features.

In the previous sections, we discussed the need for custom claims. In addition to information about who the user is, we need information about the team a user belongs to. Our application supports an access model that is loosely based on attribute-based access control. Our application is primarily concerned with documents, and we store permissions in the document metadata, similar to how files have permissions associated with them in Unix-like operating systems.

In our current implementation, each file has an array in metadata with the following shape:
{
        identifier: String,
        permission: String[],
}

Identifier is either an email address or a domain name, while permission is one or more of the following, “read,” “write,” “share,” “owner.” In the current permission model, a complete email will be matched in its entirety with the user’s email. In the interest of privacy, instead of sharing the full email, a salted hash is stored and acquired. We also allow using @domain.com identifiers which must start with the @ symbol. These allow users to share with anyone with a specific @domain.com email. Lastly, the creator of the file has all four permissions. To keep things simple, only the owner of a file is able to grant “share” privilege to others. This simple model was chosen so the program can demonstrate how attributes can be included in claims to facilitate access policy enforcement in APIs, as described in Chapter 8.

To add claims to an access token, we will have our authorization server add an extra nonstandard claim “https://dev.doc/team.” This claim is added to the token if the user is part of a team. For the sake of simplicity, the team is merely a domain name, which is hashed and salted for privacy. The extensibility feature in our chosen authorization server allows us to use code logic to augment the token. We used a snippet of code like the following to add a claim:
async function (user, context, callback) {
    user.app_metadata = user.app_metadata || {};
    user.app_metadata.teamId = user.app_metadata.teamId || async hash(getDomain(user.email));
    context.accessToken["https://dev.doc/team"] = user.app_metadata.teamId;
    callback(null, user, context);
}

We have found that access policy and logic vary quite a bit, and it is very common for applications to have unique requirements in this area. We recommend checking for some kind of extensibility feature when selecting your identity provider. If your identity provider doesn’t support this, you’ll need to handle this logic on the back end or add this in the provisioning step. However, since this is conveniently handled for us out of the box in our provider, we can focus on our business logic. The same need for extensibility applies to front-end customization as well, as most applications will want to customize consent screens and need tailored consent management or approval logic. Having an identity provider with some form of front-end extensibility for functions like login, sign-up, and consent can reduce what you have to build in your application.

So far, we’ve described the API, delivering information to the API via an access token, and how the access token is augmented with custom claims and processing the API requests. This brings us now to the front end.

Front End

Separating the business logic in the API from the presentation layer allows us to build a dynamic presentation layer to run on the client device. The advantage is that front-end logic can focus on delivering a good user experience, while the business logic and sensitive data are protected on the server. In the application, we use OpenID Connect (OIDC) to delegate authentication to our authorization server. The application receives an ID Token with claims about the user. The client can use this information about the user to tailor the application user interface, as appropriate, based on the user’s profile and privileges. It’s important to note that the use of user privileges on the client is purely for display purposes. An important design assumption is that one cannot trust what happens on the client device because developer tools, debuggers, and the like could potentially be used to tamper with client-side logic. Information about the user’s privileges can be used to render the user interface so it only shows features a user is allowed to use, but when the user takes an action, the resulting requests to the server must be sanitized and validated on the server to check if the user has the necessary permissions. Such validation and access policy enforcement logic must execute on a protected server, where users cannot tamper with it.

Front-End Functions

Keeping that in mind, on the front-end application client, the common problems to solve are authenticating the user to the application, offering the user a graceful user experience, and enabling logout. Thus the core tasks for the application front end are authenticating the user, obtaining the ID Token and the access token, storing the tokens, and then using the information acquired about the user to build the user experience. Our application front-end therefore needs four basic functions:
  • authenticate() – To authenticate the user using the identity provider

  • getToken() – To get a token to call an API with specific scopes

  • logout() – To revoke the session

  • getUserProfile(audience, scope) – To get the user’s profile for a specific API and scopes

There may be a need for additional helper functions depending on specific application needs, but something akin to these four functions are commonly needed in typical applications.

The application uses OpenID Connect to authenticate users and acquire user information from an ID Token. There are several well-designed libraries that provide an implementation for OpenID Connect clients. In our case, we chose auth0-spa-js, which is a library provided by our OIDC/OAuth 2.0 provider, as it offers some convenience methods on top of the basic functionality of authenticating a user and returning claims. We recommend using a library to handle protocol details for you and simplify your application code.

Authenticating the User

Both front-end clients use OIDC to delegate user authentication to the OpenID Provider. There are some differences between the native app and single-page app in terms of how they delegate authentication. On the Web, it is natural for the web-based single-page application to redirect the user’s browser to the identity provider in order for the user to authenticate there. However, with native applications, this may not be available, as such applications do not run inside a browser-like runtime environment. Our native application is designed for mobile devices and can use the underlying mobile device system’s shared web browser to redirect the user to the identity provider to log in. Once the authentication flow is completed, the application front end receives an ID Token and an access token. The front end uses the information in the ID Token to obtain information about the authenticated user, and it uses the access token for requests to the API. In addition, the mobile app also requests and receives a refresh token which it uses to renew an access token when it expires.

Tokens

After the user has been authenticated, the application has an ID Token, an access token, and in the case of the native app, a refresh token. The ID Token is decoded and validated by the application before using any of the claims in it. Then the ID Token is stored in memory cache on the single page application and in a local store on the mobile application. This allows the application to quickly render the user interface elements that require user information without having to repeatedly call out to the OpenID Provider. The access token is not stored by either application, to minimize the risk of access token exposure. Given the nature of the application, and since access tokens are created with a short token duration, the need to make a second call to the API within the lifetime of an access token is deemed unlikely.

Making Protected API Calls

As the user interacts with the application, it will make calls to the API, providing the access token in the HTTP authorization header for the API request. In order to reduce the risk if the access token were to be stolen, we configure the access token duration in the authorization server to be short-lived. This is a trade-off between scalability and security. A shorter duration access token means it may need to be renewed more frequently, but if it is compromised, the window of time in which it can be used will be short.

Sessions

There are two user sessions in our sample scenario, as there is an application session for the user as well as an identity provider session for the user.

The single-page app relies on the identity provider session for a user and, as such, does not store data locally, beyond storing the tokens it receives in the in-memory cache. This simplifies the application but comes with the disadvantage that every time the application is started in the browser, it has to check with the identity provider for the status of the user’s session.

The native app takes a different approach. The design model for the native app allows the user session to continue until the user explicitly logs out. The native application can continue to run on the user’s device until it is shut down. When the user interacts with the application, it may make an API call and only then will it need an access token. The native app can make use of a refresh token to obtain a new access token if the old one has expired when the native app needs to make an API call. In our current implementation, the refresh token acquired during the initial authentication is stored in the operating system–provided secure storage implementation.

As you will learn in Chapter 10, in a typical SSO deployment, a user may have multiple sessions including an application session, identity provider session, and an additional session in each of any other applications they’ve authenticated to via the same identity provider. It is desirable in some cases to have a binding between the session at an identity provider and all the relying parties it serves, so that a given application can be aware of changes to the user’s session in the identity provider and vice versa. Unfortunately, at the time of writing, there is no finalized standard way of achieving this with OIDC. So far, there is only a draft specification for session management which we elected not to use as it has not been approved.

Token Management

We abstract token management in the “getToken” method for the front end. When the token is acquired, the application uses the “expires_in” element of the response to compute an expected timeout for the token. This is stored along with the audience, scope, and other metadata associated with the token. Later, when the application needs an access token with specific scopes, the getToken method simply returns an access token from the cache, until the token expires, at which point the application needs to request a new access token.

The mechanism to obtain a new token varies by the type of application. The single-page application cannot securely store a refresh token, so cannot use this method of renewing an access token.4 It could redirect the user to the identity provider to obtain a new token, but this can be disruptive to the user experience. To avoid this, it uses a library from the OpenID Provider that implements a more sophisticated means of interacting with the OpenID Provider based on HTML5 Web Messaging. It requests this by using the web_message response_mode parameter in the authentication request. The options available for renewing tokens may differ with your chosen identity provider. We recommend using the method recommended by your provider. In our case, we abstract getToken with auth0-spa-js’ getToken method. The auth0-spa-js offers a cache implementation as well, so we do not reimplement it in the application.

On the native application, the implementation is slightly different as it can use a refresh token. The native application uses a refresh token to call the OpenID Provider’s token endpoint in order to obtain a new access token. The refresh token will be stored by the native application using secure device storage, such as KeyChain on iOS.

Logout

Logout is implemented differently on the two platforms. Both applications destroy any tokens they’ve received during the user’s session as well as any cookies and session state set by the application. In addition, the single-page application, when logging out, redirects to the OpenID Provider’s logout endpoint. This terminates the identity provider session as the provider will log the user out when the logout endpoint is invoked. The implementation for logout and session termination is vendor specific, and we recommend checking the vendor’s documentation for their specific implementation of any logout-related features. The native app deletes any session data, including the ID Token and access token, and it also revokes the refresh token with a call to the identity provider. The native application could have opted to log the user out by calling the identity provider’s logout endpoint, but we chose not to use this option as the user experience is different on a mobile device and doing so can break other application sessions relying on the same OpenID Provider.

There were two other considerations related to logout. As there is only one application in our case, there is no single logout. The implementation for single logout, if needed, or session revocation is vendor specific, and we recommend looking into the vendor’s documentation for their specific implementation.

As mentioned previously, different identity providers may use different token formats. With a JWT type of access token, it is not possible to revoke the access tokens unless the issuing provider supports a blacklist feature. If not revoked or blacklisted, the access tokens will stay valid until they expire. In practice, it’s often more convenient to use a short token expiration than to call the provider for each token to check for blacklisting. Our provider uses a JWT-format access token. The access tokens for our API are configured in the authorization server to have a sufficiently short expiration period so we can avoid the development work and performance impact of checking for blacklisting. Again, it is necessary to check for the recommendations from your chosen provider on how to terminate access associated with security tokens it has issued.

Summary

We’ve covered in this chapter how we designed and built a sample application that uses OIDC for user authentication and OAuth 2.0 for API authorization for our custom API. In this scenario, both functions are handled by the same OpenID Provider/OAuth 2.0 authorization server. We shared some key design decisions and implementation points involved in creating the application. The following chapters will discuss additional aspects of identity management that applications have to handle after the user has been initially authenticated, starting with sessions.

Key Points

  • OIDC is used to authenticate users and obtain an ID Token with claims about the authenticated user.

  • OAuth 2.0 is used to obtain an access token to authorize the application to call our custom API.

  • The access token returned by a social identity provider is for the social identity provider’s API.

  • To obtain access tokens for our custom API, we need to obtain and configure our own authorization server to protect it.

  • Our authorization server adds custom claims to the access token to provide additional information to the API about the user.The custom claims enable the API to enforce user-level access policy.

  • Both applications use the OIDC authorization code grant flow with PKCE for a user’s initial authentication.

  • The native application, implemented in Cordova, uses the device’s system browser to redirect the user to the OpenID Provider.

  • The single-page application uses the OIDC Implicit Flow with a library that uses the web_message response mode for renewing access tokens for a better user experience.

  • The native application uses a refresh token to obtain a new access token if the previous access token has expired.

  • Each of the applications uses its own client identifier so the API can distinguish between the two clients for access control and logging.

Note

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.218.62