|
| 1 | +script "com.livecode.script-library.oauth2" |
| 2 | +/* |
| 3 | +Copyright (C) 2016 LiveCode Ltd. |
| 4 | +
|
| 5 | +This file is part of LiveCode. |
| 6 | +
|
| 7 | +LiveCode is free software; you can redistribute it and/or modify it under |
| 8 | +the terms of the GNU General Public License v3 as published by the Free |
| 9 | +Software Foundation. |
| 10 | +
|
| 11 | +LiveCode is distributed in the hope that it will be useful, but WITHOUT ANY |
| 12 | +WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 13 | +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 14 | +for more details. |
| 15 | +
|
| 16 | +You should have received a copy of the GNU General Public License |
| 17 | +along with LiveCode. If not see <http://www.gnu.org/licenses/>. |
| 18 | +*/ |
| 19 | + |
| 20 | +on revLoadLibrary |
| 21 | + if the target is not me then |
| 22 | + pass revLoadLibrary |
| 23 | + end if |
| 24 | + |
| 25 | + insert the script of me into back |
| 26 | + |
| 27 | + if the environment contains "development" then |
| 28 | + set the _ideoverride of me to true |
| 29 | + revSBAddAvailableLibrary the short name of me, "OAuth2" |
| 30 | + revSBAddDependencyForInclusion "scriptLibraries", "OAuth2", "externals", "mergJSON" |
| 31 | + end if |
| 32 | +end revLoadLibrary |
| 33 | + |
| 34 | +on revUnloadLibrary |
| 35 | + if the target is not me then |
| 36 | + pass revUnloadLibrary |
| 37 | + end if |
| 38 | + |
| 39 | + remove the script of me from back |
| 40 | +end revUnloadLibrary |
| 41 | + |
| 42 | +/** |
| 43 | +
|
| 44 | +Present an authorization dialog for any web service that supports OAuth2 Authorization Code Flow |
| 45 | +
|
| 46 | +Requires mergJSON |
| 47 | +
|
| 48 | +*/ |
| 49 | + |
| 50 | +constant kRedirectURL = "http://localhost" |
| 51 | +local sPort = 49152 |
| 52 | + |
| 53 | +local sUniqueRefs |
| 54 | + |
| 55 | +/** |
| 56 | +
|
| 57 | +Present an authorization dialog for any web service that supports OAuth2 Authorization Code Flow |
| 58 | +
|
| 59 | +Description: |
| 60 | +In order to handle the redirect the library accepts socket connections on localhost on a |
| 61 | +configurable port. The redirect uri configured when setting up your application with the |
| 62 | +web service should be `http://localhost:port` where `port` is the port that can be |
| 63 | +configured with `OAuth2Set "port", port`. The default port is 49152 and it is recommended |
| 64 | +to use the range 49152-65535. |
| 65 | +
|
| 66 | +> **Warning:** The client secret should be kept securely when distributing an application in |
| 67 | +> order to protect your application from malicious use. The recommended way to do this is to |
| 68 | +> include the client secret into a script in a password protected stack. If that is not possible |
| 69 | +> allow users to configure their own application with the web service and enter their own |
| 70 | +> client id and secret into a preference instead of distributing your client id and secret. |
| 71 | +
|
| 72 | +Example: |
| 73 | +constant kAuthURL = "https://slack.com/oauth/authorize" |
| 74 | +constant kTokenURL = "https://slack.com/api/oauth.access" |
| 75 | +constant kClientID = "XXXXXXXXX.XXXXXXXX" |
| 76 | +constant kClientSecret = "XXXXXXXXXXXXXXXXXXXXX" |
| 77 | +constant kScopes = "incoming-webhook" |
| 78 | +
|
| 79 | +OAuth2 kAuthURL, kTokenURL, kClientID, kClientSecret, kScopes |
| 80 | +if the result is not empty then |
| 81 | + answer error "Not authorized!" |
| 82 | +else |
| 83 | + local tAuth |
| 84 | + put it into tAuth |
| 85 | + local tMessage |
| 86 | + ask question "What do you want to send?" |
| 87 | + if it is empty then |
| 88 | + exit mouseUp |
| 89 | + end if |
| 90 | + |
| 91 | + put it into tMessage["text"] |
| 92 | + put ArrayToJSON(tMessage) into tMessage |
| 93 | + |
| 94 | + set the httpHeaders to "Content-type: application/json" & \ |
| 95 | + return & "Authorization: token " & sAuth["access_token"] |
| 96 | + post tMessage to url tAuth["incoming_webhook"]["url"] |
| 97 | +end if |
| 98 | +
|
| 99 | +Parameters: |
| 100 | +pAuthURL (string): The URL to present for the authorization page. This can be obtained from the |
| 101 | +API documentation of the service being authorized. |
| 102 | +
|
| 103 | +pTokenURL (string): The URL to obtain the authorization token from once an authorization code is |
| 104 | +sent to the redirect uri. This can be obtained from the API documentation of the service being |
| 105 | +authorized. |
| 106 | +
|
| 107 | +pClientID (string): The application client ID obtained when setting up your application with |
| 108 | +the web service. |
| 109 | +
|
| 110 | +pClientSecret (string): The application client secret obtained when setting up your application with |
| 111 | +the web service. |
| 112 | +
|
| 113 | +pScopes (string): A comma delimited list of authorization scopes. Valid scopes will be found |
| 114 | +in the API documentation of the service being authorized. |
| 115 | +
|
| 116 | +pParams (array): An array of additional key -> value pairs of extra parameters to be sent to |
| 117 | +the authorization url. Some services implement additional options that require extra parameters. |
| 118 | +
|
| 119 | +The result: |
| 120 | +An error string if an error occured during authorization |
| 121 | +
|
| 122 | +It: |
| 123 | +An array containing the parsed JSON data returned by the token url |
| 124 | +
|
| 125 | +*/ |
| 126 | + |
| 127 | +command OAuth2 pAuthURL, pTokenURL, pClientID, pClientSecret, pScopes, pParams |
| 128 | + local tURL |
| 129 | + put pAuthURL & "?response_type=code" into tURL |
| 130 | + |
| 131 | + put "&client_id=" & urlEncode(pClientID) after tURL |
| 132 | + |
| 133 | + replace space with "%20" in pScopes |
| 134 | + put "&scope=" & urlEncode(pScopes) after tURL |
| 135 | + |
| 136 | + local tUniqueRef |
| 137 | + put uuid() into tUniqueRef |
| 138 | + put "&state=" & tUniqueRef after tURL |
| 139 | + |
| 140 | + local tKey |
| 141 | + repeat for each key tKey in pParams |
| 142 | + replace space with "%20" in pParams[tKey] |
| 143 | + put "&" & tKey & "=" & urlEncode(pParams[tKey]) after tURL |
| 144 | + end repeat |
| 145 | + |
| 146 | + put "&redirect_uri=" & urlEncode(kRedirectURL & ":" & sPort & "/") after tURL |
| 147 | + |
| 148 | + local tBrowserRect |
| 149 | + if the environment is not "mobile" then |
| 150 | + reset the templateStack |
| 151 | + create invisible stack tUniqueRef |
| 152 | + set the title of stack tUniqueRef to "Authenticate" |
| 153 | + |
| 154 | + local tOldDefault |
| 155 | + put the defaultStack into tOldDefault |
| 156 | + set the defaultStack to tUniqueRef |
| 157 | + |
| 158 | + set the width of stack tUniqueRef to 640 |
| 159 | + set the height of stack tUniqueRef to 600 |
| 160 | + put 0,0,640,600 into tBrowserRect |
| 161 | + else |
| 162 | + put the effective working screenrect into tBrowserRect |
| 163 | + end if |
| 164 | + |
| 165 | + create widget tUniqueRef as "com.livecode.widget.browser" |
| 166 | + set the rect of widget tUniqueRef to tBrowserRect |
| 167 | + set the url of widget tUniqueRef to tURL |
| 168 | + |
| 169 | + local tOldInterface |
| 170 | + put the defaultNetworkInterface into tOldInterface |
| 171 | + set the defaultNetworkInterface to "127.0.0.1" |
| 172 | + accept connections on port sPort with message "__NewConnection" |
| 173 | + set the defaultNetworkInterface to tOldInterface |
| 174 | + |
| 175 | + put sPort into sUniqueRefs[tUniqueRef] |
| 176 | + |
| 177 | + if the environment is not "mobile" then |
| 178 | + close stack tUniqueRef |
| 179 | + set the style of stack tUniqueRef to "modal" |
| 180 | + set the visible of stack tUniqueRef to true |
| 181 | + set the destroyStack of stack tUniqueRef to true |
| 182 | + |
| 183 | + set the defaultStack to tOldDefault |
| 184 | + |
| 185 | + sheet tUniqueRef |
| 186 | + else |
| 187 | + wait while exists(widget tUniqueRef) with messages |
| 188 | + end if |
| 189 | + |
| 190 | + local tResult |
| 191 | + put the dialogData into tResult |
| 192 | + |
| 193 | + if tResult["code"] is not empty then |
| 194 | + local tParams |
| 195 | + put "grant_type=authorization_code" into tParams |
| 196 | + put "&client_id=" & urlEncode(pClientID) after tParams |
| 197 | + put "&client_secret=" & urlEncode(pClientSecret) after tParams |
| 198 | + put "&code=" & urlEncode(tResult["code"]) after tParams |
| 199 | + put "&redirect_uri=" & urlEncode(kRedirectURL & ":" & sPort & "/") after tParams |
| 200 | + |
| 201 | + local tResponse |
| 202 | + set the httpHeaders to "Accept: application/json" |
| 203 | + |
| 204 | + post tParams to pTokenURL |
| 205 | + put JSONToArray(it) into tResponse |
| 206 | + |
| 207 | + return tResponse for value |
| 208 | + else |
| 209 | + return tResult["error"] for error |
| 210 | + end if |
| 211 | +end OAuth2 |
| 212 | + |
| 213 | +/** |
| 214 | +
|
| 215 | +Set the value of a property |
| 216 | +
|
| 217 | +Example: |
| 218 | +OAuth2Set "port", 65535 |
| 219 | +
|
| 220 | +Parameters: |
| 221 | +pProperty (enum): The property to set |
| 222 | +
|
| 223 | +- "port": The port to use for the redirect uri. Default value is 49152 |
| 224 | +
|
| 225 | +pValue: The property value |
| 226 | +
|
| 227 | +*/ |
| 228 | + |
| 229 | +command OAuth2Set pProperty, pValue |
| 230 | + switch pProperty |
| 231 | + case "port" |
| 232 | + if pValue is not an integer or \ |
| 233 | + pValue > 65535 or pValue < 1 then |
| 234 | + return "invalid port" for error |
| 235 | + end if |
| 236 | + |
| 237 | + put pValue into sPort |
| 238 | + break |
| 239 | + end switch |
| 240 | + |
| 241 | + return empty for error |
| 242 | +end OAuth2Set |
| 243 | + |
| 244 | +command __NewConnection pSocketID |
| 245 | + read from socket pSocketID until crlf & crlf with message "__HandleRequest" |
| 246 | +end __NewConnection |
| 247 | + |
| 248 | +command __HandleRequest pSocketID, pData |
| 249 | + close socket pSocketID |
| 250 | + |
| 251 | + local tRequest |
| 252 | + put word 2 of pData into tRequest |
| 253 | + split tRequest by "?" |
| 254 | + split tRequest[2] by "&" and "=" |
| 255 | + |
| 256 | + local tUniqueRef |
| 257 | + put tRequest[2]["state"] into tUniqueRef |
| 258 | + |
| 259 | + local tPort |
| 260 | + put sUniqueRefs[tUniqueRef] into tPort |
| 261 | + |
| 262 | + if tPort is not empty then |
| 263 | + |
| 264 | + set the dialogData to tRequest[2] |
| 265 | + |
| 266 | + delete variable sUniqueRefs[tUniqueRef] |
| 267 | + |
| 268 | + -- stop accepting only if there aren't any other dialogs |
| 269 | + -- using the same port |
| 270 | + local tStopAccepting = true |
| 271 | + local tOtherPort |
| 272 | + repeat for each element tOtherPort in sUniqueRefs |
| 273 | + if tOtherPort is tPort then |
| 274 | + put false into tStopAccepting |
| 275 | + exit repeat |
| 276 | + end if |
| 277 | + end repeat |
| 278 | + |
| 279 | + if tStopAccepting then |
| 280 | + close socket tPort |
| 281 | + end if |
| 282 | + |
| 283 | + if the environment is "mobile" then |
| 284 | + delete widget tUniqueRef |
| 285 | + else |
| 286 | + close stack tUniqueRef |
| 287 | + end if |
| 288 | + else |
| 289 | + |
| 290 | + -- The OAuth2 spec requires the state parameter |
| 291 | + -- to be returned verbatim to prevent cross site |
| 292 | + -- request forgery so this is an error state on an |
| 293 | + -- unknown request... so we just burn them all |
| 294 | + |
| 295 | + repeat for each key tUniqueRef in sUniqueRefs |
| 296 | + close socket sUniqueRefs[tUniqueRef] |
| 297 | + if the environment is "mobile" then |
| 298 | + delete widget tUniqueRef |
| 299 | + else |
| 300 | + close stack tUniqueRef |
| 301 | + end if |
| 302 | + end repeat |
| 303 | + end if |
| 304 | +end __HandleRequest |
0 commit comments