Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@i8beef
Last active July 30, 2018 18:37
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save i8beef/3f91d3a1ec543987fa2e590355246c69 to your computer and use it in GitHub Desktop.
Save i8beef/3f91d3a1ec543987fa2e590355246c69 to your computer and use it in GitHub Desktop.
Google Actions OAuth Flow
[{"id":"5d8ce437.30a97c","type":"http in","z":"a963f616.6aebe8","name":"","url":"/google/oauth-auth","method":"get","swaggerDoc":"","x":140,"y":320,"wires":[["92962c27.e26ba","3e1d718f.2973ce"]]},{"id":"814cb966.112578","type":"http response","z":"a963f616.6aebe8","name":"","x":990,"y":360,"wires":[]},{"id":"11b23fee.e932c","type":"http in","z":"a963f616.6aebe8","name":"","url":"/google/oauth-token","method":"get","swaggerDoc":"","x":150,"y":760,"wires":[["65306936.ee6b78","7b66e8bd.0c4998"]]},{"id":"6e78d24c.36084c","type":"http response","z":"a963f616.6aebe8","name":"","x":1250,"y":860,"wires":[]},{"id":"92962c27.e26ba","type":"switch","z":"a963f616.6aebe8","name":"Check response_type","property":"req.query.response_type","propertyType":"msg","rules":[{"t":"neq","v":"code","vt":"str"},{"t":"else"}],"checkall":"false","outputs":2,"x":400,"y":320,"wires":[["5f1ce470.ff6eec"],["75c4e86f.ff44b8"]]},{"id":"5f1ce470.ff6eec","type":"function","z":"a963f616.6aebe8","name":"Error: response_type incorrect","func":"msg.statusCode = 500;\nmsg.payload = 'response_type ' + msg.req.query.response_type + ' must equal \"code\"';\n\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":300,"wires":[["814cb966.112578"]]},{"id":"75c4e86f.ff44b8","type":"switch","z":"a963f616.6aebe8","name":"Check client_id","property":"req.query.client_id","propertyType":"msg","rules":[{"t":"neq","v":"googleClientId","vt":"flow"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":380,"y":360,"wires":[["f020e980.c985c8"],["d6bb41c3.6d21a"]]},{"id":"f020e980.c985c8","type":"function","z":"a963f616.6aebe8","name":"Error: client_id incorrect","func":"msg.statusCode = 500;\nmsg.payload = 'client_id ' + msg.req.query.client_id + ' invalid';\n\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":340,"wires":[["814cb966.112578"]]},{"id":"d6bb41c3.6d21a","type":"switch","z":"a963f616.6aebe8","name":"Check auth_code","property":"req.query.code","propertyType":"msg","rules":[{"t":"nnull"},{"t":"else"}],"checkall":"false","outputs":2,"x":390,"y":400,"wires":[["ee531b6f.30f228"],["ecb89c32.50ed2"]]},{"id":"ee531b6f.30f228","type":"function","z":"a963f616.6aebe8","name":"Return existing code","func":"msg.statusCode = 302;\nmsg.headers = {\n \"Location\": msg.req.query.redirect_uri + '?code=' + msg.req.query.code + '&state=' + msg.req.query.state\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":640,"y":380,"wires":[["814cb966.112578"]]},{"id":"f62893.0a09577","type":"switch","z":"a963f616.6aebe8","name":"Check client_id","property":"client_id","propertyType":"msg","rules":[{"t":"neq","v":"googleClientId","vt":"flow"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":420,"y":840,"wires":[["707a6532.7e10ac"],["f43b1f75.1e069"]]},{"id":"707a6532.7e10ac","type":"function","z":"a963f616.6aebe8","name":"Error: client_id invalid","func":"msg.statusCode = 400;\nmsg.payload = 'client_id invalid';\n\nreturn msg;","outputs":1,"noerr":0,"x":680,"y":820,"wires":[["6e78d24c.36084c"]]},{"id":"f43b1f75.1e069","type":"switch","z":"a963f616.6aebe8","name":"Check client_secret","property":"client_secret","propertyType":"msg","rules":[{"t":"neq","v":"googleClientSecret","vt":"flow"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":430,"y":880,"wires":[["31493749.7b86b8"],["783716da.4fdad8"]]},{"id":"31493749.7b86b8","type":"function","z":"a963f616.6aebe8","name":"Error: client_secret invalid","func":"msg.statusCode = 400;\nmsg.payload = 'client_secret invalid';\n\nreturn msg;","outputs":1,"noerr":0,"x":690,"y":860,"wires":[["6e78d24c.36084c"]]},{"id":"783716da.4fdad8","type":"switch","z":"a963f616.6aebe8","name":"grant_type","property":"grant_type","propertyType":"msg","rules":[{"t":"eq","v":"authorization_code","vt":"str"},{"t":"eq","v":"refresh_token","vt":"str"},{"t":"else"}],"checkall":"false","outputs":3,"x":410,"y":960,"wires":[["cc413bc4.12e038"],["c5d30db5.28857"],["d6fa41a0.987f1"]]},{"id":"d6fa41a0.987f1","type":"function","z":"a963f616.6aebe8","name":"Error: grant_type unsupported","func":"msg.statusCode = 400;\nmsg.payload = 'grant_type ' + msg.req.query.grant_type + ' is not supported';\n\nreturn msg;","outputs":1,"noerr":0,"x":850,"y":1020,"wires":[["7b406925.9afee8"]]},{"id":"719f461b.bbbe48","type":"inject","z":"a963f616.6aebe8","name":"On Startup","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":"","x":110,"y":180,"wires":[["96d6601f.9e47b","dc17a5a1.c3d5a8","dabc2ebc.452a7"]]},{"id":"96ed41b8.5f27c","type":"function","z":"a963f616.6aebe8","name":"Handle refresh_token","func":"let refreshTokenStore = global.get(\"googleRefreshTokenStore\") || {};\nlet refreshToken = refreshTokenStore[msg.refresh_token];\nif (refreshToken === null) {\n msg.statusCode = 400;\n msg.payload = 'invalid code';\n return msg;\n}\n\nif (new Date(refreshToken.expiresAt) < Date.now()) {\n // Remove bad auth code\n delete refreshTokenStore[msg.refresh_token];\n \n msg.statusCode = 400;\n msg.payload = 'expired code';\n return msg;\n}\n\nif (refreshToken.clientId != msg.client_id) {\n msg.statusCode = 400;\n msg.payload = 'invalid code - wrong client';\n return msg;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":940,"wires":[["e4e16182.f31da"]]},{"id":"cc413bc4.12e038","type":"function","z":"a963f616.6aebe8","name":"Handle authorization_code","func":"let authStore = flow.get(\"authStore\") || {};\nlet authCode = authStore[msg.code];\nif (authCode === null) {\n msg.statusCode = 400;\n msg.payload = 'invalid code';\n return msg;\n}\n\nif (new Date(authCode.expiresAt) < Date.now()) {\n // Remove expired auth code\n delete authStore[msg.code];\n flow.set(\"authStore\", authStore);\n \n msg.statusCode = 400;\n msg.payload = 'expired code';\n return msg;\n}\n\nif (authCode.clientId != msg.client_id) {\n msg.statusCode = 400;\n msg.payload = 'invalid code - wrong client';\n return msg;\n}\n\n// Remove used auth code\ndelete authStore[msg.code];\nflow.set(\"authStore\", authStore);\n\nreturn msg;","outputs":1,"noerr":0,"x":840,"y":900,"wires":[["e4e16182.f31da"]]},{"id":"c5d30db5.28857","type":"switch","z":"a963f616.6aebe8","name":"refresh_token?","property":"refresh_token","propertyType":"msg","rules":[{"t":"nnull"},{"t":"null"}],"checkall":"false","repair":false,"outputs":2,"x":600,"y":960,"wires":[["96ed41b8.5f27c"],["bcf202b7.8f9d"]]},{"id":"bcf202b7.8f9d","type":"function","z":"a963f616.6aebe8","name":"Error: refresh_token incorrect","func":"msg.statusCode = 400;\nmsg.payload = 'missing required parameter';\n\nreturn msg;","outputs":1,"noerr":0,"x":840,"y":980,"wires":[["7b406925.9afee8"]]},{"id":"5b266b4a.ad76f4","type":"comment","z":"a963f616.6aebe8","name":"OAuth Auth","info":"Google will direct the client to this endpoint to do\nlogin and generation of a temporary authorization code\nfor a specific user.","x":110,"y":280,"wires":[]},{"id":"f00f308e.4e003","type":"comment","z":"a963f616.6aebe8","name":"OAuth Token","info":"","x":110,"y":720,"wires":[]},{"id":"3e1d718f.2973ce","type":"debug","z":"a963f616.6aebe8","name":"","active":false,"console":"false","complete":"true","x":350,"y":280,"wires":[]},{"id":"65306936.ee6b78","type":"debug","z":"a963f616.6aebe8","name":"","active":false,"console":"false","complete":"true","x":390,"y":760,"wires":[]},{"id":"aebfe219.4c443","type":"http in","z":"a963f616.6aebe8","name":"","url":"/google/oauth-token","method":"post","swaggerDoc":"","x":150,"y":800,"wires":[["65306936.ee6b78","7b66e8bd.0c4998"]]},{"id":"7b66e8bd.0c4998","type":"function","z":"a963f616.6aebe8","name":"Unify GET / POST","func":"msg.client_id = msg.req.query.client_id ? msg.req.query.client_id : msg.req.body.client_id;\nmsg.client_secret = msg.req.query.client_secret ? msg.req.query.client_secret : msg.req.body.client_secret ;\nmsg.grant_type = msg.req.query.grant_type ? msg.req.query.grant_type : msg.req.body.grant_type;\nmsg.refresh_token = msg.req.query.refresh_token ? msg.req.query.refresh_token : msg.req.body.refresh_token;\nmsg.code = msg.req.query.code ? msg.req.query.code : msg.req.body.code;\n\nreturn msg;","outputs":1,"noerr":0,"x":430,"y":800,"wires":[["f62893.0a09577"]]},{"id":"e09b5331.a189e","type":"inject","z":"a963f616.6aebe8","name":"Debug","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":90,"y":100,"wires":[["b8289254.47d5e"]]},{"id":"b8289254.47d5e","type":"function","z":"a963f616.6aebe8","name":"Check stores","func":"let authStore = flow.get(\"authStore\") || {};\nlet accessTokenStore = global.get(\"googleAccessTokenStore\") || {};\nlet refreshTokenStore = global.get(\"googleRefreshTokenStore\") || {};\nlet googleClientId = flow.get(\"googleClientId\");\nlet googleClientSecret = flow.get(\"googleClientSecret\");\nlet older = new Date(Date.now() - (60 * 60000));\n\nfor (let code in authStore)\n if (new Date(authStore[code].expiresAt) <= older)\n delete authStore[code];\n\nfor (let code in accessTokenStore)\n if (new Date(accessTokenStore[code].expiresAt) <= older)\n delete accessTokenStore[code];\n\nfor (let code in refreshTokenStore)\n if (new Date(refreshTokenStore[code].expiresAt) <= older)\n delete refreshTokenStore[code];\n\nreturn { \n payload: {\n googleClientId: googleClientId,\n googleClientSecret: googleClientSecret,\n authStore: authStore,\n accessTokenStore: accessTokenStore,\n refreshTokenStore: refreshTokenStore\n } \n};","outputs":1,"noerr":0,"x":330,"y":100,"wires":[["e19fcabf.b98138"]]},{"id":"e19fcabf.b98138","type":"debug","z":"a963f616.6aebe8","name":"","active":true,"console":"false","complete":"false","x":1010,"y":100,"wires":[]},{"id":"2dfc1c3a.133194","type":"comment","z":"a963f616.6aebe8","name":"Login method","info":"Login page will post to this, which will generate a temporary\nauthorization code and redirect back to the Google endpoint.","x":110,"y":480,"wires":[]},{"id":"a0833812.3d4378","type":"http in","z":"a963f616.6aebe8","name":"","url":"/google/oauth-login","method":"post","swaggerDoc":"","x":150,"y":520,"wires":[["b251caab.8f2938"]]},{"id":"b251caab.8f2938","type":"switch","z":"a963f616.6aebe8","name":"Check redirect_uri","property":"req.body.redirect_uri","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"false","outputs":2,"x":390,"y":520,"wires":[["4e93bab2.afaa04"],["89de32c1.804d3"]]},{"id":"6a11568.a9e43a8","type":"function","z":"a963f616.6aebe8","name":"Generate new code","func":"// Validate auth_key\nlet authKey = flow.get(\"authKey\") || \"NONE\";\nif (msg.req.body.auth_key != authKey || authKey === \"NONE\") {\n msg.statusCode = 403;\n msg.payload = \"Incorrect key\";\n\n return msg;\n}\n\n// Validated, grant auth_code\nlet authCode = Math.floor(Math.random() * 10000000000000000000000000000000000000000).toString(36);\nlet authStore = flow.get(\"authStore\") || {};\n\nauthStore[authCode] = {\n type: 'AUTH_CODE',\n uid: 'homeautio',\n clientId: msg.req.body.client_id,\n expiresAt: new Date(Date.now() + (60 * 10000))\n};\n\nflow.set(\"authStore\", authStore);\n\nmsg.statusCode = 302;\nmsg.headers = {\n \"Location\": msg.req.body.redirect_uri + '?code=' + authCode + '&state=' + msg.req.body.state\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":630,"y":660,"wires":[["aff8e338.cf175"]]},{"id":"aff8e338.cf175","type":"http response","z":"a963f616.6aebe8","name":"","x":990,"y":580,"wires":[]},{"id":"4e93bab2.afaa04","type":"function","z":"a963f616.6aebe8","name":"Error: redirect_uri missing","func":"msg.statusCode = 500;\nmsg.payload = 'redirect_uri missing';\n\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":500,"wires":[["aff8e338.cf175"]]},{"id":"89de32c1.804d3","type":"switch","z":"a963f616.6aebe8","name":"Check state","property":"req.body.state","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"false","outputs":2,"x":370,"y":560,"wires":[["9ab324eb.965e38"],["b2c1b653.bffbf8"]]},{"id":"9ab324eb.965e38","type":"function","z":"a963f616.6aebe8","name":"Error: state missing","func":"msg.statusCode = 500;\nmsg.payload = 'state missing';\n\nreturn msg;","outputs":1,"noerr":0,"x":630,"y":540,"wires":[["aff8e338.cf175"]]},{"id":"8e161445.7b6598","type":"switch","z":"a963f616.6aebe8","name":"Check auth_key","property":"req.body.auth_key","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"false","outputs":2,"x":380,"y":640,"wires":[["17f6f63b.08349a"],["6a11568.a9e43a8"]]},{"id":"17f6f63b.08349a","type":"function","z":"a963f616.6aebe8","name":"Error: auth_key missing","func":"msg.statusCode = 500;\nmsg.payload = 'auth_key missing';\n\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":620,"wires":[["aff8e338.cf175"]]},{"id":"b2c1b653.bffbf8","type":"switch","z":"a963f616.6aebe8","name":"Check client_id","property":"req.body.client_id","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"false","outputs":2,"x":380,"y":600,"wires":[["fb21f3f9.fbdba"],["8e161445.7b6598"]]},{"id":"fb21f3f9.fbdba","type":"function","z":"a963f616.6aebe8","name":"Error: client_id missing","func":"msg.statusCode = 500;\nmsg.payload = 'client_id ' + msg.req.body.client_id + ' invalid';\n\nreturn msg;","outputs":1,"noerr":0,"x":640,"y":580,"wires":[["aff8e338.cf175"]]},{"id":"ecb89c32.50ed2","type":"template","z":"a963f616.6aebe8","name":"Display login form","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n <head>\n <title>Login</title>\n </head>\n <body>\n <form action=\"/google/oauth-login\" method=\"post\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"{{req.query.redirect_uri}}\" />\n <input type=\"hidden\" name=\"state\" value=\"{{req.query.state}}\" />\n <input type=\"hidden\" name=\"client_id\" value=\"{{req.query.client_id}}\" />\n Key: <input type=\"text\" name=\"auth_key\"> \n <input type=\"submit\" value=\"Submit\" />\n </form>\n </body>\n</html>","x":630,"y":420,"wires":[["814cb966.112578"]]},{"id":"e4e16182.f31da","type":"function","z":"a963f616.6aebe8","name":"Generate token","func":"// Carry through errors\nif (msg.statusCode == 400 || msg.statusCode == 500)\n return msg;\n\n// Create tokens\nlet accessTokenStore = global.get(\"googleAccessTokenStore\") || {};\nlet accessTokenId = Math.floor(Math.random() * 10000000000000000000000000000000000000000).toString(36);\naccessTokenStore[accessTokenId] = {\n type: 'ACCESS',\n uid: 'homeautio',\n clientId: msg.client_id,\n expiresAt: new Date(Date.now() + (60 * 30000))\n};\n\nlet refreshTokenStore = global.get(\"googleRefreshTokenStore\") || {};\nlet refreshTokenId = Math.floor(Math.random() * 10000000000000000000000000000000000000000).toString(36);\nrefreshTokenStore[refreshTokenId] = {\n type: 'REFRESH',\n uid: 'homeautio',\n clientId: msg.client_id,\n expiresAt: new Date(Date.now() + (60 * 60000 * 336))\n};\n\nlet access_token = {\n accessToken: accessTokenId,\n refreshToken: refreshTokenId\n};\n\nglobal.set(\"googleAccessTokenStore\", accessTokenStore);\n\n// Success\nmsg.statusCode = 200;\nmsg.payload = {\n token_type: \"bearer\",\n access_token: access_token.accessToken,\n refresh_token: access_token.refreshToken,\n expires_in: 600\n};\n\n// Remove now invalid auth code\nif (msg.refresh_token !== null)\n delete refreshTokenStore[msg.refresh_token];\n\nglobal.set(\"googleRefreshTokenStore\", refreshTokenStore);\n\nreturn [ \n msg, \n { payload: accessTokenStore }, \n { payload: refreshTokenStore}\n];","outputs":"3","noerr":0,"x":1080,"y":900,"wires":[["d0a21770.31c528","6e78d24c.36084c"],["8f49db52.3e3708","d0a21770.31c528"],["b82d5bd3.681b68","d0a21770.31c528"]]},{"id":"8d08768e.c38d98","type":"debug","z":"a963f616.6aebe8","name":"","active":false,"console":"false","complete":"true","x":990,"y":180,"wires":[]},{"id":"96d6601f.9e47b","type":"file in","z":"a963f616.6aebe8","name":"Read file","filename":"/data/flowData/googleAccessTokenStore","format":"utf8","sendError":true,"x":320,"y":180,"wires":[["8ef9b1d.99da95"]]},{"id":"8ef9b1d.99da95","type":"json","z":"a963f616.6aebe8","name":"","x":470,"y":180,"wires":[["4dc08849.9db2b8"]]},{"id":"4dc08849.9db2b8","type":"function","z":"a963f616.6aebe8","name":"Update googleAccessTokenStore","func":"if (msg.payload !== null) {\n global.set(\"googleAccessTokenStore\", msg.payload);\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":700,"y":180,"wires":[["8d08768e.c38d98"]]},{"id":"f511f2c.938251","type":"debug","z":"a963f616.6aebe8","name":"","active":false,"console":"false","complete":"true","x":990,"y":220,"wires":[]},{"id":"dc17a5a1.c3d5a8","type":"file in","z":"a963f616.6aebe8","name":"Read file","filename":"/data/flowData/googleRefreshTokenStore","format":"utf8","sendError":true,"x":320,"y":220,"wires":[["8d20feac.0335"]]},{"id":"8d20feac.0335","type":"json","z":"a963f616.6aebe8","name":"","x":470,"y":220,"wires":[["c5339dd2.06dd9"]]},{"id":"c5339dd2.06dd9","type":"function","z":"a963f616.6aebe8","name":"Update googleRefreshTokenStore","func":"if (msg.payload !== null) {\n global.set(\"googleRefreshTokenStore\", msg.payload);\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":700,"y":220,"wires":[["f511f2c.938251"]]},{"id":"8f49db52.3e3708","type":"file","z":"a963f616.6aebe8","name":"Write file","filename":"/data/flowData/googleAccessTokenStore","appendNewline":false,"createDir":false,"overwriteFile":"true","x":1260,"y":900,"wires":[]},{"id":"b82d5bd3.681b68","type":"file","z":"a963f616.6aebe8","name":"Write file","filename":"/data/flowData/googleRefreshTokenStore","appendNewline":false,"createDir":false,"overwriteFile":"true","x":1260,"y":940,"wires":[]},{"id":"d0a21770.31c528","type":"debug","z":"a963f616.6aebe8","name":"","active":false,"console":"false","complete":"false","x":1270,"y":980,"wires":[]},{"id":"dabc2ebc.452a7","type":"credentials","z":"a963f616.6aebe8","name":"","props":[{"value":"googleClientId","type":"flow"},{"value":"googleClientSecret","type":"flow"},{"value":"authKey","type":"flow"}],"x":330,"y":140,"wires":[[]]},{"id":"7b406925.9afee8","type":"http response","z":"a963f616.6aebe8","name":"","x":1070,"y":980,"wires":[]}]

OAuth 2 Authorization Code Flow For Use With Google Actions

This flow provides a "fake" OAuth 2 implementation for use with the Google Actions API.

Exposes three endpoints:

  1. /google/oauth-auth - Main auth endpoint that Google will call to initialize an OAuth 2 handshake. Checks "client" and "client secret", and returns a login form.
  2. /google/oauth-login - Processes login form, verifies "auth key" provided and generates a new OAuth auth code that the caller (Google) can then exchange for a real OAuth 2 ticket via the last endpoint.
  3. /google/oauth-token - Allows the caller to exchange an "auth key" for a valid OAuth 2 token, or request a new token via refresh token.

Usage

  1. Import this to any flow. I like to put this whole thing in it's own flow in my node-red.
  2. Edit the "Credentials" node at the top. This is set to execute once at startup, or when you trigger the "inject" node next to it, to initialize the "client" and "client secret" for Google to use, as well as an "auth key" password for the login screen that will be presented when adding devices in Google Home.
  3. There are four "file" nodes that are used to write the access and refresh token caches to disk so they persist between node-red restarts. Change the file locations in these to point at a good location. Without these, the token caches will be wiped after a restart, which will kill any active tokens (i.e., the ones that Google currently has cached to call these endpoints).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment