Using Firebase Auth with Alexa Account Linking

[ad_1]

I created a simple login page with FirebaseUI for Web:

<!DOCTYPE html>
<html>
  <head>
    <!-- meta and title -->

    <!-- update the version number as needed -->
    <script defer src="/__/firebase/5.9.2/firebase-app.js"></script>
    <!-- include only the Firebase features as you need -->
    <script defer src="/__/firebase/5.9.2/firebase-auth.js"></script>
    <script defer src="/__/firebase/5.9.2/firebase-firestore.js"></script>
    <script src="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.js"></script>
    <link
      type="text/css"
      rel="stylesheet"
      href="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.css"
    />
    <!-- initialize the SDK after all desired features are loaded -->
    <script defer src="/__/firebase/init.js"></script>

    <style media="screen" type="text/css">
      /* styling */
    </style>
  </head>
  <body>
    <div id="message">
      <h2>Welcome</h2>
      <h1>Firebase Login</h1>
      <div id="info"></div>
    </div>
    <div id="firebaseui-auth-container"></div>
    <p id="load">Firebase SDK Loading&hellip;</p>

    <script>
      // see https://stackoverflow.com/a/2117523/5168962
      function uuidv4() {
        return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
          (
            c ^
            (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
          ).toString(16)
        );
      }

      document.addEventListener("DOMContentLoaded", function() {
        try {
          let app = firebase.app();
          let db = firebase.firestore();
          let features = ["auth"].filter(
            feature => typeof app[feature] === "function"
          );
          document.getElementById(
            "load"
          ).innerHTML = `Firebase SDK loaded with ${features.join(", ")}`;

          firebase.auth().onAuthStateChanged(
            function(user) {
              if (user) {
                // User is signed in.
                var uid = user.uid;

                // Generate auth code which we will later use to get our auth_token.
                let authCode = uuidv4();

                // Save the code in our database.
                db.collection(`auth_codes`)
                  .doc(uid)
                  .set(
                    {
                      code: authCode,
                      uid: uid,
                      created: firebase.firestore.FieldValue.serverTimestamp()
                    },
                    { merge: true }
                  );

                // Send token back to client
                const urlParams = new URLSearchParams(window.location.search);
                // State sent by Alexa which we have to return.
                const state = urlParams.get("state");
                // Redirect uri sent by Alexa.
                const redirect_uri = urlParams.get("redirect_uri");

                // Combine all the uri elements.
                let url =
                  redirect_uri + "?state=" + state + "&code=" + authCode;

                // Redirect
                window.location.href = url;
              } else {
                // User is signed out, so we show the Firebase UI.

                // FirebaseUI config.
                var uiConfig = {
                  signInOptions: [
                    // Leave the lines as is for the providers you want to offer your users.
                    firebase.auth.EmailAuthProvider.PROVIDER_ID
                  ],
                  callbacks: {
                    // Turn of FirebaseUI redirect.
                    signInSuccessWithAuthResult: function(
                      authResult,
                      redirectUrl
                    ) {
                      return false;
                    }
                  },
                  credentialHelper: firebaseui.auth.CredentialHelper.NONE
                };

                // Initialize the FirebaseUI Widget using Firebase.
                var ui = new firebaseui.auth.AuthUI(firebase.auth());
                // The start method will wait until the DOM is loaded.
                ui.start("#firebaseui-auth-container", uiConfig);
              }
            },
            function(error) {
              console.log(error);
              document.getElementById("info").textContent = "Error: " + error;
            }
          );
        } catch (e) {
          console.error(e);
          document.getElementById("load").innerHTML =
            "Error loading the Firebase SDK, check the console.";
        }
      });
    </script>
  </body>
</html>

3. Access_token API via Firebase Cloud Functions

Alexa will then take this random code we returned and ask our firebase cloud function for a valid access_token:

const functions = require("firebase-functions");
const admin = require("firebase-admin");
// see https://www.npmjs.com/package/uuid
const uuidv4 = require("uuid/v4");

admin.initializeApp();

const db = admin.firestore();

async function getAccessTokenByAuthCode(req, res) {
  let code = req.body.code;

  let dbEntry = await db
    .collection("auth_codes")
    .where("code", "==", code)
    .get();

  if (dbEntry.empty) {
    return res.send(404);
  }

  let dbDoc = dbEntry.docs[0];

  let uid = dbDoc.data().uid;

  let additionalClaims = {
    alexa: true
  };

  // Create a custom token. See https://firebase.google.com/docs/auth/admin/create-custom-tokens
  let access_token = await admin
    .auth()
    .createCustomToken(uid, additionalClaims);

  let refresh_token = uuidv4();

  db.doc(`auth_codes/${uid}`).update({
    access_token: access_token,
    refresh_token: refresh_token
  });

  return res.json({
    access_token: access_token,
    token_type: "Bearer",
    refresh_token: refresh_token,
    expires_in: 60 * 60,
    id_token: ""
  });
}

async function getAccessTokenByRefreshToken(req, res) {
  let refresh_token = req.body.refresh_token;

  let dbEntry = await db
    .collection("auth_codes")
    .where("refresh_token", "==", refresh_token)
    .get();

  if (dbEntry.empty) {
    return res.send(404);
  }

  let dbDoc = dbEntry.docs[0];

  let uid = dbDoc.data().uid;

  let additionalClaims = {
    alexa: true
  };

  let access_token = await admin
    .auth()
    .createCustomToken(uid, additionalClaims);

  db.doc(`auth_codes/${uid}`).update({
    access_token: access_token
  });

  return res.json({
    access_token: access_token,
    token_type: "Bearer",
    refresh_token: refresh_token,
    expires_in: 60 * 60,
    id_token: ""
  });
}

exports.access_token = functions.https.onRequest(async (req, res) => {
  // check client_id and client_secret

  if (req.body.grant_type === "authorization_code") {
    return getAccessTokenByAuthCode(req, res);
  } else if (req.body.grant_type === "refresh_token") {
    return getAccessTokenByRefreshToken(req, res);
  } else {
    return res.send(404);
  }
});

4. Configure Alexa Account Linking

Select Auth Code Grant and set the Authorization URI = https://<project_name>.firebaseapp.com and the Access Token URI = https://us-central1-<project_name>.cloudfunctions.net/auth_token.

5. Use the custom token in the Alexa Skill

Now comes the most important part. This took me hours of googling.

const Alexa = require("ask-sdk-core");
const firebase = require("firebase");
// Required for side-effects
require("firebase/auth");
require("firebase/firestore");

// Initialize Cloud Firestore through Firebase
var config = {
  // ...
};

firebase.initializeApp(config);
var db = firebase.firestore();

const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === "LaunchRequest";
  },
  async handle(handlerInput) {
    console.log("LaunchRequest");

    var speechText = "Please link your account!";

    const accessToken =
      handlerInput.requestEnvelope.context.System.user.accessToken;

    // Test if user has linked his account.
    if (!accessToken) {
      return handlerInput.responseBuilder.speak(speechText).getResponse();
    }

    // Alexa will save our custom token in the access token field, so we can use it here.
    await firebase
      .auth()
      .signInWithCustomToken(accessToken)
      .catch((error) => {
        // Handle Errors here.
        var errorCode = error.code;
        var errorMessage = error.message;
        console.log(error.message);
        return handlerInput.responseBuilder.speak(speechText).getResponse();
      });

    let user = firebase.auth().currentUser;

    if (user) {
      // login successful
      speechText = `Welcome ${user.displayName}!`;
    } else {
      // No user is signed in.
      speechText = `Welcome, please link your account!`;
    }

    // ATTENTION: This is very important. Without this the function will time out.
    await firebase.auth().signOut();

    return handlerInput.responseBuilder.speak(speechText).getResponse();
  }
};

Please mind the firebase.auth().signOut(); at line 55. Without this, the AWS Lambda will not finish and thus you’ll get an error like Task timed out after 8.01 seconds .


Once you have set up this structure it’s very easy to develop the rest of the Alexa Skill. I am positively surprised how advanced the Alexa ecosystem is and I am happy to dive deeper. On my developing journey, I will add further articles about my findings and the quirks of Skill development.

Make sure to follow me so you won’t miss any future advancements.

This article has been published from the source link without modifications to the text. Only the deadline has been changed.

Source link