Associating Auth0 Rules With Specific Auth0 Clients In Angular 2.4.1
In an earlier post, I looked at the Auth0 authentication and authorization workflow for passwordless authentication. In that workflow (and all Auth0 authentication workflows), first the user is authenticated; then, for authorization, Auth0 runs the user through a Rules engine on WebTask.io. Every Rule in the Rules engine is invoked for every login transaction; but, we can - more or less - skip certain Rules based on the Client ID of the client making the request.
Run this demo in my JavaScript Demos project on GitHub.
When you configure Rules in Auth0, you give them an order of execution (ie, priority) and you turn them on or off. Other than that, each Rule is invoked, in series, for every single login transaction, regardless of which Client is initiating the login workflow. But, in an application that has multiple clients (ex, Mobile and Desktop), not every client needs the same type of user authorization.
To get around this, we can check the Client ID of the request context and use it to determine if we want to short-circuit the current Rule, moving on to the next Rule in the Rules engine. To demonstrate this, I've created two new Clients in my Auth0 test account:
Notice that each Auth0 Client has a unique ID. This ID is reported to the Rules engine as a property on the "context" parameter. Therefore, if we want to skip a Rule, we can check the context.clientID value and, if need-be, invoke the "next" callback immediately:
function addMessageInfo_ClientOne( user, context, callback ) {
// Since every Rule runs during Auth0 authentication / authorization, there's no
// implicit way to skip a Rule. But, each login transaction reports the Client ID
// making the request. As such, we can explicitly bail out of any Rule that is
// meant to be associated with a specific Client ID.
if ( context.clientID !== "v07m1dBLNy5yecCg1B7QDiDDYhhRlduW" ) {
return( callback( null, user, context ) );
}
user.message = "Client One: Are you thinking what I'm thinking, Pinky?";
// Move on to next Rule.
callback( null, user, context );
}
// ....
function addMessageInfo_ClientTwo( user, context, callback ) {
// Since every Rule runs during Auth0 authentication / authorization, there's no
// implicit way to skip a Rule. But, each login transaction reports the Client ID
// making the request. As such, we can explicitly bail out of any Rule that is
// meant to be associated with a specific Client ID.
if ( context.clientID !== "1rGDpEiO80x8os6sJpsTnHImxwLzzx0P" ) {
return( callback( null, user, context ) );
}
user.message = "Client Two: I think so Brain, but I don't think you can legally marry JavaScript.";
// Move on to next Rule.
callback( null, user, context );
}
Here, you can see that each Rule function body is checking the incoming clientID; and, if it's not the expected ID, I'm returning out of the Rule early, asking Auth0 to move on to the next Rule. If either of these Rule does execute fully, however, it is augmenting the User Profile with a "message" property.
To see this in action, I created a small Angular 2 application that creates two Auth0 clients - one for each of the new Client IDs I created earlier. In this demo, the user can run through the passwordless authentication workflow with either of the two clients. And, upon authentication and authorization, I'm outputting the injected "message" property to the view:
// Import the core angular services.
import { Component } from "@angular/core";
import { Inject } from "@angular/core";
// Import the application components and services.
import { AuthenticationService } from "./authentication.service";
import { IAuthorization } from "./authentication.service";
import { IProfile } from "./authentication.service";
@Component({
moduleId: module.id,
selector: "my-app",
styleUrls: [ "./app.component.css" ],
template:
`
<label>
<input type="radio" [(ngModel)]="selectedClient" [value]="clientOne" />
<strong>Client One</strong> — v07m1dBLNy5yecCg1B7QDiDDYhhRlduW
</label>
<label>
<input type="radio" [(ngModel)]="selectedClient" [value]="clientTwo" />
<strong>Client Two</strong> — 1rGDpEiO80x8os6sJpsTnHImxwLzzx0P
</label>
<br />
<strong>Email:</strong>
<input type="text" [(ngModel)]="email" />
<input type="button" value="Send Email" (click)="sendEmail()" />
<br /><br />
<strong>Code:</strong>
<input type="text" [(ngModel)]="code" />
<input type="button" value="Verify Code" (click)="verifyCode()" />
<br /><br />
<p *ngIf="message">
<strong>Message:</strong> {{ message }}
</p>
`
})
export class AppComponent {
public code: string;
public email: string;
public message: string;
private clientOne: AuthenticationService;
private clientTwo: AuthenticationService;
private selectedClient: AuthenticationService;
// I initialize the component.
constructor( @Inject( AuthenticationService ) AuthService: any ) {
this.code = "";
this.email = "";
this.message = "";
// Each of these AuthenticationService instances is going to create its own
// Auth0 client instance internally, using the giving Client ID. While all of the
// same Rules will be invoked for each Client request, the different Client IDs
// will be made available on the context of each Rule execution, allowing us to
// differentiate requests made from each Client.
this.clientOne = new AuthService( "v07m1dBLNy5yecCg1B7QDiDDYhhRlduW" );
this.clientTwo = new AuthService( "1rGDpEiO80x8os6sJpsTnHImxwLzzx0P" );
// This is the current client being used to run the passwordless authentication
// workflow (with its unique Client ID).
this.selectedClient = this.clientOne;
}
// ---
// PUBLIC METHODS.
// ---
// I send the one-time use password to the currently-entered email address. The
// one-time password is valid for about 5-minutes.
public sendEmail() : void {
this.code = "";
this.message = "";
this.selectedClient.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 {
// In the following workflow, first, we're going to log the user into the app;
// then, once the user is authenticated, we'll go back to the Auth0 API to get
// the user's full profile which includes the "Message" property which is being
// injected by each of the Auth0 Rules in this demo.
this.selectedClient.verifyEmailCode( this.email, this.code )
.then(
( authorization: IAuthorization ) : Promise<IProfile> => {
console.group( "Verify Email Code / Authorization Result" );
console.log( authorization );
console.groupEnd();
// Now that the user is logged-in, go back and get the Profile.
return( this.selectedClient.getUserInfo( authorization.accessToken ) );
}
)
.then(
( profile: IProfile ) : void => {
console.group( "Profile Result" );
console.log( profile );
console.groupEnd();
// NOTE: This is the property that is being injected by each Rule.
this.message = profile.message;
}
)
.catch(
( error: any ) : void => {
console.warn( "Something went wrong!" );
console.error( error );
}
)
;
}
}
Now, if I select the first client - Client One - and run through the authentication process, I get the following output:
As you can see, I get the "message" property injected by the Rule associated with Client One (skipping the Rule associated with Client Two).
Now, if I switch to the second client - Client Two - and the run through the authentication process, I get the following output:
As you can see, this time, I get the "message" property injected by the Rule associated with Client Two (skipping the Rule associated with Client One). In each case, all of the Rules were invoked; however, we were able to use the Client ID in the "context" argument to circumvent the logic of some of the rules in the Auth0 Rules engine on each request.
While not really the point of the demo, you may have noticed that I had Angular 2 inject an AuthenticationService into my root component. Unlike most injected values, this value was not a singleton - rather, it was a Class definition, of which I instantiate two in my root component constructor. In order to do this, I had to tell my root NgModule to provide the AuthenticationService as a "Value" not a "Class":
// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { NgModule } from "@angular/core";
// Import the application components and services.
import { AppComponent } from "./app.component";
import { AuthenticationService } from "./authentication.service";
@NgModule({
bootstrap: [ AppComponent ],
imports: [ BrowserModule, FormsModule ],
providers: [
// In previous demos, we've had Angular instantiate the AuthenticationService
// class (since we only had one of them in the application); but, in this demo,
// we're going to defer instantiation of the AuthenicationService to the root
// component. As such, we're going to use the "useValue" directive to tell
// Angular we want to inject the Class Definition itself, not an instance of
// the class, into the root component.
{
provide: AuthenticationService,
useValue: AuthenticationService // <-- Inject Class, not instance.
}
],
declarations: [ AppComponent ]
})
export class AppModule {
// ...
}
My AuthenticationService class then accepted a clientID constructor argument, which it used to create an Auth0 client instance:
// Import the core angular services.
import * as Auth0 from "auth0";
// CAUTION: I cobbled together the following interfaces in an attempt to self-document
// what the API calls were doing. These are NOT OFFICIAL interfaces provided by Auth0.
// I tried to find a "Definitely Typed" set of interfaces; but, they didn't appear to
// be up-to-date.
// --
// Definitely Types for JS - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/auth0-js/index.d.ts
export interface IAppMetadata {
couchDB: {
host: string;
name: string;
key: string;
password: string;
};
}
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.
};
refreshToken?: string; // Optional, if the offline_access scope has been requested.
state?: any; // Echoed value for cross-site request forgery protection.
}
export interface IIdentity {
connection: string;
isSocial: boolean;
provider: string;
user_id: string;
}
export interface IProfile {
// Fields that are always generated - https://auth0.com/docs/user-profile/normalized
identities: IIdentity[];
name: string;
nickname: string;
picture: string; // The profile picture of the user which is returned from the Identity Provider.
user_id: 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.
// This is the field I'm supplying for the demo (in each Rule).
message: string;
// Optional fields, but still "core" ?? !! The documentation is confusing !!
app_metadata?: IAppMetadata;
clientID: string; // The unique ID of the Auth0 client.
created_at: string; // TZ formatted date string.
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.
updated_at: string; // TZ formatted date string.
user_metadata?: IUserMetadata;
// Fields that are generated when the details are available:
email: string; // The email address of the user which is returned from the Identity Provider.
email_verified: boolean;
}
export interface IUserMetadata {
[ key: string ]: any;
}
export class AuthenticationService {
private auth0: any;
// I initialize the Authentication service.
constructor( clientID: string ) {
this.auth0 = new Auth0({
domain: "bennadel.auth0.com",
clientID: clientID,
responseType: "token"
});
}
// ---
// PUBLIC METHODS.
// ---
// I get the user info / profile for the given access token (which should have been
// returned as part of the authorization workflow).
// --
// NOTE: Internally, I am using the .getUserInfo() method, which takes the
// accessToken. In the Auth0 documentation, however, they discuss the .getProfile()
// method that takes the idToken. But, if you try to use that method, you get the
// following deprecation warning:
// --
// DEPRECATION NOTICE: This method will be soon deprecated, use `getUserInfo` instead.
// --
// Apparently Auth0 is trying to migrate to a slightly different workflow for
// accessing the API based on accessTokens. But, it is not yet fully rolled-out.
public getUserInfo( accessToken: string ) : Promise<IProfile> {
var promise = new Promise<IProfile>(
( resolve, reject ) : void => {
this.auth0.getUserInfo(
accessToken,
( error: any, result: IProfile ) : void => {
error
? reject( error )
: resolve( result )
;
}
);
}
);
return( promise );
}
// 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
},
( error: any, result: IAuthorization ) : void => {
error
? reject( error )
: resolve( result )
;
}
);
}
);
return( promise );
}
}
Anyway, that's not really relevant to the Auth0 portion of this demo; but, I wanted to share it since I was using Angular 2 dependency injection (DI) in a way that might seem unusual to other Angular developers.
Want to use code from this post? Check out the license.
Reader Comments