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(_0Session)
550 -> debug(openid(verify), 'Assert ~p in ~p', [Term, _0Session])
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(_0Session),
569 ( http_session_data(openid_login(OpenIDLogin, OpenID, Server, Target))
570 -> true
571 ; http_session_data(openid_login(_0OpenIDLogin1, _0OpenID1,
572 _0Server1, _0Target1)),
573 debug(openid(verify), '~p \\== ~p',
574 [ openid_login(OpenIDLogin, OpenID, Server, Target),
575 openid_login(_0OpenIDLogin1, _0OpenID1, _0Server1, _0Target1)
576 ]),
577 fail
578 ; debug(openid(verify), 'No openid_login/4 term in session ~p',
579 [_0Session]),
580 fail
581 ).
582
583
588
589public_url(Request, Path, URL) :-
590 openid_current_host(Request, Host, Port),
591 setting(http:public_scheme, Scheme),
592 set_port(Scheme, Port, AuthC),
593 uri_authority_data(host, AuthC, Host),
594 uri_authority_components(Auth, AuthC),
595 uri_data(scheme, Components, Scheme),
596 uri_data(authority, Components, Auth),
597 uri_data(path, Components, Path),
598 uri_components(URL, Components).
599
600set_port(Scheme, Port, _) :-
601 scheme_port(Scheme, Port),
602 !.
603set_port(_, Port, AuthC) :-
604 uri_authority_data(port, AuthC, Port).
605
606scheme_port(http, 80).
607scheme_port(https, 443).
608
609
617
618openid_current_url(Request, URL) :-
619 option(request_uri(URI), Request),
620 uri_components(URI, Components),
621 uri_data(path, Components, Path),
622 ( uri_data(search, Components, QueryString),
623 nonvar(QueryString),
624 uri_query_components(QueryString, Query),
625 memberchk(referer=Base, Query)
626 -> true
627 ; option(referer(Base), Request)
628 ), !,
629 uri_normalized(Path, Base, URL).
630openid_current_url(Request, URL) :-
631 http_public_url(Request, URL).
632
639
640openid_current_host(Request, Host, Port) :-
641 http_current_host(Request, Host, Port,
642 [ global(true)
643 ]).
644
645
651
652redirect_browser(URL, FormExtra) :-
653 uri_components(URL, C0),
654 uri_data(search, C0, Search0),
655 ( var(Search0)
656 -> uri_query_components(Search, FormExtra)
657 ; uri_query_components(Search0, Form0),
658 append(FormExtra, Form0, Form),
659 uri_query_components(Search, Form)
660 ),
661 uri_data(search, C0, Search, C),
662 uri_components(Redirect, C),
663 throw(http_reply(moved_temporary(Redirect))).
664
665
666 669
684
685openid_resolve(URL, OpenID, OpenID, Server, [xrds_types(Types)]) :-
686 xrds_dom(URL, DOM),
687 xpath(DOM, //(_:'Service'), Service),
688 findall(Type, xpath(Service, _:'Type'(text), Type), Types),
689 memberchk('http://specs.openid.net/auth/2.0/server', Types),
690 xpath(Service, _:'URI'(text), Server),
691 !,
692 debug(openid(yadis), 'Yadis: server: ~q, types: ~q', [Server, Types]),
693 ( xpath(Service, _:'LocalID'(text), OpenID)
694 -> true
695 ; openid_identifier_select_url(OpenID)
696 ).
697openid_resolve(URL, OpenID0, OpenID, Server, []) :-
698 debug(openid(resolve), 'Opening ~w ...', [URL]),
699 dtd(html, DTD),
700 setup_call_cleanup(
701 http_open(URL, Stream,
702 [ final_url(OpenID0),
703 cert_verify_hook(ssl_verify)
704 ]),
705 load_structure(Stream, Term,
706 [ dtd(DTD),
707 dialect(sgml),
708 shorttag(false),
709 syntax_errors(quiet)
710 ]),
711 close(Stream)),
712 debug(openid(resolve), 'Scanning HTML document ...', []),
713 contains_term(element(head, _, Head), Term),
714 ( link(Head, 'openid.server', Server)
715 -> debug(openid(resolve), 'OpenID Server=~q', [Server])
716 ; debug(openid(resolve), 'No server in ~q', [Head]),
717 fail
718 ),
719 ( link(Head, 'openid.delegate', OpenID)
720 -> debug(openid(resolve), 'OpenID = ~q (delegated)', [OpenID])
721 ; OpenID = OpenID0,
722 debug(openid(resolve), 'OpenID = ~q', [OpenID])
723 ).
724
725openid_identifier_select_url(
726 'http://specs.openid.net/auth/2.0/identifier_select').
727
728:- public ssl_verify/5. 729
735
736ssl_verify(_SSL,
737 _ProblemCertificate, _AllCertificates, _FirstCertificate,
738 _Error).
739
740
741link(DOM, Type, Target) :-
742 sub_term(element(link, Attrs, []), DOM),
743 memberchk(rel=Type, Attrs),
744 memberchk(href=Target, Attrs).
745
746
747 750
754
755openid_authenticate(Request) :-
756 memberchk(accept(Accept), Request),
757 Accept = [media(application/'xrds+xml',_,_,_)],
758 !,
759 http_link_to_id(openid_xrds, [], XRDSLocation),
760 http_absolute_uri(XRDSLocation, XRDSServer),
761 debug(openid(yadis), 'Sending XRDS server: ~q', [XRDSServer]),
762 format('X-XRDS-Location: ~w\n', [XRDSServer]),
763 format('Content-type: text/plain\n\n').
764openid_authenticate(Request) :-
765 openid_authenticate(Request, _OpenIdServer, OpenID, _ReturnTo),
766 openid_server(User, OpenID, _, Target),
767 openid_login(User),
768 redirect_browser(Target, []).
769
770
792
793openid_authenticate(Request, OpenIdServer, Identity, ReturnTo) :-
794 memberchk(method(get), Request),
795 http_parameters(Request,
796 [ 'openid.mode'(Mode, [optional(true)])
797 ]),
798 ( var(Mode)
799 -> fail
800 ; Mode == cancel
801 -> throw(openid(cancel))
802 ; Mode == id_res
803 -> debug(openid(authenticate), 'Mode=id_res, validating response', []),
804 http_parameters(Request,
805 [ 'openid.identity'(Identity, []),
806 'openid.assoc_handle'(Handle, []),
807 'openid.return_to'(ReturnTo, []),
808 'openid.signed'(AtomFields, []),
809 'openid.sig'(Base64Signature, []),
810 'openid.invalidate_handle'(Invalidate,
811 [optional(true)])
812 ],
813 [ form_data(Form)
814 ]),
815 atomic_list_concat(SignedFields, ',', AtomFields),
816 check_obligatory_fields(SignedFields),
817 signed_pairs(SignedFields,
818 [ mode-Mode,
819 identity-Identity,
820 assoc_handle-Handle,
821 return_to-ReturnTo,
822 invalidate_handle-Invalidate
823 ],
824 Form,
825 SignedPairs),
826 ( openid_associate(OpenIdServer, Handle, Assoc)
827 -> signature(SignedPairs, Assoc, Sig),
828 atom_codes(Base64Signature, Base64SigCodes),
829 phrase(base64(Signature), Base64SigCodes),
830 ( Sig == Signature
831 -> true
832 ; throw(openid(signature_mismatch))
833 )
834 ; check_authentication(Request, Form)
835 ),
836 ax_store(Form)
837 ).
838
843
844signed_pairs([], _, _, []).
845signed_pairs([Field|T0], Pairs, Form, [Field-Value|T]) :-
846 memberchk(Field-Value, Pairs),
847 !,
848 signed_pairs(T0, Pairs, Form, T).
849signed_pairs([Field|T0], Pairs, Form, [Field-Value|T]) :-
850 atom_concat('openid.', Field, OpenIdField),
851 memberchk(OpenIdField=Value, Form),
852 !,
853 signed_pairs(T0, Pairs, Form, T).
854signed_pairs([Field|T0], Pairs, Form, T) :-
855 format(user_error, 'Form = ~p~n', [Form]),
856 throw(error(existence_error(field, Field),
857 context(_, 'OpenID Signed field is not present'))),
858 signed_pairs(T0, Pairs, Form, T).
859
860
867
868check_obligatory_fields(Fields) :-
869 ( obligatory_field(Field),
870 ( memberchk(Field, Fields)
871 -> true
872 ; throw(error(existence_error(field, Field),
873 context(_, 'OpenID field is not in signed fields')))
874 ),
875 fail
876 ; true
877 ).
878
879obligatory_field(identity).
880
881
887
888check_authentication(_Request, Form) :-
889 openid_server(_OpenIDLogin, _OpenID, Server),
890 debug(openid(check_authentication),
891 'Using stateless verification with ~q form~n~q', [Server, Form]),
892 select('openid.mode' = _, Form, Form1),
893 setup_call_cleanup(
894 http_open(Server, In,
895 [ post(form([ 'openid.mode' = check_authentication
896 | Form1
897 ])),
898 cert_verify_hook(ssl_verify)
899 ]),
900 read_stream_to_codes(In, Reply),
901 close(In)),
902 debug(openid(check_authentication),
903 'Reply: ~n~s~n', [Reply]),
904 key_values_data(Pairs, Reply),
905 forall(member(invalidate_handle-Handle, Pairs),
906 retractall(association(_, Handle, _))),
907 memberchk(is_valid-true, Pairs).
908
909
910 913
918
919ax_options(ServerOptions, Options, AXAttrs) :-
920 option(ax(Spec), Options),
921 option(xrds_types(Types), ServerOptions),
922 memberchk('http://openid.net/srv/ax/1.0', Types),
923 !,
924 http_ax_attributes(Spec, AXAttrs),
925 debug(openid(ax), 'AX attributes: ~q', [AXAttrs]).
926ax_options(_, _, []) :-
927 debug(openid(ax), 'AX: not supported', []).
928
938
939ax_store(Form) :-
940 debug(openid(ax), 'Form: ~q', [Form]),
941 ax_form_attributes(Form, Values),
942 debug(openid(ax), 'AX: ~q', [Values]),
943 ( Values \== []
944 -> ( openid_hook(ax(Values))
945 -> true
946 ; http_session_assert(ax(Values))
947 )
948 ; true
949 ).
950
951
952 955
956:- dynamic
957 server_association/3. 958
963
964openid_server(Options, Request) :-
965 http_parameters(Request,
966 [ 'openid.mode'(Mode)
967 ],
968 [ attribute_declarations(openid_attribute),
969 form_data(Form)
970 ]),
971 ( Mode == associate
972 -> associate_server(Request, Form, Options)
973 ; Mode == checkid_setup
974 -> checkid_setup_server(Request, Form, Options)
975 ).
976
981
982associate_server(Request, Form, Options) :-
983 memberchk('openid.assoc_type' = AssocType, Form),
984 memberchk('openid.session_type' = SessionType, Form),
985 memberchk('openid.dh_modulus' = P64, Form),
986 memberchk('openid.dh_gen' = G64, Form),
987 memberchk('openid.dh_consumer_public' = CPX64, Form),
988 base64_btwoc(P, P64),
989 base64_btwoc(G, G64),
990 base64_btwoc(CPX, CPX64),
991 Y is 1+random(P-1), 992 DiffieHellman is powm(CPX, Y, P),
993 btwoc(DiffieHellman, DHBytes),
994 signature_algorithm(SessionType, SHA_Algo),
995 sha_hash(DHBytes, SHA1, [encoding(octet), algorithm(SHA_Algo)]),
996 CPY is powm(G, Y, P),
997 base64_btwoc(CPY, CPY64),
998 mackey_bytes(SessionType, MacBytes),
999 new_assoc_handle(MacBytes, Handle),
1000 random_bytes(MacBytes, MacKey),
1001 xor_codes(MacKey, SHA1, EncKey),
1002 phrase(base64(EncKey), Base64EncKey),
1003 DefExpriresIn is 24*3600,
1004 option(expires_in(ExpriresIn), Options, DefExpriresIn),
1005
1006 get_time(Now),
1007 ExpiresAt is integer(Now+ExpriresIn),
1008 make_association([ session_type(SessionType),
1009 expires_at(ExpiresAt),
1010 mac_key(MacKey)
1011 ],
1012 Record),
1013 memberchk(peer(Peer), Request),
1014 assert(server_association(Peer, Handle, Record)),
1015
1016 key_values_data([ assoc_type-AssocType,
1017 assoc_handle-Handle,
1018 expires_in-ExpriresIn,
1019 session_type-SessionType,
1020 dh_server_public-CPY64,
1021 enc_mac_key-Base64EncKey
1022 ],
1023 Text),
1024 format('Content-type: text/plain~n~n~s', [Text]).
1025
1026mackey_bytes('DH-SHA1', 20).
1027mackey_bytes('DH-SHA256', 32).
1028
1029new_assoc_handle(Length, Handle) :-
1030 random_bytes(Length, Bytes),
1031 phrase(base64(Bytes), HandleCodes),
1032 atom_codes(Handle, HandleCodes).
1033
1034
1048
1049checkid_setup_server(_Request, Form, _Options) :-
1050 memberchk('openid.identity' = Identity, Form),
1051 memberchk('openid.assoc_handle' = Handle, Form),
1052 memberchk('openid.return_to' = ReturnTo, Form),
1053 ( memberchk('openid.realm' = Realm, Form) -> true
1054 ; memberchk('openid.trust_root' = Realm, Form)
1055 ),
1056
1057 server_association(_, Handle, _Association), 1058
1059 reply_html_page(
1060 [ title('OpenID login')
1061 ],
1062 [ \openid_title,
1063 div(class('openid-message'),
1064 ['Site ', a(href(TrustRoot), TrustRoot),
1065 ' requests permission to login with OpenID ',
1066 a(href(Identity), Identity), '.'
1067 ]),
1068 table(class('openid-form'),
1069 [ tr(td(form([ action(grant), method('GET') ],
1070 [ \hidden('openid.grant', yes),
1071 \hidden('openid.identity', Identity),
1072 \hidden('openid.assoc_handle', Handle),
1073 \hidden('openid.return_to', ReturnTo),
1074 \hidden('openid.realm', Realm),
1075 \hidden('openid.trust_root', Realm),
1076 div(['Password: ',
1077 input([ type(password),
1078 name('openid.password')
1079 ]),
1080 input([ type(submit),
1081 value('Grant')
1082 ])
1083 ])
1084 ]))),
1085 tr(td(align(right),
1086 form([ action(grant), method('GET') ],
1087 [ \hidden('openid.grant', no),
1088 \hidden('openid.return_to', ReturnTo),
1089 input([type(submit), value('Deny')])
1090 ])))
1091 ])
1092 ]).
1093
1094hidden(Name, Value) -->
1095 html(input([type(hidden), id(return_to), name(Name), value(Value)])).
1096
1097
1098openid_title -->
1099 { http_absolute_location(icons('openid-logo-square.png'), SRC, []) },
1100 html_requires(css('openid.css')),
1101 html(div(class('openid-title'),
1102 [ a(href('http://openid.net/'),
1103 img([ src(SRC), alt('OpenID') ])),
1104 span('Login')
1105 ])).
1106
1107
1114
1115openid_grant(Request) :-
1116 http_parameters(Request,
1117 [ 'openid.grant'(Grant),
1118 'openid.return_to'(ReturnTo)
1119 ],
1120 [ attribute_declarations(openid_attribute)
1121 ]),
1122 ( Grant == yes
1123 -> http_parameters(Request,
1124 [ 'openid.identity'(Identity),
1125 'openid.assoc_handle'(Handle),
1126 'openid.trust_root'(TrustRoot),
1127 'openid.password'(Password)
1128 ],
1129 [ attribute_declarations(openid_attribute)
1130 ]),
1131 server_association(_, Handle, Association),
1132 grant_login(Request,
1133 [ identity(Identity),
1134 password(Password),
1135 trustroot(TrustRoot)
1136 ]),
1137 SignedPairs = [ 'mode'-id_res,
1138 'identity'-Identity,
1139 'assoc_handle'-Handle,
1140 'return_to'-ReturnTo
1141 ],
1142 signed_fields(SignedPairs, Signed),
1143 signature(SignedPairs, Association, Signature),
1144 phrase(base64(Signature), Bas64SigCodes),
1145 string_codes(Bas64Sig, Bas64SigCodes),
1146 redirect_browser(ReturnTo,
1147 [ 'openid.mode' = id_res,
1148 'openid.identity' = Identity,
1149 'openid.assoc_handle' = Handle,
1150 'openid.return_to' = ReturnTo,
1151 'openid.signed' = Signed,
1152 'openid.sig' = Bas64Sig
1153 ])
1154 ; redirect_browser(ReturnTo,
1155 [ 'openid.mode' = cancel
1156 ])
1157 ).
1158
1159
1168
1169grant_login(Request, Options) :-
1170 openid_hook(grant(Request, Options)).
1171
1177
1178trusted(OpenID, Server) :-
1179 openid_hook(trusted(OpenID, Server)).
1180
1181
1186
1187signed_fields(Pairs, Signed) :-
1188 signed_field_names(Pairs, Names),
1189 atomic_list_concat(Names, ',', Signed).
1190
1191signed_field_names([], []).
1192signed_field_names([H0-_|T0], [H|T]) :-
1193 ( atom_concat('openid.', H, H0)
1194 -> true
1195 ; H = H0
1196 ),
1197 signed_field_names(T0, T).
1198
1202
1203signature(Pairs, Association, Signature) :-
1204 key_values_data(Pairs, TokenContents),
1205 association_mac_key(Association, MacKey),
1206 association_session_type(Association, SessionType),
1207 signature_algorithm(SessionType, SHA),
1208 hmac_sha(MacKey, TokenContents, Signature, [algorithm(SHA)]),
1209 debug(openid(crypt),
1210 'Signed:~n~s~nSignature: ~w', [TokenContents, Signature]).
1211
1212signature_algorithm('DH-SHA1', sha1).
1213signature_algorithm('DH-SHA256', sha256).
1214
1215
1216 1219
1220:- dynamic
1221 association/3. 1222
1223:- record
1224 association(session_type='DH-SHA1',
1225 expires_at, 1226 mac_key). 1227
1235
1236openid_associate(URL, Handle, Assoc) :-
1237 openid_associate(URL, Handle, Assoc, []).
1238
1251
1252openid_associate(URL, Handle, Assoc, _Options) :-
1253 nonvar(Handle),
1254 !,
1255 debug(openid(associate),
1256 'OpenID: Lookup association with handle ~q', [Handle]),
1257 ( association(URL, Handle, Assoc)
1258 -> true
1259 ; debug(openid(associate),
1260 'OpenID: no association with handle ~q', [Handle]),
1261 fail
1262 ).
1263openid_associate(URL, Handle, Assoc, _Options) :-
1264 must_be(atom, URL),
1265 association(URL, Handle, Assoc),
1266 association_expires_at(Assoc, Expires),
1267 get_time(Now),
1268 ( Now < Expires
1269 -> !,
1270 debug(openid(associate),
1271 'OpenID: Reusing association with ~q', [URL])
1272 ; retractall(association(URL, Handle, _)),
1273 fail
1274 ).
1275openid_associate(URL, Handle, Assoc, Options) :-
1276 associate_data(Data, P, _G, X, Options),
1277 debug(openid(associate), 'OpenID: Associating with ~q', [URL]),
1278 setup_call_cleanup(
1279 http_open(URL, In,
1280 [ post(form(Data)),
1281 cert_verify_hook(ssl_verify)
1282 ]),
1283 read_stream_to_codes(In, Reply),
1284 close(In)),
1285 debug(openid(associate), 'Reply: ~n~s', [Reply]),
1286 key_values_data(Pairs, Reply),
1287 shared_secret(Pairs, P, X, MacKey),
1288 expires_at(Pairs, ExpiresAt),
1289 memberchk(assoc_handle-Handle, Pairs),
1290 memberchk(session_type-Type, Pairs),
1291 make_association([ session_type(Type),
1292 expires_at(ExpiresAt),
1293 mac_key(MacKey)
1294 ], Assoc),
1295 assert(association(URL, Handle, Assoc)).
1296
1297
1302
1303shared_secret(Pairs, _, _, Secret) :-
1304 memberchk(mac_key-Base64, Pairs),
1305 !,
1306 atom_codes(Base64, Base64Codes),
1307 phrase(base64(Base64Codes), Secret).
1308shared_secret(Pairs, P, X, Secret) :-
1309 memberchk(dh_server_public-Base64Public, Pairs),
1310 memberchk(enc_mac_key-Base64EncMacKey, Pairs),
1311 memberchk(session_type-SessionType, Pairs),
1312 base64_btwoc(ServerPublic, Base64Public),
1313 DiffieHellman is powm(ServerPublic, X, P),
1314 atom_codes(Base64EncMacKey, Base64EncMacKeyCodes),
1315 phrase(base64(EncMacKey), Base64EncMacKeyCodes),
1316 btwoc(DiffieHellman, DiffieHellmanBytes),
1317 signature_algorithm(SessionType, SHA_Algo),
1318 sha_hash(DiffieHellmanBytes, DHHash,
1319 [encoding(octet), algorithm(SHA_Algo)]),
1320 xor_codes(DHHash, EncMacKey, Secret).
1321
1322
1327
1328expires_at(Pairs, Time) :-
1329 memberchk(expires_in-ExpAtom, Pairs),
1330 atom_number(ExpAtom, Seconds),
1331 get_time(Now),
1332 Time is integer(Now)+Seconds.
1333
1334
1339
1340associate_data(Data, P, G, X, Options) :-
1341 openid_dh_p(P),
1342 openid_dh_g(G),
1343 X is 1+random(P-1), 1344 CP is powm(G, X, P),
1345 base64_btwoc(P, P64),
1346 base64_btwoc(G, G64),
1347 base64_btwoc(CP, CP64),
1348 option(ns(NS), Options, 'http://specs.openid.net/auth/2.0'),
1349 ( assoc_type(NS, DefAssocType, DefSessionType)
1350 -> true
1351 ; domain_error('openid.ns', NS)
1352 ),
1353 option(assoc_type(AssocType), Options, DefAssocType),
1354 option(assoc_type(SessionType), Options, DefSessionType),
1355 Data = [ 'openid.ns' = NS,
1356 'openid.mode' = associate,
1357 'openid.assoc_type' = AssocType,
1358 'openid.session_type' = SessionType,
1359 'openid.dh_modulus' = P64,
1360 'openid.dh_gen' = G64,
1361 'openid.dh_consumer_public' = CP64
1362 ].
1363
1364assoc_type('http://specs.openid.net/auth/2.0',
1365 'HMAC-SHA256',
1366 'DH-SHA256').
1367assoc_type('http://openid.net/signon/1.1',
1368 'HMAC-SHA1',
1369 'DH-SHA1').
1370
1371
1372 1375
1379
1380random_bytes(N, [H|T]) :-
1381 N > 0,
1382 !,
1383 H is random(256),
1384 N2 is N - 1,
1385 random_bytes(N2, T).
1386random_bytes(_, []).
1387
1388
1389 1392
1393openid_dh_p(155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443).
1394
1395openid_dh_g(2).
1396
1397
1398 1401
1408
1409key_values_data(Pairs, Data) :-
1410 nonvar(Data),
1411 !,
1412 phrase(data_form(Pairs), Data).
1413key_values_data(Pairs, Data) :-
1414 phrase(gen_data_form(Pairs), Data).
1415
1416data_form([Key-Value|Pairs]) -->
1417 utf8_string(KeyCodes), ":", utf8_string(ValueCodes), "\n",
1418 !,
1419 { atom_codes(Key, KeyCodes),
1420 atom_codes(Value, ValueCodes)
1421 },
1422 data_form(Pairs).
1423data_form([]) -->
1424 ws.
1425
1429
1430utf8_string([]) -->
1431 [].
1432utf8_string([H|T]) -->
1433 utf8_codes([H]),
1434 utf8_string(T).
1435
1436ws -->
1437 [C],
1438 { C =< 32 },
1439 !,
1440 ws.
1441ws -->
1442 [].
1443
1444
1445gen_data_form([]) -->
1446 [].
1447gen_data_form([Key-Value|T]) -->
1448 field(Key), ":", field(Value), "\n",
1449 gen_data_form(T).
1450
1451field(Field) -->
1452 { to_codes(Field, Codes)
1453 },
1454 utf8_codes(Codes).
1455
1456to_codes(Codes, Codes) :-
1457 is_list(Codes),
1458 !.
1459to_codes(Atomic, Codes) :-
1460 atom_codes(Atomic, Codes).
1461
1465
1466base64_btwoc(Int, Base64) :-
1467 integer(Int),
1468 !,
1469 btwoc(Int, Bytes),
1470 phrase(base64(Bytes), Base64).
1471base64_btwoc(Int, Base64) :-
1472 atom(Base64),
1473 !,
1474 atom_codes(Base64, Codes),
1475 phrase(base64(Bytes), Codes),
1476 btwoc(Int, Bytes).
1477base64_btwoc(Int, Base64) :-
1478 phrase(base64(Bytes), Base64),
1479 btwoc(Int, Bytes).
1480
1481
1487
1488btwoc(Int, Bytes) :-
1489 integer(Int),
1490 !,
1491 int_to_bytes(Int, Bytes).
1492btwoc(Int, Bytes) :-
1493 is_list(Bytes),
1494 bytes_to_int(Bytes, Int).
1495
1496int_to_bytes(Int, Bytes) :-
1497 int_to_bytes(Int, [], Bytes).
1498
1499int_to_bytes(Int, Bytes0, [Int|Bytes0]) :-
1500 Int < 128,
1501 !.
1502int_to_bytes(Int, Bytes0, Bytes) :-
1503 Last is Int /\ 0xff,
1504 Int1 is Int >> 8,
1505 int_to_bytes(Int1, [Last|Bytes0], Bytes).
1506
1507
1508bytes_to_int([B|T], Int) :-
1509 bytes_to_int(T, B, Int).
1510
1511bytes_to_int([], Int, Int).
1512bytes_to_int([B|T], Int0, Int) :-
1513 Int1 is (Int0<<8)+B,
1514 bytes_to_int(T, Int1, Int).
1515
1516
1523
1524xor_codes([], [], []) :- !.
1525xor_codes([H1|T1], [H2|T2], [H|T]) :-
1526 !,
1527 H is H1 xor H2,
1528 !,
1529 xor_codes(T1, T2, T).
1530xor_codes(L1, L2, _) :-
1531 throw(error(length_mismatch(L1, L2), _)).
1532
1533
1534 1537
1538openid_attribute('openid.mode',
1539 [ oneof([ associate,
1540 checkid_setup,
1541 cancel,
1542 id_res
1543 ])
1544 ]).
1545openid_attribute('openid.assoc_type',
1546 [ oneof(['HMAC-SHA1'])
1547 ]).
1548openid_attribute('openid.session_type',
1549 [ oneof([ 'DH-SHA1',
1550 'DH-SHA256'
1551 ])
1552 ]).
1553openid_attribute('openid.dh_modulus', [length > 1]).
1554openid_attribute('openid.dh_gen', [length > 1]).
1555openid_attribute('openid.dh_consumer_public', [length > 1]).
1556openid_attribute('openid.assoc_handle', [length > 1]).
1557openid_attribute('openid.return_to', [length > 1]).
1558openid_attribute('openid.trust_root', [length > 1]).
1559openid_attribute('openid.identity', [length > 1]).
1560openid_attribute('openid.password', [length > 1]).
1561openid_attribute('openid.grant', [oneof([yes,no])])