Angular 2 and OpenID Connect with Azure Active Directory

Scot Hillier

by Scot Hillier on 2/9/2016

Share this:
Print

Article Details

Date Revised:
2/10/2016

[IF condition="s1.Length > 0" strings="Angular 2Angular 2 SPA using AAD as the authentication server AuthenticationAuthN using Angular 2AuthN/AuthZ/delegation patternsAzure Active DirectoryDevelopmentIdentity ManagementJavascript and jQuerykey identity topicsOffice 365Open Authentication (OAuth) Implicit Flowsecurity"]

Applies to:
Angular 2, Angular 2 SPA using AAD as the authentication server , Authentication, AuthN using Angular 2, AuthN/AuthZ/delegation patterns, Azure Active Directory, Development, Identity Management, Javascript and jQuery, key identity topics, Office 365, Open Authentication (OAuth) Implicit Flow, security

[ENDIF]

http://itunity.com/c/X3


As I continue my series on Angular 2 beta, I want to begin to move into key identity topics. For this article, I want to define three fundamental security terms right away: authentication, authorization, and delegation. Authentication (AuthN) is the mechanism for securely determining the identity of an application user. Authorization (AuthZ) is the mechanism for determining the level of access that user has to the application. Delegation is the mechanism for allowing a service or application to act on behalf of a user.

Because Angular 2 applications are built as single-page applications (SPA) that access back-end web services, AuthN, AuthZ, and delegation concepts are often considered together in technical articles. However, I’m going to limit my coverage of AuthZ and delegation in this article to discussion-only so that I can focus the code sample on the details of AuthN using Angular 2. You might be surprised how much is involved in simply logging into such an application. The sample I’m using for this article is available in the IT Unity GitHub repo.

Reviewing common anti-patterns

AuthN/AuthZ/delegation patterns have historically been poorly understood and difficult for application developers to properly implement. An excellent example that is still valid today is the “double-hop” problem. One variant of double-hop is a failed attempt by an ASPX page to access resources on a server outside the current Internet Information Services (IIS) server. In this scenario, an ASPX page is accessed by a user, who is properly authentication by NTLM and impersonated on the IIS server. The code behind the page then attempts to call a web service or database. Developers expect the call to be made with the current-user’s credentials – which seems reasonable – but that does not happen because NTLM is not trusted for delegation with those credentials. So, the call fails. At this point, things get ugly as developers have often solved the problem by granting elevated permissions directly to pool accounts in IIS, which opens a huge security hole. The correct answer to the problem is to implement Kerberos constrained delegation, which is far beyond the scope of this article.

While many Office and SharePoint developers have experience with the double-hop problem, AuthN/AuthZ/delegation patterns are also common outside the firewall. I recently consulted on a problem where an Outlook add-in running outside the firewall was attempting to reach resources inside the firewall through Active Directory Federated Services (ADFS). The security code in the add-in was written for Windows authentication and the team did not understand why it wasn’t working. So, they relaxed security by sending credentials in plain text across the Internet, which opened a huge security hole. The correct answer in this case was to implement a Web API gateway, which you can read about in a nice article by Bob German.

Along with these examples, AuthN/AuthZ/delegation patterns are also important in cloud-based development. There are many times when we want to delegate permissions to another web application or service. A classic example is the scenario where you want Facebook to Tweet for you every time you update your status. As in all the examples, this was historically accomplished by relaxing security. Specifically, apps like Facebook would simply ask for your Twitter credentials. Like all these scenarios, it works, but opens up a huge security hole. The correct answer in the cloud is to implement Open Authentication (OAuth), which I discuss in the next section.

The point of these anti-patterns is to show that in our drive to get applications to work, we often circumvent security. Such anti-patterns are never good, but as developers write more code that runs outside the firewall, these practices become downright dangerous. For the Office 365 and SharePoint online developers using Angular 2, everything starts by understanding the “implicit flow” variant of OAuth.

Understanding OAuth Implicit Flow

OAuth is an important mechanism for allowing delegation to an app or service without the user having to give up credentials. OAuth works by requesting tokens from an authorization server that can be presented to a protected resource server. Typically, an OAuth discussion like this one is accompanied by detailed flow diagrams showing the intricacies of the OAuth “dance”, but I’m going to stick to the highlights here and add some more detail when I discuss the code. If you want an entire book on the topic, I recommend Modern Authentication with Azure Active Directory for Web Applications by Vittorio Bertocci, who is a principal program manager on the Azure Active Directory team.

The key thing to understand is that OAuth was originally created as an authorization protocol. However, during the authorization process, it’s possible to get information about the current user. Because of this, many developers began to leverage OAuth as an authentication protocol. In an effort to standardize the authentication process, the OpenID Connect protocol was created as a layer on top of OAuth. For this article, I am going to implement OpenID Connect for authentication. I’ll implement OAuth for authorization in a later article. So, for the rest of the article, I’m just going to focus on OpenID Connect.

OpenID Connect recognizes that web applications can have different architectures. In some cases, a page might post back to the server; in other cases JavaScript may be calling web services from the client. Resources may exist within the same boundary or across security boundaries. All of this means that the specifications define different “flows” that may be utilized by different architectures.

For developers using Angular 2, the typical architecture is a SPA calling RESTful web services. In a Microsoft world, both the user and the SPA are registered with Azure Active Directory (AAD). OpenID Connect defines a specific flow called “implicit flow” that recognizes JavaScript-based frameworks like Angular 2 cannot protect any secret information so they need a simplified flow to handle AuthN. From the OpenID Connect Implicit Client Implementer's Guide, you can get the steps that define implicit flow. The following is the set of steps for an Angular 2 SPA using AAD as the authentication server. These are the steps I will implement in the sample code.

  1. Angular 2 SPA prepares an authentication request
  2. Angular 2 SPA sends the request to AAD.
  3. AAD presents the user with a login screen and authenticates
  4. AAD obtains user consent for delegation
  5. AAD sends the user back to the Angular 2 SPA with an ID token
  6. Angular 2 SPA validates the tokens

Registering users and applications

Implicit flow is dependent upon the registration of both the user and the application with AAD. If you have an Office 365 tenant, then you already have an instance of AAD dedicated to that tenant. All of your Office 365 users will already have accounts in this instance. Figure 1 shows a view of a typical directory in the Azure portal.


Figure 1, User accounts in AAD

Once you have a directory instance, you can register applications as well. The steps for registering a web-based application are well-presented in this MSDN article so I will not repeat them here. What’s important to understand is that implicit flow is not allowed by default for a registered application. So, you must execute an additional set of steps to allow implicit flow.

Once the application is registered, you can go to the Configure tab and click the Manage Manifest button shown in figure 2, which will allow you to download a JSON manifest file. Within the file is an entry named oauth2AllowImplicitFlow, which should be changed from false to true. Then you can upload the manifest back into the application definition.


Figure 2, Managing manifests

Creating the Angular 2 SPA

Once the users and application is registered in the directory and implicit flow is enabled, you are ready to create the Angular 2 SPA. For this article, I’m going to follow the steps for implicit flow and show how each step is implemented. The end result is an application that authenticates the current user and presents a welcome message as shown in figure 3.


Figure 3, Welcoming the authenticated user

Preparing an authentication request

The first step is preparing the authentication request. The request will be a GET request made to the authorization endpoint advertised by AAD. From the configuration page associated with the application definition in AAD, you can click View Endpoints and view a dialog with several endpoints including the OAuth 2.0 Authorization Endpoint, which is the one to use with OpenID Connect. The endpoint takes the following form:

https://login.microsoftonline.com/{tenantId}/oauth2/authorize

The authentication request must include a set of query parameters that specify how the request should be handled. In my Angular 2 SPA, I prepare the request using the method shown in Listing 1. Table 1 defines the parameters in the request. All of this code is contained within a service named AuthContext, which manages the state of authentication for the app.

Listing 1, Preparing the authentication request

public getSignInEndpoint(): string {
    let endpoint =
        "https://login.microsoftonline.com/" +  
        this.tenantId + "/oauth2/authorize?" +
        "response_type=id_token&" +
        "response_mode=fragment&" +
        "client_id=" + this.clientId + "&" +
        "redirect_uri=https://localhost:44330/&" +
        "scope=openid&" +
        "state=" + this.state + "&" +
        "nonce=" + this.nonce;
        return endpoint;
    }

Table 1, Authentication parameters

Parameter  value  description 
 response_type  id_token Requests an authentication token 
 response_mode  Fragment Requests the token be returned as a URL fragment 
 client_id  GUID The client identifier from the application registration configuration page 
 redirect_uri  https://localhost:44330 The entry point to the SPA application 
 scope  openid Requests access to user authentication information 
 state  string A value used to maintain state between the call to AAD and the return to the SPA 
 nonce string A value used to help validate that the returned token was not altered maliciously 

Sending the request to AAD

The next step is to send the request to AAD, which is accomplished by redirecting the browser to the authentication endpoint previously prepared. In the Angular 2 SPA, I have a simple log-in button that performs a redirect using the following code.

logIn() { window.location.href = this.authContext.getSignInEndpoint(); }

It’s not very SPA-like to use a hard redirection, but you really have no choice. AAD presents its own log-in screen that you are leveraging to perform user authentication. Before redirecting, however, I establish a session and save it in web storage so that I can reconstitute it when the user returns to the app. For the sample, I am using the state value as a key for the session information. I then store the nonce value in that session before redirecting as shown in the following code:

//create a new session
if (!window.location.hash) {
    this.state = this.createNonce();
    this.nonce = this.createNonce();
    sessionStorage[this.state] = this.nonce;
}

Authenticating the user to AAD

Upon redirect, the user will be presented with the log-in screen from AAD. Here, the user simply enters their credentials. AAD authenticates them. If our SPA wanted to request an access token for authorization, AAD would also ask for the user to consent to this delegation. In this case, I am just investigating authentication, so no consent will appear. Once complete, the user is redirected back to the application.

Sending the user back to the SPA

After authentication, the user is redirected back to the URL specified by the redirect_uri parameter. The redirect URL will contain the token as a fragment appearing after a hash. The state value is also returned so that you can reconstitute the state of the app. The following is an example of the redirect from the sample application.

https://localhost:44330/#id_token=eyJ0e...K4Q&state=ek3y5HhPESxnPp9HfCqtRVrEWGAxezSU&session_state=26d1b7da-3530-429b-9ae6-c7a3b69783df

The ID token is returned as a base 64 encoded string with 3 parts. Each part is delimited by a period. The first part is the header, the second parts contains user claims, and the third part is the signature applied by AAD. Each part of the token will be validated by the SPA as part of the authentication process. Once authenticated, you can use the claims about the user knowing they are valid.

Validating the token

The most significant part of the authentication flow is the token validation process. The OpenID Connect Implicit Client Implementer's Guide specifies how the ID token should be validated, which involves validating parts of the token as well as its cryptographic signature. The data to be validated includes the state, nonce, issuer, audience, and expiration. In order to perform the token validation, I created a validation service that can be called when the user returns from the authentication process with AAD. Listing 2 shows the main code that performs the validation.

Listing 2, Validating the ID token

public validateToken(tenantId: string, clientId: string, state: string, nonce: string, token: string, tokenHeader: TokenHeader, tokenClaims: TokenClaims): Observable<boolean> {

        return Observable.create(observer => {

            //validate state
            if (typeof (state) === 'undefined') {
                console.error(`invalid state: ${state}`);
                observer.next(false);
                return;
            }

            //validate nonce
            if (sessionStorage[state] !== nonce) {
                console.error(`invalid nonce: ${nonce}`);
                observer.next(false);
                return;
            }

            //validate issuer
            if (tokenClaims.iss !== "https://sts.windows.net/" + tenantId + "/") {
                console.error(`invalid issuer: ${tokenClaims.iss}`);
                observer.next(false);
                return;
            }

            //validate audience
            if (tokenClaims.aud !== clientId) {
                console.error(`invalid audience: ${tokenClaims.aud}`);
                observer.next(false);
                return;
            }

            //validate expiration and signature
            this.validateSignature(token).subscribe(
                validated => {
                    if (validated) {
                        observer.next(true);
                    }
                    else {
                        observer.next(false);
                    }
                },
                err => {
                    console.error(err);
                    observer.next(false);
                },
                () => { observer.complete(); }
            );

            return;

        });

    }

The service takes all of the key pieces of data from the token and the session state and compares them for validity. It first checks the state value returned in the fragment to make sure it corresponds with the information that was saved in web storage. The same thing is done with the nonce value returned in the token. These checks help to ensure that the token received was not altered between AAD and the SPA.

Next, the issuer claim and audience claim in the token are validated. The issuer is checked to make sure the token actually came from AAD. The audience claim is compared to the client ID of the registered app to make sure the values match.

Finally, the token expiration is checked along with the signature. To accomplish this task, I am using a third-party library named jsrsasign, which takes an X509 certificate and the token signature as inputs and then validates the token signature. Listing 3 shows the method I created in the SPA to perform signature validation.

Listing 3, Validating the signature

public validateSignature(token): Observable<boolean> {
    /* Retrieve from federated metadata endpoint.
    In this sample, the document was downloaded locally */
    return this.httpService.get("metadata/metadata.xml")
        .map((res: Response) => {
            let dom = (new DOMParser()).parseFromString(res.text(), "text/xml");
            let json = xml2json(dom, "");
            let cert = "-----BEGIN CERTIFICATE-----" + 
            JSON.parse(json).EntityDescriptor[0]["ds:Signature"]
                ["KeyInfo"]["X509Data"]["X509Certificate"] + 
            "-----END CERTIFICATE-----";
            let key = KEYUTIL.getKey(cert);
            return KJUR.jws.JWS.verifyJWT(token, key, { alg: ['RS256'] });
        })
    }

In order to validate the signature, you must first download the latest certificates from the Federation Metadata endpoint associated with your tenancy. For this sample, I downloaded the certificates as an XML file and dropped it into my project. I then use the Angular 2 Http service to load the XML document. I then parse the certificate out of the document. Validation is done by the verfiyJWT method that takes the certificate and the token hash and returns a boolean. Once this final validation is passed, the authentication process is complete.

Welcoming the user

In order to welcome the user, you can simply use the claims contained within the ID token. One of the things that OpenID Connect standardized was the set of claims available within the ID token. This way you have some assurance of the available claims regardless of the identity provider you are using. In the sample, I use the jsrsasign library to parse the returned token and access the claims. Listing 4 shows the code.

Listing 4, Retrieving user claims

let tokenClaims: TokenClaims =
    KJUR.jws.JWS.readSafeJSONString(b64utos(token.split('.')[1]));
this.fullName = tokenClaims.name;

Summary

This article was written to focus on OpenID Connect and ID token validation. OpenID Connect specifies the claims that are present in the ID token so that our client can reliably use the information for authentication and basic user profile data. For Angular 2, we made use of implicit flow to retrieve an ID token and then performed the required token validation steps. In future articles, I’ll expand on this idea to include the use of access tokens for accessing protected resources.


Topic: Development

Sign in with

Or register

  • With regards to token visibility, there is no issue in OpenIDConnect with the token id being visible. Remember, that this token is intended to be used just to validate that the user has properly signed in.

    When using the same approach with OAuth (called 'implicit flow'), it is the job of the service receiving the token to validate it.

    Regarding renewal, yes, there is a refresh token available as well.
  • Hey, Thanks for your great tutorial. Is it possible to renew the token without the need to enter the credentials again?
  • Thanks for taking the time to put together this example. I do have a question with regards to the storage of the ClientId and AppSecret. Where are these pieces of data stored so that the client cannot see those values. Since (as I understand it) Angular runs in the browser anyone can review the source code and see these values. Is there a way that you can keep them safely tucked away on the server?