mingchaoma / Auth0_Verify

Integrate Auth0 with Twilio Verify for MFA
1 stars 1 forks source link

Auth0 with Verify Custom Webhook #2

Closed rb090 closed 10 months ago

rb090 commented 11 months ago

Hi @mingchaoma,

1st of all thank you so much for the Twilio blogpost How to configure Auth0 MFA using Twilio Verify you wrote together with your colleague Kelley Robinson.

I also like the Twilio Verify a lot and all its advantages. That is why we would like to use it with our Auth0 integration like you explain it in your article.

I have one question regarding the section "Create Custom Log Streams Using Webhook". You wrote

Please follow this instruction to create custom log streams using webhook and call Verify Feedback API whenever the OTP is successfully validated. For example, when using Auth0 MFA with Twilio Verify SMS OTP, you can filter the event "Success Login" which indicates a successful login or more specifically filter “MFA Auth success” event and then call Twilio Verify feedback API (you only need to call Verify feedback API for successful login event). If you are not sure, you can always check what log events are triggered for a successful MFA login with the OTP and then use them as the triggers to call Verify feedback API.

You shared the link from Auth0 which describes how to create a Custom Webhook integration:

Bildschirmfoto 2023-10-15 um 17 23 17

Should this be really created like this? If yes, can you please tell me what "Payload URL" needs to be entered?

Looking at the Verify API documentation, the feedback about the result of the verification needs to be done like this:

curl -X POST "https://verify.twilio.com/v2/Services/VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/VerificationCheck" \
--data-urlencode "To=+15017122661" \
--data-urlencode "Code=123456" \
-u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN

And the To as well as the Code params are nothing we have when creating such a Webhook.

I would create 2 Actions:

The question for me is still, what role plays this webhook you describe in the section "Create Custom Log Streams Using Webhook"?

I am really thankful for each advice on this.

mingchaoma commented 11 months ago

Hi there,

Thanks for your feedback. Per the blog post, below is the feedback API. Yes, you are right, you won't be able to call this API directly as it requires phone number as the parameter. However, you need to create a webhook function and host it somewhere (for example, use Twilio function) which will in turn call Verify feedback API. The webhook URL (if you use Twilio Function, it will be the URL of the Twillio function) will be the payload URL when you configure Create Custom Log Streams

image

I like the PostLoginAction idea, it could be a viable option although I haven't tested it. But again, I do not know if and how to get the phone number of the successful login user when use PostLoginAction. I need to look into it when I have time. Have you tested already?

Thanks,

Mingchao

rb090 commented 11 months ago

Hi Mingchao,

wow cool, thanks a lot for the reply on this and all the explanations 🙌.

Then I will give the PostLogin Action a try and let you know how it goes 👍. ATM I am waiting for the Twilio Support to enable the custom code feature on my service. I already reached out and send them the required information.

ATM my 2 actions in Auth0 look like this:

SendPhoneMessage Action:

exports.onExecuteSendPhoneMessage = async (event, api) => {
  // https://www.twilio.com/blog/configure-auth0-mfa-twilio-verify
  const { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_VERIFY_SID } = event.secrets

  const client = require('twilio')(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)

  const { recipient, code } = event.message_options

  // add this to fix " 1333444999  "
  // "333 444 5555"
  const sanitizedNumber = recipient.replace(/\s/g, '').trim()

  try {
    await client.verify.services(TWILIO_VERIFY_SID).verifications.create({
      to: sanitizedNumber,
      channel: 'sms',
      customCode: code
    })
     // Add the received 'verification.sid' to 'user_metadata' of Auth0 user to have it when talking to the feedback API in a PostLogin action
    .then (verification => event.user.setUserMetadata("verification_sid", verification.sid))
     // Some logging for testing and monitoring purpose - only on dev Auth0 tenant
    .then(verification => console.log(`Success send sms and set verification_sid [${verification.sid}] to user_metadata!`))
  } catch(error) {
    console.log('Error when trying to create verification', error)
    throw {
      statusCode: 400,
      body: JSON.stringify({ error: `An error occurred when trying to send code to phone ${sanitizedNumber}` })
    };
  }
};
Bildschirmfoto 2023-10-16 um 13 58 31

PostLogin Action:

/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  const accountSid = event.secrets.TWILIO_ACCOUNT_SID
  const twilioVerifyServiceSid = event.secrets.TWILIO_VERIFY_SID
  const authToken = event.secrets.TWILIO_AUTH_TOKEN
  const client = require('twilio')(accountSid, authToken)

  client.verify.v2.services(twilioVerifyServiceSid)
    .verifications(event.user.user_metadata.verification_sid)
    .update({status: 'approved'})
     // Log verification status on dev tenant for debugging purposes
    .then(verification => console.log(verification.status))
     // Remove 'verification_sid' from 'user_metadata' after updating the verification status on Twilio
    .then(api.user.setUserMetadata("verification_sid", null))
};

/**
* Handler that will be invoked when this action is resuming after an external redirect. If your
* onExecutePostLogin function does not perform a redirect, this function can be safely ignored.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
// exports.onContinuePostLogin = async (event, api) => {
// };
Bildschirmfoto 2023-10-16 um 14 03 02

If both of them works I probably will see some logging in my Twilio account for the corresponding Verify service. And if this works, then maybe Webhook function is also not needed anymore? Because feedback is given to Twilio by the PostLogin action?

Those Auth0 actions are a really painful topic until understanding them in detail. But once you went through the painful challenge there is a rainbow at the end of the journey 😄.

GautamGupta commented 11 months ago

Thanks for all the documentation @mingchaoma and @rb090.

@rb090: Did the code you posted work for you? The approach does look correct. I will be trying it out very soon so will report back as well.

GautamGupta commented 11 months ago

@rb090: api doesn't exist as an argument in the signature of onExecuteSendPhoneMessage so unfortunately it doesn't quite work. Docs.

To try out the method suggested in the blog post of streaming events to a Twilio Function, I also looked at the Auth0 logs for the raw OTP Auth Succeed events and they don't contain the phone number that was just 2FA'd - but they do contain the Auth0 user id.

So if one was to send feedback to the Verify API upon a successful auth, I think this needs to happen:

  1. Stream Auth0 events to Twilio Functions (or another REST API/cloud function)
  2. Filter for "type": "gd_auth_succeed" events. Can not filter in Auth0 UI as gd_auth_succeed isn't part of the Login - Success event category afaict.
  3. Query Auth0 API or your own API with the user_id in the event to get the user's phone number that's used for 2FA
  4. Call Twilio Verify Verification Update API with this phone number and {status: 'approved'}
rb090 commented 11 months ago

Hi @mingchaoma, @GautamGupta

1st of all please apologize for getting back so late to this GitHub post. I am on this topic for 1,5 weeks now since I opened this issue and I am also in exchange with the Auth0 support.

Nonetheless what I did until now:

  1. Change SendPhoneMessage Action - I wanted to put the unique verification SID, received on a verification attempt into the Auth0 user object so that after successful MFA it can be used to notify the Twilio verification update API. Setting user_metadata within a send phone message action only works when going over the Auth0 management API:
/**
* Handler that will be called during the execution of a SendPhoneMessage flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {SendPhoneMessageAPI} api - Methods and utilities to help change the behavior of sending a phone message.
*/

// Needed to do requests over Auth0 management API
const axios = require('axios').default

// Identifier of the 'Auth0 Management API' we need to talk to
const MANAGEMENT_API_BASE_URL = 'https://xxxxx.eu.auth0.com'
const MANAGEMENT_API_AUDIENCE = `${MANAGEMENT_API_BASE_URL}/api/v2`

exports.onExecuteSendPhoneMessage = async (event, api) => {
  // https://www.twilio.com/blog/configure-auth0-mfa-twilio-verify
  const { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_VERIFY_SID } = event.secrets

  const client = require('twilio')(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)

  const { recipient, code } = event.message_options

  // add this to fix " 1333444999  "
  // "333 444 5555"
  const sanitizedNumber = recipient.replace(/\s/g, '').trim()

  try {
    // 1st try to get management API token for further communication after requesting SMS over Twilio API
    const managementApiAccessToken = await getAccessToken(event.secrets.AUTH0_MTM_CLIENT_ID, event.secrets.AUTH0_MTM_CLIENT_SECRET)    

    console.log(`Try to send sms to user with phone ${sanitizedNumber}.`)

    const verification = await client.verify.services(TWILIO_VERIFY_SID).verifications.create({
      to: sanitizedNumber,
      channel: 'sms',
      customCode: code
    })
    // Some logging for testing and monitoring purpose - only on dev Auth0 tenant
    .then(console.log(`Success send sms to ${sanitizedNumber}!`))

    // Add the received 'verification.sid' to 'user_metadata' of Auth0 user to have it when talking to the feedback API in a PostLogin action
    await updateUserMetadata(event.user.user_id, verification.sid, managementApiAccessToken)
  } catch(exception) {
    console.log('Error when trying to create verification in SendPhoneMessage-Twilio-Verify action', JSON.stringify(exception))
  }
};

/**
Function to set the Verify SID received from Twilio to 'user_metadata'
We need to go over Auth0 management API with our MTM app, else it will not work!
@userId - String - the Auth0 userid of the user which triggers this action
@verifySidToken - String - unique verification SID we get from Twilio on an verification attempt
@managementApiToken - String - an access token we get from [getAccessToken] function in order to request the Auth0 API 
@return Void
*/
async function updateUserMetadata(userId, verifySidToken, managementApiToken) {
  // Set up the request options with axios
  const requestOptions = {
    method: 'PATCH',
    url: `${MANAGEMENT_API_AUDIENCE}/users/${userId}`,
    headers: {authorization: `Bearer ${managementApiToken}`, 'content-type': 'application/json'},
    data: {
      user_metadata: { verification_sid: verifySidToken }
    }
  }

  console.log('url for request', `${MANAGEMENT_API_AUDIENCE}/users/${userId}`)
  console.log(`Try to set verification_sid in user_metadata ${verifySidToken} for user ${userId}`)

  // Update user metadata within axios request with request options defined above
  await axios.request(requestOptions).then(function (response) {
    console.log(`Success set user_metadata for user ${userId}`)
  }).catch(function (exception) {
      console.error('Error when trying to set user_metadata verification_sid attribute over management API in SendPhoneMessage-Twilio-Verify action:', JSON.stringify(exception))
  });
}

/**
 Helper function which gets 'accessToken' from management API over MTM application with MTM credentials saved in secrets
 This 'accessToken' can be used for further requests on the management API
 @clientId - String - the client id attribute of the Auth0 MTM application connected to the management API
 @clientSecret - String - the client secret attribute of the Auth0 MTM application connected to the management API
 @return a String with an accessToken to access the Auth0 management API
 */
async function getAccessToken(clientId, clientSecrect) {
  const requestOptions = {
    method: 'POST',
    url: `${MANAGEMENT_API_BASE_URL}/oauth/token`,
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    data: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: clientId,
      client_secret: clientSecrect,
      audience: `${MANAGEMENT_API_AUDIENCE}/`
    })
  }

  try {
    const response = await axios.request(requestOptions);
    console.log('Success get accessToken from management API!');
    return response.data.access_token;
  } catch (error) {
    console.error('Error when trying to fetch API token in SendPhoneMessage-Twilio-Verify action:', error);
  }
}

If and how I will use the unique verification SID token within user_metadata to update Twilio verification status is still an open topic.

  1. My initial idea was on the Post Login to notify the Twilio Verification feedback API and use the value of verification_sid from the user_metadata. This didn't worked out because the Post Login actions on Auth0 get called after the user enters email and password. Not after MFA is entered.

I started to write with the Auth0 support regarding this to check the exiting possibilities.

They suggested me to:

I created a serverless function on my Twilio account which does only logging in the 1st step to understand what I receive on Twilio side:

exports.handler = function(context, event, callback) {
  // Here's an example of setting up some TWiML to respond to with this function
const twiml = new Twilio.twiml.MessagingResponse()

  console.log('Webhook triggered!')
  console.log('Context:', JSON.stringify(context, null, 2))
  console.log('Received request', JSON.stringify(event, null, 2))
  console.log('twinml object', twiml.toString())

  // This callback is what is returned in response to this function being invoked.
  // It's really important! E.g. you might respond with TWiML here for a voice or SMS response.
  // Or you might return JSON data to a studio flow. Don't forget it!
  return callback(null, twiml)
};

The Auth0 webhook triggers the Twilio on the following events:

I guess the Token events are not needed, so I can disable them again.

But the data I get in my Twilio serverless function is this one:

Oct 23, 2023, 05:11:15 PM

Fetching content for /myAuth0FunctionCallback
Oct 23, 2023, 05:11:52 PM

Execution started...
Oct 23, 2023, 05:11:52 PM

Webhook triggered!
Oct 23, 2023, 05:11:52 PM

Context: {"PATH":"/myAuth0FunctionCallback","AUTH_TOKEN":"xxxxxxxxxx","SERVICE_SID":"ZSxxxxxxxxxx","ENVIRONMENT_SID":"ZExxxxxxxxxx","ACCOUNT_SID":"ACxxxxxxxxx","TWILIO_VERIFY_SID":"VAxxxxxxx","DOMAIN_NAME":"auth0-verification-webhook-xxxxx.twil.io"}
Oct 23, 2023, 05:11:52 PM

Received request {"request":{"headers":{"x-request-id":"xxxxxx-xxx-xxx-xxx-xxxx","authorization":"fcxxxxxxxxxx","content-length":"1381","t-request-id":"RQxxxxxxxxxxxxxxx","content-type":"application/json","accept-encoding":"gzip,deflate","accept":"*/*","user-agent":"auth0-logstream/1.0"},"cookies":{}}}
Oct 23, 2023, 05:11:52 PM

twinml object <?xml version="1.0" encoding="UTF-8"?><Response/>
Oct 23, 2023, 05:11:52 PM

Execution ended in 198.14ms using 102MB
Oct 23, 2023, 05:12:06 PM

Execution started...
Oct 23, 2023, 05:12:06 PM

Webhook triggered!
Oct 23, 2023, 05:12:06 PM

Context: {"PATH":"/myAuth0FunctionCallback","AUTH_TOKEN":"xxxxxxxxxxxxxx","SERVICE_SID":"ZSxxxxxxxxxx","ENVIRONMENT_SID":"ZExxxxxxxxx","ACCOUNT_SID":"ACxxxxxxxxxx","TWILIO_VERIFY_SID":"VAxxxxxxxxx","DOMAIN_NAME":"auth0-verification-webhook-xxxx.twil.io"}
Oct 23, 2023, 05:12:06 PM

Received request {"request":{"headers":{"x-request-id":"xxxxxxx-xxxx-xxxxx-xxxx-xxxxx","authorization":"fcbxxxxxxxxxxxx","content-length":"2515","t-request-id":"RQxxxxxxxxxxxxxxxxxxx","content-type":"application/json","accept-encoding":"gzip,deflate","accept":"*/*","user-agent":"auth0-logstream/1.0"},"cookies":{}}}
Oct 23, 2023, 05:12:06 PM

twinml object <?xml version="1.0" encoding="UTF-8"?><Response/>
Oct 23, 2023, 05:12:06 PM

Execution ended in 79.56ms using 105MB

So nothing in there with which I can update the verification status atm. I am still in contact with the Auth0 support regarding this. To clarify:

I really hope that the verification SID or the phone number can be transported to the Twilio server less function by the Auth0 webhook without the need to query the Auth0 API with userId from within the Twilio function.

I will keep this issue updated once I have a working implementation.

rb090 commented 10 months ago

Hi 👋

Sorry for getting back so late to this GitHub issue.

With the help of Auth0 support I was able to implement an Twilio function which listens to an Auth0 webhook and transport Auth0 information within the webhook to Twilio I need to update the verification status.

For this, I needed to create an Auth0 webhook which points to a Twilio serverless function. The Auth0 webhook needs to listen and send event of type “Other Events”. In the Twilio function it is needed to listen for event.data.type == 'gd_auth_succeed'. When the webhook sends an event of this type to the Twilio serverless function there is also event.data.user_id attribute in the webhook payload.

With that we need to talk to Auth0 to get the Auth0 user (like @GautamGupta wrote) and from there we can get the users phone number/verification sid we might put manually in the user_metadata like I did within my Auth0 send phone message action.

And with that, the verification status of an sms verification attempt can be updated:

twilioClient.verify.v2.services(twilioVerificationServiceId)
        .verifications(verificationSid)
        .update({status: 'approved'})

Another BIG issue with Twilio serverless function was for me that it had to be public so that Auth0 webhook can access it. Protected would not work because Auth0 webhook seems to not be able to send this X-Twilio-Signature headerwhich is also described on the Twilio docs Understanding Visibility of Functions and Assets: Public, Protected and Private.

To not end up in having a public function I implemented JWT like described within Twilio docs Protect your Function with JSON Web Token. And the Auth0 webhook sends a JWT and the Twilio function called by the webhooks checks this JWT and if it is expected code gets processed, else request is rejected with 401.

So tbh this verification feedback implemention ended up in a really big thing in the end….

GautamGupta commented 10 months ago

@rb090 glad you figured it out. Are you able to share the code of your Twilio function?

rb090 commented 10 months ago

@GautamGupta the function with the implementation which is updating the verification status looks finally like this:


// This is your new function. To start, set the name and path on the left.
exports.handler = async function(context, event, callback) {

  // Prepare a new Twilio response
  const response = new Twilio.Response()

  // Grab the auth token from the request header
  const authHeader = event.request.headers.authorization

  // The auth type and token are separated by a space, split them
  const [authType, authToken] = authHeader.split(' ')

  // If the auth type is not Bearer, return error!
  if (authType.toLowerCase() !== 'bearer') {
    console.error('We did not get a Bearer')
    response
      .setBody('Bad request - No Bearer as authentication header')
      .setStatusCode(400)
    return callback(null, response)
  }

  // Check authorization 1st here
  if (!isAuthenticated(authToken, context.JWT_TOKEN_SECRET)) {
    response
      .setBody('Unauthorized')
      .setStatusCode(401)
      .appendHeader(
        'WWW-Authenticate',
        'Bearer realm="Access to set sms verification attempt status"'
      )
    return callback(null, response)
  }

  // If the authentication check succeeded, we continue setting the correct verification status on Twilio side
  console.log('Webhook triggered!')

  // 1st we check if our webhook data transmitted us data here by checking `event.data`
  // 2nd we check weather we got submitted an 'event.data.type'
  // 3rd we check that the 'event.data.type' of the webhook is 'gd_auth_succeed' 
  // -> this means MFA with SMS succeeded in the Auth0 Universal Login 
  if (event.data != null && event.data.type != null && event.data.type == 'gd_auth_succeed') {
    const management = new ManagementClient({
      domain: context.AUTH0_MTM_DOMAIN,
      clientId: context.AUTH0_MTM_CLIENT_ID,
      clientSecret: context.AUTH0_MTM_CLIENT_SECRET,
    });

    const userId = event.data.user_id

    try {
      const user = await management.users.get({ id: userId })
      const verificationSid = user.user_metadata.verification_sid

      // Update the verification status for user verification attempt
      // For this, use the unique verification token we receive from Twilio 
      // and the user_metadata we set up when requesting an SMS within 'SendPhoneMessage-Twilio-Verify' on the Auth0 SendPhone message flow

      const twilioClient = require('twilio')(context.ACCOUNT_SID, context.AUTH_TOKEN)

      twilioClient.verify.v2.services(context.TWILIO_VERIFY_SID)
        .verifications(verificationSid)
        .update({status: 'approved'})

      // Remove 'verification_sid' from 'user_metadata' of auth0 user after the verification
      const userParams =  { id : userId }
      const data = { "user_metadata":{ "verification_sid": null } }
      await management.updateUser(userParams, data)

      response
        .setBody('OK')
        .setStatusCode(200)

      return callback(null, response)

    } catch (error) {
      console.error('An error occurred when trying to get user and set verfication status for verification attempt: ', error)
      return callback(error)
    }
  } else {
    console.log('Auth0 Webhook did not deliver sms event - ignore!')
    response
      .setBody('No SMS authentication event here')
      .setStatusCode(200)

    return callback(null, response)
  }
};

/**
 * Checks weather user is authenticated and returns a boolean with this information
 * @param {String} authHeader The authentication header send in the request without 'Bearer' prefix
 * @param {String} secret The secret used also in 'jwtGeneration' function of Auth-Service to create an Bearer token. 
 * @returns {boolean} with information if the jwt send is valid 
 */ 
function isAuthenticated(authHeader, secret) {
  // Reject requests that don't have an Authorization header
  if (!authHeader) {
    console.error('No authentication header')
    return false
  }

  try {
    // Verify the token against the secret. If the token is invalid,
    // verify will throw an error and we'll proceed to the catch block and return false for 'isAuthenticated'
    const jwt = require('jsonwebtoken')
    jwt.verify(authHeader, secret)
    return true
  } catch (error) {
    console.error('Could not verify AT: ', error)
    return false
  }
};

The one which generates the JWT looks basically like the /jwt described in Twilio docs Protect your Function with JSON Web Token.

gsmith-yprime commented 7 months ago

@rb090 Hi, thanks for posting your solution. Just wondering at what point you're setting verification_sid in the user_metadata object. That wouldn't be there by default, correct?