Created
January 3, 2017 12:20
-
-
Save bennadel/41217a3a33786b932ca2dccf8f189d6a to your computer and use it in GitHub Desktop.
Experimenting With Auth0 Passwordless Email Authentication In Angular 2.4.1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} | |
) | |
; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 ); | |
} | |
) | |
; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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