]> code.delx.au - gnu-emacs-elpa/blob - packages/oauth2/oauth2.el
oauth2: send authentication token via Authorization header
[gnu-emacs-elpa] / packages / oauth2 / oauth2.el
1 ;;; oauth2.el --- OAuth 2.0 Authorization Protocol
2
3 ;; Copyright (C) 2011-2016 Free Software Foundation, Inc
4
5 ;; Author: Julien Danjou <julien@danjou.info>
6 ;; Version: 0.11
7 ;; Keywords: comm
8
9 ;; This file is part of GNU Emacs.
10
11 ;; GNU Emacs is free software: you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
15
16 ;; GNU Emacs is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
20
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
23
24 ;;; Commentary:
25
26 ;; Implementation of the OAuth 2.0 draft.
27 ;;
28 ;; The main entry point is `oauth2-auth-and-store' which will return a token
29 ;; structure. This token structure can be then used with
30 ;; `oauth2-url-retrieve-synchronously' or `oauth2-url-retrieve' to retrieve
31 ;; any data that need OAuth authentication to be accessed.
32 ;;
33 ;; If the token needs to be refreshed, the code handles it automatically and
34 ;; store the new value of the access token.
35
36 ;;; Code:
37
38 (eval-when-compile (require 'cl))
39 (require 'plstore)
40 (require 'json)
41 (require 'url-http)
42
43 (defun oauth2-request-authorization (auth-url client-id &optional scope state redirect-uri)
44 "Request OAuth authorization at AUTH-URL by launching `browse-url'.
45 CLIENT-ID is the client id provided by the provider.
46 It returns the code provided by the service."
47 (browse-url (concat auth-url
48 (if (string-match-p "\?" auth-url) "&" "?")
49 "client_id=" (url-hexify-string client-id)
50 "&response_type=code"
51 "&redirect_uri=" (url-hexify-string (or redirect-uri "urn:ietf:wg:oauth:2.0:oob"))
52 (if scope (concat "&scope=" (url-hexify-string scope)) "")
53 (if state (concat "&state=" (url-hexify-string state)) "")))
54 (read-string "Enter the code your browser displayed: "))
55
56 (defun oauth2-request-access-parse ()
57 "Parse the result of an OAuth request."
58 (goto-char (point-min))
59 (when (search-forward-regexp "^$" nil t)
60 (json-read)))
61
62 (defun oauth2-make-access-request (url data)
63 "Make an access request to URL using DATA in POST."
64 (let ((url-request-method "POST")
65 (url-request-data data)
66 (url-request-extra-headers
67 '(("Content-Type" . "application/x-www-form-urlencoded"))))
68 (with-current-buffer (url-retrieve-synchronously url)
69 (let ((data (oauth2-request-access-parse)))
70 (kill-buffer (current-buffer))
71 data))))
72
73 (defstruct oauth2-token
74 plstore
75 plstore-id
76 client-id
77 client-secret
78 access-token
79 refresh-token
80 token-url
81 access-response)
82
83 (defun oauth2-request-access (token-url client-id client-secret code &optional redirect-uri)
84 "Request OAuth access at TOKEN-URL.
85 The CODE should be obtained with `oauth2-request-authorization'.
86 Return an `oauth2-token' structure."
87 (when code
88 (let ((result
89 (oauth2-make-access-request
90 token-url
91 (concat
92 "client_id=" client-id
93 "&client_secret=" client-secret
94 "&code=" code
95 "&redirect_uri=" (url-hexify-string (or redirect-uri "urn:ietf:wg:oauth:2.0:oob"))
96 "&grant_type=authorization_code"))))
97 (make-oauth2-token :client-id client-id
98 :client-secret client-secret
99 :access-token (cdr (assoc 'access_token result))
100 :refresh-token (cdr (assoc 'refresh_token result))
101 :token-url token-url
102 :access-response result))))
103
104 ;;;###autoload
105 (defun oauth2-refresh-access (token)
106 "Refresh OAuth access TOKEN.
107 TOKEN should be obtained with `oauth2-request-access'."
108 (setf (oauth2-token-access-token token)
109 (cdr (assoc 'access_token
110 (oauth2-make-access-request
111 (oauth2-token-token-url token)
112 (concat "client_id=" (oauth2-token-client-id token)
113 "&client_secret=" (oauth2-token-client-secret token)
114 "&refresh_token=" (oauth2-token-refresh-token token)
115 "&grant_type=refresh_token")))))
116 ;; If the token has a plstore, update it
117 (let ((plstore (oauth2-token-plstore token)))
118 (when plstore
119 (plstore-put plstore (oauth2-token-plstore-id token)
120 nil `(:access-token
121 ,(oauth2-token-access-token token)
122 :refresh-token
123 ,(oauth2-token-refresh-token token)
124 :access-response
125 ,(oauth2-token-access-response token)
126 ))
127 (plstore-save plstore)))
128 token)
129
130 ;;;###autoload
131 (defun oauth2-auth (auth-url token-url client-id client-secret &optional scope state redirect-uri)
132 "Authenticate application via OAuth2."
133 (oauth2-request-access
134 token-url
135 client-id
136 client-secret
137 (oauth2-request-authorization
138 auth-url client-id scope state redirect-uri)
139 redirect-uri))
140
141 (defcustom oauth2-token-file (concat user-emacs-directory "oauth2.plstore")
142 "File path where store OAuth tokens."
143 :group 'oauth2
144 :type 'file)
145
146 (defun oauth2-compute-id (auth-url token-url resource-url)
147 "Compute an unique id based on URLs.
148 This allows to store the token in an unique way."
149 (secure-hash 'md5 (concat auth-url token-url resource-url)))
150
151 ;;;###autoload
152 (defun oauth2-auth-and-store (auth-url token-url resource-url client-id client-secret &optional redirect-uri)
153 "Request access to a resource and store it using `plstore'."
154 ;; We store a MD5 sum of all URL
155 (let* ((plstore (plstore-open oauth2-token-file))
156 (id (oauth2-compute-id auth-url token-url resource-url))
157 (plist (cdr (plstore-get plstore id))))
158 ;; Check if we found something matching this access
159 (if plist
160 ;; We did, return the token object
161 (make-oauth2-token :plstore plstore
162 :plstore-id id
163 :client-id client-id
164 :client-secret client-secret
165 :access-token (plist-get plist :access-token)
166 :refresh-token (plist-get plist :refresh-token)
167 :token-url token-url
168 :access-response (plist-get plist :access-response))
169 (let ((token (oauth2-auth auth-url token-url
170 client-id client-secret resource-url nil redirect-uri)))
171 ;; Set the plstore
172 (setf (oauth2-token-plstore token) plstore)
173 (setf (oauth2-token-plstore-id token) id)
174 (plstore-put plstore id nil `(:access-token
175 ,(oauth2-token-access-token token)
176 :refresh-token
177 ,(oauth2-token-refresh-token token)
178 :access-response
179 ,(oauth2-token-access-response token)))
180 (plstore-save plstore)
181 token))))
182
183 (defun oauth2-url-append-access-token (token url)
184 "Append access token to URL."
185 (concat url
186 (if (string-match-p "\?" url) "&" "?")
187 "access_token=" (oauth2-token-access-token token)))
188
189 (defvar oauth--url-advice nil)
190 (defvar oauth--token-data)
191
192 (defun oauth2-authz-bearer-header (token)
193 "Return 'Authoriztions: Bearer' header with TOKEN."
194 (cons "Authorization" (format "Bearer %s" token)))
195
196 (defun oauth2-extra-headers (extra-headers)
197 "Return EXTRA-HEADERS with 'Authorization: Bearer' added."
198 (cons (oauth2-authz-bearer-header (oauth2-token-access-token (car oauth--token-data)))
199 extra-headers))
200
201
202 ;; FIXME: We should change URL so that this can be done without an advice.
203 (defadvice url-http-handle-authentication (around oauth-hack activate)
204 (if (not oauth--url-advice)
205 ad-do-it
206 (let ((url-request-method url-http-method)
207 (url-request-data url-http-data)
208 (url-request-extra-headers
209 (oauth2-extra-headers url-http-extra-headers))))
210 (oauth2-refresh-access (car oauth--token-data))
211 (url-retrieve-internal (cdr oauth--token-data)
212 url-callback-function
213 url-callback-arguments)
214 ;; This is to make `url' think it's done.
215 (when (boundp 'success) (setq success t)) ;For URL library in Emacs<24.4.
216 (setq ad-return-value t))) ;For URL library in Emacsā‰„24.4.
217
218 ;;;###autoload
219 (defun oauth2-url-retrieve-synchronously (token url &optional request-method request-data request-extra-headers)
220 "Retrieve an URL synchronously using TOKEN to access it.
221 TOKEN can be obtained with `oauth2-auth'."
222 (let* ((oauth--token-data (cons token url)))
223 (let ((oauth--url-advice t) ;Activate our advice.
224 (url-request-method request-method)
225 (url-request-data request-data)
226 (url-request-extra-headers
227 (oauth2-extra-headers request-extra-headers)))
228 (url-retrieve-synchronously url))))
229
230 ;;;###autoload
231 (defun oauth2-url-retrieve (token url callback &optional
232 cbargs
233 request-method request-data request-extra-headers)
234 "Retrieve an URL asynchronously using TOKEN to access it.
235 TOKEN can be obtained with `oauth2-auth'. CALLBACK gets called with CBARGS
236 when finished. See `url-retrieve'."
237 ;; TODO add support for SILENT and INHIBIT-COOKIES. How to handle this in `url-http-handle-authentication'.
238 (let* ((oauth--token-data (cons token url)))
239 (let ((oauth--url-advice t) ;Activate our advice.
240 (url-request-method request-method)
241 (url-request-data request-data)
242 (url-request-extra-headers
243 (oauth2-extra-headers request-extra-headers)))
244 (url-retrieve url callback cbargs))))
245
246 (provide 'oauth2)
247
248 ;;; oauth2.el ends here