diff --git a/redirect-rules/progressive-profiling/README.md b/redirect-rules/progressive-profiling/README.md deleted file mode 100644 index fa69527b..00000000 --- a/redirect-rules/progressive-profiling/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Redirect Rule: Progressive Profiling - -You can use [redirect rules](https://auth0.com/docs/rules/redirect) to collect additional information for a user's profile, otherwise known as [progressive profiling](https://auth0.com/docs/user-profile/progressive-profiling). There are often two types of information you want to collect: -* Core information that was missing during the actual sign-up (like first and last name) -* Additional first-party data that you'd like to collect progressively (like the user's birthday) - -This sample shows how to collect both kinds of data. First, it will prompt the user for their first and last name (but only if they didn't sign up using a social provider that already provided it): - -![](./core-fields.png) - -Second, it will prompt for the user's birthday, but only after the third login: - -![](./birthday.png) - -The user profile website is hosted using a [Webtask](https://webtask.io/) that you can easily modify and provision and use in your webtask tenant. - -## Auth0 Setup - -### Rules - -In your Auth0 tenant, create the following [rules](https://auth0.com/docs/rules/current/redirect) in the below order: - -1. [redirect-to-update-profile-website](./redirect-to-update-profile-website.js) -2. [continue-from-update-profile-website](./continue-from-update-profile-website.js) - -Configure the following [Rule Settings](https://auth0.com/docs/rules/current#using-the-configuration-object): - -| Key | Value | -| --- | --- | -| `TOKEN_ISSUER` | The issuer claim for the self-signed JWT that is generated by the [redirect-to-update-profile-website](./redirect-to-update-profile-website.js) rule and sent to the [update-profile-website](./update-profile-website.js) webtask website. (eg. `https://example.com`)| -| `TOKEN_AUDIENCE` | The audience claim for that JWT | -| `TOKEN_SECRET` | The secret used to sign the JWT using HS256 | -| `UPDATE_PROFILE_WEBSITE_URL`| The URL of the [update-profile-website](./update-profile-website.js) webtask website (eg. `https://wt-bob-example_com-0.sandbox.auth0-extend.com/update-profile-website`) | - -## Webtask Setup - -> Tenants created after *July 16, 2018* will not have access to the underlying Auth0 Webtask Sandbox via the Webtask CLI. Please contact Auth0 at sales@auth0.com to request access. - -If you don't already have a [webtask.io](https://webtask.io) account, create one. Then in your webtask tenant, create the following webtasks, either via the [Webtask Editor](https://webtask.io/make) or the [CLI](https://webtask.io/docs/wt-cli): - -### Update Profile Webpage - -Create a webtask called `update-profile-website` using this [source code](./update-profile-website.js). - -#### NPM Modules - -Configure the webtask with these **NPM Modules**: - -* `body-parser` -* `cookie-session` -* `csurf` -* `ejs` -* `express` -* `jsonwebtoken` -* `lodash` -* `moment` -* `webtask-tools` - -#### Secrets - -Configure the webtask with the following [secrets](https://webtask.io/docs/editor/secrets): - -| Key | Value | -| --- | --- | -| `AUTH0_DOMAIN` | The domain of your Auth0 tenant | -| `TOKEN_ISSUER` | (Same value as the [Rules](#rules) section above) | -| `TOKEN_AUDIENCE` | (Same value as the [Rules](#rules) section above) | -| `TOKEN_SECRET` | (Same value as the [Rules](#rules) section above) | - -## How It Works - -The `redirect-to-update-profile-website` rule checks to see if the user profile is missing any required fields. If so, it will perform a redirect to the external **Update Profile Website**. In this sample the website is hosted as a webtask: `update-profile-website`. However, it could be hosted anywhere, like Heroku. When the redirect is performed, the required field names are passed via a self-signed [JWT](https://jwt.io). - -> **NOTE**: If a user signs in with a Database Connection identity, then the `redirect-to-update-profile-website` rule will generate a prompt for first and last name. However, if they use a social connection (eg. Google) then chances are those fields will aleady exist in the identity provider attributes, so no prompt will be necessary. - -The webtask renders a form that prompts the user for whatever fields were provided in the JWT. If the user provides the field values and they pass validation, the webtask renders a self-posting form with hidden fields, designed to POST the values back to the Auth0 `/continue` endpoint. - -The `continue-from-update-profile-website` rule then picks up the POST request from the webtask and updates the user profile. All fields are stored in `user_metadata`. - -> **NOTE**: Using webtasks is just one way of implementing and deploying the Update Profile Webpage. Any HTTP server that provided the same behavior would suffice. - -A completed `user_metadata` profile might look like this: - -```json -{ - "given_name": "John", - "family_name": "Smith", - "birthdate": "1980-01-15" -} -``` - -## Security? - -The handoff redirect from the `redirect-to-update-profile-website` rule to the `update-profile-website` webtask is made secure via the self-signed JWT. It prevents someone from calling the webtask directly to invoke a new rendering of the update form. However, it's possible that someone could replay the same exact request (URL) before the JWT token has expired. This is prevented by virtue of the redirect protocol's `state` parameter, which binds the Auth0 session to the website session. To complete the Auth0 authentication transaction, the website must redirect (or POST) back to the Auth0 `/continue` endpoint, passing the same `state` value. And since The `state` value can only be used once, it's impossible to replay the same transaction. - -It should also be noted that in this sample a JWT is only required for the redirect from the `redirect-to-update-profile-website` rule to the `update-profile-website` webtask. The return trip is secured by virtue of the `state` parameter. And for added security and flexibility, the field values are returned to Auth0 via a POST vs. query parameters in a redirect (GET). There are cases where a JWT should be used on the return to Auth0. See this [docs section](https://auth0.com/docs/rules/current/redirect#how-to-securely-process-results) for more information. diff --git a/redirect-rules/progressive-profiling/birthday.png b/redirect-rules/progressive-profiling/birthday.png deleted file mode 100644 index c36a5791..00000000 Binary files a/redirect-rules/progressive-profiling/birthday.png and /dev/null differ diff --git a/redirect-rules/progressive-profiling/continue-from-update-profile-website.js b/redirect-rules/progressive-profiling/continue-from-update-profile-website.js deleted file mode 100644 index b29dfa4e..00000000 --- a/redirect-rules/progressive-profiling/continue-from-update-profile-website.js +++ /dev/null @@ -1,26 +0,0 @@ -function continueFromUpdateProfileWebsite(user, context, callback) { - const _ = require('lodash'); - const RULE_NAME = 'continue-from-update-profile-website'; - - user.user_metadata = user.user_metadata || {}; - - // skip if we're not returning from the update profile site - if (context.protocol !== "redirect-callback") { - return callback(null, user, context); - } - - // build complete user profile - user.user_metadata = Object.assign(user.user_metadata, - _.pick( - context.request.body, - ['given_name', 'family_name', 'birthdate'])); - - // update user profile in Auth0 - console.log(`${RULE_NAME}: ${user.user_id}: Updating user profile`); - auth0.users.updateUserMetadata(user.user_id, user.user_metadata) - .then(() => callback(null, user, context)) - .catch((err) => { - console.log(`${RULE_NAME} ERROR:`, err); - callback(err); - }); -} diff --git a/redirect-rules/progressive-profiling/core-fields.png b/redirect-rules/progressive-profiling/core-fields.png deleted file mode 100644 index e9918aa1..00000000 Binary files a/redirect-rules/progressive-profiling/core-fields.png and /dev/null differ diff --git a/redirect-rules/progressive-profiling/redirect-to-update-profile-website.js b/redirect-rules/progressive-profiling/redirect-to-update-profile-website.js deleted file mode 100644 index 3fcb6b76..00000000 --- a/redirect-rules/progressive-profiling/redirect-to-update-profile-website.js +++ /dev/null @@ -1,58 +0,0 @@ -function redirectToUpdateProfileWebsite(user, context, callback) { - const RULE_NAME = 'redirect-to-update-profile-website'; - const jwt = require('jsonwebtoken'); - - user.user_metadata = user.user_metadata || {}; - - // skip if returning from the profile site - if (context.protocol === "redirect-callback") { - return callback(null, user, context); - } - - const requiredFields = []; - - // check for missing require fields - if (!user.given_name && !user.user_metadata.given_name) { - requiredFields.push('given_name'); - } - if (!user.family_name && !user.user_metadata.family_name) { - requiredFields.push('family_name'); - } - - // check for progressive desired fields - if (!user.user_metadata.birthdate && context.stats.loginsCount >= 3) { - requiredFields.push('birthdate'); - } - - // exit if no missing required fields - if (requiredFields.length === 0) { - console.log(`${RULE_NAME}: ${user.user_id}: No missing required fields`); - - return callback(null, user, context); - } - - // generate self-signed JWT for the update profile website - const options = { - issuer: configuration.TOKEN_ISSUER, - audience: configuration.TOKEN_AUDIENCE, - subject: user.user_metadata.name || user.name || user.email, - expiresIn: '5 minutes' - }; - const data = {}; - data[`${configuration.TOKEN_ISSUER}/claims/required_fields`] = requiredFields; - - let token; - try { - token = jwt.sign(data, configuration.TOKEN_SECRET, options); - } catch (err) { - return callback(err); - } - - // redirect to update profile site - console.log(`${RULE_NAME}: ${user.user_id}: Redirecting to populate missing fields: ${requiredFields}`); - context.redirect = { - url: `${configuration.UPDATE_PROFILE_WEBSITE_URL}?token=${token}` - }; - - callback(null, user, context); -} diff --git a/redirect-rules/progressive-profiling/update-profile-website.js b/redirect-rules/progressive-profiling/update-profile-website.js deleted file mode 100644 index d03b2949..00000000 --- a/redirect-rules/progressive-profiling/update-profile-website.js +++ /dev/null @@ -1,213 +0,0 @@ -'use latest'; - -import express from 'express'; -import { fromExpress } from 'webtask-tools'; -import bodyParser from 'body-parser'; -import cookieSession from 'cookie-session'; -import csurf from 'csurf'; -import moment from 'moment'; -import jwt from 'jsonwebtoken'; -import ejs from 'ejs'; -import _ from 'lodash'; - -const app = express(); - -app.use(cookieSession({ - name: 'session', - secret: 'shhh...', - maxAge: 24 * 60 * 60 * 1000 // 24 hours -})); - -const csrfProtection = csurf(); - -app.get('/', verifyInputToken, csrfProtection, (req, res) => { - // get required fields from JWT passed from Auth0 rule - const requiredFields = req.tokenPayload[`${req.webtaskContext.secrets.TOKEN_ISSUER}/claims/required_fields`]; - // store data in session that needs to survive the POST - req.session.subject = req.tokenPayload.sub; - req.session.requiredFields = requiredFields; - req.session.state = req.query.state; - - // render the profile form - const data = { - subject: req.tokenPayload.sub, - csrfToken: req.csrfToken(), - fields: {}, - action: req.originalUrl.split('?')[0] - }; - requiredFields.forEach((field) => { - data.fields[field] = {}; - }); - - const html = renderProfileView(data); - - res.set('Content-Type', 'text/html'); - res.status(200).send(html); -}); - -const parseBody = bodyParser.urlencoded({ extended: false }); - -app.post('/', parseBody, csrfProtection, validateForm, (req, res) => { - if (req.invalidFields.length > 0) { - // render the profile form again, showing validation errors - const data = { - subject: req.session.subject, - csrfToken: req.csrfToken(), - fields: {}, - action: '' - }; - req.session.requiredFields.forEach((field) => { - data.fields[field] = { - value: req.body[field], - invalid: req.invalidFields.includes(field) - }; - }); - - const html = renderProfileView(data); - - res.set('Content-Type', 'text/html'); - return res.status(200).send(html); - } - - // render form that auth-posts back to Auth0 with collected data - const formData = _.omit(req.body, '_csrf'); - const HTML = renderReturnView({ - action: `https://${req.webtaskContext.secrets.AUTH0_DOMAIN}/continue?state=${req.session.state}`, - formData - }); - - // clear session - req.session = null; - - res.set('Content-Type', 'text/html'); - res.status(200).send(HTML); -}); - -module.exports = fromExpress(app); - -// middleware functions - -function verifyInputToken(req, res, next) { - const options = { - issuer: req.webtaskContext.secrets.TOKEN_ISSUER, - audience: req.webtaskContext.secrets.TOKEN_AUDIENCE - } - - try { - req.tokenPayload = jwt.verify(req.query.token, req.webtaskContext.secrets.TOKEN_SECRET, options); - } catch (err) { - return next(err); - } - return next(); -} - -function validateForm(req, res, next) { - const requiredFields = req.session.requiredFields; - - const validation = { - given_name: value => value && value.trim().length > 0, - family_name: value => value && value.trim().length > 0, - birthdate: value => value && value === moment(value).format('YYYY-MM-DD') - } - - req.invalidFields = []; - requiredFields.forEach((field) => { - if (!validation[field](req.body[field])) { - req.invalidFields.push(field); - } - }); - - next(); -} - -// view functions - -function renderProfileView(data) { - const template = ` - - - - - User Profile - - - - -
-
-
-
-

Hello <%= subject %>, we just need a couple more things from you to complete your profile:

-
-
- -
- - - <% if (fields.given_name) { %> -
- -
- -
-
- <% } %> - - <% if (fields.family_name) { %> -
- -
- -
-
- <% } %> - - <% if (fields.birthdate) { %> -
- -
- -
-
- <% } %> - -
-
- -
-
-
-
-
- - - `; - - return ejs.render(template, data); -} - -function renderReturnView (data) { - const template = ` - - - - - - - -
- <% Object.keys(formData).forEach((key) => { %> - - <% }); %> -
- - - - `; - - return ejs.render(template, data); -}