Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Josh Barber
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Josh Barber

Consuming Auth0-Secured WebTask.io Resources In Angular 2.4.1

By
Published in

As I've been preparing to build my first offline-first Angular 2 application with PouchDB, Auth0, and IBM Cloudant (CouchDB as a Service), I can't help but feel uneasy not having a more robust backend service on which to run arbitrary code, should I need it. And, luckily, I don't have to. WebTask.io - which powers the Rules engine for the Auth0 authorization workflow - also acts as a general Function-as-a-Service (FaaS) platform. This means that I can run arbitrary server-side code on the WebTask.io Node.js platform. And, what's even cooler is that I can use the JWT (JSON Web Token) provisioned during Auth0 authentication as a means to secure these WebTask.io resources.

Run this demo in my JavaScript Demos project on GitHub.

As we looked at in a previous post about Auth0's passwordless authentication workflow, Auth0 performs "authentication" internally; but, it then calls out to WebTask.io to perform the "authorization" workflow defined by the Rules engine. This WebTask.io system is an auto-scaling, Function-as-a-Service (Faas) platform that we can leverage outside of the Auth0 Rules engine ecosystem.

But, since WebTask.io was written by the Auth0 team as a means to power their Auth0 workflows, the two systems play very well together. In fact, we can take any WebTask.io end-point - which is public be default - and easily manage access to it using the Auth0 JWT (JSON Web Token) that we provision for a user during authentication. This way, users of our Angular 2 application can authenticate against Auth0 and then make secure HTTP calls to WebTask.io using the same authentication payload:

Auth0 JWT values can be used to secure WebTask.io resources.

To experiment with this, I wanted to create a small Angular 2 that uses passwordless authentication and then calls out to WebTask.io to generate cryptographically secure random numbers (using the Node.js Crypto module). It's a silly idea; but, it showcases the workflow nicely without too much complexity.

First, we need to create our WebTask.io resource. For this, I'm using the command-line tool that Auth0 provide as an NPM install (wt). When creating the resource, we have to provide the credentials of the Auth0 client that will be used to securely consume this resource. This way, our resource will be able to validate the JWT bearer token. Since the command is a bit complex, I wrapped it up in an executable bash script:

wt create ./secure-rand.js \
--name 'secure-rand' \
--secret AUTH0_CLIENT_ID='RuBk9q1wDso7Agh4TNIu2xv2hAmmTOdA' \
--secret AUTH0_CLIENT_SECRET='Dv_ZpGfz4UC-MnF8k***************gXE47epV8luqOaDVpj' \
--secret AUTH0_DOMAIN='bennadel.auth0.com'

echo '* * * * * * * * * * * * * * * * * * *'
echo '* CAUTION: Secret stored in UTF-8. *'
echo '* * * * * * * * * * * * * * * * * * *'

The wt command-line tool just wraps HTTP calls to the WebTask.io API. Here, we're creating a resource called "secure-rand" using the given Node.js file, "secure-rand.js". When we create the resource, we're also providing several configuration values that will be securely stored in the WebTask.io platform. In this case, the configuration values will tell WebTask.io which credentials will be needed to secure access to the provisioned resource.

It's important that we provide our WebTask.io resources with the same Client information that will be used to generate the JWT during user authentication. If we don't do this, the WebTask.io validation won't work because it won't be able to generate the correct JWT signature. As such, all requests to our WebTask.io resource will be rejected with a 401 Unauthorized response.

Now, by default, WebTask.io aren't actually secured - they're open to the public. But, when we define the WebTask.io resource, we can "wrap it" in Auth0 security as we're exporting the main executable. To do this, we have to import the "webtask-tools" NPM module and pass our invokable Function through the .auth0() middleware:

// CAUTION: We are using the "webtask-tools" module to wrap our Webtask execution with
// Auth0-based security (ie, anyone who makes this call has to be authenticated using a
// JWT from Auth0 authentication). As such, we cannot use the newer sub-domain version
// of the API:
// --
// https://wt-ben-bennadel-com-0.run.webtask.io/secure-rand
// --
// Instead, we have to use the "Run" version of the WebTask API:
// --
// https://webtask.it.auth0.com/api/run/wt-ben-bennadel-com-0/secure-rand
// --
// Notice that the "sub-domain" is part of the resource key, not the domain. This may
// change in the future; but, at the time of this writing, failing to use this form of
// the URL will result in the 400 error, "Error processing request URL path.".

// Require the core node services and libraries.
var crypto = require( "crypto" );
var webtask = require( "webtask-tools" );


// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //


// I generate a secure random 32-byte number, greater than or equal to zero.
// --
// NOTE: By default, there are a number of method signatures that we can use in Webtask
// (that give us various levels of control over the response); however, since we wrapping
// this handler with .auth0() authentication (see below), we have to use the function
// signature: ( context, request, response ).
function generateRandomNumber( context, request, response ) {

	var randomBytes = crypto.randomBytes( 32 );
	var randomNumber = Math.abs( randomBytes.readInt32BE( 0 ) );

	response.writeHead(
		200,
		{
			"Content-Type": "application/json"
		}
	);
	response.end( JSON.stringify( randomNumber ) );

	// NOTE: While we are not really using it in this demo, the decoded content of the
	// JWT (JSON Web Token) can be accessed at request.user or context.user.
	console.log( context.user );

}


// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //


// Export the function that will handle incoming requests. By default, the Webtask end-
// point is open to the public. But, by using the .auth0() method, we can ensure that
// the end-point is secured by an Auth0 JWT (the JWT is validated on each call).
module.exports = webtask.auth0(
	generateRandomNumber,
	{
		// By default, any user with a valid JWT is authorized. But, we can override the
		// authorization logic - in this case, we'll block any user that contains the
		// plus-style email (+blocked).
		authorized: function( context, request ) {

			// CAUTION: We only have "email" because of the "scope" used when generating
			// the JWT on the client.
			return( context.user.email.indexOf( "+blocked@" ) === -1 );

		},

		// This is very tricky! Apparently, as of December 2016, your Client Secret is no
		// longer stored as Base64 encoding in Auth0. However, when WebTask.io uses the
		// Client Secret to validate the JWT (JSON Web Token) signature, it expects the
		// Client Secret to be in Base64. In order to make the Copy/Paste out of Auth0
		// easier (which presents the secret in UTF-8), we're storing the secret as UTF-8;
		// then, we're encoding it as Base64 on-the-fly when we provide it.
		// --
		// Read more on this:
		// * Base64 Discussion: https://auth0.com/forum/t/client-secret-stored-without-base64-encoding/4338
		// * Base64 Requirement: https://github.com/auth0/webtask-tools/blob/ad0d7e55f45af67d4fa38d788e4faa48af057ee6/lib/auth0/auth0.js#L82
		clientSecret: function( context, request ) {

			var secretInUTF8 = context.secrets.AUTH0_CLIENT_SECRET;

			// CAUTION: In later versions of node, the Buffer constructor is deprecated.
			// However, in the version of node that runs on WebTask.io (v4.5.5 at the
			// time of this writing) this is the way to create Buffers from strings.
			return( new Buffer( secretInUTF8, "utf8" ).toString( "base64" ) );

		},

		// We need to provide a loginError() function or unauthenticated calls will
		// redirected the request to the Auth0 login portal in order to provide a login.
		// Instead, by providing an explicit handler, we can control the response, in
		// this case, returning an HTTP 401.
		loginError: function( error, context, request, response, baseUrl ) {

			response.writeHead(
				401,
				{
					"Content-Type": "application/json"
				}
			);
			response.end( JSON.stringify( "You must be authenticated to make this call." ) );

		}
	}
);

In the above WebTask.io resource, notice that our main executable - generateRandomNumber() - is being exported through a call to .auth0():

module.exports = webtask.auth0( generateRandomNumber, { /* Auth0 options */ } );

This is the middleware that will extract the JWT value from the incoming request and validate it. To be clear, WebTask.io does not make any calls to Auth0 during this validation process - it just validates the JWT value locally. It does this, in part, by regenerating the signature using the AUTH0_CLIENT_SECRET that we supplied as "--secret" option when configuring the resource. This is why it is critical that the WebTask.io resource and the Auth0 authentication workflows use the same Client information. If they don't, the regenerated signature won't match and all requests will be rejected.

At the time of this writing, Auth0 Client secrets are no longer stored in Base64 encoding (as of December 2016). But, the webtask-tools NPM module still expects the secrets to be made available in Base64 encoding. In order to bridge this expectation gap, I'm providing a configuration option that converts the plain-text secret to Base64 on-the-fly when the .auth0() middleware requests it.

NOTE: This stop-gap measure will no longer be needed once Auth0 updates the webtask-tools module.

When the .auth0() middleware is applied to our WebTask.io resource, the default behavior is to redirect unauthenticated users over to Auth0, where the requesting user has a chance to login. But, we're not going to be consuming this resource like a website - we're consuming this resource as an API from our Angular 2 application. As such, we don't want to follow any redirects. Instead, we want to return an explicit "401 Unauthorized" response. To do this, we have to provide a custom loginError() option that overrides the default redirect and manually ends the HTTP response stream with a 401 status code.

Now, to dig a little deeper into the .auth0() middleware, I'm also defining an "authorized" option which, in this case, inspects the email address of the requesting user and rejects any user that has "+blocked@" in their email. Remember, however, that this middleware isn't actually making any API calls to Auth0 during the JWT validation and authorization. As such, it can only use the email address for validation if the email address is part of the JWT payload. By default, however, it is not.

The default "scope" of the JWT produced by Auth0 is "openid". This scope only includes the following properties in the JWT payload:

  • aud: string - The Client ID of your Auth0 Client.
  • exp: number - Expiration date (UTC seconds).
  • iat: number - Issued date (UTC seconds).
  • iss: string - The URL of your Auth0 tenant.
  • sub: string - The unique identifier of the user in Auth0.

If we want to consume the user's email address during WebTask.io validation (without making a server-to-server API call back to Auth0 to gather profile information), we have to include the email in the JWT. To do this, we have to increase the scope of the JWT to include email:

scope: "openid email"

Doing this will add the following JWT payload properties:

  • email: string;
  • email_verified: boolean;

This works; but, keep in mind that it increases the size - and the HTTP bandwith requirements - for our JWT value.

Since the JWT value is provisioned as part of our Angular 2 application (during the Auth0 passwordless authentication workflow), it's our client-side library that has to ask for the expanded JWT scope. In our Angular 2 AuthenticationService, we can do this by providing a "scope" property in our auth0.verifyEmailCode() API call:

// Import the core angular services.
import { Injectable } from "@angular/core";
import * as Auth0 from "auth0";

export interface IAuthorization {
	accessToken: string;
	idToken: string; // JWT token.
	idTokenPayload: { // Parsed JWT content.
		aud: string; // The audience. Either a single case-sensitive string or URI or an array of such values that uniquely identify the intended recipients of this JWT. For an Auth0 issued id_token, this will be the Client ID of your Auth0 Client.
		exp: number; // Expires at (UTC seconds).
		iat: number; // Issued at (UTC seconds).
		iss: string; // The issuer. A case-sensitive string or URI that uniquely identifies the party that issued the JWT. For an Auth0 issued id_token, this will be the URL of your Auth0 tenant.
		sub: string; // The unique identifier of the user. This is guaranteed to be unique per user and will be in the format (identity provider)|(unique id in the provider), e.g. github|1234567890.

		// These are here because I added "scope: email" to the authentication request.
		email: string;
		email_verified: boolean;
	};
	refreshToken?: string; // Optional, if the offline_access scope has been requested.
	state?: any; // Echoed value for cross-site request forgery protection.
}

export class AuthenticationService {

	private auth0: any;


	// I initialize the Authentication service.
	constructor() {

		this.auth0 = new Auth0({
			domain: "bennadel.auth0.com",
			clientID: "RuBk9q1wDso7Agh4TNIu2xv2hAmmTOdA", // JavaScript Demos Client.
			responseType: "token"
		});

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I send a one-time use password to the given email address.
	public requestEmailCode( email: string ) : Promise<void> {

		var promise = new Promise<void>(
			( resolve, reject ) : void => {

				this.auth0.requestEmailCode(
					{
						email: email
					},
					( error: any ) : void => {

						error
							? reject( error )
							: resolve()
						;

					}
				);

			}
		);

		return( promise );

	}


	// I log the user into the application by verifying that the given one-time use
	// password was provisioned for the given email address.
	public verifyEmailCode( email: string, code: string ) : Promise<IAuthorization> {

		var promise = new Promise<IAuthorization>(
			( resolve, reject ) : void => {

				this.auth0.verifyEmailCode(
					{
						email: email,
						code: code,

						// When authenticating, the default scope is "openid". But, if
						// we add "email", then we'll get the "email" as part of the
						// actual JWT (JSON Web Token) payload. This way, when we pass
						// the JWT to WebTask.io, we'll be able to validate BOTH the
						// JWT and the email address of the authenticated user.
						scope: "openid email"
					},
					( error: any, result: IAuthorization ) : void => {

						error
							? reject( error )
							: resolve( result )
						;

					}
				);

			}
		);

		return( promise );

	}

}

As you can see, when calling the underlying auth0.verifyEmailCode() method, I'm passing-in a "scope" property in addition to the "email" and "code" authentication credentials.

To tie this all together, I then created a root component in my Angular 2 application that makes an HTTP call to this WebTask.io resource. The Auth0 JWT value is applied to this outgoing request as part of the "Authorization" header. This way, we can see what kind of response we get both before and after authenticating with the Auth0 service.

NOTE: You can also pass JWT tokens to WebTask.io using the "access_token" query-string parameter. But, generally speaking, you should avoid storing session and authentication values in the query-string as these values are much more likely to be logged by monitoring tools (as opposed to HTTP Header values).

// Import the core angular services.
import { Component } from "@angular/core";
import { Headers } from "@angular/http";
import { Http } from "@angular/http";
import { Response } from "@angular/http";

// Import the application components and services.
import { AuthenticationService } from "./authentication.service";
import { IAuthorization } from "./authentication.service";

@Component({
	moduleId: module.id,
	selector: "my-app",
	styleUrls: [ "./app.component.css" ],
	template:
	`
		<p>
			<strong>Email:</strong>
			<input type="text" [(ngModel)]="email" />
			<input type="button" value="Send Email" (click)="sendEmail()" />
		</p>

		<p>
			<strong>Code:</strong>
			<input type="text" [(ngModel)]="code" />
			<input type="button" value="Verify Code" (click)="verifyCode()" />
		</p>

		<!-- Only show the JWT once the user has logged-in. -->
		<template [ngIf]="jwt">

			<p>
				<strong>JWT:</strong> {{ jwt }}
			</p>

			<p>
				<a (click)="logout()">Logout</a>.
			</p>

		</template>

		<hr />

		<p>
			<strong><a (click)="generateNumber()">Generate random number</a></strong>:
			{{ randomNumber }}
		</p>

	`
})
export class AppComponent {

	public code: string;
	public email: string;
	public jwt: string;
	public randomNumber: string;

	private authenticationService: AuthenticationService;
	private http: Http;


	// I initialize the component.
	constructor(
		authenticationService: AuthenticationService,
		http: Http
		) {

		this.authenticationService = authenticationService;
		this.http = http;

		this.code = "";
		this.email = "";
		this.jwt = "";
		this.randomNumber = "";

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I consume the WebTask.io for generating cryptographically secure random numbers.
	public generateNumber() : void {

		this.http
			.get(
				"https://webtask.it.auth0.com/api/run/wt-ben-bennadel-com-0/secure-rand",
				{
					// Because the WebTask.io resource is protected by Auth0, we have
					// to include our Auth0 JWT (JSON Web Token) in the Authorization
					// header. WebTask will validate the token to ensure that it is
					// still valid.
					// --
					// NOTE: We could have also included it in the query-string parameter
					// "access_token"; but, the header is recommended to prevent it from
					// being logged (or logged less easily).
					headers: new Headers({
						"Authorization": `Bearer ${ this.jwt }`
					})
				}
			)
			.subscribe(
				( response: Response ) : void => {

					this.randomNumber = response.json().toString();

				},
				( response: Response ) : void => {

					console.warn( "Error while generating random number." );
					console.error( response );

					this.randomNumber = response.json
						? response.json()
						: "Something went wrong."
					;

				}
			)
		;

	}


	// I log out, clearing the JWT (JSON Web Token) that is used when invoking the
	// WebTask.io secured resources.
	public logout() : void {

		this.code = "";
		this.jwt = "";
		this.randomNumber = "";

	}


	// I send the one-time use password to the currently-entered email address. The
	// one-time password is valid for about an hour.
	public sendEmail() : void {

		// Since we're sending a new authentication code, log out of the current
		// session so that we can reset to a pristine state.
		this.logout();

		this.authenticationService
			.requestEmailCode( this.email )
			.then(
				() => {

					console.log( "Email sent (with one-time use code)." );

				}
			)
			.catch(
				( error: any ) : void => {

					console.error( error );

				}
			)
		;

	}


	// I log the current user into the application using the currently-entered email
	// address and the one-time use token.
	public verifyCode() : void {

		this.authenticationService
			.verifyEmailCode( this.email, this.code )
			.then(
				( authorization: IAuthorization ) : void => {

					console.group( "Verify Email Code / Authorization Result" );
					console.log( authorization );
					console.groupEnd();

					// The user's session is essentially defined by the existence of a
					// valid and unexpired JWT (JSON Web Token). This is the value that
					// we will need in order to consume Auth0-secured WebTask.io resources.
					this.jwt = authorization.idToken;

				}
			)
			.catch(
				( error: any ) : void => {

					console.warn( "Something went wrong!" );
					console.error( error );

				}
			)
		;

	}

}

Now, if we run this code, authenticate with Auth0 using the passwordless login, and then make a request to WebTask.io, we can see that the JWT token is passed in the request:

Auth0 JWT values can be passed as Authorization headers when making WebTask.io calls.

And, as you can see, the JWT token that we received during the Auth0 passwordless authentication was passed through the WebTask.io resource as part of the Authorization header. This header value was then automatically parsed and validated by the webtask-tools middleware (based on our configuration), at which point the request was allowed to proceed and the cryptographically secure random number was generated and returned to the client.

Now, if you recall from above, not all users are allowed to access this resource simply because the have an Auth0 JWT. In our WebTask.io Auth0 middleware configuration, we're blocking any user that has "+blocked@" in their email address. To demonstrate that we can use this fine-grained control, I'm going to re-authenticate with a "bad" email address and try to access the WebTask.io end-point:

We can block specific email addresses in WebTask.io, even if the user has a valid JWT value.

As you can see, even though I have a valid Auth0 JWT value, provisioned during authentication, I still get rejected from the WebTask.io resource.

This is pretty cool stuff. I don't really have a good mental model for how "Functions" can be aggregated to provide larger-scale, cohesive functionality especially without modules that can be shared across Functions. But, at the very least, for small tasks, this seems powerful. And, it will make me feel much more comfortable creating an offline-first application since I know I can always fallback on some server-side logic in WebTask.io.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel