Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created January 3, 2017 12:20
Show Gist options
  • Save bennadel/41217a3a33786b932ca2dccf8f189d6a to your computer and use it in GitHub Desktop.
Save bennadel/41217a3a33786b932ca2dccf8f189d6a to your computer and use it in GitHub Desktop.
Experimenting With Auth0 Passwordless Email Authentication In Angular 2.4.1
function addLoginAudit( user, context, callback ) {
// Ensure that the user_meteadata exists.
// --
// NOTE: It won't exist on the user object until one of the rules explicitly
// creates it (or we assign metadata to the user through something like the API).
user.user_metadata = ( user.user_metadata || {} );
// Ensure that the login audit log exists.
user.user_metadata.logins = ( user.user_metadata.logins || [] );
// Track the current login (in descending order).
user.user_metadata.logins.unshift({
ip: context.request.ip,
userAgent: context.request.userAgent,
createdAt: Date.now()
});
// Limit the audit log to only 10 most recent logins.
user.user_metadata.logins = user.user_metadata.logins.slice( 0, 10 );
// At this point, all we've done is updated the in-memory user-metadata associated
// with this login request. Now, we have to push this data back over to Auth0 (from
// the current webtask.io server), using an API call, so that these changes will be
// persisted over to the next login.
auth0.users
.updateUserMetadata( user.user_id, user.user_metadata )
.then(
function handleResolve() {
callback( null, user, context );
}
)
.catch(
function handleError( error ) {
callback( error );
}
)
;
}
function addLoginCount( user, context, callback ) {
// Ensure that the user_meteadata exists.
// --
// NOTE: It won't exist on the user object until one of the rules explicitly
// creates it (or we assign metadata to the user through something like the API).
user.user_metadata = ( user.user_metadata || {} );
// NOTE: I believe that stats.loginsCount always exists; but, the documentation only
// goes so far as confirming "stats" - it doesn't state that loginsCount is always
// available. As such, I am trying to be safe about accessing it.
user.user_metadata.loginCount = ( context.stats && context.stats.loginsCount )
? context.stats.loginsCount
: 0
;
callback( null, user, context );
}
// Import the core angular services.
import { Component } 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:
`
<strong>Email:</strong>
<input type="text" [value]="email" (input)="email = $event.target.value;" />
<input type="button" value="Send Email" (click)="sendEmail()" />
<br /><br />
<strong>Code:</strong>
<input type="text" [value]="code" (input)="code = $event.target.value;" />
<input type="button" value="Verify Code" (click)="verifyCode()" />
<div *ngIf="name">
<h3>
Welcome {{ name }}
</h3>
<img [src]="avatarUrl" />
</div>
`
})
export class AppComponent {
public avatarUrl: string;
public code: string;
public email: string;
public name: string;
private authenticationService: AuthenticationService;
// I initialize the component.
constructor( authenticationService: AuthenticationService ) {
this.authenticationService = authenticationService;
this.email = "";
this.code = "";
}
// ---
// 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.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 {
// 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 (included persisted metadata).
this.authenticationService
.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, let's go back to the API to get
// the full user profile.
// --
// NOTE: There is an earlier API method call .getProfile() which
// takes the idToken. That workflow, however, is being deprecated
// in favor of a new workflow that emphasizes accessToken.
return( this.authenticationService.getUserInfo( authorization.accessToken ) );
}
)
.then(
( profile: IProfile ) : void => {
console.group( "Profile Result" );
console.log( profile );
console.groupEnd();
this.name = profile.nickname;
this.avatarUrl = profile.picture;
}
)
.catch(
( error: any ) : void => {
console.warn( "Something went wrong!" );
console.error( error );
}
)
;
}
}
// Import the core angular services.
import { Injectable } from "@angular/core";
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 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;
}
export interface IIdentity {
connection: string;
isSocial: boolean;
provider: string;
user_id: string;
}
export interface IMetadata {
[ key: string ]: any;
}
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.
// Optional fields, but still "core" ?? !! The documentation is confusing !!
app_metadata?: IMetadata;
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?: IMetadata;
// 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 class AuthenticationService {
private auth0: any;
// I initialize the Authentication service.
constructor() {
this.auth0 = new Auth0({
domain: "bennadel.auth0.com",
clientID: "erNlgZHZ4MyDFrfwFOc0JCAJ1Znzg6Fm", // JavaScript Demos Client.
responseType: "token"
// Since I am using an email-based token workflow, I don't need to define
// a callback URL - this would only be necessary if I was passing the user
// control over to Auth0's website.
// --
// callbackURL: "{YOUR APP URL}"
});
}
// ---
// 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 );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment