LessonsExtensions: Built inLesson 76

Signing in to web services

Let people sign your program in to a web service with OAuth2, send the access token it gives with each request, refresh it when it runs out, use Dropbox, and check that a message a service sends you is really from it.

  • Extensions
  • Lesson 76 of 83
  • About 12 minutes
  • 6 examples

Signing in without a password: OAuth2

A program that reads someone's files on Dropbox, or posts to their team's Slack, needs their permission, but should never see their password. OAuth2 is how almost every web service handles this. Your program opens the service's own sign-in page; the person signs in there and agrees to what your program asks for, its scopes; and the service gives your program an access token, a long random string that stands for that permission and nothing more. The person can take it back at any time from the service's settings.

First you register your program with the service, on its developer pages. It gives you a client id and a client secret, and the addresses of its sign-in page and its token page. You also give it a redirect address, where the sign-in page sends the person when they are done: for a desktop program it is http://127.0.0.1: and a port, because the OAuth2 library listens there itself. Then one command does the whole dance:

ScriptPut it in an object's script
constant kAuthURL = "https://www.dropbox.com/oauth2/authorize"
constant kTokenURL = "https://api.dropboxapi.com/oauth2/token"
constant kClientID = "your client id"
constant kClientSecret = "your client secret"

on mouseUp
   local tAuth
   OAuth2 kAuthURL, kTokenURL, kClientID, kClientSecret, empty, 54303
   if the result is not empty then
      answer error "Not signed in:" && the result
      exit mouseUp
   end if
   put it into tAuth
   set the cAccessToken of this stack to tAuth["access_token"]
   set the cRefreshToken of this stack to tAuth["refresh_token"]
   answer "Signed in."
end mouseUp

OAuth2 shows the sign-in page in a window of its own and waits. When the person is done, it is an array of what the token page sent: access_token, and often refresh_token and expires_in, the seconds the access token lasts. If they close the window or say no, the result says so. The port, 54303 here, must match the redirect address you registered; one from 49152 to 65535 is unlikely to be in use.

Using the token

With each request to the service, send the token in an Authorization header, as Bearer and the token. A service answers a request without a good token with status 401. Here a small server of our own (A web server of your own) plays the part of the service:

ExampleRun it in the playground
on serve pSocket, pRequest
   if pRequest["headers"]["Authorization"] is "Bearer sl.abc123" then
      httpdResponse pSocket, 200, "Your files: notes.txt, photo.jpg"
   else
      httpdResponse pSocket, 401, "Sign in first"
   end if
end serve

httpdStart "serve", 0
put "http://127.0.0.1:" & it & "/files" into tFiles
get URL tFiles
put the result
set the httpHeaders to "Authorization: Bearer sl.abc123"
put URL tFiles
set the httpHeaders to empty
httpdStop
Output
error 401 Unauthorized
Your files: notes.txt, photo.jpg

the httpHeaders goes with every request after it is set, so set it back to empty when you are done, or the token goes to every server you ask for anything.

When the token runs out

An access token usually lasts hours, not months. When the service starts answering 401, the refresh token gets a new one without asking the person again: OAuth2Refresh takes the token page, your id and secret, the refresh token and the port, and puts a new array in it:

ScriptPut it in an object's script
constant kTokenURL = "https://api.dropboxapi.com/oauth2/token"
constant kClientID = "your client id"
constant kClientSecret = "your client secret"

function fetchWithToken pURL
   local tPage, tAuth
   set the httpHeaders to "Authorization: Bearer" && the cAccessToken of this stack
   put URL pURL into tPage
   if the result contains "401" then
      OAuth2Refresh kTokenURL, kClientID, kClientSecret, the cRefreshToken of this stack, 54303
      if the result is empty then
         put it into tAuth
         set the cAccessToken of this stack to tAuth["access_token"]
         set the httpHeaders to "Authorization: Bearer" && tAuth["access_token"]
         put URL pURL into tPage
      end if
   end if
   set the httpHeaders to empty
   return tPage
end fetchWithToken

If the refresh fails too, the person has taken the permission back, or the refresh token has run out as well, and only signing in again with OAuth2 will do.

Dropbox

The Dropbox library wraps Dropbox's whole API, a command for each thing it can do: dropboxListFolder, dropboxUpload, dropboxDownload, dropboxCreateFolder, dropboxMove, dropboxDelete, sharing links and more. Each takes the access token first. With no callback it waits for the answer, which is JSON in it, encoded as UTF-8:

ScriptPut it in an object's script
function dropboxNames pToken, pFolder
   local tList, tEntry, tNames
   dropboxListFolder pToken, pFolder, false, false
   if the result is not empty then
      return "error:" && the result
   end if
   put JSONImport(textDecode(it, "UTF-8")) into tList
   repeat for each element tEntry in tList["entries"]
      put tEntry["name"] & return after tNames
   end repeat
   return line 1 to -1 of tNames
end dropboxNames

command saveNote pToken, pText
   dropboxUpload pToken, "/Notes/today.txt", "overwrite", false, false, textEncode(pText, "UTF-8")
end saveNote

Dropbox paths start with a slash, and the top of the person's Dropbox is the empty path. Register your program in the Dropbox App Console to get its id and secret; its pages also say what each call sends back.

Messages from a service: checking the signature

Many services can also call you: when something happens, they send a webhook, a request to a server of yours. Anyone could send such a request, so the service signs each one with an HMAC of its content and a secret you both know (Hashing and security), in a header. Work out the signature yourself and compare: only a request from the service matches:

ExampleRun it in the playground
constant kSecret = "whsec_our_shared_secret"

function signatureOf pBody
   local tHex
   get binaryDecode("H*", messageAuthenticationCode(pBody, kSecret, "HMAC-SHA-256"), tHex)
   return tHex
end signatureOf

on serve pSocket, pRequest
   if pRequest["headers"]["X-Signature"] is signatureOf(pRequest["content"]) then
      httpdResponse pSocket, 200, "accepted:" && pRequest["content"]
   else
      httpdResponse pSocket, 403, "refused"
   end if
end serve

httpdStart "serve", 0
put "http://127.0.0.1:" & it & "/hook" into tHook
put "order 1042 paid" into tBody
set the httpHeaders to "Content-Type: text/plain" & return & "X-Signature:" && signatureOf(tBody)
post tBody to URL tHook
put it
post "order 1042 refunded" to URL tHook
put the result
set the httpHeaders to empty
httpdStop
Output
accepted: order 1042 paid
error 403 Forbidden

The second request carries a real signature, copied from the first, but for a different message, so it is refused. The Content-Type matters too: post sends form data unless told otherwise, and the server library takes form data apart into parameters and leaves content empty, so the signature would be worked out over nothing. Each service says how it signs (which hash, which header, whether a time is mixed in to stop an old message being sent again), so follow its documentation exactly.

Try it yourself

A token array from OAuth2 has expires_in, the seconds it lasts from when it was given. Write a command keepToken that stores the array with the moment it arrived, and a function tokenIsFresh that takes that moment and is true while there is more than a minute left, so a program can refresh before a request fails rather than after.

Show a solution
ExampleRun it in the playground
global gAuth

command keepToken pAuth, pNow
   global gAuth
   put pAuth into gAuth
   put pNow into gAuth["obtained"]
end keepToken

function tokenIsFresh pNow
   global gAuth
   return pNow < gAuth["obtained"] + gAuth["expires_in"] - 60
end tokenIsFresh

local tAuth
put "sl.abc123" into tAuth["access_token"]
put 14400 into tAuth["expires_in"]
keepToken tAuth, 1000000
put tokenIsFresh(1000000)
put tokenIsFresh(1000000 + 14000)
put tokenIsFresh(1000000 + 14400)
Output
true
true
false

A real program passes the seconds as the moment; passing it in as a parameter, as here, lets you test the function with any moment you like.

Words in this lesson

Each one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.