Chris@0
|
1 /* $Id$
|
Chris@0
|
2
|
Chris@0
|
3 Part of SWI-Prolog
|
Chris@0
|
4
|
Chris@0
|
5 Author: Jan Wielemaker
|
Chris@0
|
6 E-mail: wielemak@science.uva.nl
|
Chris@0
|
7 WWW: http://www.swi-prolog.org
|
Chris@0
|
8 Copyright (C): 2007, University of Amsterdam
|
Chris@0
|
9
|
Chris@0
|
10 This program is free software; you can redistribute it and/or
|
Chris@0
|
11 modify it under the terms of the GNU General Public License
|
Chris@0
|
12 as published by the Free Software Foundation; either version 2
|
Chris@0
|
13 of the License, or (at your option) any later version.
|
Chris@0
|
14
|
Chris@0
|
15 This program is distributed in the hope that it will be useful,
|
Chris@0
|
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@0
|
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@0
|
18 GNU General Public License for more details.
|
Chris@0
|
19
|
Chris@0
|
20 You should have received a copy of the GNU Lesser General Public
|
Chris@0
|
21 License along with this library; if not, write to the Free Software
|
Chris@0
|
22 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
Chris@0
|
23
|
Chris@0
|
24 As a special exception, if you link this library with other files,
|
Chris@0
|
25 compiled with a Free Software compiler, to produce an executable, this
|
Chris@0
|
26 library does not by itself cause the resulting executable to be covered
|
Chris@0
|
27 by the GNU General Public License. This exception does not however
|
Chris@0
|
28 invalidate any other reasons why the executable file might be covered by
|
Chris@0
|
29 the GNU General Public License.
|
Chris@0
|
30 */
|
Chris@0
|
31
|
Chris@0
|
32 :- module(http_openid,
|
Chris@0
|
33 [ openid_login/1, % +OpenID
|
Chris@0
|
34 openid_logout/1, % +OpenID
|
Chris@0
|
35 openid_logged_in/1, % -OpenID
|
Chris@0
|
36
|
Chris@0
|
37 % transparent login
|
Chris@0
|
38 openid_user/3, % +Request, -User, +Options
|
Chris@0
|
39
|
Chris@0
|
40 % low-level primitives
|
Chris@0
|
41 openid_verify/2, % +Options, +Request
|
Chris@0
|
42 openid_authenticate/4, % +Request, -Server, -User, -ReturnTo
|
Chris@0
|
43 openid_associate/3, % +OpenIDServer, -Handle, -Association
|
Chris@0
|
44 openid_server/2, % +Request
|
Chris@0
|
45 openid_grant/1, % +Request
|
Chris@0
|
46 openid_file/1, % +Request (?name=File)
|
Chris@0
|
47 openid_server/3, % ?OpenIDLogin, ?OpenID, ?Server
|
Chris@0
|
48
|
Chris@0
|
49 openid_login_form/4, % +ReturnTo, +Options, //
|
Chris@0
|
50 openid_css/2, % +Emit link to CSS page
|
Chris@0
|
51
|
Chris@0
|
52 openid_current_host/3 % +Request, -Host, -Port
|
Chris@0
|
53 ]).
|
Chris@0
|
54 :- use_module(library('http/http_open')).
|
Chris@0
|
55 :- use_module(library('http/http_client')).
|
Chris@0
|
56 :- use_module(library('http/html_write')).
|
Chris@0
|
57 :- use_module(library('http/http_parameters')).
|
Chris@0
|
58 :- use_module(library('http/http_wrapper')).
|
Chris@0
|
59 :- use_module(library('http/thread_httpd')).
|
Chris@0
|
60 :- use_module(library('http/http_dispatch')).
|
Chris@0
|
61 :- use_module(library('http/http_session')).
|
Chris@0
|
62 :- use_module(library('http/http_host')).
|
Chris@0
|
63 :- use_module(library(utf8)).
|
Chris@0
|
64 :- use_module(library(error)).
|
Chris@0
|
65 :- use_module(library(sgml)).
|
Chris@0
|
66 :- use_module(library(url)).
|
Chris@0
|
67 :- use_module(library(occurs)).
|
Chris@0
|
68 :- use_module(library(base64)).
|
Chris@0
|
69 :- use_module(library(debug)).
|
Chris@0
|
70 :- use_module(library(record)).
|
Chris@0
|
71 :- use_module(library(option)).
|
Chris@0
|
72 :- use_module(library(sha)).
|
Chris@0
|
73 :- use_module(library(socket)).
|
Chris@0
|
74 :- use_module(library(lists)).
|
Chris@0
|
75
|
Chris@0
|
76
|
Chris@0
|
77 /** <module> OpenID consumer library
|
Chris@0
|
78
|
Chris@0
|
79 This library implements the OpenID protocol (http://openid.net/). OpenID
|
Chris@0
|
80 is a protocol to share identities on the network. The protocol itself
|
Chris@0
|
81 uses simple basic HTTP, adding reliability using digitally signed
|
Chris@0
|
82 messages.
|
Chris@0
|
83
|
Chris@0
|
84 Steps, as seen from the _consumer_ (or _|relying partner|_).
|
Chris@0
|
85
|
Chris@0
|
86 1. Show login form, asking for =openid_identifier=
|
Chris@0
|
87 2. Get HTML page from =openid_identifier= and lookup
|
Chris@0
|
88 =|<link rel="openid.server" href="server">|=
|
Chris@0
|
89 3. Associate to _server_
|
Chris@0
|
90 4. Redirect browser (302) to server using mode =checkid_setup=,
|
Chris@0
|
91 asking to validate the given OpenID.
|
Chris@0
|
92 5. OpenID server redirects back, providing digitally signed
|
Chris@0
|
93 conformation of the claimed identity.
|
Chris@0
|
94 6. Validate signature and redirect to the target location.
|
Chris@0
|
95
|
Chris@0
|
96 This module is typically used through openid_user/3.
|
Chris@0
|
97
|
Chris@0
|
98 @author Jan Wielemaker
|
Chris@0
|
99 */
|
Chris@0
|
100
|
Chris@0
|
101 %% openid_hook(+Action)
|
Chris@0
|
102 %
|
Chris@0
|
103 % Call hook on the OpenID management library. Defined hooks are:
|
Chris@0
|
104 %
|
Chris@0
|
105 % * login(+OpenID)
|
Chris@0
|
106 % Consider OpenID logged in.
|
Chris@0
|
107 %
|
Chris@0
|
108 % * logout(+OpenID)
|
Chris@0
|
109 % Logout OpenID
|
Chris@0
|
110 %
|
Chris@0
|
111 % * logged_in(?OpenID)
|
Chris@0
|
112 % True if OpenID is logged in
|
Chris@0
|
113 %
|
Chris@0
|
114 % * grant(+Request, +Options)
|
Chris@0
|
115 % Server: Reply positive on OpenID
|
Chris@0
|
116 %
|
Chris@0
|
117 % * trusted_server(?Server)
|
Chris@0
|
118 % True if Server is a trusted OpenID server
|
Chris@0
|
119
|
Chris@0
|
120 :- multifile
|
Chris@0
|
121 openid_hook/1. % +Action
|
Chris@0
|
122
|
Chris@0
|
123 /*******************************
|
Chris@0
|
124 * DIRECT LOGIN/OUT *
|
Chris@0
|
125 *******************************/
|
Chris@0
|
126
|
Chris@0
|
127 %% openid_login(+OpenID) is det.
|
Chris@0
|
128 %
|
Chris@0
|
129 % Associate the current HTTP session with OpenID. If another
|
Chris@0
|
130 % OpenID is already associated, this association is first removed.
|
Chris@0
|
131
|
Chris@0
|
132 openid_login(OpenID) :-
|
Chris@0
|
133 openid_hook(login(OpenID)), !.
|
Chris@0
|
134 openid_login(OpenID) :-
|
Chris@0
|
135 openid_logout(_),
|
Chris@0
|
136 http_session_assert(openid(OpenID)).
|
Chris@0
|
137
|
Chris@0
|
138 %% openid_logout(+OpenID) is det.
|
Chris@0
|
139 %
|
Chris@0
|
140 % Remove the association of the current session with any OpenID
|
Chris@0
|
141
|
Chris@0
|
142 openid_logout(OpenID) :-
|
Chris@0
|
143 openid_hook(logout(OpenID)), !.
|
Chris@0
|
144 openid_logout(OpenID) :-
|
Chris@0
|
145 http_session_retractall(openid(OpenID)).
|
Chris@0
|
146
|
Chris@0
|
147 %% openid_logged_in(-OpenID) is semidet.
|
Chris@0
|
148 %
|
Chris@0
|
149 % True if session is associated with OpenID.
|
Chris@0
|
150
|
Chris@0
|
151 openid_logged_in(OpenID) :-
|
Chris@0
|
152 openid_hook(logged_in(OpenID)), !.
|
Chris@0
|
153 openid_logged_in(OpenID) :-
|
Chris@0
|
154 http_session_data(openid(OpenID)).
|
Chris@0
|
155
|
Chris@0
|
156
|
Chris@0
|
157 /*******************************
|
Chris@0
|
158 * TOPLEVEL *
|
Chris@0
|
159 *******************************/
|
Chris@0
|
160
|
Chris@0
|
161 %% openid_user(+Request:http_request, -OpenID:url, +Options) is det.
|
Chris@0
|
162 %
|
Chris@0
|
163 % True if OpenID is a validated OpenID associated with the current
|
Chris@0
|
164 % session. The scenario for which this predicate is designed is to
|
Chris@0
|
165 % allow an HTTP handler that requires a valid login to
|
Chris@0
|
166 % use the transparent code below.
|
Chris@0
|
167 %
|
Chris@0
|
168 % ==
|
Chris@0
|
169 % handler(Request) :-
|
Chris@0
|
170 % openid_user(Request, OpenID, []),
|
Chris@0
|
171 % ...
|
Chris@0
|
172 % ==
|
Chris@0
|
173 %
|
Chris@0
|
174 % If the user is not yet logged on a sequence of redirects will
|
Chris@0
|
175 % follow:
|
Chris@0
|
176 %
|
Chris@0
|
177 % 1. Show a page for login (default: page /openid/login),
|
Chris@0
|
178 % predicate reply_openid_login/1)
|
Chris@0
|
179 % 2. Redirect to OpenID server to validate
|
Chris@0
|
180 % 3. Redirect to validation
|
Chris@0
|
181 %
|
Chris@0
|
182 % Options:
|
Chris@0
|
183 %
|
Chris@0
|
184 % * login_url(Login)
|
Chris@0
|
185 % (Local) URL of page to enter OpenID information. Default
|
Chris@0
|
186 % is =|/openid/login|=.
|
Chris@0
|
187 %
|
Chris@0
|
188 % @see openid_authenticate/4 produces errors if login is invalid
|
Chris@0
|
189 % or cancelled.
|
Chris@0
|
190
|
Chris@0
|
191 :- http_handler('/openid/login', openid_login_page, []).
|
Chris@0
|
192 :- http_handler('/openid/verify', openid_verify([]), []).
|
Chris@0
|
193
|
Chris@0
|
194 openid_user(_Request, OpenID, _Options) :-
|
Chris@0
|
195 openid_logged_in(OpenID), !.
|
Chris@0
|
196 openid_user(Request, User, _Options) :-
|
Chris@0
|
197 openid_authenticate(Request, _OpenIdServer, OpenID, _ReturnTo), !,
|
Chris@0
|
198 openid_server(User, OpenID, _),
|
Chris@0
|
199 openid_login(User).
|
Chris@0
|
200 openid_user(Request, _OpenID, Options) :-
|
Chris@0
|
201 option(login_url(Login), Options, '/openid/login'),
|
Chris@0
|
202 current_url(Request, Here),
|
Chris@0
|
203 redirect_browser(Login,
|
Chris@0
|
204 [ 'openid.return_to' = Here
|
Chris@0
|
205 ]).
|
Chris@0
|
206
|
Chris@0
|
207
|
Chris@0
|
208 %% openid_login_page(+Request) is det.
|
Chris@0
|
209 %
|
Chris@0
|
210 % Present a login-form for OpenID. There are two ways to redefine
|
Chris@0
|
211 % this default login page. One is to provide the option
|
Chris@0
|
212 % =login_url= to openid_user/3 and the other is to define a new
|
Chris@0
|
213 % handler for =|/openid/login|= using http_handler/3.
|
Chris@0
|
214 %
|
Chris@0
|
215 % @tbd Add CSS to page
|
Chris@0
|
216 % @tbd Use http_current_handler/2 to make the link more dynamic.
|
Chris@0
|
217
|
Chris@0
|
218 openid_login_page(Request) :-
|
Chris@0
|
219 http_parameters(Request,
|
Chris@0
|
220 [ 'openid.return_to'(ReturnTo, [])
|
Chris@0
|
221 ]),
|
Chris@0
|
222 reply_html_page([ title('OpenID login'),
|
Chris@0
|
223 \openid_css
|
Chris@0
|
224 ],
|
Chris@0
|
225 [ \openid_login_form(ReturnTo, [])
|
Chris@0
|
226 ]).
|
Chris@0
|
227
|
Chris@0
|
228 %% openid_css// is det.
|
Chris@0
|
229 %
|
Chris@0
|
230 % Emit a link to the OpenID CSS file.
|
Chris@0
|
231
|
Chris@0
|
232 openid_css -->
|
Chris@0
|
233 html(link([ rel(stylesheet),
|
Chris@0
|
234 type('text/css'),
|
Chris@0
|
235 href('file?name=openid_css')
|
Chris@0
|
236 ])).
|
Chris@0
|
237
|
Chris@0
|
238 %% openid_login_form(+ReturnTo, +Options)// is det.
|
Chris@0
|
239 %
|
Chris@0
|
240 % Create the OpenID form. This exported as a seperate DCG,
|
Chris@0
|
241 % allowing applications to redefine /openid/login and reuse this
|
Chris@0
|
242 % part of the page.
|
Chris@0
|
243
|
Chris@0
|
244 openid_login_form(ReturnTo, Options) -->
|
Chris@0
|
245 { option(action(Action), Options, verify)
|
Chris@0
|
246 },
|
Chris@0
|
247 html(div(class('openid-login'),
|
Chris@0
|
248 [ \openid_title,
|
Chris@0
|
249 form([ name(login),
|
Chris@0
|
250 action(Action),
|
Chris@0
|
251 method('GET')
|
Chris@0
|
252 ],
|
Chris@0
|
253 [ \hidden('openid.return_to', ReturnTo),
|
Chris@0
|
254 div([ input([ class('openid-input'),
|
Chris@0
|
255 name(openid_url),
|
Chris@0
|
256 size(30)
|
Chris@0
|
257 ]),
|
Chris@0
|
258 input([ type(submit),
|
Chris@0
|
259 value('Verify!')
|
Chris@0
|
260 ])
|
Chris@0
|
261 ])
|
Chris@0
|
262 ])
|
Chris@0
|
263 ])).
|
Chris@0
|
264
|
Chris@0
|
265
|
Chris@0
|
266
|
Chris@0
|
267 /*******************************
|
Chris@0
|
268 * HTTP REPLIES *
|
Chris@0
|
269 *******************************/
|
Chris@0
|
270
|
Chris@0
|
271 %% openid_verify(+Options, +Request)
|
Chris@0
|
272 %
|
Chris@0
|
273 % Handle the initial login form presented to the user by the
|
Chris@0
|
274 % relying party (consumer). This predicate discovers the OpenID
|
Chris@0
|
275 % server, associates itself with this server and redirects the
|
Chris@0
|
276 % user's browser to the OpenID server, providing the extra
|
Chris@0
|
277 % openid.X name-value pairs. Options is, against the conventions,
|
Chris@0
|
278 % placed in front of the Request to allow for smooth cooperation
|
Chris@0
|
279 % with http_dispatch.pl.
|
Chris@0
|
280 %
|
Chris@0
|
281 % The OpenId server will redirect to the openid.return_to URL.
|
Chris@0
|
282 %
|
Chris@0
|
283 % @throws http_reply(moved_temporary(Redirect))
|
Chris@0
|
284
|
Chris@0
|
285 openid_verify(Options, Request) :-
|
Chris@0
|
286 http_parameters(Request,
|
Chris@0
|
287 [ openid_url(URL, [length>1]),
|
Chris@0
|
288 'openid.return_to'(ReturnTo0, [optional(true)])
|
Chris@0
|
289 ]),
|
Chris@0
|
290 ( option(return_to(ReturnTo1), Options) % Option
|
Chris@0
|
291 -> current_url(Request, CurrentLocation),
|
Chris@0
|
292 global_url(ReturnTo1, CurrentLocation, ReturnTo)
|
Chris@0
|
293 ; nonvar(ReturnTo0)
|
Chris@0
|
294 -> ReturnTo = ReturnTo0 % Form-data
|
Chris@0
|
295 ; current_url(Request, CurrentLocation),
|
Chris@0
|
296 ReturnTo = CurrentLocation % Current location
|
Chris@0
|
297 ),
|
Chris@0
|
298 current_root_url(Request, CurrentRoot),
|
Chris@0
|
299 option(trust_root(TrustRoot), Options, CurrentRoot),
|
Chris@0
|
300 openid_resolve(URL, OpenIDLogin, OpenID, Server),
|
Chris@0
|
301 trusted(OpenID, Server),
|
Chris@0
|
302 openid_associate(Server, Handle, _Assoc),
|
Chris@0
|
303 assert_openid(OpenIDLogin, OpenID, Server),
|
Chris@0
|
304 redirect_browser(Server, [ 'openid.mode' = checkid_setup,
|
Chris@0
|
305 'openid.identity' = OpenID,
|
Chris@0
|
306 'openid.assoc_handle' = Handle,
|
Chris@0
|
307 'openid.return_to' = ReturnTo,
|
Chris@0
|
308 'openid.trust_root' = TrustRoot
|
Chris@0
|
309 ]).
|
Chris@0
|
310
|
Chris@0
|
311
|
Chris@0
|
312 %% assert_openid(+OpenIDLogin, +OpenID, +Server) is det.
|
Chris@0
|
313 %
|
Chris@0
|
314 % Associate the OpenID as typed by the user, the OpenID as
|
Chris@0
|
315 % validated by the Server with the current HTTP session.
|
Chris@0
|
316 %
|
Chris@0
|
317 % @param OpenIDLogin Canonized OpenID typed by user
|
Chris@0
|
318 % @param OpenID OpenID verified by Server.
|
Chris@0
|
319
|
Chris@0
|
320 assert_openid(OpenIDLogin, OpenID, Server) :-
|
Chris@0
|
321 http_session_assert(openid_login(OpenIDLogin, OpenID, Server)).
|
Chris@0
|
322
|
Chris@0
|
323 %% openid_server(?OpenIDLogin, ?OpenID, ?Server) is nondet.
|
Chris@0
|
324 %
|
Chris@0
|
325 % True if OpenIDLogin is the typed id for OpenID verified by
|
Chris@0
|
326 % Server.
|
Chris@0
|
327 %
|
Chris@0
|
328 % @param OpenIDLogin ID as typed by user (canonized)
|
Chris@0
|
329 % @param OpenID ID as verified by server
|
Chris@0
|
330 % @param Server URL of the OpenID server
|
Chris@0
|
331
|
Chris@0
|
332 openid_server(OpenIDLogin, OpenID, Server) :-
|
Chris@0
|
333 http_session_data(openid_login(OpenIDLogin, OpenID, Server)), !.
|
Chris@0
|
334
|
Chris@0
|
335
|
Chris@0
|
336 %% current_url(+Request, -Root) is det.
|
Chris@0
|
337 %% current_root_url(+Request, -Root) is det.
|
Chris@0
|
338 %
|
Chris@0
|
339 % Return URL of current request or current root.
|
Chris@0
|
340
|
Chris@0
|
341 current_root_url(Request, Root) :-
|
Chris@0
|
342 openid_current_host(Request, Host, Port),
|
Chris@0
|
343 parse_url(Root, [protocol(http), host(Host), port(Port), path(/)]).
|
Chris@0
|
344
|
Chris@0
|
345 current_url(Request, URL) :-
|
Chris@0
|
346 openid_current_host(Request, Host, Port),
|
Chris@0
|
347 ( option(x_redirected_path(Path), Request)
|
Chris@0
|
348 -> true
|
Chris@0
|
349 ; option(path(Path), Request, /)
|
Chris@0
|
350 ),
|
Chris@0
|
351 option(search(Search), Request, []),
|
Chris@0
|
352 parse_url(URL, [ protocol(http), host(Host), port(Port),
|
Chris@0
|
353 path(Path), search(Search)
|
Chris@0
|
354 ]).
|
Chris@0
|
355
|
Chris@0
|
356 %% openid_current_host(Request, Host, Port)
|
Chris@0
|
357 %
|
Chris@0
|
358 % Find current location of the server.
|
Chris@0
|
359
|
Chris@0
|
360 openid_current_host(Request, Host, Port) :-
|
Chris@0
|
361 http_current_host(Request, Host, Port,
|
Chris@0
|
362 [ global(true)
|
Chris@0
|
363 ]).
|
Chris@0
|
364
|
Chris@0
|
365
|
Chris@0
|
366 %% redirect_browser(+URL, +FormExtra)
|
Chris@0
|
367 %
|
Chris@0
|
368 % Generate a 302 temporary redirect to URL, adding the extra form
|
Chris@0
|
369 % information from FormExtra. The specs says we must retain the
|
Chris@0
|
370 % search specification already attached to the URL.
|
Chris@0
|
371
|
Chris@0
|
372 redirect_browser(URL, FormExtra) :-
|
Chris@0
|
373 is_absolute_url(URL), !,
|
Chris@0
|
374 parse_url(URL, Parts0),
|
Chris@0
|
375 ( select(search(List), Parts0, Parts1)
|
Chris@0
|
376 -> append(List, FormExtra, Search),
|
Chris@0
|
377 Parts = [search(Search)|Parts1]
|
Chris@0
|
378 ; Parts = [search(FormExtra)|Parts0]
|
Chris@0
|
379 ),
|
Chris@0
|
380 parse_url(Redirect, Parts),
|
Chris@0
|
381 throw(http_reply(moved_temporary(Redirect))).
|
Chris@0
|
382 redirect_browser(Location, FormExtra) :-
|
Chris@0
|
383 http_location(Parts0, Location),
|
Chris@0
|
384 ( select(search(List), Parts0, Parts1)
|
Chris@0
|
385 -> append(List, FormExtra, Search),
|
Chris@0
|
386 Parts = [search(Search)|Parts1]
|
Chris@0
|
387 ; Parts = [search(FormExtra)|Parts0]
|
Chris@0
|
388 ),
|
Chris@0
|
389 http_location(Parts, Redirect),
|
Chris@0
|
390 throw(http_reply(moved_temporary(Redirect))).
|
Chris@0
|
391
|
Chris@0
|
392
|
Chris@0
|
393 /*******************************
|
Chris@0
|
394 * RESOLVE *
|
Chris@0
|
395 *******************************/
|
Chris@0
|
396
|
Chris@0
|
397 %% openid_resolve(+URL, -OpenIDOrig, -OpenID, -Server)
|
Chris@0
|
398 %
|
Chris@0
|
399 % True if OpenID is the claimed OpenID that belongs to URL and
|
Chris@0
|
400 % Server is the URL of the OpenID server that can be asked to
|
Chris@0
|
401 % verify this claim.
|
Chris@0
|
402 %
|
Chris@0
|
403 % @param URL The OpenID typed by the user
|
Chris@0
|
404 % @param OpenIDOrig Canonized OpenID typed by user
|
Chris@0
|
405 % @param OpenID Possibly delegated OpenID
|
Chris@0
|
406 % @param Server OpenID server that must validate OpenID
|
Chris@0
|
407 %
|
Chris@0
|
408 % @tbd Implement complete URL canonization as defined by the
|
Chris@0
|
409 % OpenID 2.0 proposal.
|
Chris@0
|
410
|
Chris@0
|
411 openid_resolve(URL, OpenID0, OpenID, Server) :-
|
Chris@0
|
412 debug(openid(resolve), 'Opening ~w ...', [URL]),
|
Chris@0
|
413 http_open(URL, Stream,
|
Chris@0
|
414 [ final_url(OpenID0)
|
Chris@0
|
415 ]),
|
Chris@0
|
416 dtd(html, DTD),
|
Chris@0
|
417 call_cleanup(load_structure(Stream, Term,
|
Chris@0
|
418 [ dtd(DTD),
|
Chris@0
|
419 dialect(sgml),
|
Chris@0
|
420 shorttag(false),
|
Chris@0
|
421 syntax_errors(quiet)
|
Chris@0
|
422 ]),
|
Chris@0
|
423 close(Stream)),
|
Chris@0
|
424 debug(openid(resolve), 'Scanning HTML document ...', [URL]),
|
Chris@0
|
425 contains_term(element(head, _, Head), Term),
|
Chris@0
|
426 ( link(Head, 'openid.server', Server)
|
Chris@0
|
427 -> debug(openid(resolve), 'OpenID Server=~q', [Server])
|
Chris@0
|
428 ; debug(openid(resolve), 'No server in ~q', [Head]),
|
Chris@0
|
429 fail
|
Chris@0
|
430 ),
|
Chris@0
|
431 ( link(Head, 'openid.delegate', OpenID)
|
Chris@0
|
432 -> debug(openid(resolve), 'OpenID = ~q (delegated)', [OpenID])
|
Chris@0
|
433 ; OpenID = OpenID0,
|
Chris@0
|
434 debug(openid(resolve), 'OpenID = ~q', [OpenID])
|
Chris@0
|
435 ).
|
Chris@0
|
436
|
Chris@0
|
437
|
Chris@0
|
438 link(DOM, Type, Target) :-
|
Chris@0
|
439 sub_term(element(link, Attrs, []), DOM),
|
Chris@0
|
440 memberchk(rel=Type, Attrs),
|
Chris@0
|
441 memberchk(href=Target, Attrs).
|
Chris@0
|
442
|
Chris@0
|
443
|
Chris@0
|
444 /*******************************
|
Chris@0
|
445 * AUTHENTICATE *
|
Chris@0
|
446 *******************************/
|
Chris@0
|
447
|
Chris@0
|
448
|
Chris@0
|
449 %% openid_authenticate(+Request, -Server:url, -OpenID:url,
|
Chris@0
|
450 %% -ReturnTo:url) is semidet.
|
Chris@0
|
451 %
|
Chris@0
|
452 % Succeeds if Request comes from the OpenID server and confirms
|
Chris@0
|
453 % that User is a verified OpenID user. ReturnTo provides the URL
|
Chris@0
|
454 % to return to.
|
Chris@0
|
455 %
|
Chris@0
|
456 % After openid_verify/2 has redirected the browser to the OpenID
|
Chris@0
|
457 % server, and the OpenID server did its magic, it redirects the
|
Chris@0
|
458 % browser back to this address. The work is fairly trivial. If
|
Chris@0
|
459 % =mode= is =cancel=, the OpenId server denied. If =id_res=, the
|
Chris@0
|
460 % OpenId server replied positive, but we must verify what the
|
Chris@0
|
461 % server tells us by checking the HMAC-SHA signature.
|
Chris@0
|
462 %
|
Chris@0
|
463 % This call fails silently if their is no =|openid.mode|= field in
|
Chris@0
|
464 % the request.
|
Chris@0
|
465 %
|
Chris@0
|
466 % @throws openid(cancel)
|
Chris@0
|
467 % if request was cancelled by the OpenId server
|
Chris@0
|
468 % @throws openid(signature_mismatch)
|
Chris@0
|
469 % if the HMAC signature check failed
|
Chris@0
|
470
|
Chris@0
|
471 openid_authenticate(Request, OpenIdServer, Identity, ReturnTo) :-
|
Chris@0
|
472 http_parameters(Request,
|
Chris@0
|
473 [ 'openid.mode'(Mode, [optional(true)])
|
Chris@0
|
474 ]),
|
Chris@0
|
475 ( var(Mode)
|
Chris@0
|
476 -> fail
|
Chris@0
|
477 ; Mode == cancel
|
Chris@0
|
478 -> throw(openid(cancel))
|
Chris@0
|
479 ; Mode == id_res
|
Chris@0
|
480 -> http_parameters(Request,
|
Chris@0
|
481 [ 'openid.identity'(Identity, []),
|
Chris@0
|
482 'openid.assoc_handle'(Handle, []),
|
Chris@0
|
483 'openid.return_to'(ReturnTo, []),
|
Chris@0
|
484 'openid.signed'(AtomFields, []),
|
Chris@0
|
485 'openid.sig'(Base64Signature, []),
|
Chris@0
|
486 'openid.invalidate_handle'(Invalidate,
|
Chris@0
|
487 [optional(true)])
|
Chris@0
|
488 ],
|
Chris@0
|
489 [ form_data(Form)
|
Chris@0
|
490 ]),
|
Chris@0
|
491 concat_atom(SignedFields, ',', AtomFields),
|
Chris@0
|
492 check_obligatory_fields(SignedFields),
|
Chris@0
|
493 signed_pairs(SignedFields,
|
Chris@0
|
494 [ mode-Mode,
|
Chris@0
|
495 identity-Identity,
|
Chris@0
|
496 assoc_handle-Handle,
|
Chris@0
|
497 return_to-ReturnTo,
|
Chris@0
|
498 invalidate_handle-Invalidate
|
Chris@0
|
499 ],
|
Chris@0
|
500 Form,
|
Chris@0
|
501 SignedPairs),
|
Chris@0
|
502 ( openid_associate(OpenIdServer, Handle, Assoc)
|
Chris@0
|
503 -> signature(SignedPairs, Assoc, Sig)
|
Chris@0
|
504 ; existence_error(assoc_handle, Handle)
|
Chris@0
|
505 ),
|
Chris@0
|
506
|
Chris@0
|
507 atom_codes(Base64Signature, Base64SigCodes),
|
Chris@0
|
508 phrase(base64(Signature), Base64SigCodes),
|
Chris@0
|
509 ( Sig == Signature
|
Chris@0
|
510 -> true
|
Chris@0
|
511 ; throw(openid(signature_mismatch))
|
Chris@0
|
512 )
|
Chris@0
|
513 ).
|
Chris@0
|
514
|
Chris@0
|
515 %% signed_pairs(+FieldNames, +Pairs:list(Field-Value), +Form, -SignedPairs) is det.
|
Chris@0
|
516 %
|
Chris@0
|
517 % Extract the signed field in the order they appear in FieldNames.
|
Chris@0
|
518
|
Chris@0
|
519 signed_pairs([], _, _, []).
|
Chris@0
|
520 signed_pairs([Field|T0], Pairs, Form, [Field-Value|T]) :-
|
Chris@0
|
521 memberchk(Field-Value, Pairs), !,
|
Chris@0
|
522 signed_pairs(T0, Pairs, Form, T).
|
Chris@0
|
523 signed_pairs([Field|T0], Pairs, Form, [Field-Value|T]) :-
|
Chris@0
|
524 atom_concat('openid.', Field, OpenIdField),
|
Chris@0
|
525 memberchk(OpenIdField=Value, Form), !,
|
Chris@0
|
526 signed_pairs(T0, Pairs, Form, T).
|
Chris@0
|
527 signed_pairs([Field|T0], Pairs, Form, T) :-
|
Chris@0
|
528 format(user_error, 'Form = ~p~n', [Form]),
|
Chris@0
|
529 throw(error(existence_error(field, Field),
|
Chris@0
|
530 context(_, 'OpenID Signed field is not present'))),
|
Chris@0
|
531 signed_pairs(T0, Pairs, Form, T).
|
Chris@0
|
532
|
Chris@0
|
533
|
Chris@0
|
534 %% check_obligatory_fields(+SignedFields:list) is det.
|
Chris@0
|
535 %
|
Chris@0
|
536 % Verify fields from obligatory_field/1 are in the signed field
|
Chris@0
|
537 % list.
|
Chris@0
|
538 %
|
Chris@0
|
539 % @error existence_error(field, Field)
|
Chris@0
|
540
|
Chris@0
|
541 check_obligatory_fields(Fields) :-
|
Chris@0
|
542 ( obligatory_field(Field),
|
Chris@0
|
543 ( memberchk(Field, Fields)
|
Chris@0
|
544 -> true
|
Chris@0
|
545 ; throw(error(existence_error(field, Field),
|
Chris@0
|
546 context(_, 'OpenID field is not in signed fields')))
|
Chris@0
|
547 ),
|
Chris@0
|
548 fail
|
Chris@0
|
549 ; true
|
Chris@0
|
550 ).
|
Chris@0
|
551
|
Chris@0
|
552 obligatory_field(identity).
|
Chris@0
|
553
|
Chris@0
|
554
|
Chris@0
|
555 /*******************************
|
Chris@0
|
556 * OPENID SERVER *
|
Chris@0
|
557 *******************************/
|
Chris@0
|
558
|
Chris@0
|
559 :- dynamic
|
Chris@0
|
560 server_association/3. % URL, Handle, Term
|
Chris@0
|
561
|
Chris@0
|
562 %% openid_server(+Options, +Request)
|
Chris@0
|
563 %
|
Chris@0
|
564 % Realise the OpenID server. The protocol demands a POST request
|
Chris@0
|
565 % here.
|
Chris@0
|
566
|
Chris@0
|
567 openid_server(Options, Request) :-
|
Chris@0
|
568 http_parameters(Request,
|
Chris@0
|
569 [ 'openid.mode'(Mode)
|
Chris@0
|
570 ],
|
Chris@0
|
571 [ attribute_declarations(openid_attribute),
|
Chris@0
|
572 form_data(Form)
|
Chris@0
|
573 ]),
|
Chris@0
|
574 ( Mode == associate
|
Chris@0
|
575 -> associate_server(Request, Form, Options)
|
Chris@0
|
576 ; Mode == checkid_setup
|
Chris@0
|
577 -> checkid_setup_server(Request, Form, Options)
|
Chris@0
|
578 ).
|
Chris@0
|
579
|
Chris@0
|
580 %% associate_server(+Request, +Form, +Options)
|
Chris@0
|
581 %
|
Chris@0
|
582 % Handle the association-request. If successful, create a clause
|
Chris@0
|
583 % for server_association/3 to record the current association.
|
Chris@0
|
584
|
Chris@0
|
585 associate_server(Request, Form, Options) :-
|
Chris@0
|
586 memberchk('openid.assoc_type' = AssocType, Form),
|
Chris@0
|
587 memberchk('openid.session_type' = SessionType, Form),
|
Chris@0
|
588 memberchk('openid.dh_modulus' = P64, Form),
|
Chris@0
|
589 memberchk('openid.dh_gen' = G64, Form),
|
Chris@0
|
590 memberchk('openid.dh_consumer_public' = CPX64, Form),
|
Chris@0
|
591 base64_btwoc(P, P64),
|
Chris@0
|
592 base64_btwoc(G, G64),
|
Chris@0
|
593 base64_btwoc(CPX, CPX64),
|
Chris@0
|
594 dh_x(P, Y), % Our secret
|
Chris@0
|
595 DiffieHellman is powm(CPX, Y, P),
|
Chris@0
|
596 btwoc(DiffieHellman, DHBytes),
|
Chris@0
|
597 sha_hash(DHBytes, SHA1, [algorithm(sha1)]),
|
Chris@0
|
598 CPY is powm(G, Y, P),
|
Chris@0
|
599 base64_btwoc(CPY, CPY64),
|
Chris@0
|
600 new_assoc_handle(Handle),
|
Chris@0
|
601 random_bytes(20, MacKey),
|
Chris@0
|
602 xor_codes(MacKey, SHA1, EncKey),
|
Chris@0
|
603 phrase(base64(EncKey), Base64EncKey),
|
Chris@0
|
604 DefExpriresIn is 24*3600,
|
Chris@0
|
605 option(expires_in(ExpriresIn), Options, DefExpriresIn),
|
Chris@0
|
606
|
Chris@0
|
607 get_time(Now),
|
Chris@0
|
608 ExpiresAt is integer(Now+ExpriresIn),
|
Chris@0
|
609 make_association([ session_type(SessionType),
|
Chris@0
|
610 expires_at(ExpiresAt),
|
Chris@0
|
611 mac_key(MacKey)
|
Chris@0
|
612 ],
|
Chris@0
|
613 Record),
|
Chris@0
|
614 memberchk(peer(Peer), Request),
|
Chris@0
|
615 assert(server_association(Peer, Handle, Record)),
|
Chris@0
|
616
|
Chris@0
|
617 key_values_data([ assoc_type-AssocType,
|
Chris@0
|
618 assoc_handle-Handle,
|
Chris@0
|
619 expires_in-ExpriresIn,
|
Chris@0
|
620 session_type-SessionType,
|
Chris@0
|
621 dh_server_public-CPY64,
|
Chris@0
|
622 enc_mac_key-Base64EncKey
|
Chris@0
|
623 ],
|
Chris@0
|
624 Text),
|
Chris@0
|
625 format('Content-type: text/plain~n~n~s', [Text]).
|
Chris@0
|
626
|
Chris@0
|
627
|
Chris@0
|
628 new_assoc_handle(Handle) :-
|
Chris@0
|
629 random_bytes(20, Bytes),
|
Chris@0
|
630 phrase(base64(Bytes), HandleCodes),
|
Chris@0
|
631 atom_codes(Handle, HandleCodes).
|
Chris@0
|
632
|
Chris@0
|
633
|
Chris@0
|
634 %% checkid_setup_server(+Request, +Form, +Options)
|
Chris@0
|
635 %
|
Chris@0
|
636 % Validate an OpenID for a TrustRoot and redirect the browser back
|
Chris@0
|
637 % to the ReturnTo argument. There are many possible scenarios
|
Chris@0
|
638 % here:
|
Chris@0
|
639 %
|
Chris@0
|
640 % 1. Check some cookie and if present, grant immediately
|
Chris@0
|
641 % 2. Use a 401 challenge page
|
Chris@0
|
642 % 3. Present a normal grant/password page
|
Chris@0
|
643 % 4. As (3), but use HTTPS for the exchange
|
Chris@0
|
644 % 5. etc.
|
Chris@0
|
645 %
|
Chris@0
|
646 % First thing to check is the immediate acknowledgement.
|
Chris@0
|
647
|
Chris@0
|
648 checkid_setup_server(_Request, Form, _Options) :-
|
Chris@0
|
649 memberchk('openid.identity' = Identity, Form),
|
Chris@0
|
650 memberchk('openid.assoc_handle' = Handle, Form),
|
Chris@0
|
651 memberchk('openid.return_to' = ReturnTo, Form),
|
Chris@0
|
652 memberchk('openid.trust_root' = TrustRoot, Form),
|
Chris@0
|
653
|
Chris@0
|
654 server_association(_, Handle, _Association), % check
|
Chris@0
|
655
|
Chris@0
|
656 reply_html_page([ title('OpenID login'),
|
Chris@0
|
657 \openid_css
|
Chris@0
|
658 ],
|
Chris@0
|
659 [ \openid_title,
|
Chris@0
|
660 div(class('openid-message'),
|
Chris@0
|
661 ['Site ', a(href(TrustRoot), TrustRoot), ' requests permission \
|
Chris@0
|
662 to login with OpenID ', a(href(Identity), Identity), '.'
|
Chris@0
|
663 ]),
|
Chris@0
|
664 table(class('openid-form'),
|
Chris@0
|
665 [ tr(td(form([ action(grant), method('GET') ],
|
Chris@0
|
666 [ \hidden('openid.grant', yes),
|
Chris@0
|
667 \hidden('openid.identity', Identity),
|
Chris@0
|
668 \hidden('openid.assoc_handle', Handle),
|
Chris@0
|
669 \hidden('openid.return_to', ReturnTo),
|
Chris@0
|
670 \hidden('openid.trust_root', TrustRoot),
|
Chris@0
|
671 div(['Password: ',
|
Chris@0
|
672 input([type(password), name('openid.password')]),
|
Chris@0
|
673 input([type(submit), value('Grant')])
|
Chris@0
|
674 ])
|
Chris@0
|
675 ]))),
|
Chris@0
|
676 tr(td(align(right),
|
Chris@0
|
677 form([ action(grant), method('GET') ],
|
Chris@0
|
678 [ \hidden('openid.grant', no),
|
Chris@0
|
679 \hidden('openid.return_to', ReturnTo),
|
Chris@0
|
680 input([type(submit), value('Deny')])
|
Chris@0
|
681 ])))
|
Chris@0
|
682 ])
|
Chris@0
|
683 ]).
|
Chris@0
|
684
|
Chris@0
|
685 hidden(Name, Value) -->
|
Chris@0
|
686 html(input([type(hidden), name(Name), value(Value)])).
|
Chris@0
|
687
|
Chris@0
|
688
|
Chris@0
|
689 openid_title -->
|
Chris@0
|
690 html(div(class('openid-title'),
|
Chris@0
|
691 [ a(href('http://openid.net/'),
|
Chris@0
|
692 img([ src('file?name=openid_logo'), alt('OpenID') ])),
|
Chris@0
|
693 span('Login')
|
Chris@0
|
694 ])).
|
Chris@0
|
695
|
Chris@0
|
696
|
Chris@0
|
697 %% openid_grant(+Request)
|
Chris@0
|
698 %
|
Chris@0
|
699 % Handle the reply from checkid_setup_server/3. If the reply is
|
Chris@0
|
700 % =yes=, check the authority (typically the password) and if all
|
Chris@0
|
701 % looks good redirect the browser to ReturnTo, adding the OpenID
|
Chris@0
|
702 % properties needed by the Relying Party to verify the login.
|
Chris@0
|
703
|
Chris@0
|
704 openid_grant(Request) :-
|
Chris@0
|
705 http_parameters(Request,
|
Chris@0
|
706 [ 'openid.grant'(Grant),
|
Chris@0
|
707 'openid.return_to'(ReturnTo)
|
Chris@0
|
708 ],
|
Chris@0
|
709 [ attribute_declarations(openid_attribute)
|
Chris@0
|
710 ]),
|
Chris@0
|
711 ( Grant == yes
|
Chris@0
|
712 -> http_parameters(Request,
|
Chris@0
|
713 [ 'openid.identity'(Identity),
|
Chris@0
|
714 'openid.assoc_handle'(Handle),
|
Chris@0
|
715 'openid.trust_root'(TrustRoot),
|
Chris@0
|
716 'openid.password'(Password)
|
Chris@0
|
717 ],
|
Chris@0
|
718 [ attribute_declarations(openid_attribute)
|
Chris@0
|
719 ]),
|
Chris@0
|
720 server_association(_, Handle, Association),
|
Chris@0
|
721 grant_login(Request,
|
Chris@0
|
722 [ identity(Identity),
|
Chris@0
|
723 password(Password),
|
Chris@0
|
724 trustroot(TrustRoot)
|
Chris@0
|
725 ]),
|
Chris@0
|
726 SignedPairs = [ 'mode'-id_res,
|
Chris@0
|
727 'identity'-Identity,
|
Chris@0
|
728 'assoc_handle'-Handle,
|
Chris@0
|
729 'return_to'-ReturnTo
|
Chris@0
|
730 ],
|
Chris@0
|
731 signed_fields(SignedPairs, Signed),
|
Chris@0
|
732 signature(SignedPairs, Association, Signature),
|
Chris@0
|
733 phrase(base64(Signature), Bas64Sig),
|
Chris@0
|
734 redirect_browser(ReturnTo,
|
Chris@0
|
735 [ 'openid.mode' = id_res,
|
Chris@0
|
736 'openid.identity' = Identity,
|
Chris@0
|
737 'openid.assoc_handle' = Handle,
|
Chris@0
|
738 'openid.return_to' = ReturnTo,
|
Chris@0
|
739 'openid.signed' = Signed,
|
Chris@0
|
740 'openid.sig' = Bas64Sig
|
Chris@0
|
741 ])
|
Chris@0
|
742 ; redirect_browser(ReturnTo,
|
Chris@0
|
743 [ 'openid.mode' = cancel
|
Chris@0
|
744 ])
|
Chris@0
|
745 ).
|
Chris@0
|
746
|
Chris@0
|
747
|
Chris@0
|
748 %% grant_login(+Request, +Options) is det.
|
Chris@0
|
749 %
|
Chris@0
|
750 % Validate login from Request (can be used to get cookies) and
|
Chris@0
|
751 % Options, which contains at least:
|
Chris@0
|
752 %
|
Chris@0
|
753 % * identity(Identity)
|
Chris@0
|
754 % * password(Password)
|
Chris@0
|
755 % * trustroot(TrustRoot)
|
Chris@0
|
756
|
Chris@0
|
757 grant_login(Request, Options) :-
|
Chris@0
|
758 openid_hook(grant(Request, Options)).
|
Chris@0
|
759
|
Chris@0
|
760 %% trusted(+OpenID, +Server)
|
Chris@0
|
761 %
|
Chris@0
|
762 % True if we trust the given OpenID server. Must throw an
|
Chris@0
|
763 % exception, possibly redirecting to a page with trusted servers
|
Chris@0
|
764 % if the given server is not trusted.
|
Chris@0
|
765 %
|
Chris@0
|
766 % @tbd How do we manage this? Broadcast? Settings? Hook?
|
Chris@0
|
767
|
Chris@0
|
768 trusted(OpenID, Server) :-
|
Chris@0
|
769 openid_hook(trusted(OpenID, Server)).
|
Chris@0
|
770
|
Chris@0
|
771
|
Chris@0
|
772 %% signed_fields(+Pairs, -Signed) is det.
|
Chris@0
|
773 %
|
Chris@0
|
774 % Create a comma-separated atom from the field-names without
|
Chris@0
|
775 % 'openid.' from Pairs.
|
Chris@0
|
776
|
Chris@0
|
777 signed_fields(Pairs, Signed) :-
|
Chris@0
|
778 signed_field_names(Pairs, Names),
|
Chris@0
|
779 concat_atom(Names, ',', Signed).
|
Chris@0
|
780
|
Chris@0
|
781 signed_field_names([], []).
|
Chris@0
|
782 signed_field_names([H0-_|T0], [H|T]) :-
|
Chris@0
|
783 ( atom_concat('openid.', H, H0)
|
Chris@0
|
784 -> true
|
Chris@0
|
785 ; H = H0
|
Chris@0
|
786 ),
|
Chris@0
|
787 signed_field_names(T0, T).
|
Chris@0
|
788
|
Chris@0
|
789 %% signature(+Pairs, +Association, -Signature)
|
Chris@0
|
790 %
|
Chris@0
|
791 % Determine the signature for Pairs
|
Chris@0
|
792
|
Chris@0
|
793 signature(Pairs, Association, Signature) :-
|
Chris@0
|
794 key_values_data(Pairs, TokenContents),
|
Chris@0
|
795 association_mac_key(Association, MacKey),
|
Chris@0
|
796 association_session_type(Association, SessionType),
|
Chris@0
|
797 signature_algorithm(SessionType, SHA),
|
Chris@0
|
798 hmac_sha(MacKey, TokenContents, Signature, [algorithm(SHA)]),
|
Chris@0
|
799 debug(openid(crypt), 'Signed:~n~s~nSignature: ~w', [TokenContents, Signature]).
|
Chris@0
|
800
|
Chris@0
|
801 signature_algorithm('DH-SHA1', sha1).
|
Chris@0
|
802 signature_algorithm('DH-SHA256', sha256).
|
Chris@0
|
803
|
Chris@0
|
804
|
Chris@0
|
805 /*******************************
|
Chris@0
|
806 * IMAGES *
|
Chris@0
|
807 *******************************/
|
Chris@0
|
808
|
Chris@0
|
809 %% openid_file(+Request)
|
Chris@0
|
810 %
|
Chris@0
|
811 % Serve fiels we use as logos, style-sheets, etc.
|
Chris@0
|
812
|
Chris@0
|
813 openid_file(Request) :-
|
Chris@0
|
814 http_parameters(Request,
|
Chris@0
|
815 [ name(Name, [])
|
Chris@0
|
816 ]),
|
Chris@0
|
817 image_file(Name, File),
|
Chris@0
|
818 http_reply_file(File, [], Request).
|
Chris@0
|
819
|
Chris@0
|
820
|
Chris@0
|
821 image_file(openid_logo, library('http/openid-logo-square.png')).
|
Chris@0
|
822 image_file(openid_logo_tiny, library('http/openid-logo-tiny.png')).
|
Chris@0
|
823 image_file(openid_css, library('http/openid.css')).
|
Chris@0
|
824
|
Chris@0
|
825
|
Chris@0
|
826 /*******************************
|
Chris@0
|
827 * ASSOCIATE *
|
Chris@0
|
828 *******************************/
|
Chris@0
|
829
|
Chris@0
|
830 :- dynamic
|
Chris@0
|
831 association/3. % URL, Handle, Data
|
Chris@0
|
832
|
Chris@0
|
833 :- record
|
Chris@0
|
834 association(session_type='DH-SHA1',
|
Chris@0
|
835 expires_at, % time-stamp
|
Chris@0
|
836 mac_key). % code-list
|
Chris@0
|
837
|
Chris@0
|
838 %% openid_associate(+URL, -Handle, -Assoc) is det.
|
Chris@0
|
839 %% openid_associate(?URL, +Handle, -Assoc) is semidet.
|
Chris@0
|
840 %
|
Chris@0
|
841 % Associate with an open-id server. We first check for a still
|
Chris@0
|
842 % valid old association. If there is none or it is expired, we
|
Chris@0
|
843 % esstablish one and remember it.
|
Chris@0
|
844 %
|
Chris@0
|
845 % @tbd Should we store known associations permanently? Where?
|
Chris@0
|
846
|
Chris@0
|
847 openid_associate(URL, Handle, Assoc) :-
|
Chris@0
|
848 association(URL, Handle, Assoc),
|
Chris@0
|
849 association_expires_at(Assoc, Expires),
|
Chris@0
|
850 get_time(Now),
|
Chris@0
|
851 ( Now < Expires
|
Chris@0
|
852 -> debug(openid(associate), '~w: Reusing association', [URL])
|
Chris@0
|
853 ; retractall(association(URL, Handle, _)),
|
Chris@0
|
854 fail
|
Chris@0
|
855 ).
|
Chris@0
|
856 openid_associate(URL, Handle, Assoc) :-
|
Chris@0
|
857 ground(URL),
|
Chris@0
|
858 associate_data(Data, P, _G, X),
|
Chris@0
|
859 http_post(URL, form(Data), Reply, [to(codes)]),
|
Chris@0
|
860 debug(openid(associate), 'Reply: ~n~s', [Reply]),
|
Chris@0
|
861 key_values_data(Pairs, Reply),
|
Chris@0
|
862 shared_secret(Pairs, P, X, MacKey),
|
Chris@0
|
863 expires_at(Pairs, ExpiresAt),
|
Chris@0
|
864 memberchk(assoc_handle-Handle, Pairs),
|
Chris@0
|
865 memberchk(session_type-Type, Pairs),
|
Chris@0
|
866 make_association([ session_type(Type),
|
Chris@0
|
867 expires_at(ExpiresAt),
|
Chris@0
|
868 mac_key(MacKey)
|
Chris@0
|
869 ], Assoc),
|
Chris@0
|
870 assert(association(URL, Handle, Assoc)).
|
Chris@0
|
871
|
Chris@0
|
872
|
Chris@0
|
873 %% shared_secret(+Pairs, +P, +X, -Secret:list(codes))
|
Chris@0
|
874 %
|
Chris@0
|
875 % Find the shared secret from the peer's reply and our data. First
|
Chris@0
|
876 % clause deals with the (deprecated) non-encoded version.
|
Chris@0
|
877
|
Chris@0
|
878 shared_secret(Pairs, _, _, Secret) :-
|
Chris@0
|
879 memberchk(mac_key-Base64, Pairs), !,
|
Chris@0
|
880 atom_codes(Base64, Base64Codes),
|
Chris@0
|
881 phrase(base64(Base64Codes), Secret).
|
Chris@0
|
882 shared_secret(Pairs, P, X, Secret) :-
|
Chris@0
|
883 memberchk(dh_server_public-Base64Public, Pairs),
|
Chris@0
|
884 memberchk(enc_mac_key-Base64EncMacKey, Pairs),
|
Chris@0
|
885 base64_btwoc(ServerPublic, Base64Public),
|
Chris@0
|
886 DiffieHellman is powm(ServerPublic, X, P),
|
Chris@0
|
887 atom_codes(Base64EncMacKey, Base64EncMacKeyCodes),
|
Chris@0
|
888 phrase(base64(EncMacKey), Base64EncMacKeyCodes),
|
Chris@0
|
889 btwoc(DiffieHellman, DiffieHellmanBytes),
|
Chris@0
|
890 sha_hash(DiffieHellmanBytes, DHHash, [algorithm(sha1)]),
|
Chris@0
|
891 xor_codes(DHHash, EncMacKey, Secret).
|
Chris@0
|
892
|
Chris@0
|
893
|
Chris@0
|
894 %% expires_at(+Pairs, -Time) is det.
|
Chris@0
|
895 %
|
Chris@0
|
896 % Unify Time with a time-stamp stating when the association
|
Chris@0
|
897 % exires.
|
Chris@0
|
898
|
Chris@0
|
899 expires_at(Pairs, Time) :-
|
Chris@0
|
900 memberchk(expires_in-ExpAtom, Pairs),
|
Chris@0
|
901 atom_number(ExpAtom, Seconds),
|
Chris@0
|
902 get_time(Now),
|
Chris@0
|
903 Time is integer(Now)+Seconds.
|
Chris@0
|
904
|
Chris@0
|
905
|
Chris@0
|
906 %% associate_data(-Data, -X) is det.
|
Chris@0
|
907 %
|
Chris@0
|
908 % Generate the data to initiate an association using Diffie-Hellman
|
Chris@0
|
909 % shared secret key negotiation.
|
Chris@0
|
910
|
Chris@0
|
911 associate_data(Data, P, G, X) :-
|
Chris@0
|
912 openid_dh_p(P),
|
Chris@0
|
913 openid_dh_g(G),
|
Chris@0
|
914 dh_x(P, X),
|
Chris@0
|
915 CP is powm(G, X, P),
|
Chris@0
|
916 base64_btwoc(P, P64),
|
Chris@0
|
917 base64_btwoc(G, G64),
|
Chris@0
|
918 base64_btwoc(CP, CP64),
|
Chris@0
|
919 Data = [ 'openid.mode' = associate,
|
Chris@0
|
920 'openid.assoc_type' = 'HMAC-SHA1',
|
Chris@0
|
921 'openid.session_type' = 'DH-SHA1',
|
Chris@0
|
922 'openid.dh_modulus' = P64,
|
Chris@0
|
923 'openid.dh_gen' = G64,
|
Chris@0
|
924 'openid.dh_consumer_public' = CP64
|
Chris@0
|
925 ].
|
Chris@0
|
926
|
Chris@0
|
927
|
Chris@0
|
928 /*******************************
|
Chris@0
|
929 * RANDOM *
|
Chris@0
|
930 *******************************/
|
Chris@0
|
931
|
Chris@0
|
932 %% random_bytes(+N, -Bytes) is det.
|
Chris@0
|
933 %
|
Chris@0
|
934 % Bytes is a list of N random bytes (integers 0..255).
|
Chris@0
|
935
|
Chris@0
|
936 random_bytes(N, [H|T]) :-
|
Chris@0
|
937 N > 0, !,
|
Chris@0
|
938 H is random(256),
|
Chris@0
|
939 N2 is N - 1,
|
Chris@0
|
940 random_bytes(N2, T).
|
Chris@0
|
941 random_bytes(_, []).
|
Chris@0
|
942
|
Chris@0
|
943
|
Chris@0
|
944 %% dh_x(+Max, -X)
|
Chris@0
|
945 %
|
Chris@0
|
946 % Generate a random key X where 1<=X<P-1)
|
Chris@0
|
947 %
|
Chris@0
|
948 % @tbd If we have /dev/urandom, use that.
|
Chris@0
|
949
|
Chris@0
|
950 dh_x(P, X) :-
|
Chris@0
|
951 X0 is random(65536),
|
Chris@0
|
952 Max is P - 1,
|
Chris@0
|
953 dh_x(Max, X0, X).
|
Chris@0
|
954
|
Chris@0
|
955 dh_x(Max, X0, X) :-
|
Chris@0
|
956 X1 is X0<<16+random(65536),
|
Chris@0
|
957 ( X1 >= Max
|
Chris@0
|
958 -> X = X0
|
Chris@0
|
959 ; dh_x(Max, X1, X)
|
Chris@0
|
960 ).
|
Chris@0
|
961
|
Chris@0
|
962
|
Chris@0
|
963 /*******************************
|
Chris@0
|
964 * CONSTANTS *
|
Chris@0
|
965 *******************************/
|
Chris@0
|
966
|
Chris@0
|
967 openid_dh_p(155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443).
|
Chris@0
|
968
|
Chris@0
|
969 openid_dh_g(2).
|
Chris@0
|
970
|
Chris@0
|
971
|
Chris@0
|
972 /*******************************
|
Chris@0
|
973 * UTIL *
|
Chris@0
|
974 *******************************/
|
Chris@0
|
975
|
Chris@0
|
976 %% key_values_data(+KeyValues:list(Key-Value), -Data:list(code)) is det.
|
Chris@0
|
977 %% key_values_data(-KeyValues:list(Key-Value), +Data:list(code)) is det.
|
Chris@0
|
978 %
|
Chris@0
|
979 % Encoding and decoding of key-value pairs for OpenID POST
|
Chris@0
|
980 % messages according to Appendix C of the OpenID 1.1
|
Chris@0
|
981 % specification.
|
Chris@0
|
982
|
Chris@0
|
983 key_values_data(Pairs, Data) :-
|
Chris@0
|
984 nonvar(Data), !,
|
Chris@0
|
985 phrase(data_form(Pairs), Data).
|
Chris@0
|
986 key_values_data(Pairs, Data) :-
|
Chris@0
|
987 phrase(gen_data_form(Pairs), Data).
|
Chris@0
|
988
|
Chris@0
|
989 data_form([Key-Value|Pairs]) -->
|
Chris@0
|
990 utf8_string(KeyCodes), ":", utf8_string(ValueCodes), "\n", !,
|
Chris@0
|
991 { atom_codes(Key, KeyCodes),
|
Chris@0
|
992 atom_codes(Value, ValueCodes)
|
Chris@0
|
993 },
|
Chris@0
|
994 data_form(Pairs).
|
Chris@0
|
995 data_form([]) -->
|
Chris@0
|
996 ws.
|
Chris@0
|
997
|
Chris@0
|
998 %% utf8_string(-Codes)// is nondet.
|
Chris@0
|
999 %
|
Chris@0
|
1000 % Take a short UTF-8 code-list from input. Extend on backtracking.
|
Chris@0
|
1001
|
Chris@0
|
1002 utf8_string([]) -->
|
Chris@0
|
1003 [].
|
Chris@0
|
1004 utf8_string([H|T]) -->
|
Chris@0
|
1005 utf8_codes([H]),
|
Chris@0
|
1006 utf8_string(T).
|
Chris@0
|
1007
|
Chris@0
|
1008 ws -->
|
Chris@0
|
1009 [C],
|
Chris@0
|
1010 { C =< 32 }, !,
|
Chris@0
|
1011 ws.
|
Chris@0
|
1012 ws -->
|
Chris@0
|
1013 [].
|
Chris@0
|
1014
|
Chris@0
|
1015
|
Chris@0
|
1016 gen_data_form([]) -->
|
Chris@0
|
1017 [].
|
Chris@0
|
1018 gen_data_form([Key-Value|T]) -->
|
Chris@0
|
1019 field(Key), ":", field(Value), "\n",
|
Chris@0
|
1020 gen_data_form(T).
|
Chris@0
|
1021
|
Chris@0
|
1022 field(Field) -->
|
Chris@0
|
1023 { to_codes(Field, Codes)
|
Chris@0
|
1024 },
|
Chris@0
|
1025 utf8_codes(Codes).
|
Chris@0
|
1026
|
Chris@0
|
1027 to_codes(Codes, Codes) :-
|
Chris@0
|
1028 is_list(Codes), !.
|
Chris@0
|
1029 to_codes(Atomic, Codes) :-
|
Chris@0
|
1030 atom_codes(Atomic, Codes).
|
Chris@0
|
1031
|
Chris@0
|
1032 %% base64_btwoc(+Int, -Base64:list(code)) is det.
|
Chris@0
|
1033 %% base64_btwoc(-Int, +Base64:list(code)) is det.
|
Chris@0
|
1034 %% base64_btwoc(-Int, +Base64:atom) is det.
|
Chris@0
|
1035
|
Chris@0
|
1036 base64_btwoc(Int, Base64) :-
|
Chris@0
|
1037 integer(Int), !,
|
Chris@0
|
1038 btwoc(Int, Bytes),
|
Chris@0
|
1039 phrase(base64(Bytes), Base64).
|
Chris@0
|
1040 base64_btwoc(Int, Base64) :-
|
Chris@0
|
1041 atom(Base64), !,
|
Chris@0
|
1042 atom_codes(Base64, Codes),
|
Chris@0
|
1043 phrase(base64(Bytes), Codes),
|
Chris@0
|
1044 btwoc(Int, Bytes).
|
Chris@0
|
1045 base64_btwoc(Int, Base64) :-
|
Chris@0
|
1046 phrase(base64(Bytes), Base64),
|
Chris@0
|
1047 btwoc(Int, Bytes).
|
Chris@0
|
1048
|
Chris@0
|
1049
|
Chris@0
|
1050 %% btwoc(+Integer, -Bytes) is det.
|
Chris@0
|
1051 %% btwoc(-Integer, +Bytes) is det.
|
Chris@0
|
1052 %
|
Chris@0
|
1053 % Translate between a big integer and and its representation in
|
Chris@0
|
1054 % bytes. The first bit is always 0, as Integer is nonneg.
|
Chris@0
|
1055
|
Chris@0
|
1056 btwoc(Int, Bytes) :-
|
Chris@0
|
1057 integer(Int), !,
|
Chris@0
|
1058 int_to_bytes(Int, Bytes).
|
Chris@0
|
1059 btwoc(Int, Bytes) :-
|
Chris@0
|
1060 is_list(Bytes),
|
Chris@0
|
1061 bytes_to_int(Bytes, Int).
|
Chris@0
|
1062
|
Chris@0
|
1063 int_to_bytes(Int, Bytes) :-
|
Chris@0
|
1064 int_to_bytes(Int, [], Bytes).
|
Chris@0
|
1065
|
Chris@0
|
1066 int_to_bytes(Int, Bytes0, [Int|Bytes0]) :-
|
Chris@0
|
1067 Int < 128, !.
|
Chris@0
|
1068 int_to_bytes(Int, Bytes0, Bytes) :-
|
Chris@0
|
1069 Last is Int /\ 0xff,
|
Chris@0
|
1070 Int1 is Int >> 8,
|
Chris@0
|
1071 int_to_bytes(Int1, [Last|Bytes0], Bytes).
|
Chris@0
|
1072
|
Chris@0
|
1073
|
Chris@0
|
1074 bytes_to_int([B|T], Int) :-
|
Chris@0
|
1075 bytes_to_int(T, B, Int).
|
Chris@0
|
1076
|
Chris@0
|
1077 bytes_to_int([], Int, Int).
|
Chris@0
|
1078 bytes_to_int([B|T], Int0, Int) :-
|
Chris@0
|
1079 Int1 is (Int0<<8)+B,
|
Chris@0
|
1080 bytes_to_int(T, Int1, Int).
|
Chris@0
|
1081
|
Chris@0
|
1082
|
Chris@0
|
1083 %% xor_codes(+C1:list(int), +C2:list(int), -XOR:list(int)) is det.
|
Chris@0
|
1084 %
|
Chris@0
|
1085 % Compute xor of two strings.
|
Chris@0
|
1086 %
|
Chris@0
|
1087 % @error length_mismatch(L1, L2) if the two lists do not have equal
|
Chris@0
|
1088 % length.
|
Chris@0
|
1089
|
Chris@0
|
1090 xor_codes([], [], []).
|
Chris@0
|
1091 xor_codes([H1|T1], [H2|T2], [H|T]) :-
|
Chris@0
|
1092 H is H1 xor H2, !,
|
Chris@0
|
1093 xor_codes(T1, T2, T).
|
Chris@0
|
1094 xor_codes(L1, L2, _) :-
|
Chris@0
|
1095 throw(error(length_mismatch(L1, L2), _)).
|
Chris@0
|
1096
|
Chris@0
|
1097
|
Chris@0
|
1098 /*******************************
|
Chris@0
|
1099 * HTTP ATTRIBUTES *
|
Chris@0
|
1100 *******************************/
|
Chris@0
|
1101
|
Chris@0
|
1102 openid_attribute('openid.mode',
|
Chris@0
|
1103 [ oneof([ associate,
|
Chris@0
|
1104 checkid_setup,
|
Chris@0
|
1105 cancel,
|
Chris@0
|
1106 id_res
|
Chris@0
|
1107 ])
|
Chris@0
|
1108 ]).
|
Chris@0
|
1109 openid_attribute('openid.assoc_type',
|
Chris@0
|
1110 [ oneof(['HMAC-SHA1'])
|
Chris@0
|
1111 ]).
|
Chris@0
|
1112 openid_attribute('openid.session_type',
|
Chris@0
|
1113 [ oneof([ 'DH-SHA1',
|
Chris@0
|
1114 'DH-SHA256'
|
Chris@0
|
1115 ])
|
Chris@0
|
1116 ]).
|
Chris@0
|
1117 openid_attribute('openid.dh_modulus', [length > 1]).
|
Chris@0
|
1118 openid_attribute('openid.dh_gen', [length > 1]).
|
Chris@0
|
1119 openid_attribute('openid.dh_consumer_public', [length > 1]).
|
Chris@0
|
1120 openid_attribute('openid.assoc_handle', [length > 1]).
|
Chris@0
|
1121 openid_attribute('openid.return_to', [length > 1]).
|
Chris@0
|
1122 openid_attribute('openid.trust_root', [length > 1]).
|
Chris@0
|
1123 openid_attribute('openid.identity', [length > 1]).
|
Chris@0
|
1124 openid_attribute('openid.password', [length > 1]).
|
Chris@0
|
1125 openid_attribute('openid.grant', [oneof([yes,no])]).
|