User Data Storage Guidance

Auth0 provides multiple places to store data used to authenticate a Client's users. This document covers best practices on how to store your data securely and efficiently. Additionally, this document uses a sample Client (a mobile music application) that reflects the end-to-end user experience of using Auth0 with an external database to illustrate specific topics.

Example: Mobile Music Application

The sample Client is a basic iOS app utilizing the Auth0 iOS seed project. The backend uses the Node.js API. See the Mobile + API architecture scenario for a visualization of the Client's overall structure.

Where should I store my authentication data?

To store user data points beyond the basic information Auth0 uses for authentication, you can use:

  1. The Auth0 data store;
  2. A custom database.

However, if you use these additional data points for authentication purposes, we recommend using the Auth0 data store, as this allows you to manage your user data through the Auth0 Management Dashboard.

How should I use the Auth0 data store?

Any data you store in Auth0 that's not already a part of the user profile should go into one of the two provided metadata :

  • app_metadata
  • user_metadata

These fields contain JSON snippets and can be used during the Auth0 authentication process.

App Metadata

You can store data points that are read-only to the user in app_metadata. Three common types of data for the app_metadata field:

  • Permissions: privileges granted to certain users allowing them rights within the Client that others do not have;
  • Plan information: settings that cannot be changed by the user without confirmation from someone with the appropriate authority;
  • External IDs: identifying information used to associate users with external accounts.

For a list of fields that cannot be stored within app_metadata, please see the metadata overview page.

Example: app_metadata for a Mobile Music Application

The following data points from our mobile music application appropriate to store in app_metadata:

  • A user's subscription plan;
  • A user's right (or lack thereof) to edit featured playlists.

These two should be stored in app_metadata instead of user_metadata because they should not be directly changeable by the user.

The following section details rules we use to implement permissions on whether a user can edit featured playlists or not.

The first rule sends a request to our Node API, which then queries the database connected to Heroku to check how many plays the user’s playlist has. If the number is 100 or greater, we assign playlist_editor as a value in the roles array in app_metadata.

function (user, context, callback) {
  user.app_metadata = user.app_metadata || {};
  user.app_metadata.roles = user.roles || [];

  var CLIENT_SECRET = configuration.AUTH0_CLIENT_SECRET;
  var CLIENT_ID = configuration.AUTH0_CLIENT_ID;

  var scope = {
    user_id: user.user_id,
    email: user.email,
    name: user.name
  };

  var options = {
    subject: user.user_id,
    expiresInMinutes: 600,
    audience: CLIENT_ID,
    issuer: 'https://example.auth0.com'
  };

  var id_token = jwt.sign(scope, CLIENT_SECRET, options);

  var auth = 'Bearer ' + id_token;

  request.get({
    url: 'https://example.com/playlists/getPlays',
    headers: {
       'Authorization': auth,
      'Content-Type': 'text/html'
    },
    timeout: 15000
  }, function(err, response, body){
    if (err)
      return callback(new Error(err));
    var plays = parseInt(body, 10);

    if (plays >= 100 && user.roles.indexOf('playlist_editor') < 0){
      user.app_metadata.roles.push('playlist_editor');
      auth0.users.updateAppMetadata(user.user_id, user.app_metadata)
        .then(function(){
          callback(null, user, context);
        })
        .catch(callback);
    }

    else if (plays < 100 && user.roles.indexOf('playlist_editor') >= 0){
      user.app_metadata.roles = [];
      auth0.users.updateAppMetadata(user.user_id, user.app_metadata)
        .then(function(){
          callback(null, user, context);
        })
        .catch(callback);
    }
    else{
      callback(null, user, context);
    }

  });

}

The second rule gets the app_metadata field and assigns the roles array to a field in the user object so it can be accessed without calling app_metadata on the client. The scope parameter can then specify roles upon the user logging in without including everything in app_metadata in the user object:

function(user, context, callback) {
   if (user.app_metadata) {
      user.roles = user.app_metadata.roles;
   }
   user.roles = user.roles || [];
   callback(null, user, context);
}

After we've implemented these two rules, the app recognizes whether the user is a playlist editor or not and changes the welcome screen accordingly. If playlist_editor is in the roles array stored in the user's app_metadata, the user will be welcomed as an EDITOR after signing in:

User Metadata

The following data points from our mobile music application are appropriate to store in user_metadata:

  • Application preferences;
  • Avatar customization;
  • Any details chosen by the user to alter their experience of the app upon login.

Note that, unlike the data points for app_metadata, the user can easily and readily change those stored in user_metadata.

Example: user_metadata for a Mobile Music Application

We can let the user change their displayName, which is the name the user sees upon logging in and is displayed to other users of the app.

To display the user's chosen identifier whenever they log in, we use a rule to get the user.user_metadata value.

function(user, context, callback){
  user.user_metadata = user.user_metadata || {};
  user.user_metadata.displayName = user.user_metadata.displayName || "user";

  auth0.users.updateUserMetadata(user.user_id, user.user_metadata)
    .then(function(){
      callback(null, user, context);
    })
    .catch(function(err){
      callback(err);
    });
}

Here's a look at the screen the user would use to change their displayName:

To save the changes to the database, the application makes a call to the Get a User endpoint of the Management API to identify the appropriate user:


curl --request GET \
  --url https://youraccount.auth0.com/api/v2/users/user_id \
  --header 'authorization: Bearer YOUR_ID_TOKEN_HERE'
var client = new RestClient("https://youraccount.auth0.com/api/v2/users/user_id");
var request = new RestRequest(Method.GET);
request.AddHeader("authorization", "Bearer YOUR_ID_TOKEN_HERE");
IRestResponse response = client.Execute(request);
package main

import (
	"fmt"
	"net/http"
	"io/ioutil"
)

func main() {

	url := "https://youraccount.auth0.com/api/v2/users/user_id"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("authorization", "Bearer YOUR_ID_TOKEN_HERE")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := ioutil.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.get("https://youraccount.auth0.com/api/v2/users/user_id")
  .header("authorization", "Bearer YOUR_ID_TOKEN_HERE")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://youraccount.auth0.com/api/v2/users/user_id",
  "method": "GET",
  "headers": {
    "authorization": "Bearer YOUR_ID_TOKEN_HERE"
  }
}

$.ajax(settings).done(function (response) {
  console.log(response);
});
var request = require("request");

var options = { method: 'GET',
  url: 'https://youraccount.auth0.com/api/v2/users/user_id',
  headers: { authorization: 'Bearer YOUR_ID_TOKEN_HERE' } };

request(options, function (error, response, body) {
  if (error) throw new Error(error);

  console.log(body);
});
#import <Foundation/Foundation.h>

NSDictionary *headers = @{ @"authorization": @"Bearer YOUR_ID_TOKEN_HERE" };

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://youraccount.auth0.com/api/v2/users/user_id"]
                                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                   timeoutInterval:10.0];
[request setHTTPMethod:@"GET"];
[request setAllHTTPHeaderFields:headers];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                if (error) {
                                                    NSLog(@"%@", error);
                                                } else {
                                                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
                                                    NSLog(@"%@", httpResponse);
                                                }
                                            }];
[dataTask resume];
$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => "https://youraccount.auth0.com/api/v2/users/user_id",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "GET",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer YOUR_ID_TOKEN_HERE"
  ),
));

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
import http.client

conn = http.client.HTTPSConnection("youraccount.auth0.com")

headers = { 'authorization': "Bearer YOUR_ID_TOKEN_HERE" }

conn.request("GET", "/api/v2/users/user_id", headers=headers)

res = conn.getresponse()
data = res.read()

print(data.decode("utf-8"))
require 'uri'
require 'net/http'

url = URI("https://youraccount.auth0.com/api/v2/users/user_id")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

request = Net::HTTP::Get.new(url)
request["authorization"] = 'Bearer YOUR_ID_TOKEN_HERE'

response = http.request(request)
puts response.read_body
import Foundation

let headers = ["authorization": "Bearer YOUR_ID_TOKEN_HERE"]

var request = NSMutableURLRequest(URL: NSURL(string: "https://youraccount.auth0.com/api/v2/users/user_id")!,
                                        cachePolicy: .UseProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.HTTPMethod = "GET"
request.allHTTPHeaderFields = headers

let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    println(error)
  } else {
    let httpResponse = response as? NSHTTPURLResponse
    println(httpResponse)
  }
})

dataTask.resume()

This is followed by a call to the Update a User endpoint to update the user_metadata field:

'''har { "method": "PATCH", "url": "https://YOURACCOUNT.auth0.com/api/v2/users/user_id", "httpVersion": "HTTP/1.1", "cookies": [], "headers": [{ "name": "Authorization", "value": "Bearer ABCD" }, { "name": "Content-Type", "value": "application/json" }], "queryString": [], "postData": { "mimeType": "application/json", "text": "{"user_metadata": {"displayName": "J-vald3z"}" }, "headersSize": -1, "bodySize": -1, "comment": "" } '''

Why shouldn't I put all my Client's data in the Auth0 data store?

Because the Auth0 data store is customized for authentication data, storing anything beyond the default user information should be done only in limited cases. Here's why:

  • Scalability: The Auth0 data store is limited in scalability, and your Client's data may exceed the appropriate limits. By using an external database, you keep your Auth0 data store simple, while the more efficient external database contains the extra data;
  • Performance: Your authentication data is likely accessed at lower frequencies than your other data. The Auth0 data store isn't optimized for high frequency use, so you should store data that needs to be retrieved more often elsewhere;
  • Flexibility: Because the Auth0 data store was built to accomodate only user profiles and their associated metadata, you are limited in terms of the actions you can perform on the database. By using separate databases for your other data, you can manage your data as appropriate.

Example

We need to associate a user's music with that user, but this information is not required for authentication. Here's how to store this information in a separate database that is integrated with the backend of our Client.

The user's unique identifier is their user_id. Here is a sample row from the songs table in our database:

song_id songname user_id
1 Number One Hit google-oauth2

The Node.js backend authenticates requests to the URI associated with getting the user’s personal data from the database by validating a JSON Web Token.

Learn about token-based authentication and how to implement JWT in your Clients.

Here is the code implementing JWT validation from the Node.js seed project:

var genres = require('./routes/genres');
var songs = require('./routes/songs');
var playlists = require('./routes/playlists');
var displayName = require('./routes/displayName');

var authenticate = jwt({
  secret: process.env.AUTH0_CLIENT_SECRET,
  audience: process.env.AUTH0_CLIENT_ID
});

app.use('/genres', authenticate, genres);
app.use('/songs', authenticate, songs);
app.use('/playlists', authenticate, playlists);
app.use('/displayName', authenticate, displayName);

We can add functionality to handle different data requests from our Client. For example, if we receive a GET request to /secured/getFavGenre, the API calls the queryGenre() function, which queries the database for and responds with the user’s favorite genre.

@IBAction func getGenre(sender: AnyObject) {
        let request = buildAPIRequest("/genres/getFav", type:"GET")
        let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {[unowned self](data, response, error) in
            let genre = NSString(data: data!, encoding: NSUTF8StringEncoding)
            dispatch_async(dispatch_get_main_queue(), {
                self.favGenre.text = "Favorite Genre:  \(genre!)"
            })
        }
        task.resume()
    }

The function buildAPIRequest() takes the path and HTTP method of the request as parameters and builds a request using the base URL of our Node.js API that's hosted on Heroku.

In the Client, the getGenre() function makes a request to the API and changes the app's interface to display the request response to /genres/getFav. The backend retrieves the required data for this action using the queryGenre() function and returns the results to the Client:

function queryGenre(user_id, res){

  db.connect(process.env.DATABASE_URL, function(err, client) {
  if (err) throw err;

  client
    .query('SELECT fav_genre as value FROM user_data WHERE user_id = $1', [user_id], function(err, result) {

      if(err) {
        return console.error('error running query', err);
      }
      res.send(result.rows[0].value);
    });
  });

};

Summary

When determining where you should store specific pieces of data about your user, here are the general rules of thumb:

Type of Data Storage Location
Data that should be read-only to the user app_metadata
Data that should be editable by the user user_metadata
Data unrelated to user authentication External database