Valid bearer token

Good evening. Does anyone perhaps have a sample flow of how to deal with refreshing a bearer token?
I am sending payload to database with the http request node, but have a function node with a valid bearer token for authentication. The token expires every 24hours, so want to have a call to fetch valid token, be able to insert that valid token and then do the put request with the payload. Any help here would be majorly appreciated.

I have a flow that consumes the Spotify API that does this, let me sanitise it a bit and upload it for you. This token is valid for 1 hour. There's 2 options you can take: request either way and if it comes back as invalid refresh and store the token again, or keep a timestamp of the last refresh and if it's longer ago than 95% of your token expiration time refresh it just to be safe that it doesn't run out during your request sequence.

I have the OAuth callback set up in an http-in/http-response structure, so that the initial bearer token and the refresh token arrive in Node-RED immediately, and I store them in the flow context. I store the time of last refresh there too. I've a subflow that handles the checking if the request has expired yet and if so refresh before continuing with the actual request.

Turns out it's already pretty sanitised. It's all core nodes, except for one instance of node-red-contrib-credentials that I use to store client id/client secret.

[{"id":"3de50bd9.3572a4","type":"subflow","name":"Spotify: refresh token","info":"","category":"","in":[{"x":60,"y":80,"wires":[{"id":"1cd24155.73c72f"}]}],"out":[{"x":600,"y":220,"wires":[{"id":"3f20070a.f1bed","port":1},{"id":"d05015c8.d2934","port":0}]}],"env":[{"name":"spotify_obj_path","type":"str","value":"flow.spotify","ui":{"label":{"en-US":"Where is the spotify object located? (local path)"},"type":"input","opts":{"types":["str","num","bool","json","bin","env"]}}}],"color":"#DDAA99"},{"id":"1cd24155.73c72f","type":"function","z":"3de50bd9.3572a4","name":"Needs refresh?","func":"let tokens;\nconst path = env.get('spotify_obj_path');\n\nif (path.startsWith('flow.')) {\n    msg._spotify_store_type = 'flow';\n    const pos = 'flow.'.length;\n    tokens = flow.get('$parent.' + path.slice(pos)).tokens;\n}\nelse if (path.startsWith('global.')) {\n    msg._spotify_store_type = 'global';\n    const pos = 'global.'.length;\n    tokens = global.get(path.slice(pos)).tokens;\n}\nelse {\n    node.error(\"Spotify token storage type unknown. Please prefix with `flow.` or `global.`.\", msg);\n    return null\n}\n\nnow = Date.now();\nif (now - 60000 >= tokens.expires_at) {\n    // Token will expire within in a minute or is already gone. \n    // Refresh now to prevent the following flow part to fail.\n    msg.payload = true;\n    msg._tokens = tokens;\n}\nelse {\n    msg.payload = false;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":220,"y":80,"wires":[["3f20070a.f1bed"]]},{"id":"3f20070a.f1bed","type":"switch","z":"3de50bd9.3572a4","name":"","property":"payload","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"true","repair":false,"outputs":2,"x":390,"y":80,"wires":[["d113569f.677b7"],[]],"outputLabels":["refresh","skip"]},{"id":"d113569f.677b7","type":"change","z":"3de50bd9.3572a4","name":"Prepare request","rules":[{"t":"set","p":"payload","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"payload.grant_type","pt":"msg","to":"refresh_token","tot":"str"},{"t":"set","p":"payload.refresh_token","pt":"msg","to":"_tokens.refresh_token","tot":"msg"},{"t":"set","p":"headers","pt":"msg","to":"(\t   $spotifyObj := $$._spotify_store_type = 'flow' ? $flowContext(\t       '$parent.' & $substringAfter(\t           $env('spotify_obj_path'),\t           'flow.'\t       )\t   ) : $$._spotify_store_type = 'global' ? $globalContext(\t       $substringAfter(\t           $env('spotify_obj_path'),\t           'global.'\t       )\t   ) : null;\t   {\t       \"content-type\": \"application/x-www-form-urlencoded\",\t       \"authorization\": \"Basic \" & $base64encode(\t           $spotifyObj.client_id & \":\" & $spotifyObj.client_secret\t       )\t    \t   }\t\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":660,"y":40,"wires":[["df74278c.9825e8"]]},{"id":"df74278c.9825e8","type":"http request","z":"3de50bd9.3572a4","name":"","method":"POST","ret":"obj","paytoqs":false,"url":"https://accounts.spotify.com/api/token","tls":"","persist":false,"proxy":"","authType":"","x":650,"y":80,"wires":[["a0a69ccb.48766"]]},{"id":"a0a69ccb.48766","type":"function","z":"3de50bd9.3572a4","name":"Prep for saving","func":"msg.payload.expires_at = msg.payload.expires_in * 1000 + Date.now()\nreturn msg;","outputs":1,"noerr":0,"x":660,"y":120,"wires":[["d05015c8.d2934"]]},{"id":"d05015c8.d2934","type":"function","z":"3de50bd9.3572a4","name":"Save tokens","func":"if (msg._spotify_store_type === 'flow') {\n    let base = flow.get('$parent.' + env.get('spotify_obj_path').slice('flow.'.length));\n    base.tokens.access_token = msg.payload.access_token;\n    base.tokens.expires_at = msg.payload.expires_at;\n    flow.set('$parent.' + env.get('spotify_obj_path').slice('flow.'.length), base);\n}\nelse if (msg._spotify_store_type === 'global') {\n    let base = global.get(env.get('spotify_obj_path').slice('global.'.length));\n    base.tokens.access_token = msg.payload.access_token;\n    base.tokens.expires_at = msg.payload.expires_at;\n    flow.set(env.get('spotify_obj_path').slice('global.'.length), base);\n}\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":160,"wires":[[]]},{"id":"5c7f0115.050d38","type":"tab","label":"Spotify","disabled":false,"info":""},{"id":"ac470a4b.382358","type":"http in","z":"5c7f0115.050d38","name":"","url":"/oauth/neela/spotify-callback","method":"get","upload":false,"swaggerDoc":"","x":180,"y":260,"wires":[["f3a46d61.17a578"]]},{"id":"b3dd92fa.0cef28","type":"http response","z":"5c7f0115.050d38","name":"","statusCode":"200","headers":{},"x":1040,"y":260,"wires":[]},{"id":"2e292b14.8c4f14","type":"http request","z":"5c7f0115.050d38","name":"Code to Access Token","method":"POST","ret":"obj","paytoqs":false,"url":"https://accounts.spotify.com/api/token","tls":"","proxy":"","authType":"basic","x":660,"y":306,"wires":[["a095d367.140408","882839ec.1ed54"]]},{"id":"f3a46d61.17a578","type":"change","z":"5c7f0115.050d38","name":"Prepare body","rules":[{"t":"set","p":"payload.grant_type","pt":"msg","to":"authorization_code","tot":"str"},{"t":"set","p":"payload.redirect_uri","pt":"msg","to":"spotify.constants.redirect_uri","tot":"flow"},{"t":"set","p":"payload.client_id","pt":"msg","to":"spotify.client_id","tot":"flow"},{"t":"set","p":"payload.client_secret","pt":"msg","to":"spotify.client_secret","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":444,"y":259,"wires":[["fa6662f6.f5f9f8"]]},{"id":"5cc32435.25ce7c","type":"debug","z":"5c7f0115.050d38","name":"request prepped","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":637,"y":370,"wires":[]},{"id":"fa6662f6.f5f9f8","type":"change","z":"5c7f0115.050d38","name":"Prepare headers","rules":[{"t":"set","p":"headers['content-type']","pt":"msg","to":"application/x-www-form-urlencoded","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":448,"y":306,"wires":[["5cc32435.25ce7c","2e292b14.8c4f14"]]},{"id":"879219b2.d83ec","type":"comment","z":"5c7f0115.050d38","name":"Spotify Connect OAuth 2.0","info":"","x":150,"y":40,"wires":[]},{"id":"ff46d68c.7f4c48","type":"change","z":"5c7f0115.050d38","name":"Save tokens","rules":[{"t":"set","p":"spotify.tokens","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1050,"y":320,"wires":[["dcac984d.847a08"]]},{"id":"a095d367.140408","type":"template","z":"5c7f0115.050d38","name":"","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<!doctype html>\n<html>\n    <head><title>Spotify Connect</title></head>\n    <body>\n        <h1>Thank you for connecting to Spotify</h1>\n        <p>You can now close this page.</p>\n    </body>\n</html>","output":"str","x":860,"y":260,"wires":[["b3dd92fa.0cef28"]]},{"id":"dcac984d.847a08","type":"mqtt out","z":"5c7f0115.050d38","d":true,"name":"","topic":"neela/music/spotify/register","qos":"2","retain":"","broker":"9646c685.9f4cc","x":1280,"y":320,"wires":[]},{"id":"89fb4a3d.870588","type":"comment","z":"5c7f0115.050d38","name":"Spotify API","info":"","x":100,"y":480,"wires":[]},{"id":"972280e6.e8e698","type":"link in","z":"5c7f0115.050d38","name":"Spotify: Pause","links":["b03276a4.d13e58"],"x":75,"y":580,"wires":[["996432e5.5d99b8"]]},{"id":"a92e4cfb.323e28","type":"change","z":"5c7f0115.050d38","name":"Prepare request","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"headers","pt":"msg","to":"{\t    \"Authorization\": \"Bearer \" & $flowContext('spotify').tokens.access_token\t}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":580,"wires":[["543aa23f.1564dc"]]},{"id":"ae78ac7c.0b864","type":"credentials","z":"5c7f0115.050d38","name":"Spotify api credentials","props":[{"value":"spotify.client_id","type":"flow"},{"value":"spotify.client_secret","type":"flow"}],"x":600,"y":80,"wires":[[]]},{"id":"c970c7eb.9536d","type":"inject","z":"5c7f0115.050d38","name":"On flow start...","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":140,"y":80,"wires":[["9a292c6e.1fe4e"]]},{"id":"50af3ee7.e87cd","type":"change","z":"5c7f0115.050d38","name":"URL parameters","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"payload.client_id","pt":"msg","to":"spotify.client_id","tot":"flow"},{"t":"set","p":"payload.scope","pt":"msg","to":"$join(\t   [\t       'user-read-playback-state',\t       'user-modify-playback-state',\t       'user-read-currently-playing',\t       'user-read-recently-played'\t   ],\t   ' '\t)","tot":"jsonata"},{"t":"set","p":"payload.response_type","pt":"msg","to":"code","tot":"str"},{"t":"set","p":"payload.redirect_uri","pt":"msg","to":"spotify.constants.redirect_uri","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":280,"y":160,"wires":[["eb2a48a1.cf119"]]},{"id":"9a292c6e.1fe4e","type":"change","z":"5c7f0115.050d38","name":"Set Spotify constants","rules":[{"t":"set","p":"spotify.constants","pt":"flow","to":"{\"authorize_url\":\"https://accounts.spotify.com/authorize\",\"redirect_uri\":\"http://neela.local:1880/oauth/neela/spotify-callback\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":80,"wires":[["ae78ac7c.0b864"]]},{"id":"eb2a48a1.cf119","type":"change","z":"5c7f0115.050d38","name":"Format URL","rules":[{"t":"set","p":"payload","pt":"msg","to":"$encodeUrl(\t   $flowContext('spotify').constants.authorize_url & \"?\" & $join(\t       $each(\t           $$.payload,\t           function($v, $k) {$k & \"=\" & $v}\t       ),\t       '&'\t   )\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":160,"wires":[["c897fb76.95738"]]},{"id":"cbf276e6.459498","type":"inject","z":"5c7f0115.050d38","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":160,"wires":[["50af3ee7.e87cd"]]},{"id":"c897fb76.95738","type":"debug","z":"5c7f0115.050d38","name":"Authorise URL","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":640,"y":160,"wires":[]},{"id":"543aa23f.1564dc","type":"http request","z":"5c7f0115.050d38","name":"","method":"PUT","ret":"txt","paytoqs":false,"url":"https://api.spotify.com/v1/me/player/pause","tls":"","persist":false,"proxy":"","authType":"","x":570,"y":580,"wires":[["93ae8aa1.485138"]]},{"id":"93ae8aa1.485138","type":"debug","z":"5c7f0115.050d38","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":750,"y":580,"wires":[]},{"id":"34f27eed.a4e222","type":"comment","z":"5c7f0115.050d38","name":"Pause","info":"","x":110,"y":540,"wires":[]},{"id":"5a753fd7.eb028","type":"comment","z":"5c7f0115.050d38","name":"Play","info":"","x":110,"y":640,"wires":[]},{"id":"93e42394.480bb","type":"comment","z":"5c7f0115.050d38","name":"Playback state","info":"","x":140,"y":740,"wires":[]},{"id":"551f55de.e95fac","type":"link in","z":"5c7f0115.050d38","name":"Spotify: Playback state","links":["86eb1c1.8b018e"],"x":75,"y":780,"wires":[["a3c830b1.8fdba"]]},{"id":"435a5c32.d31884","type":"change","z":"5c7f0115.050d38","name":"Prepare request","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"headers","pt":"msg","to":"{\t    \"Authorization\": \"Bearer \" & $flowContext('spotify').tokens.access_token\t}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":780,"wires":[["8a0bf5a8.05c958"]]},{"id":"8a0bf5a8.05c958","type":"http request","z":"5c7f0115.050d38","name":"","method":"GET","ret":"obj","paytoqs":false,"url":"https://api.spotify.com/v1/me/player","tls":"","persist":false,"proxy":"","authType":"","x":570,"y":780,"wires":[["ab324d1b.d0e49","7afdfd11.be2094"]]},{"id":"ab324d1b.d0e49","type":"link out","z":"5c7f0115.050d38","name":"Spotify: Playback state result","links":["bfc752a1.de3b1"],"x":715,"y":780,"wires":[]},{"id":"f378eb76.3dfca8","type":"link in","z":"5c7f0115.050d38","name":"Spotify: Play","links":["b3d3972e.ab4e68"],"x":75,"y":680,"wires":[["157dc1c8.73c1ae"]]},{"id":"6d7067bf.86dee8","type":"change","z":"5c7f0115.050d38","name":"Prepare request","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"headers","pt":"msg","to":"{\t    \"Authorization\": \"Bearer \" & $flowContext('spotify').tokens.access_token\t}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":680,"wires":[["84636e14.0faa1"]]},{"id":"84636e14.0faa1","type":"http request","z":"5c7f0115.050d38","name":"","method":"PUT","ret":"txt","paytoqs":false,"url":"https://api.spotify.com/v1/me/player/play","tls":"","persist":false,"proxy":"","authType":"","x":570,"y":680,"wires":[["18618839.5b3e38"]]},{"id":"18618839.5b3e38","type":"debug","z":"5c7f0115.050d38","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":730,"y":680,"wires":[]},{"id":"7afdfd11.be2094","type":"debug","z":"5c7f0115.050d38","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":730,"y":740,"wires":[]},{"id":"141c4d82.468632","type":"comment","z":"5c7f0115.050d38","name":"Refresh auth token","info":"","x":130,"y":360,"wires":[]},{"id":"30c68144.460e6e","type":"change","z":"5c7f0115.050d38","name":"Prepare request","rules":[{"t":"set","p":"payload","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"payload.grant_type","pt":"msg","to":"refresh_token","tot":"str"},{"t":"set","p":"payload.refresh_token","pt":"msg","to":"spotify.tokens.refresh_token","tot":"flow"},{"t":"set","p":"headers","pt":"msg","to":"{\t    \"content-type\": \"application/x-www-form-urlencoded\",\t    \"authorization\": \"Basic \" & $base64encode($flowContext('spotify').client_id & \":\" & $flowContext('spotify').client_secret)\t}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":420,"wires":[["82ea7a7d.e66508"]]},{"id":"82ea7a7d.e66508","type":"http request","z":"5c7f0115.050d38","name":"","method":"POST","ret":"obj","paytoqs":false,"url":"https://accounts.spotify.com/api/token","tls":"","persist":false,"proxy":"","authType":"","x":430,"y":420,"wires":[["7180e1ff.7c0fd8"]]},{"id":"e91101f7.4c6b98","type":"inject","z":"5c7f0115.050d38","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":100,"y":420,"wires":[["30c68144.460e6e"]]},{"id":"882839ec.1ed54","type":"function","z":"5c7f0115.050d38","name":"Prep for saving","func":"msg.payload.expires_at = msg.payload.expires_in * 1000 + Date.now()\nreturn msg;","outputs":1,"noerr":0,"x":880,"y":320,"wires":[["ff46d68c.7f4c48"]]},{"id":"f3fcd18e.5ff94","type":"change","z":"5c7f0115.050d38","name":"Save tokens","rules":[{"t":"set","p":"spotify.tokens.access_token","pt":"flow","to":"payload.access_token","tot":"msg"},{"t":"set","p":"spotify.tokens.expires_at","pt":"flow","to":"payload.expires_at","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":420,"wires":[[]]},{"id":"7180e1ff.7c0fd8","type":"function","z":"5c7f0115.050d38","name":"Prep for saving","func":"msg.payload.expires_at = msg.payload.expires_in * 1000 + Date.now()\nreturn msg;","outputs":1,"noerr":0,"x":600,"y":420,"wires":[["f3fcd18e.5ff94"]]},{"id":"996432e5.5d99b8","type":"subflow:3de50bd9.3572a4","z":"5c7f0115.050d38","name":"refresh token","env":[],"x":190,"y":580,"wires":[["a92e4cfb.323e28"]]},{"id":"157dc1c8.73c1ae","type":"subflow:3de50bd9.3572a4","z":"5c7f0115.050d38","name":"refresh token","env":[],"x":190,"y":680,"wires":[["6d7067bf.86dee8"]]},{"id":"a3c830b1.8fdba","type":"subflow:3de50bd9.3572a4","z":"5c7f0115.050d38","name":"refresh token","env":[],"x":190,"y":780,"wires":[["435a5c32.d31884"]]},{"id":"133ec8bb.51a767","type":"comment","z":"5c7f0115.050d38","name":"Skip to next","info":"","x":130,"y":840,"wires":[]},{"id":"f9081f5f.c16c3","type":"link in","z":"5c7f0115.050d38","name":"Spotify: Skip to next","links":["be012162.33227"],"x":75,"y":880,"wires":[["51425c0f.4e6514"]]},{"id":"51425c0f.4e6514","type":"subflow:3de50bd9.3572a4","z":"5c7f0115.050d38","name":"refresh token","env":[],"x":190,"y":880,"wires":[["86e0af97.84b6c"]]},{"id":"86e0af97.84b6c","type":"change","z":"5c7f0115.050d38","name":"Prepare request","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"headers","pt":"msg","to":"{\t    \"Authorization\": \"Bearer \" & $flowContext('spotify').tokens.access_token\t}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":880,"wires":[["cedadf84.42fb6"]]},{"id":"cedadf84.42fb6","type":"http request","z":"5c7f0115.050d38","name":"","method":"POST","ret":"txt","paytoqs":false,"url":"https://api.spotify.com/v1/me/player/next","tls":"","persist":false,"proxy":"","authType":"","x":570,"y":880,"wires":[["727a5b52.6f4024"]]},{"id":"727a5b52.6f4024","type":"debug","z":"5c7f0115.050d38","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":750,"y":880,"wires":[]},{"id":"7fbe5154.ed3a2","type":"comment","z":"5c7f0115.050d38","name":"Skip to previous","info":"","x":140,"y":940,"wires":[]},{"id":"62295e66.84126","type":"link in","z":"5c7f0115.050d38","name":"Spotify: Skip to previous","links":["7315d190.88587"],"x":75,"y":980,"wires":[["9800468a.4bc3e8"]]},{"id":"9800468a.4bc3e8","type":"subflow:3de50bd9.3572a4","z":"5c7f0115.050d38","name":"refresh token","env":[],"x":190,"y":980,"wires":[["65706661.85b498"]]},{"id":"65706661.85b498","type":"change","z":"5c7f0115.050d38","name":"Prepare request","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"headers","pt":"msg","to":"{\t    \"Authorization\": \"Bearer \" & $flowContext('spotify').tokens.access_token\t}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":980,"wires":[["25699b4.0e12364"]]},{"id":"25699b4.0e12364","type":"http request","z":"5c7f0115.050d38","name":"","method":"POST","ret":"txt","paytoqs":false,"url":"https://api.spotify.com/v1/me/player/previous","tls":"","persist":false,"proxy":"","authType":"","x":570,"y":980,"wires":[["4e0a369d.53b888"]]},{"id":"4e0a369d.53b888","type":"debug","z":"5c7f0115.050d38","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":750,"y":980,"wires":[]},{"id":"9646c685.9f4cc","type":"mqtt-broker","z":"","name":"local","broker":"neela.local","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]


This is the code that handles connecting and an injectable refresh for testing. One thing to keep in mind: this runs on a tab, so the flow context is that of within the tab. However, the subflow would have its own context but can reach to the parent context. I utilise that by having the subflow have a property that asks "how would you access this context variable from the current view?", and within the subflow it takes the parent of that to acces it. If you keep a similar structure up, you can even do this on nested subflows and keep it working. The code is prepared for the tokens stored on both flow and global context. I've tried documenting the jsonata syntax that I use in places, can't remember the exact state of that documentation though...

3 Likes

Good morning. Thank you for the prompt and detailed response. I will work my way through it and see if I can implement.

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.