35
36:- module(http_openid,
37 [ openid_login/1, 38 openid_logout/1, 39 openid_logged_in/1, 40
41 42 openid_user/3, 43
44 45 openid_verify/2, 46 openid_authenticate/4, 47 openid_associate/3, 48 openid_associate/4, 49 50 openid_server/2, 51 openid_server/3, 52 openid_grant/1, 53
54 openid_login_form//2, 55
56 openid_current_url/2, 57 openid_current_host/3 58 ]).
59:- use_module(library(http/http_open)).
60:- use_module(library(http/html_write)).
61:- use_module(library(http/http_parameters)).
62:- use_module(library(http/http_dispatch)).
63:- use_module(library(http/http_session)).
64:- use_module(library(http/http_host)).
65:- use_module(library(http/http_path)).
66:- use_module(library(http/html_head)).
67:- use_module(library(http/http_server_files), []).
68:- use_module(library(http/yadis)).
69:- use_module(library(http/ax)).
70:- use_module(library(utf8)).
71:- use_module(library(error)).
72:- use_module(library(xpath)).
73:- use_module(library(sgml)).
74:- use_module(library(uri)).
75:- use_module(library(occurs)).
76:- use_module(library(base64)).
77:- use_module(library(debug)).
78:- use_module(library(record)).
79:- use_module(library(option)).
80:- use_module(library(sha)).
81:- use_module(library(lists)).
82:- use_module(library(settings)).
83
84:- predicate_options(openid_login_form/4, 2,
85 [ action(atom),
86 buttons(list),
87 show_stay(boolean)
88 ]).
89:- predicate_options(openid_server/2, 1,
90 [ expires_in(any)
91 ]).
92:- predicate_options(openid_user/3, 3,
93 [ login_url(atom)
94 ]).
95:- predicate_options(openid_verify/2, 1,
96 [ return_to(atom),
97 trust_root(atom),
98 realm(atom),
99 ax(any)
100 ]).
101
152
153 156
157http:location(openid, root(openid), [priority(-100)]).
158
184
185:- multifile
186 openid_hook/1. 187
188 191
196
197openid_login(OpenID) :-
198 openid_hook(login(OpenID)),
199 !,
200 handle_stay_signed_in(OpenID).
201openid_login(OpenID) :-
202 openid_logout(_),
203 http_session_assert(openid(OpenID)),
204 handle_stay_signed_in(OpenID).
205
209
210openid_logout(OpenID) :-
211 openid_hook(logout(OpenID)),
212 !.
213openid_logout(OpenID) :-
214 http_session_retractall(openid(OpenID)).
215
219
220openid_logged_in(OpenID) :-
221 openid_hook(logged_in(OpenID)),
222 !.
223openid_logged_in(OpenID) :-
224 http_in_session(_SessionId), 225 http_session_data(openid(OpenID)).
226
227
228 231
267
268:- http_handler(openid(login), openid_login_page, [priority(-10)]).
269:- http_handler(openid(verify), openid_verify([]), []).
270:- http_handler(openid(authenticate), openid_authenticate, []).
271:- http_handler(openid(xrds), openid_xrds, []).
272
273openid_user(_Request, OpenID, _Options) :-
274 openid_logged_in(OpenID),
275 !.
276openid_user(Request, _OpenID, Options) :-
277 http_link_to_id(openid_login_page, [], DefLoginPage),
278 option(login_url(LoginPage), Options, DefLoginPage),
279 openid_current_url(Request, Here),
280 redirect_browser(LoginPage,
281 [ 'openid.return_to' = Here
282 ]).
283
293
294openid_xrds(Request) :-
295 http_link_to_id(openid_authenticate, [], Autheticate),
296 public_url(Request, Autheticate, Public),
297 format('Content-type: text/xml\n\n'),
298 format('<?xml version="1.0" encoding="UTF-8"?>\n'),
299 format('<xrds:XRDS\n'),
300 format(' xmlns:xrds="xri://$xrds"\n'),
301 format(' xmlns="xri://$xrd*($v*2.0)">\n'),
302 format(' <XRD>\n'),
303 format(' <Service>\n'),
304 format(' <Type>http://specs.openid.net/auth/2.0/return_to</Type>\n'),
305 format(' <URI>~w</URI>\n', [Public]),
306 format(' </Service>\n'),
307 format(' </XRD>\n'),
308 format('</xrds:XRDS>\n').
309
310
317
318openid_login_page(Request) :-
319 http_open_session(_, []),
320 http_parameters(Request,
321 [ 'openid.return_to'(Target, [])
322 ]),
323 reply_html_page([ title('OpenID login')
324 ],
325 [ \openid_login_form(Target, [])
326 ]).
327
349
350openid_login_form(ReturnTo, Options) -->
351 { http_link_to_id(openid_verify, [], VerifyLocation),
352 option(action(Action), Options, VerifyLocation),
353 http_session_retractall(openid(_)),
354 http_session_retractall(openid_login(_,_,_,_)),
355 http_session_retractall(ax(_))
356 },
357 html(div([ class('openid-login')
358 ],
359 [ \openid_title,
360 form([ name(login),
361 id(login),
362 action(Action),
363 method('GET')
364 ],
365 [ \hidden('openid.return_to', ReturnTo),
366 div([ input([ class('openid-input'),
367 name(openid_url),
368 id(openid_url),
369 size(30),
370 placeholder('Your OpenID URL')
371 ]),
372 input([ type(submit),
373 value('Verify!')
374 ])
375 ]),
376 \buttons(Options),
377 \stay_logged_on(Options)
378 ])
379 ])).
380
381stay_logged_on(Options) -->
382 { option(show_stay(true), Options) },
383 !,
384 html(div(class('openid-stay'),
385 [ input([ type(checkbox), id(stay), name(stay), value(yes)]),
386 'Stay signed in'
387 ])).
388stay_logged_on(_) --> [].
389
390buttons(Options) -->
391 { option(buttons(Buttons), Options),
392 Buttons \== []
393 },
394 html(div(class('openid-buttons'),
395 [ 'Sign in with '
396 | \prelogin_buttons(Buttons)
397 ])).
398buttons(_) --> [].
399
400prelogin_buttons([]) --> [].
401prelogin_buttons([H|T]) --> prelogin_button(H), prelogin_buttons(T).
402
411
412prelogin_button(img(Attrs)) -->
413 { select_option(href(HREF), Attrs, RestAttrs),
414 uri_is_global(HREF), !
415 },
416 html(img([ onClick('javascript:{$("#openid_url").val("'+HREF+'");'+
417 '$("form#login").submit();}'
418 )
419 | RestAttrs
420 ])).
421prelogin_button(img(Attrs)) -->
422 { select_option(href(HREF), Attrs, RestAttrs)
423 },
424 html(img([ onClick('window.location = "'+HREF+
425 '?openid.return_to="'+
426 '+encodeURIComponent($("#return_to").val())'+
427 '+"&stay="'+
428 '+$("#stay").val()')
429 | RestAttrs
430 ])).
431
432
433 436
463
464openid_verify(Options, Request) :-
465 http_parameters(Request,
466 [ openid_url(URL, [length>1]),
467 'openid.return_to'(ReturnTo0, [optional(true)]),
468 stay(Stay, [optional(true), default(no)])
469 ]),
470 ( option(return_to(ReturnTo1), Options) 471 -> openid_current_url(Request, CurrentLocation),
472 global_url(ReturnTo1, CurrentLocation, ReturnTo)
473 ; nonvar(ReturnTo0)
474 -> ReturnTo = ReturnTo0 475 ; openid_current_url(Request, CurrentLocation),
476 ReturnTo = CurrentLocation 477 ),
478 public_url(Request, /, CurrentRoot),
479 option(trust_root(TrustRoot), Options, CurrentRoot),
480 option(realm(Realm), Options, TrustRoot),
481 openid_resolve(URL, OpenIDLogin, OpenID, Server, ServerOptions),
482 trusted(OpenID, Server),
483 openid_associate(Server, Handle, _Assoc),
484 assert_openid(OpenIDLogin, OpenID, Server, ReturnTo),
485 stay(Stay),
486 option(ns(NS), Options, 'http://specs.openid.net/auth/2.0'),
487 ( realm_attribute(NS, RealmAttribute)
488 -> true
489 ; domain_error('openid.ns', NS)
490 ),
491 findall(P=V, openid_hook(x_parameter(Server, P, V)), XAttrs, AXAttrs),
492 debug(openid(verify), 'XAttrs: ~p', [XAttrs]),
493 ax_options(ServerOptions, Options, AXAttrs),
494 http_link_to_id(openid_authenticate, [], AuthenticateLoc),
495 public_url(Request, AuthenticateLoc, Authenticate),
496 redirect_browser(Server, [ 'openid.ns' = NS,
497 'openid.mode' = checkid_setup,
498 'openid.identity' = OpenID,
499 'openid.claimed_id' = OpenID,
500 'openid.assoc_handle' = Handle,
501 'openid.return_to' = Authenticate,
502 RealmAttribute = Realm
503 | XAttrs
504 ]).
505
506realm_attribute('http://specs.openid.net/auth/2.0', 'openid.realm').
507realm_attribute('http://openid.net/signon/1.1', 'openid.trust_root').
508
509
515
516stay(yes) :-
517 !,
518 http_session_assert(openid_stay_signed_in(true)).
519stay(_).
520
524
525handle_stay_signed_in(OpenID) :-
526 http_session_retract(openid_stay_signed_in(true)),
527 !,
528 http_set_session(timeout(0)),
529 ignore(openid_hook(stay_signed_in(OpenID))).
530handle_stay_signed_in(_).
531
539
540assert_openid(OpenIDLogin, OpenID, Server, Target) :-
541 openid_identifier_select_url(OpenIDLogin),
542 openid_identifier_select_url(OpenID),
543 !,
544 assert_openid_in_session(openid_login(Identity, Identity, Server, Target)).
545assert_openid(OpenIDLogin, OpenID, Server, Target) :-
546 assert_openid_in_session(openid_login(OpenIDLogin, OpenID, Server, Target)).
547
548assert_openid_in_session(Term) :-
549 ( http_in_session(Session)
550 -> debug(openid(verify), 'Assert ~p in ~p', [Term, Session])
551 ; debug(openid(verify), 'No session!', [])
552 ),
553 http_session_assert(Term).
554
563
564openid_server(OpenIDLogin, OpenID, Server) :-
565 openid_server(OpenIDLogin, OpenID, Server, _Target).
566
567openid_server(OpenIDLogin, OpenID, Server, Target) :-
568 http_in_session(Session),
569 ( http_session_data(openid_login(OpenIDLogin, OpenID, Server, Target))
570 -> true
571 ; http_session_data(openid_login(OpenIDLogin1, OpenID1, Server1, Target1)),
572 debug(openid(verify), '~p \\== ~p',
573 [ openid_login(OpenIDLogin, OpenID, Server, Target),
574 openid_login(OpenIDLogin1, OpenID1, Server1, Target1)
575 ]),
576 fail
577 ; debug(openid(verify), 'No openid_login/4 term in session ~p', [Session]),
578 fail
579 ).
580
581
586
587public_url(Request, Path, URL) :-
588 openid_current_host(Request, Host, Port),
589 setting(http:public_scheme, Scheme),
590 set_port(Scheme, Port, AuthC),
591 uri_authority_data(host, AuthC, Host),
592 uri_authority_components(Auth, AuthC),
593 uri_data(scheme, Components, Scheme),
594 uri_data(authority, Components, Auth),
595 uri_data(path, Components, Path),
596 uri_components(URL, Components).
597
598set_port(Scheme, Port, _) :-
599 scheme_port(Scheme, Port),
600 !.
601set_port(_, Port, AuthC) :-
602 uri_authority_data(port, AuthC, Port).
603
604scheme_port(http, 80).
605scheme_port(https, 443).
606
607
612
613openid_current_url(Request, URL) :-
614 http_public_url(Request, URL).
615
622
623openid_current_host(Request, Host, Port) :-
624 http_current_host(Request, Host, Port,
625 [ global(true)
626 ]).
627
628
634
635redirect_browser(URL, FormExtra) :-
636 uri_components(URL, C0),
637 uri_data(search, C0, Search0),
638 ( var(Search0)
639 -> uri_query_components(Search, FormExtra)
640 ; uri_query_components(Search0, Form0),
641 append(FormExtra, Form0, Form),
642 uri_query_components(Search, Form)
643 ),
644 uri_data(search, C0, Search, C),
645 uri_components(Redirect, C),
646 throw(http_reply(moved_temporary(Redirect))).
647
648
649 652
667
668openid_resolve(URL, OpenID, OpenID, Server, [xrds_types(Types)]) :-
669 xrds_dom(URL, DOM),
670 xpath(DOM, //(_:'Service'), Service),
671 findall(Type, xpath(Service, _:'Type'(text), Type), Types),
672 memberchk('http://specs.openid.net/auth/2.0/server', Types),
673 xpath(Service, _:'URI'(text), Server),
674 !,
675 debug(openid(yadis), 'Yadis: server: ~q, types: ~q', [Server, Types]),
676 ( xpath(Service, _:'LocalID'(text), OpenID)
677 -> true
678 ; openid_identifier_select_url(OpenID)
679 ).
680openid_resolve(URL, OpenID0, OpenID, Server, []) :-
681 debug(openid(resolve), 'Opening ~w ...', [URL]),
682 dtd(html, DTD),
683 setup_call_cleanup(
684 http_open(URL, Stream,
685 [ final_url(OpenID0),
686 cert_verify_hook(ssl_verify)
687 ]),
688 load_structure(Stream, Term,
689 [ dtd(DTD),
690 dialect(sgml),
691 shorttag(false),
692 syntax_errors(quiet)
693 ]),
694 close(Stream)),
695 debug(openid(resolve), 'Scanning HTML document ...', [URL]),
696 contains_term(element(head, _, Head), Term),
697 ( link(Head, 'openid.server', Server)
698 -> debug(openid(resolve), 'OpenID Server=~q', [Server])
699 ; debug(openid(resolve), 'No server in ~q', [Head]),
700 fail
701 ),
702 ( link(Head, 'openid.delegate', OpenID)
703 -> debug(openid(resolve), 'OpenID = ~q (delegated)', [OpenID])
704 ; OpenID = OpenID0,
705 debug(openid(resolve), 'OpenID = ~q', [OpenID])
706 ).
707
708openid_identifier_select_url(
709 'http://specs.openid.net/auth/2.0/identifier_select').
710
711:- public ssl_verify/5.
712
718
719ssl_verify(_SSL,
720 _ProblemCertificate, _AllCertificates, _FirstCertificate,
721 _Error).
722
723
724link(DOM, Type, Target) :-
725 sub_term(element(link, Attrs, []), DOM),
726 memberchk(rel=Type, Attrs),
727 memberchk(href=Target, Attrs).
728
729
730 733
737
738openid_authenticate(Request) :-
739 memberchk(accept(Accept), Request),
740 Accept = [media(application/'xrds+xml',_,_,_)],
741 !,
742 http_link_to_id(openid_xrds, [], XRDSLocation),
743 http_absolute_uri(XRDSLocation, XRDSServer),
744 debug(openid(yadis), 'Sending XRDS server: ~q', [XRDSServer]),
745 format('X-XRDS-Location: ~w\n', [XRDSServer]),
746 format('Content-type: text/plain\n\n').
747openid_authenticate(Request) :-
748 openid_authenticate(Request, _OpenIdServer, OpenID, _ReturnTo),
749 openid_server(User, OpenID, _, Target),
750 openid_login(User),
751 redirect_browser(Target, []).
752
753
775
776openid_authenticate(Request, OpenIdServer, Identity, ReturnTo) :-
777 memberchk(method(get), Request),
778 http_parameters(Request,
779 [ 'openid.mode'(Mode, [optional(true)])
780 ]),
781 ( var(Mode)
782 -> fail
783 ; Mode == cancel
784 -> throw(openid(cancel))
785 ; Mode == id_res
786 -> debug(openid(authenticate), 'Mode=id_res, validating response', []),
787 http_parameters(Request,
788 [ 'openid.identity'(Identity, []),
789 'openid.assoc_handle'(Handle, []),
790 'openid.return_to'(ReturnTo, []),
791 'openid.signed'(AtomFields, []),
792 'openid.sig'(Base64Signature, []),
793 'openid.invalidate_handle'(Invalidate,
794 [optional(true)])
795 ],
796 [ form_data(Form)
797 ]),
798 atomic_list_concat(SignedFields, ',', AtomFields),
799 check_obligatory_fields(SignedFields),
800 signed_pairs(SignedFields,
801 [ mode-Mode,
802 identity-Identity,
803 assoc_handle-Handle,
804 return_to-ReturnTo,
805 invalidate_handle-Invalidate
806 ],
807 Form,
808 SignedPairs),
809 ( openid_associate(OpenIdServer, Handle, Assoc)
810 -> signature(SignedPairs, Assoc, Sig),
811 atom_codes(Base64Signature, Base64SigCodes),
812 phrase(base64(Signature), Base64SigCodes),
813 ( Sig == Signature
814 -> true
815 ; throw(openid(signature_mismatch))
816 )
817 ; check_authentication(Request, Form)
818 ),
819 ax_store(Form)
820 ).
821
826
827signed_pairs([], _, _, []).
828signed_pairs([Field|T0], Pairs, Form, [Field-Value|T]) :-
829 memberchk(Field-Value, Pairs),
830 !,
831 signed_pairs(T0, Pairs, Form, T).
832signed_pairs([Field|T0], Pairs, Form, [Field-Value|T]) :-
833 atom_concat('openid.', Field, OpenIdField),
834 memberchk(OpenIdField=Value, Form),
835 !,
836 signed_pairs(T0, Pairs, Form, T).
837signed_pairs([Field|T0], Pairs, Form, T) :-
838 format(user_error, 'Form = ~p~n', [Form]),
839 throw(error(existence_error(field, Field),
840 context(_, 'OpenID Signed field is not present'))),
841 signed_pairs(T0, Pairs, Form, T).
842
843
850
851check_obligatory_fields(Fields) :-
852 ( obligatory_field(Field),
853 ( memberchk(Field, Fields)
854 -> true
855 ; throw(error(existence_error(field, Field),
856 context(_, 'OpenID field is not in signed fields')))
857 ),
858 fail
859 ; true
860 ).
861
862obligatory_field(identity).
863
864
870
871check_authentication(_Request, Form) :-
872 openid_server(_OpenIDLogin, _OpenID, Server),
873 debug(openid(check_authentication),
874 'Using stateless verification with ~q form~n~q', [Server, Form]),
875 select('openid.mode' = _, Form, Form1),
876 setup_call_cleanup(
877 http_open(Server, In,
878 [ post(form([ 'openid.mode' = check_authentication
879 | Form1
880 ])),
881 cert_verify_hook(ssl_verify)
882 ]),
883 read_stream_to_codes(In, Reply),
884 close(In)),
885 debug(openid(check_authentication),
886 'Reply: ~n~s~n', [Reply]),
887 key_values_data(Pairs, Reply),
888 forall(member(invalidate_handle-Handle, Pairs),
889 retractall(association(_, Handle, _))),
890 memberchk(is_valid-true, Pairs).
891
892
893 896
901
902ax_options(ServerOptions, Options, AXAttrs) :-
903 option(ax(Spec), Options),
904 option(xrds_types(Types), ServerOptions),
905 memberchk('http://openid.net/srv/ax/1.0', Types),
906 !,
907 http_ax_attributes(Spec, AXAttrs),
908 debug(openid(ax), 'AX attributes: ~q', [AXAttrs]).
909ax_options(_, _, []) :-
910 debug(openid(ax), 'AX: not supported', []).
911
921
922ax_store(Form) :-
923 debug(openid(ax), 'Form: ~q', [Form]),
924 ax_form_attributes(Form, Values),
925 debug(openid(ax), 'AX: ~q', [Values]),
926 ( Values \== []
927 -> ( openid_hook(ax(Values))
928 -> true
929 ; http_session_assert(ax(Values))
930 )
931 ; true
932 ).
933
934
935 938
939:- dynamic
940 server_association/3. 941
946
947openid_server(Options, Request) :-
948 http_parameters(Request,
949 [ 'openid.mode'(Mode)
950 ],
951 [ attribute_declarations(openid_attribute),
952 form_data(Form)
953 ]),
954 ( Mode == associate
955 -> associate_server(Request, Form, Options)
956 ; Mode == checkid_setup
957 -> checkid_setup_server(Request, Form, Options)
958 ).
959
964
965associate_server(Request, Form, Options) :-
966 memberchk('openid.assoc_type' = AssocType, Form),
967 memberchk('openid.session_type' = SessionType, Form),
968 memberchk('openid.dh_modulus' = P64, Form),
969 memberchk('openid.dh_gen' = G64, Form),
970 memberchk('openid.dh_consumer_public' = CPX64, Form),
971 base64_btwoc(P, P64),
972 base64_btwoc(G, G64),
973 base64_btwoc(CPX, CPX64),
974 Y is 1+random(P-1), 975 DiffieHellman is powm(CPX, Y, P),
976 btwoc(DiffieHellman, DHBytes),
977 signature_algorithm(SessionType, SHA_Algo),
978 sha_hash(DHBytes, SHA1, [encoding(octet), algorithm(SHA_Algo)]),
979 CPY is powm(G, Y, P),
980 base64_btwoc(CPY, CPY64),
981 mackey_bytes(SessionType, MacBytes),
982 new_assoc_handle(MacBytes, Handle),
983 random_bytes(MacBytes, MacKey),
984 xor_codes(MacKey, SHA1, EncKey),
985 phrase(base64(EncKey), Base64EncKey),
986 DefExpriresIn is 24*3600,
987 option(expires_in(ExpriresIn), Options, DefExpriresIn),
988
989 get_time(Now),
990 ExpiresAt is integer(Now+ExpriresIn),
991 make_association([ session_type(SessionType),
992 expires_at(ExpiresAt),
993 mac_key(MacKey)
994 ],
995 Record),
996 memberchk(peer(Peer), Request),
997 assert(server_association(Peer, Handle, Record)),
998
999 key_values_data([ assoc_type-AssocType,
1000 assoc_handle-Handle,
1001 expires_in-ExpriresIn,
1002 session_type-SessionType,
1003 dh_server_public-CPY64,
1004 enc_mac_key-Base64EncKey
1005 ],
1006 Text),
1007 format('Content-type: text/plain~n~n~s', [Text]).
1008
1009mackey_bytes('DH-SHA1', 20).
1010mackey_bytes('DH-SHA256', 32).
1011
1012new_assoc_handle(Length, Handle) :-
1013 random_bytes(Length, Bytes),
1014 phrase(base64(Bytes), HandleCodes),
1015 atom_codes(Handle, HandleCodes).
1016
1017
1031
1032checkid_setup_server(_Request, Form, _Options) :-
1033 memberchk('openid.identity' = Identity, Form),
1034 memberchk('openid.assoc_handle' = Handle, Form),
1035 memberchk('openid.return_to' = ReturnTo, Form),
1036 ( memberchk('openid.realm' = Realm, Form) -> true
1037 ; memberchk('openid.trust_root' = Realm, Form)
1038 ),
1039
1040 server_association(_, Handle, _Association), 1041
1042 reply_html_page(
1043 [ title('OpenID login')
1044 ],
1045 [ \openid_title,
1046 div(class('openid-message'),
1047 ['Site ', a(href(TrustRoot), TrustRoot),
1048 ' requests permission to login with OpenID ',
1049 a(href(Identity), Identity), '.'
1050 ]),
1051 table(class('openid-form'),
1052 [ tr(td(form([ action(grant), method('GET') ],
1053 [ \hidden('openid.grant', yes),
1054 \hidden('openid.identity', Identity),
1055 \hidden('openid.assoc_handle', Handle),
1056 \hidden('openid.return_to', ReturnTo),
1057 \hidden('openid.realm', Realm),
1058 \hidden('openid.trust_root', Realm),
1059 div(['Password: ',
1060 input([ type(password),
1061 name('openid.password')
1062 ]),
1063 input([ type(submit),
1064 value('Grant')
1065 ])
1066 ])
1067 ]))),
1068 tr(td(align(right),
1069 form([ action(grant), method('GET') ],
1070 [ \hidden('openid.grant', no),
1071 \hidden('openid.return_to', ReturnTo),
1072 input([type(submit), value('Deny')])
1073 ])))
1074 ])
1075 ]).
1076
1077hidden(Name, Value) -->
1078 html(input([type(hidden), id(return_to), name(Name), value(Value)])).
1079
1080
1081openid_title -->
1082 { http_absolute_location(icons('openid-logo-square.png'), SRC, []) },
1083 html_requires(css('openid.css')),
1084 html(div(class('openid-title'),
1085 [ a(href('http://openid.net/'),
1086 img([ src(SRC), alt('OpenID') ])),
1087 span('Login')
1088 ])).
1089
1090
1097
1098openid_grant(Request) :-
1099 http_parameters(Request,
1100 [ 'openid.grant'(Grant),
1101 'openid.return_to'(ReturnTo)
1102 ],
1103 [ attribute_declarations(openid_attribute)
1104 ]),
1105 ( Grant == yes
1106 -> http_parameters(Request,
1107 [ 'openid.identity'(Identity),
1108 'openid.assoc_handle'(Handle),
1109 'openid.trust_root'(TrustRoot),
1110 'openid.password'(Password)
1111 ],
1112 [ attribute_declarations(openid_attribute)
1113 ]),
1114 server_association(_, Handle, Association),
1115 grant_login(Request,
1116 [ identity(Identity),
1117 password(Password),
1118 trustroot(TrustRoot)
1119 ]),
1120 SignedPairs = [ 'mode'-id_res,
1121 'identity'-Identity,
1122 'assoc_handle'-Handle,
1123 'return_to'-ReturnTo
1124 ],
1125 signed_fields(SignedPairs, Signed),
1126 signature(SignedPairs, Association, Signature),
1127 phrase(base64(Signature), Bas64SigCodes),
1128 string_codes(Bas64Sig, Bas64SigCodes),
1129 redirect_browser(ReturnTo,
1130 [ 'openid.mode' = id_res,
1131 'openid.identity' = Identity,
1132 'openid.assoc_handle' = Handle,
1133 'openid.return_to' = ReturnTo,
1134 'openid.signed' = Signed,
1135 'openid.sig' = Bas64Sig
1136 ])
1137 ; redirect_browser(ReturnTo,
1138 [ 'openid.mode' = cancel
1139 ])
1140 ).
1141
1142
1151
1152grant_login(Request, Options) :-
1153 openid_hook(grant(Request, Options)).
1154
1160
1161trusted(OpenID, Server) :-
1162 openid_hook(trusted(OpenID, Server)).
1163
1164
1169
1170signed_fields(Pairs, Signed) :-
1171 signed_field_names(Pairs, Names),
1172 atomic_list_concat(Names, ',', Signed).
1173
1174signed_field_names([], []).
1175signed_field_names([H0-_|T0], [H|T]) :-
1176 ( atom_concat('openid.', H, H0)
1177 -> true
1178 ; H = H0
1179 ),
1180 signed_field_names(T0, T).
1181
1185
1186signature(Pairs, Association, Signature) :-
1187 key_values_data(Pairs, TokenContents),
1188 association_mac_key(Association, MacKey),
1189 association_session_type(Association, SessionType),
1190 signature_algorithm(SessionType, SHA),
1191 hmac_sha(MacKey, TokenContents, Signature, [algorithm(SHA)]),
1192 debug(openid(crypt),
1193 'Signed:~n~s~nSignature: ~w', [TokenContents, Signature]).
1194
1195signature_algorithm('DH-SHA1', sha1).
1196signature_algorithm('DH-SHA256', sha256).
1197
1198
1199 1202
1203:- dynamic
1204 association/3. 1205
1206:- record
1207 association(session_type='DH-SHA1',
1208 expires_at, 1209 mac_key). 1210
1218
1219openid_associate(URL, Handle, Assoc) :-
1220 openid_associate(URL, Handle, Assoc, []).
1221
1234
1235openid_associate(URL, Handle, Assoc, _Options) :-
1236 nonvar(Handle),
1237 !,
1238 debug(openid(associate),
1239 'OpenID: Lookup association with handle ~q', [Handle]),
1240 ( association(URL, Handle, Assoc)
1241 -> true
1242 ; debug(openid(associate),
1243 'OpenID: no association with handle ~q', [Handle]),
1244 fail
1245 ).
1246openid_associate(URL, Handle, Assoc, _Options) :-
1247 must_be(atom, URL),
1248 association(URL, Handle, Assoc),
1249 association_expires_at(Assoc, Expires),
1250 get_time(Now),
1251 ( Now < Expires
1252 -> !,
1253 debug(openid(associate),
1254 'OpenID: Reusing association with ~q', [URL])
1255 ; retractall(association(URL, Handle, _)),
1256 fail
1257 ).
1258openid_associate(URL, Handle, Assoc, Options) :-
1259 associate_data(Data, P, _G, X, Options),
1260 debug(openid(associate), 'OpenID: Associating with ~q', [URL]),
1261 setup_call_cleanup(
1262 http_open(URL, In,
1263 [ post(form(Data)),
1264 cert_verify_hook(ssl_verify)
1265 ]),
1266 read_stream_to_codes(In, Reply),
1267 close(In)),
1268 debug(openid(associate), 'Reply: ~n~s', [Reply]),
1269 key_values_data(Pairs, Reply),
1270 shared_secret(Pairs, P, X, MacKey),
1271 expires_at(Pairs, ExpiresAt),
1272 memberchk(assoc_handle-Handle, Pairs),
1273 memberchk(session_type-Type, Pairs),
1274 make_association([ session_type(Type),
1275 expires_at(ExpiresAt),
1276 mac_key(MacKey)
1277 ], Assoc),
1278 assert(association(URL, Handle, Assoc)).
1279
1280
1285
1286shared_secret(Pairs, _, _, Secret) :-
1287 memberchk(mac_key-Base64, Pairs),
1288 !,
1289 atom_codes(Base64, Base64Codes),
1290 phrase(base64(Base64Codes), Secret).
1291shared_secret(Pairs, P, X, Secret) :-
1292 memberchk(dh_server_public-Base64Public, Pairs),
1293 memberchk(enc_mac_key-Base64EncMacKey, Pairs),
1294 memberchk(session_type-SessionType, Pairs),
1295 base64_btwoc(ServerPublic, Base64Public),
1296 DiffieHellman is powm(ServerPublic, X, P),
1297 atom_codes(Base64EncMacKey, Base64EncMacKeyCodes),
1298 phrase(base64(EncMacKey), Base64EncMacKeyCodes),
1299 btwoc(DiffieHellman, DiffieHellmanBytes),
1300 signature_algorithm(SessionType, SHA_Algo),
1301 sha_hash(DiffieHellmanBytes, DHHash,
1302 [encoding(octet), algorithm(SHA_Algo)]),
1303 xor_codes(DHHash, EncMacKey, Secret).
1304
1305
1310
1311expires_at(Pairs, Time) :-
1312 memberchk(expires_in-ExpAtom, Pairs),
1313 atom_number(ExpAtom, Seconds),
1314 get_time(Now),
1315 Time is integer(Now)+Seconds.
1316
1317
1322
1323associate_data(Data, P, G, X, Options) :-
1324 openid_dh_p(P),
1325 openid_dh_g(G),
1326 X is 1+random(P-1), 1327 CP is powm(G, X, P),
1328 base64_btwoc(P, P64),
1329 base64_btwoc(G, G64),
1330 base64_btwoc(CP, CP64),
1331 option(ns(NS), Options, 'http://specs.openid.net/auth/2.0'),
1332 ( assoc_type(NS, DefAssocType, DefSessionType)
1333 -> true
1334 ; domain_error('openid.ns', NS)
1335 ),
1336 option(assoc_type(AssocType), Options, DefAssocType),
1337 option(assoc_type(SessionType), Options, DefSessionType),
1338 Data = [ 'openid.ns' = NS,
1339 'openid.mode' = associate,
1340 'openid.assoc_type' = AssocType,
1341 'openid.session_type' = SessionType,
1342 'openid.dh_modulus' = P64,
1343 'openid.dh_gen' = G64,
1344 'openid.dh_consumer_public' = CP64
1345 ].
1346
1347assoc_type('http://specs.openid.net/auth/2.0',
1348 'HMAC-SHA256',
1349 'DH-SHA256').
1350assoc_type('http://openid.net/signon/1.1',
1351 'HMAC-SHA1',
1352 'DH-SHA1').
1353
1354
1355 1358
1362
1363random_bytes(N, [H|T]) :-
1364 N > 0,
1365 !,
1366 H is random(256),
1367 N2 is N - 1,
1368 random_bytes(N2, T).
1369random_bytes(_, []).
1370
1371
1372 1375
1376openid_dh_p(155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443).
1377
1378openid_dh_g(2).
1379
1380
1381 1384
1391
1392key_values_data(Pairs, Data) :-
1393 nonvar(Data),
1394 !,
1395 phrase(data_form(Pairs), Data).
1396key_values_data(Pairs, Data) :-
1397 phrase(gen_data_form(Pairs), Data).
1398
1399data_form([Key-Value|Pairs]) -->
1400 utf8_string(KeyCodes), ":", utf8_string(ValueCodes), "\n",
1401 !,
1402 { atom_codes(Key, KeyCodes),
1403 atom_codes(Value, ValueCodes)
1404 },
1405 data_form(Pairs).
1406data_form([]) -->
1407 ws.
1408
1412
1413utf8_string([]) -->
1414 [].
1415utf8_string([H|T]) -->
1416 utf8_codes([H]),
1417 utf8_string(T).
1418
1419ws -->
1420 [C],
1421 { C =< 32 },
1422 !,
1423 ws.
1424ws -->
1425 [].
1426
1427
1428gen_data_form([]) -->
1429 [].
1430gen_data_form([Key-Value|T]) -->
1431 field(Key), ":", field(Value), "\n",
1432 gen_data_form(T).
1433
1434field(Field) -->
1435 { to_codes(Field, Codes)
1436 },
1437 utf8_codes(Codes).
1438
1439to_codes(Codes, Codes) :-
1440 is_list(Codes),
1441 !.
1442to_codes(Atomic, Codes) :-
1443 atom_codes(Atomic, Codes).
1444
1448
1449base64_btwoc(Int, Base64) :-
1450 integer(Int),
1451 !,
1452 btwoc(Int, Bytes),
1453 phrase(base64(Bytes), Base64).
1454base64_btwoc(Int, Base64) :-
1455 atom(Base64),
1456 !,
1457 atom_codes(Base64, Codes),
1458 phrase(base64(Bytes), Codes),
1459 btwoc(Int, Bytes).
1460base64_btwoc(Int, Base64) :-
1461 phrase(base64(Bytes), Base64),
1462 btwoc(Int, Bytes).
1463
1464
1470
1471btwoc(Int, Bytes) :-
1472 integer(Int),
1473 !,
1474 int_to_bytes(Int, Bytes).
1475btwoc(Int, Bytes) :-
1476 is_list(Bytes),
1477 bytes_to_int(Bytes, Int).
1478
1479int_to_bytes(Int, Bytes) :-
1480 int_to_bytes(Int, [], Bytes).
1481
1482int_to_bytes(Int, Bytes0, [Int|Bytes0]) :-
1483 Int < 128,
1484 !.
1485int_to_bytes(Int, Bytes0, Bytes) :-
1486 Last is Int /\ 0xff,
1487 Int1 is Int >> 8,
1488 int_to_bytes(Int1, [Last|Bytes0], Bytes).
1489
1490
1491bytes_to_int([B|T], Int) :-
1492 bytes_to_int(T, B, Int).
1493
1494bytes_to_int([], Int, Int).
1495bytes_to_int([B|T], Int0, Int) :-
1496 Int1 is (Int0<<8)+B,
1497 bytes_to_int(T, Int1, Int).
1498
1499
1506
1507xor_codes([], [], []) :- !.
1508xor_codes([H1|T1], [H2|T2], [H|T]) :-
1509 !,
1510 H is H1 xor H2,
1511 !,
1512 xor_codes(T1, T2, T).
1513xor_codes(L1, L2, _) :-
1514 throw(error(length_mismatch(L1, L2), _)).
1515
1516
1517 1520
1521openid_attribute('openid.mode',
1522 [ oneof([ associate,
1523 checkid_setup,
1524 cancel,
1525 id_res
1526 ])
1527 ]).
1528openid_attribute('openid.assoc_type',
1529 [ oneof(['HMAC-SHA1'])
1530 ]).
1531openid_attribute('openid.session_type',
1532 [ oneof([ 'DH-SHA1',
1533 'DH-SHA256'
1534 ])
1535 ]).
1536openid_attribute('openid.dh_modulus', [length > 1]).
1537openid_attribute('openid.dh_gen', [length > 1]).
1538openid_attribute('openid.dh_consumer_public', [length > 1]).
1539openid_attribute('openid.assoc_handle', [length > 1]).
1540openid_attribute('openid.return_to', [length > 1]).
1541openid_attribute('openid.trust_root', [length > 1]).
1542openid_attribute('openid.identity', [length > 1]).
1543openid_attribute('openid.password', [length > 1]).
1544openid_attribute('openid.grant', [oneof([yes,no])]).