{"id":299,"date":"2018-04-15T17:59:07","date_gmt":"2018-04-15T06:59:07","guid":{"rendered":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/?p=299"},"modified":"2018-04-15T17:59:07","modified_gmt":"2018-04-15T06:59:07","slug":"connecting-auth0-to-dynamodb-and-cloudwatch","status":"publish","type":"post","link":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/archives\/299","title":{"rendered":"Connecting Auth0 to DynamoDb and CloudWatch"},"content":{"rendered":"<p>If your website uses Auth0 for user authentication, and you have subscribed to an Enterprise Plan (https:\/\/auth0.com\/pricing), you can store your user data in your own database, rather that Auth0&#8217;s internal database. The connection between Auth0 and your database is done through a set of Node.js scripts that you supply. Auth0 provides template scripts for many databases, but not AWS DynamoDb.<\/p>\n<p>What if you want to store your users on DynamoDb, and log log-in\/log-out etc. events in a daily CloudWatch log stream? Here is how you do it &#8230;.<\/p>\n<h1>Log-in script<\/h1>\n<p>Auth0&#8217;s custom database connection requires 6 scripts to be defined: Login, Create, Verify, Change Password, Get User and Delete. Here is a script for Login. Once you get the picture, you can copy and adapt this script for the 5 others. Note that I have assumed a certain structure in my Dynamodb user table. If your structure (fields and global indicies) are different, you will need to do some minor adjustment accordingly.<\/p>\n<pre class=\"brush: jscript; title: Login; notranslate\" title=\"Login\">\r\nfunction login(email, password, callback) {\r\n\r\nvar AWS = require('aws-sdk');\r\nvar crypto = require('crypto');\r\n\r\nvar region = configuration.region;\r\nvar accessKeyId = configuration.accessKeyId;\r\nvar secretAccessKey = configuration.secretAccessKey;\r\nvar groupName = configuration.groupName;\r\nvar userTableName = configuration.userTableName;\r\nvar salt = configuration.salt;\r\nvar streamNamePrefix = configuration.streamNamePrefix;\r\n\r\nvar logEvents = &#x5B;];\r\n\r\n\r\nvar cloudwatchlogs = new AWS.CloudWatchLogs({\r\n    apiVersion: '2014-03-28',\r\n    region: region,\r\n    accessKeyId: accessKeyId,\r\n    secretAccessKey: secretAccessKey\r\n});\r\n\r\nvar dynamodb = new AWS.DynamoDB({\r\n    apiVersion: '2012-08-10',\r\n    region: region,\r\n    accessKeyId: accessKeyId,\r\n    secretAccessKey: secretAccessKey\r\n});\r\n\r\n\r\n\r\nfunction zeroPad(num, places) {\r\n    var zero = places - num.toString().length + 1;\r\n    return Array(+(zero &gt; 0 &amp;&amp; zero)).join(&quot;0&quot;) + num;\r\n}\r\n\r\nfunction todayAsString() {\r\n    var mydate = new Date();\r\n    return zeroPad(1900 + mydate.getYear(), 4) +\r\n        '-' + zeroPad(mydate.getMonth(), 2) +\r\n        '-' + zeroPad(mydate.getDate(), 2);\r\n}\r\n\r\nvar streamName = streamNamePrefix + '\/' + todayAsString();\r\n\r\nfunction makeLogGroup(cb) {\r\n    cloudwatchlogs.createLogGroup({\r\n            logGroupName: groupName\r\n        },\r\n        cb\r\n    );\r\n}\r\n\r\nfunction makeLogStream(cb) {\r\n    cloudwatchlogs.createLogStream({\r\n            logGroupName: groupName,\r\n            logStreamName: streamName\r\n        },\r\n        cb);\r\n}\r\n\r\nfunction createLogGroupIfNotExists(cb) {\r\n    var tryCount = 0;\r\n    var maxTries = 3;\r\n\r\n    function tryOnce() {\r\n        cloudwatchlogs.describeLogStreams({\r\n                logGroupName: groupName,\r\n                logStreamNamePrefix: streamName\r\n            },\r\n            function(err, data) {\r\n                if (err) {\r\n                    if ((err.code === 'ResourceNotFoundException') &amp;&amp; (tryCount++ &lt; = maxTries)) {\r\n                        makeLogGroup(function(gerr, gdata) {\r\n                            if (gerr) {\r\n                                cb(gerr);\r\n                            } else {\r\n                                tryOnce();\r\n                            }\r\n                        });\r\n                    } else {\r\n                        cb(err);\r\n                    }\r\n                } else {\r\n                    cb(null, data);\r\n                }\r\n            });\r\n    }\r\n\r\n    tryOnce();\r\n}\r\n\r\nfunction createLogStreamIfNotExists(cb) {\r\n    var tryCount = 0;\r\n    var maxTries = 3;\r\n\r\n    function tryOnce() {\r\n        createLogGroupIfNotExists(function(err, data) {\r\n            if (err) {\r\n                cb(err);\r\n            } else {\r\n                if (data.logStreams.length === 0) {\r\n                    makeLogStream(function(lerr, ldata) {\r\n                        if ((lerr) &amp;&amp; (tryCount++ &lt;= maxTries)) {\r\n                            cb(lerr);\r\n                        } else {\r\n                            tryOnce();\r\n                        }\r\n                    });\r\n                } else {\r\n                    cb(null, data.logStreams&#x5B;0]);\r\n                }\r\n            }\r\n        });\r\n    }\r\n\r\n    tryOnce();\r\n}\r\n\r\nfunction createLogEvent(rec) {\r\n    return {\r\n        message: typeof rec === 'string' ?\r\n            rec : JSON.stringify(rec),\r\n        timestamp: typeof rec === 'object' &amp;&amp; rec.time ?\r\n            new Date(rec.time).getTime() : Date.now()\r\n    };\r\n}\r\n\r\nfunction cloudLog(event) {\r\n    logEvents.push(createLogEvent(event));\r\n}\r\n\r\nfunction emit(cb) {\r\n    function doCallBack(err, data) {\r\n        if (typeof cb === 'function') {\r\n            cb(err, data);\r\n        }\r\n    }\r\n\r\n    if (logEvents.length) {\r\n        createLogStreamIfNotExists(function(err, streamMetaData) {\r\n            var nextToken = streamMetaData ? streamMetaData.uploadSequenceToken : null;\r\n            if (err) {\r\n                if (typeof cb === 'function') {\r\n                    cb(err);\r\n                }\r\n            } else {\r\n                var params = {\r\n                    logEvents: logEvents,\r\n                    logGroupName: groupName,\r\n                    logStreamName: streamName\r\n                };\r\n                logEvents = &#x5B;];\r\n                if (nextToken) {\r\n                    params.sequenceToken = nextToken;\r\n                }\r\n                cloudwatchlogs.putLogEvents(params, function(err, data) {\r\n                    doCallBack(err, data);\r\n                });\r\n            }\r\n        });\r\n    } else {\r\n        doCallBack();\r\n    }\r\n}\r\n\r\nfunction getUserByEmail(email, cb) {\r\n    dynamodb.query({\r\n            TableName: userTableName,\r\n            IndexName: 'email-index',\r\n            ExpressionAttributeNames: {\r\n                &quot;#u&quot;: &quot;user-id&quot;\r\n            },\r\n            ExpressionAttributeValues: {\r\n                &quot;:v1&quot;: {\r\n                    S: email\r\n                }\r\n            },\r\n            KeyConditionExpression: 'email = :v1',\r\n            ProjectionExpression: '#u,email,nick,phash'\r\n        },\r\n        cb);\r\n}\r\n\r\nfunction hashString(datum) {\r\n    var hash = crypto.createHash('sha256');\r\n    hash.update(datum);\r\n    return hash.digest('base64');\r\n}\r\n\r\nfunction hashPassword(user_id,given_password) {\r\n    return hashString(user_id + '|' + salt + '|' + given_password);\r\n}\r\n\r\nfunction pass(user_id, given_password, phash) {\r\n    return hashPassword(user_id,given_password) === phash;\r\n}\r\n\r\nfunction testCredentials(email, password, cb) {\r\n    getUserByEmail(email, function(err, data) {\r\n        var user = null;\r\n        if ((!err) &amp;&amp; (data.Items.length &gt;= 0)) {\r\n            user = {\r\n                user_id: data.Items&#x5B;0]&#x5B;'user-id'].S,\r\n                nickname: data.Items&#x5B;0].nick.S,\r\n                email: data.Items&#x5B;0].email.S,\r\n                phash: data.Items&#x5B;0].phash.S\r\n            };\r\n        }\r\n        if (user &amp;&amp; ((user.email !== email) || (!pass(user.user_id, password, user.phash)))) user = null;\r\n        cb(err, user);\r\n    });\r\n}\r\n\r\n    cloudLog({method:'login',control:'ENTER'});\r\n    testCredentials(email, password, function(err, user) {\r\n        if (err) {\r\n            cloudLog(err);\r\n\r\n        } else if (user) {\r\n            cloudLog({\r\n                method: 'login',\r\n                pass: 'true',\r\n                email: email,\r\n                user_id: user.user_id\r\n            });\r\n\r\n        } else {\r\n            cloudLog({\r\n                method: 'login',\r\n                pass: 'false',\r\n                email: email\r\n            });\r\n        }\r\n        cloudLog({method:'login',control:'EXIT'});\r\n        emit(function(emit_err, datum) {\r\n            callback(err);\r\n        });\r\n    });\r\n}\r\n<\/pre>\n<h1>Settings<\/h1>\n<p>In the settings, you will need to define the following configuration items:<\/p>\n<ul>\n<li><em>region<\/em> AWS region code for both the dynamodb table and cloudwatch logs.<\/li>\n<li><em>accessKeyId<\/em> IAM Access key for AWS operations. See the section on User Policies below.<\/li>\n<li><em>secretAccessKey<\/em> Goes with accessKeyId.<\/li>\n<li><em>groupName<\/em> The ClouldWatch Log Group name. The group will be created if it does not exist.<\/li>\n<li><em>userTableName<\/em> The DynamoDb table name.<\/li>\n<li><em>salt<\/em> Just some random secret string to salt the passwords.<\/li>\n<li><em>streamNamePrefix<\/em> Prefix for the CloudWatch log stream name.<\/li>\n<\/ul>\n<h1>IAM User Policies<\/h1>\n<p>You will need to assign at least the following policies (after substitution of place-markers) to the user, who&#8217;s credentials were passed in the Auth0 custom database connection settings above.<\/p>\n<pre class=\"brush: jscript; title: Policy; notranslate\" title=\"Policy\">\r\n{\r\n    &quot;Version&quot;: &quot;2012-10-17&quot;,\r\n    &quot;Statement&quot;: &#x5B;\r\n        {\r\n            &quot;Effect&quot;: &quot;Allow&quot;,\r\n            &quot;Action&quot;: &quot;logs:CreateLogGroup&quot;,\r\n            &quot;Resource&quot;: &quot;arn:aws:logs:&lt;#region&gt;:&lt;#account&gt;:*&quot;\r\n        },\r\n        {\r\n            &quot;Effect&quot;: &quot;Allow&quot;,\r\n            &quot;Action&quot;: &#x5B;\r\n                &quot;logs:DescribeLogStreams&quot;,\r\n                &quot;logs:CreateLogStream&quot;,\r\n                &quot;logs:PutLogEvents&quot;\r\n            ],\r\n            &quot;Resource&quot;: &#x5B;\r\n                &quot;arn:aws:logs:&lt;#region&gt;:&lt;#account&gt;:log-group:&lt;#group&gt;:*&quot;\r\n            ]\r\n        },\r\n        {\r\n            &quot;Effect&quot;: &quot;Allow&quot;,\r\n            &quot;Action&quot;: &#x5B;\r\n                &quot;dynamodb:DeleteItem&quot;,\r\n                &quot;dynamodb:GetItem&quot;,\r\n                &quot;dynamodb:PutItem&quot;,\r\n                &quot;dynamodb:Scan&quot;,\r\n                &quot;dynamodb:UpdateItem&quot;\r\n            ],\r\n            &quot;Resource&quot;: &quot;arn:aws:dynamodb:&lt;#region&gt;:&lt;#account&gt;:table\/&lt;#table&gt;&quot;\r\n        },\r\n        {\r\n            &quot;Effect&quot;: &quot;Allow&quot;,\r\n            &quot;Action&quot;: &#x5B;\r\n                &quot;dynamodb:Query&quot;\r\n            ],\r\n            &quot;Resource&quot;: &quot;arn:aws:dynamodb:&lt;#region&gt;:&lt;#account&gt;:table\/&lt;#table&gt;\/index\/&lt;#index&gt;&quot;\r\n        }\r\n    ]\r\n}\r\n<\/pre>\n<p>&#8230; where the following place-markers are substituted for your particular values:<\/p>\n<ul>\n<li><em>&lt;#region&gt;<\/em> The AWS region code for dynamodb and cloudwatch. Eg. ap-southeast-2 .<\/li>\n<li><em>&lt;#account&gt;<\/em> Your AWS account number\/identifier.<\/li>\n<li><em>&lt;#group&gt;<\/em> The name of the CloudWatch log group.<\/li>\n<li><em>&lt;#table&gt;<\/em> The name of the DynamoDb user table.<\/li>\n<li><em>&lt;#index&gt;<\/em> The name of the global index used to look-up the user table by email address. In the supplied code fragment, this name is &#8217;email-index&#8217;. Change as you require.<\/li>\n<\/ul>\n<h1>Dynamodb schema<\/h1>\n<p>I have assumed that the user table has schema that follows this pattern of item:<\/p>\n<pre class=\"brush: jscript; title: Record Item Example; notranslate\" title=\"Record Item Example\">\r\n{\r\n  &quot;email&quot;: &quot;sean@seanbdurkin.id.au&quot;,\r\n  &quot;email_verified&quot;: true,\r\n  &quot;nick&quot;: &quot;Sean&quot;,\r\n  &quot;phash&quot;: &quot;&lt;#redacted&gt;&quot;,\r\n  &quot;user-id&quot;: &quot;sean&quot;\r\n}\r\n<\/pre>\n<p>where the primary key is user-id. You will also need a global index to look-up users based on email. Probably you should also add fields for username and user_metadata.<\/p>\n<h2>A note about logging<\/h2>\n<p>The CloudWatch log name will be &#8216;< #streamNamePrefix>\/< #Date>&#8216; where < #streamNamePrefix> is as given by the settings, and < #Date> is today&#8217;s date. There is an assumption that there will be no other log streams which begin with &#8216;< #streamNamePrefix>\/< #Date>&#8216;, but are not &#8216;< #streamNamePrefix>\/< #Date>&#8216;. So when we search for streams prefixed with &#8216;< #streamNamePrefix>\/< #Date>&#8216;, we only get zero streams, or exactly one stream, which is the stream we want. If this assumption is not going to hold in your architecture, adjust the code accordingly.<\/p>\n<h2>What about the other 5 scripts?<\/h2>\n<p>You can develop them yourself. Once you see how the login script is made, it&#8217;s just a case of cut and paste, with some obvious modification.<\/p>\n<h2>After-thoughts<\/h2>\n<p>The custom database connection is only available on the expensive Enterprise plan, or on a 30 day trial. If it was on the free plan, I would use Auth0 for my amateur and Start-Up projects. It is not good keeping your user table in a foreign database, because you can&#8217;t join it with other tables.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If your website uses Auth0 for user authentication, and you have subscribed to an Enterprise Plan (https:\/\/auth0.com\/pricing), you can store your user data in your own database, rather that Auth0&#8217;s internal database. The connection between Auth0 and your database is &hellip; <a href=\"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/archives\/299\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[9],"tags":[],"class_list":["post-299","post","type-post","status-publish","format-standard","hentry","category-web-hosting"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p2QXbt-4P","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/posts\/299","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/comments?post=299"}],"version-history":[{"count":7,"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/posts\/299\/revisions"}],"predecessor-version":[{"id":306,"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/posts\/299\/revisions\/306"}],"wp:attachment":[{"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/media?parent=299"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/categories?post=299"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/seanbdurkin.id.au\/pascaliburnus2\/wp-json\/wp\/v2\/tags?post=299"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}