Security

Securing Angular+Node.js Applications using App ID

Share this post:

One of the most common architectures of modern applications is Single Page Applications (SPAs), where a single HTML page interacts with a backend application via JavaScript to dynamically generate its content. In this blog, we look at how a single page application can integrate authentication and authorization via IBM Cloud App ID. We consider a single page application with Angular frontend and Node.js backend that:

  • Authenticates the user using App ID
  • Once Authenticated provides access to protected resources on the backend

App ID allows developers to easily add authentication, authorization and user profile services to apps and APIs running on IBM Cloud. With App ID SDKs and APIs, you can get a sign-in flow working in minutes, enable social log-in through Google and Facebook, and add email/password sign-in using App ID Cloud Directory. The App ID User Profiles feature can be used to store information about your users, such as their preferences. In short, App ID enables your application to be used only by authorized users and that authorized users have access to only what they should have access to. The experience is custom, personalized and most importantly, secure.

What about non Angular based Single Page Applications?

Although we are using Angular to implement our frontend, the architecture and concepts that we cover in this blog are agnostic of the language and can be supported by other frameworks such as React, Backbone.js, and Ember.js.

Note : You can download the code to our sample SPA application.

Security Considerations

While integrating user authentication into an application, it is critical to consider the trust level and threat surface of the different components of the application. This affects how and where confidential data such as secrets and user data are stored.

A single page application, usually in HTML and JavaScript is executed on the user’s browser and the end user has complete access to view and modify the code. The application cannot keep secrets from malicious users and hence any secrets or logic that are stored on the frontend should be considered as insecure.

In contrast, a backend application is hosted on a secure remote server and only visible to the external user via controlled APIs. Because of this restricted access, any secret data or logic that is hosted on the server-side can be considered trusted.

This trust model makes it clear that:

  • No sensitive data should be stored on the frontend i.e., in the SPA
  • Sensitive data should be stored only on the backend
  • Frontend should obtain sensitive data from the backend if and only if necessary and only after authentication and authorization.

Our Sample Cloud Land Application

The sample application that we will be building today is a fictional Cloud Land application. It allows a user to sign in to the application with a social identity provider, such as Google and Facebook, or use email to sign-in using App ID’s Cloud Directory. Once the user is signed in, it displays basic information about the user and also their Cloud Land reward points. Each user’s reward points can be accessed only by that user.  To summarize, our application will use App ID to personalize the user experience and control the application’s access to the user data (i.e., reward points). Click on the link below to download all the sample code. Refer to the README.md file in the downloaded zip file for instructions to run the Cloud Land Application.

Download Cloud Land Application Code

Architecture of a Single Page Application with App ID

At a high-level, our Cloud Land application consists of an Angular based SPA and a Node.js REST API running on separate servers. The SPA makes Ajax requests to the Node.js REST API to access resources. The user must be authenticated before providing access to protected resources.

To handle user authentication, we will integrate App ID with our Node.js backend. App ID makes it easy to add authentication, authorization, and user profile services to applications with several SDKs it offers. Specifically, to authenticate a user, App ID establishes an OIDC/OAuth2 Authorization code flow with the identity provider, e.g., Google. For more information on setting up App ID, please refer to the App ID documentation.
Architecture of a Single Page Application with App ID

In the next section, let’s see how we can authenticate a user from the frontend and ultimately provide access to protected resources on the backend to authenticated users.

Authenticating users with App ID

App ID provides a customizable login widget for user authentication. Please follow this tutorial on how to customize your App ID login widget. When a user wants to sign in, they are redirected to the App ID login widget. The user can then choose to authenticate against an identity provider. Once the authentication flow is complete, the application obtains both an App ID access token as well as an App ID identity token. The tokens are formatted as JSON Web Tokens (JWTs). In addition to access and identity tokens, based on your App ID configuration, refresh tokens are also shared with the application.

  • Refresh Tokens : Refresh tokens are optional and is used to obtain new access and identity tokens without prompting the user to re-authenticate. These refresh tokens are shared with the backend application by the App ID SDK. Due to the sensitivity of the refresh token, it must never be shared with the frontend.
  • Access token: Access tokens represent authorization and conforms to JavaScript Object Signing and Encryption (JOSE) specifications. These tokens contain information related to authorization between the identity provider and App ID. The access token payload may contain information such as subject of the authentication, token expiration time, issued at time, authentication method used to obtain the token, scopes etc.
  • Identity token: Identity tokens represent authentication and contain information about the user. It provides information such as name, email, profile picture about the authenticated user. For more information regarding access and identity tokens, refer to the App ID Documentation.

Since refresh tokens, access token and identity token all contain sensitive information (in order of priority from most to least), it is important that they are protected both in transit and at rest. It is a good practice for these tokens (especially the refresh token) to be stored only the trusted modules of the app, such as the backend. Developers must consider the tradeoff between security and token usage when transferring the App ID access tokens to untrusted modules such as the frontend.

Session Based Authentication

Now that we have authenticated the user and obtained the tokens, we need to restrict access to the protected resources only to authenticated users. This can be achieved in several ways either by using cookies, sessions, or tokens. The below sections describe more about how it can be achieved.

Once our application has obtained access and identity tokens from App ID after user authenticates with the identity provider, the backend can setup a session for the user and return the session id to the frontend. On all subsequent requests from the user to access protected resources, the user session can be verified at the backend before granting access to the resources. While using sessions, it is extremely important to keep in mind that session cookies are susceptible to CSRF attacks. Luckily, there are a multitude of mitigation strategies available to prevent CSRF attacks. Discussing those strategies here is in itself a whole new ballgame and beyond the scope of this blog.

For the purposes of the sample application, we will be using Node.js Express session management to keep track of the authenticated users. More robust architectures can use session stores to store and manage user sessions.

An alternative and stateless method of managing the interaction between frontend and backend is through the use of tokens. In this method, upon authenticating the user, the backend will generate and return a secure token to the frontend. The frontend then includes the token in all subsequent requests, and the backend verifies the authenticity of the token before responding to the request.

Retrieving User Info in Frontend

As mentioned earlier, App ID returns the user information in the identity token. Although it is possible for the backend to simply pass the identity token to the frontend, the frontend accesses this information by calling an API in the backend. The backend can pick and choose what information in the identity token it shares with the frontend in the form of a JSON response to an API in the backend.

In the sample application that we will build today, the frontend obtains the user identity information stored in Express sessions, extracted from the identity token, by calling an endpoint. This endpoint checks if the user is authenticated by checking if a user session is present in Express and ultimately returns a JSON payload that includes the user authentication state. If the user is authenticated, the payload will also include the user identity information such as name and email.

App ID User Authentication Sequence Flow

Now that we understand the basic concepts, let’s see how a single page app with an Angular frontend and a Node.js backend can be integrated with App ID. Before we dive into some code snippets, let’s briefly review the Cloud Land application’s user authentication flow with App ID.

Note: All the authentication flow in the box labeled “User Authentication Provided by App ID” is fully managed by App ID SDKs behind the scenes.

 

  1. User requests a protected resource, in our case the Cloud Land rewards points endpoint. If the user is not signed in, the application prompts the user to sign in to view any protected resources/access endpoints.
  2. User is redirected to the App ID login widget; the user chooses the identity provider they wish to authenticate against. The browser is then redirected to the identity provider login website.
  3. Upon successful user authentication with identity provider, the identity provider redirects back to the App ID callback url with the authorization code. App ID exchanges the authorization code for the access and identity tokens and redirects the user back to the application callback url registered with the App ID and a session is setup in express.
  4. On every successive call to the backend, the user’s authentication is verified by looking up the session associated with the user, if any.
  5. If the user is authenticated, the requested resource is sent back to the user.

Implementing the flow

Let’s start by securing the backend first:

  • Load all the required dependencies express, express-sessions, App ID SDK, passport.js, helmet
const express = require("express");
const session = require("express-session");
const passport = require("passport");
const nconf = require("nconf");
const appID = require("bluemix-appid");

const helmet = require("helmet");
const express_enforces_ssl = require("express-enforces-ssl");
const cfEnv = require("cfenv");
const cookieParser = require("cookie-parser");
const cors = require("cors");

const WebAppStrategy = appID.WebAppStrategy;
const userAttributeManager = appID.UserAttributeManager;
const UnauthorizedException = appID.UnauthorizedException;
  • Setup an explicit login end point, for the App ID authentication. Configure passport to use App ID WebAppStrategy and specify the success redirect URL to the client application. More information on setting up App ID can be found here.
let webAppStrategy = new WebAppStrategy(config);
passport.use(webAppStrategy);

app.get("/auth/login", passport.authenticate(WebAppStrategy.STRATEGY_NAME, {successRedirect : SUCCESS_REDIRECT_URL, forceLogin: true}));

// Callback to finish the authorization process. Will retrieve access and identity tokens/
// from AppID service and redirect to either (in below order)
// 1. the original URL of the request that triggered authentication, as persisted in HTTP session under WebAppStrategy.ORIGINAL_URL key.
// 2. successRedirect as specified in passport.authenticate(name, {successRedirect: "...."}) invocation
// 3. application root ("/")

app.get(CALLBACK_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, {allowAnonymousLogin: true}));
  • Lock down the /user/rewardpoints protected backend service as show below. If the user is not authenticated return back a 401 response.
function isLoggedIn(req, res, next) {
	if(req.isAuthenticated()) {
        next();
    } else {
        res.sendStatus(401);
    }
}

app.use('/user/rewardpoints', isLoggedIn);
  • Expose an endpoint to check if the current session is authenticated and to retrieve the user profile once logged.
app.get('/auth/logged', (req,res) => {

    let loggedInAs = {};
    if(req.isAuthenticated()) {
        loggedInAs['name'] = req.user.name;
        loggedInAs['email'] = req.user.email;
    }
    
    res.send({
        logged: req.isAuthenticated(),
        loggedInAs: loggedInAs
    })
});

 

Next, we need to verify on the client side, with the backend/server the user is actually authenticated before allowing access to any protected resources or routes.

Note: This blog post uses Angular for the Cloud Land application

  • Implement an Authentication service to verify with the server if the user is actually authenticated. If authenticated, it also retrieves the user profile, which contains a subset of attributes from the identity token.
@Injectable()
export class AuthService {

  userState: UserState;

  constructor(private http: HttpClient) { }

  public isAuthenticated(): Observable<UserState> {
    
    if(typeof this.userState === 'undefined') {
      return this.checkAuthenticated().do(data => {
        this.userState = data;
      });
    } else {
      
      return Observable.of(this.userState);
    }
    
  }


  private isLoggedUrl = "http://localhost:3000/auth/logged";
  checkAuthenticated(): Observable<UserState>{
    return this.http.get<UserState>(this.isLoggedUrl, {withCredentials: true});
  }
}
export class UserState {
    logged: boolean;
    loggedInAs:{
      name: string;
      email: string;
    }
  }
  • Let’s create a Cloud Land home component, which prompts the user to sign in if they are not authenticated and finally display their profile information and their Cloud Land rewards point when they sign in.
@Component({
  selector: 'app-cloudland-home',
  templateUrl: './cloudland-home.component.html',
  styleUrls: ['./cloudland-home.component.css'],
  providers: [RewardPointsService]
})
export class CloudlandHomeComponent implements OnInit {

  constructor(private authService : AuthService, private rewardsService : RewardPointsService) { }

  userState : UserState;
  cloudLandRewardPoints : number;

  ngOnInit() {
    this.getUserInfo();
    this.getCloudLandRewardPoints();
  }


  getUserInfo() {
    this.authService.isAuthenticated()
        .subscribe(user => this.userState = user);
  }

  getCloudLandRewardPoints() {
    this.rewardsService.getCloudLandRewardPoints()
        .subscribe(points => this.cloudLandRewardPoints = points);
  }

}
  • Create a reward points service, that fetches points from the Cloud Land rewards endpoint.
@Injectable()
export class RewardPointsService {

  private cloudLandRewardPointsURL = "http://localhost:3000/user/rewardpoints";

  constructor(private http: HttpClient) { }

  getCloudLandRewardPoints() : Observable<number> {
    return this.http.get(this.cloudLandRewardPointsURL, {withCredentials: true}).map((res:Response) => res['points']);
  }

}

 

Check out another one of our blogs which describes a simpler yet less scalable architecture for single page apps. In contrast to Cloud Land’s architecture where the frontend and backend are running on separate servers, this blog’s architecture serves the frontend and the backend from the same server.

For more articles on App ID, check out IBM Cloud App ID archives here.

More Security stories

Certificate Manager now Sends you Notifications before your Certificates Expire

Even the most successful or genius apps can fail if there are issues with availability. While development teams often engineer for availability, with lots of redundancy, health checks, and load balancing, sometimes outages occur because of simple human errors. One common error is that teams fail to renew SSL/TLS certificates on time.

Continue reading

IBM Cloud Platform now adds support for Multi-Factor Authentication

In April 2018, IBM Cloud Platform added support for Multi-Factor Authentication (MFA). This adds an extra layer of security to users’ accounts by requiring all users to provide a time-based one-time passcode in addition to their standard IBMid and password when logging in. Having this option enables IT admins to rest a little easier knowing that they’re protecting their company’s network and workloads while keeping access flexible and easy.

Continue reading

Cloning an App ID instance configuration

With App ID, you can now manage your service instance with an API!

Continue reading