[ad_1]
If you are just interested in the time out problem within the AWS lambda function skip to 5. Use the custom token in the Alexa Skill
.
Authorization URI
and an Access Token URI
. Because I found no way that Firebase would support this on its own I set it up myself.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…</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.