PageRenderTime 49ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/src/core/consumer.lisp

http://github.com/skypher/cl-oauth
Lisp | 327 lines | 301 code | 22 blank | 4 comment | 2 complexity | 49bc5fbfb852d140e1ae1555829ba1f1 MD5 | raw file
Possible License(s): LGPL-3.0
  1. (in-package :oauth)
  2. (defun uri-with-additional-query-part (uri query-part)
  3. "Given a URI string or PURI uri, adds the string QUERY-PART to the end of the URI. If
  4. it has query params already they are added onto it."
  5. (let* ((puri (puri:uri uri))
  6. (existing-query-part (puri:uri-query puri)))
  7. (setf (puri:uri-query puri)
  8. (if (and existing-query-part query-part)
  9. (concatenate 'string existing-query-part "&" query-part)
  10. (or existing-query-part query-part)))
  11. (puri:render-uri puri nil)))
  12. (defun build-auth-string (parameters)
  13. (format nil "OAuth ~{~A=~S~^, ~}"
  14. (alexandria:flatten (mapcar
  15. (lambda (x y) (list x y))
  16. (mapcar (compose #'url-encode #'car) parameters)
  17. (mapcar (compose #'url-encode #'cdr) parameters)))))
  18. (defun http-request
  19. (uri &key (auth-location :header) (method :get) auth-parameters parameters additional-headers drakma-args)
  20. (apply #'drakma:http-request
  21. uri
  22. :method method
  23. :parameters (if (eq auth-location :parameters)
  24. (append parameters auth-parameters)
  25. parameters)
  26. :additional-headers (if (eq auth-location :header)
  27. (cons `("Authorization" . ,(build-auth-string auth-parameters))
  28. additional-headers)
  29. additional-headers)
  30. drakma-args))
  31. ;;; SBCL 1.1.6 on OS X does not generate proper random values with (random most-positive-fixnum).
  32. (defun generate-nonce (&optional (size 30))
  33. (with-open-file (in "/dev/urandom" :direction :input :element-type '(unsigned-byte 8))
  34. (with-output-to-string (out)
  35. (loop :repeat size
  36. :do (write (read-byte in) :stream out :pretty nil :base 36)))))
  37. (defun generate-auth-parameters
  38. (consumer signature-method timestamp version &optional token)
  39. (let ((parameters `(("oauth_consumer_key" . ,(token-key consumer))
  40. ("oauth_signature_method" . ,(string signature-method))
  41. ("oauth_timestamp" . ,(princ-to-string timestamp))
  42. #+unix ("oauth_nonce" . ,(generate-nonce))
  43. #-unix ("oauth_nonce" . ,(princ-to-string
  44. (random most-positive-fixnum)))
  45. ("oauth_version" . ,(princ-to-string version)))))
  46. (if token
  47. (cons `("oauth_token" . ,(url-decode (token-key token))) parameters)
  48. parameters)))
  49. (defun obtain-request-token (uri consumer-token
  50. &key (version :1.0) user-parameters drakma-args
  51. (timestamp (get-unix-time))
  52. (auth-location :header)
  53. (request-method :post)
  54. callback-uri
  55. additional-headers
  56. (signature-method :hmac-sha1)
  57. (include-user-parameters-in-signature-p t))
  58. "Additional parameters will be stored in the USER-DATA slot of the token."
  59. ;; TODO: support 1.0a too
  60. (let* ((callback-uri (or callback-uri "oob"))
  61. (auth-parameters (cons `("oauth_callback" . ,callback-uri)
  62. (generate-auth-parameters consumer-token
  63. signature-method
  64. timestamp
  65. version)))
  66. (sbs (signature-base-string :uri uri :request-method request-method
  67. :parameters (sort-parameters (copy-alist (if include-user-parameters-in-signature-p
  68. (append user-parameters auth-parameters)
  69. auth-parameters)))))
  70. (key (hmac-key (token-secret consumer-token)))
  71. (signature (encode-signature (hmac-sha1 sbs key) nil))
  72. (signed-parameters (cons `("oauth_signature" . ,signature) auth-parameters)))
  73. (multiple-value-bind (body status)
  74. (http-request uri
  75. :method request-method
  76. :auth-location auth-location
  77. :auth-parameters signed-parameters
  78. :parameters user-parameters
  79. :additional-headers additional-headers
  80. :drakma-args drakma-args)
  81. (if (eql status 200)
  82. (let* ((response (query-string->alist (typecase body
  83. (string body)
  84. (t (map 'string #'code-char body)))))
  85. (key (cdr (assoc "oauth_token" response :test #'equal)))
  86. (secret (cdr (assoc "oauth_token_secret" response :test #'equal)))
  87. (user-data (set-difference response '("oauth_token" "oauth_token_secret")
  88. :test (lambda (e1 e2)
  89. (equal (car e1) e2)))))
  90. (assert key)
  91. (assert secret)
  92. (make-request-token :consumer consumer-token :key key :secret secret ;; TODO url-decode
  93. :callback-uri (puri:uri callback-uri) :user-data user-data))
  94. (error "Server returned status ~D: ~A" status body)))))
  95. (defun make-authorization-uri (uri request-token &key callback-uri user-parameters)
  96. "Return the service provider's authorization URI. Use the resulting PURI
  97. for a redirect. [6.2.1] in 1.0." ; TODO 1.0a section number
  98. ;; TODO: does 1.0 support oob callbacks?
  99. (when (and request-token (request-token-authorized-p request-token))
  100. (error "Request token ~A already authorized" request-token))
  101. (let* ((parameters (append user-parameters
  102. (when request-token
  103. (list (cons "oauth_token" (token-key request-token))))
  104. (when callback-uri
  105. (list (cons "oauth_callback" callback-uri)))))
  106. (puri (puri:copy-uri (puri:parse-uri uri))))
  107. (setf (puri:uri-query puri)
  108. (if (puri:uri-query puri)
  109. (concatenate 'string
  110. (puri:uri-query puri)
  111. (alist->query-string parameters))
  112. (alist->query-string parameters :include-leading-ampersand nil)))
  113. puri))
  114. (defun authorize-request-token-from-request (request-token-lookup-fn)
  115. "Authorize a request token. Must be running in request context.
  116. REQUEST-TOKEN-LOOKUP-FN will be called with the request token key
  117. and must return a valid unauthorized request token or NIL.
  118. Returns the authorized token or NIL if the token couldn't be found."
  119. (let* ((parameters (get-parameters))
  120. (token-key (cdr (assoc "oauth_token" parameters :test #'equal)))
  121. (verification-code (cdr (assoc "oauth_verifier" parameters :test #'equal))))
  122. (unless token-key
  123. (error "No token key passed"))
  124. (let ((token (funcall request-token-lookup-fn token-key))
  125. (user-parameters (remove-oauth-parameters parameters)))
  126. (cond
  127. (token
  128. (authorize-request-token token)
  129. (setf (request-token-verification-code token) verification-code)
  130. (setf (token-user-data token) user-parameters)
  131. token)
  132. (t
  133. (error "Cannot find request token with key ~A ~
  134. (never requested or already authorized)" token-key))))))
  135. (defun authorize-request-token (request-token)
  136. "Authorize a request token explicitly. Returns the authorized token."
  137. ;; TODO test
  138. (setf (request-token-authorized-p request-token) t)
  139. request-token)
  140. (defun obtain-access-token (uri request-or-access-token &key
  141. (consumer-token (token-consumer request-or-access-token))
  142. (request-method :post)
  143. (auth-location :header)
  144. (version :1.0)
  145. (timestamp (get-unix-time))
  146. xauth-username xauth-password
  147. drakma-args
  148. (signature-method :hmac-sha1))
  149. "Additional parameters will be stored in the USER-DATA slot of the
  150. token. POST is recommended as request method. [6.3.1]" ; TODO 1.0a section number
  151. (let ((refresh-p (typep request-or-access-token 'access-token)))
  152. (when (and request-or-access-token
  153. (not refresh-p))
  154. (assert (request-token-authorized-p request-or-access-token)))
  155. (let* ((parameters (append
  156. (generate-auth-parameters consumer-token
  157. signature-method
  158. timestamp
  159. version
  160. request-or-access-token)
  161. (cond
  162. (refresh-p
  163. `(("oauth_session_handle" . ,(access-token-session-handle
  164. request-or-access-token))))
  165. ((null request-or-access-token)
  166. `(("x_auth_mode" . "client_auth")
  167. ("x_auth_username" . ,xauth-username)
  168. ("x_auth_password" . ,xauth-password)))
  169. (t
  170. (awhen (request-token-verification-code request-or-access-token)
  171. `(("oauth_verifier" . ,it)))))))
  172. (sbs (signature-base-string :uri uri :request-method request-method
  173. :parameters (sort-parameters (copy-alist parameters))))
  174. (key (hmac-key (token-secret consumer-token)
  175. (when request-or-access-token
  176. (url-decode (token-secret request-or-access-token)))))
  177. (signature (encode-signature (hmac-sha1 sbs key) nil))
  178. (signed-parameters (cons `("oauth_signature" . ,signature) parameters)))
  179. (multiple-value-bind (body status)
  180. (http-request uri
  181. :method request-method
  182. :auth-location auth-location
  183. :auth-parameters signed-parameters
  184. :drakma-args drakma-args)
  185. (if (eql status 200)
  186. (let ((response (query-string->alist (if (stringp body)
  187. body
  188. (babel:octets-to-string body)))))
  189. (flet ((field (name)
  190. (cdr (assoc name response :test #'equal))))
  191. (let ((key (field "oauth_token"))
  192. (secret (field "oauth_token_secret"))
  193. (session-handle (field "oauth_session_handle"))
  194. (expires (awhen (field "oauth_expires_in")
  195. (parse-integer it)))
  196. (authorization-expires (awhen (field "oauth_authorization_expires_in")
  197. (parse-integer it)))
  198. (user-data (remove-oauth-parameters response)))
  199. (assert key)
  200. (assert secret)
  201. (make-access-token :consumer consumer-token
  202. :key (url-decode key)
  203. :secret (url-decode secret)
  204. :session-handle session-handle
  205. :expires (awhen expires
  206. (+ (get-universal-time) it))
  207. :authorization-expires (awhen authorization-expires
  208. (+ (get-universal-time) it))
  209. :origin-uri uri
  210. :user-data user-data))))
  211. (error "Couldn't obtain access token: server returned status ~D" status))))))
  212. (defun refresh-access-token (access-token)
  213. (obtain-access-token (access-token-origin-uri access-token) access-token))
  214. (defun maybe-refresh-access-token (access-token &optional on-refresh)
  215. (if (access-token-expired-p access-token)
  216. (let ((new-token (refresh-access-token access-token)))
  217. (when on-refresh
  218. (funcall on-refresh new-token))
  219. new-token)
  220. access-token))
  221. (defun get-problem-report-from-headers (headers)
  222. (let ((authenticate-header (drakma:header-value :www-authenticate headers)))
  223. (when (and authenticate-header (>= (length authenticate-header) 5))
  224. (let ((type (subseq authenticate-header 0 5)))
  225. (when (and (equalp type "OAuth")
  226. (> (length authenticate-header) 5))
  227. (let ((parameters (mapcar (lambda (token)
  228. (destructuring-bind (name value)
  229. (split-sequence #\= token)
  230. (cons name (string-trim '(#\") value))))
  231. (drakma:split-tokens
  232. (subseq authenticate-header 6)))))
  233. parameters))))))
  234. (defun get-problem-report (headers body)
  235. (declare (ignore body)) ; TODO
  236. (let ((from-headers (get-problem-report-from-headers headers)))
  237. from-headers))
  238. (defun access-protected-resource (uri access-token
  239. &rest kwargs
  240. &key
  241. (consumer-token (token-consumer access-token))
  242. on-refresh
  243. (timestamp (get-unix-time))
  244. user-parameters
  245. additional-headers
  246. (version :1.0)
  247. drakma-args
  248. (auth-location :header)
  249. (request-method :get)
  250. (signature-method :hmac-sha1)
  251. (include-user-parameters-in-signature-p t))
  252. "Access the protected resource at URI using ACCESS-TOKEN.
  253. If the token contains OAuth Session information it will be checked for
  254. validity before the request is made. Should the server notify us that
  255. it has prematurely expired the token will be refresh as well and the
  256. request sent again using the new token. ON-REFRESH will be called
  257. whenever the access token is renewed."
  258. (setf access-token (maybe-refresh-access-token access-token on-refresh))
  259. (multiple-value-bind (normalized-uri query-string-parameters) (normalize-uri uri)
  260. (let* ((auth-parameters (generate-auth-parameters consumer-token
  261. signature-method
  262. timestamp
  263. version
  264. access-token))
  265. (sbs (signature-base-string :uri normalized-uri
  266. :request-method request-method
  267. :parameters (sort-parameters (copy-alist (if include-user-parameters-in-signature-p
  268. (append query-string-parameters user-parameters auth-parameters)
  269. auth-parameters)))))
  270. (key (hmac-key (token-secret consumer-token) (token-secret access-token)))
  271. (signature (encode-signature (hmac-sha1 sbs key) nil))
  272. (signed-parameters (cons `("oauth_signature" . ,signature) auth-parameters)))
  273. (when (and (eql request-method :post)
  274. user-parameters)
  275. (assert (and (not (getf drakma-args :content-type))
  276. (not (getf drakma-args :content)))
  277. () "User parameters and content/content-type in drakma arguments cannot be combined")
  278. (setf drakma-args (list* :content-type "application/x-www-form-urlencoded"
  279. :content (alist->query-string user-parameters
  280. :url-encode t
  281. :include-leading-ampersand nil)
  282. drakma-args)))
  283. (multiple-value-bind (body status headers)
  284. (http-request uri
  285. :method request-method
  286. :auth-location auth-location
  287. :auth-parameters signed-parameters
  288. :parameters user-parameters
  289. :additional-headers additional-headers
  290. :drakma-args drakma-args)
  291. (if (eql status 200)
  292. (values body status nil nil headers)
  293. (let* ((problem-report (get-problem-report headers body))
  294. (problem-hint (cdr (assoc "oauth_problem" problem-report :test #'equalp)))
  295. (problem-advice (cdr (assoc "oauth_problem_advice" problem-report :test #'equalp))))
  296. (cond
  297. ((and (eql status 401)
  298. (equalp problem-hint "token_expired"))
  299. (format t "INFO: refreshing access token~%")
  300. (let ((new-token (refresh-access-token access-token)))
  301. (when on-refresh
  302. (funcall on-refresh new-token))
  303. (apply #'access-protected-resource uri new-token kwargs)))
  304. (t
  305. (values body status problem-hint problem-advice headers)))))))))