35
36:- module(config_auth_stackoverflow, []). 37:- use_module(swish(lib/oauth2)). 38:- use_module(swish(lib/plugin/login)). 39:- use_module(library(http/http_open)). 40:- use_module(library(http/http_dispatch)). 41:- use_module(library(http/http_header)). 42:- use_module(library(http/http_session)). 43:- use_module(library(http/http_json)). 44:- use_module(library(http/json)). 45:- use_module(library(http/http_path)). 46:- use_module(library(uri)). 47:- use_module(library(debug)). 48:- use_module(library(apply)). 49
68
69:- multifile
70 oauth2:login/3,
71 oauth2:server_attribute/3,
72 swish_config:login_item/2, 73 swish_config:login/2, 74 swish_config:user_info/2. 75
76:- http_set_session_options([create(noauto)]). 77
78:- http_handler(swish(logout), stackexchange_logout, []). 79
95oauth2:server_attribute(stackexchange, url,
96 'https://stackexchange.com').
97oauth2:server_attribute(stackexchange, redirect_uri,
98 'http://cplint.eu/oauth2/stackexchange/reply').
99oauth2:server_attribute(stackexchange, authorization_endpoint,
100 '/oauth').
101oauth2:server_attribute(stackexchange, token_endpoint,
102 '/oauth/access_token').
103oauth2:server_attribute(stackexchange, api_endpoint,
104 'https://api.stackexchange.com').
105oauth2:server_attribute(stackexchange, client_id,
106 '12983').
107oauth2:server_attribute(stackexchange, client_secret,
108 'gTRdP5ub6ktEVrBXmOaOCA((').
109oauth2:server_attribute(stackexchange, key,
110 'eAFMxVCc)KtvEvIZvCfiOA((').
111oauth2:server_attribute(stackexchange, site,
112 'stackoverflow').
113oauth2:server_attribute(stackexchange, scope,
114 '').
115
116
117 120
121swish_config:login_item(stackexchange, 10-Item) :-
122 http_absolute_location(icons('so-icon.png'), Img, []),
123 Item = img([ src(Img),
124 class('login-with'),
125 'data-server'(stackexchange),
126 'data-frame'(popup),
127 title('Login with StackOverflow')
128 ]).
129
130swish_config:login(stackexchange, Request) :-
131 oauth2_login(Request, [server(stackexchange)]).
132
133oauth2:login(_Request, stackexchange, TokenInfo) :-
134 debug(oauth, 'TokenInfo: ~p', [TokenInfo]),
135 stackexchange_me(TokenInfo.access_token, Claim),
136 debug(oauth, 'Claim: ~p', [Claim]),
137 map_user_info(Claim, UserInfo),
138 http_open_session(_SessionID, []),
139 session_remove_user_data,
140 http_session_assert(oauth2(stackexchange, TokenInfo)),
141 http_session_assert(user_info(stackexchange, UserInfo)),
142 reply_logged_in([ identity_provider('StackOverflow'),
143 name(UserInfo.name),
144 user_info(UserInfo)
145 ]).
146
153
154stackexchange_me(AccessToken, Info) :-
155 oauth2:server_attribute(stackexchange, api_endpoint, URLBase),
156 oauth2:server_attribute(stackexchange, client_id, ClientID),
157 oauth2:server_attribute(stackexchange, client_secret, ClientSecret),
158 oauth2:server_attribute(stackexchange, key, Key),
159 oauth2:server_attribute(stackexchange, site, Site),
160
161 uri_extend(URLBase, '/2.2/me',
162 [ key(Key),
163 site(Site),
164 access_token(AccessToken)
165 ],
166 URL),
167
168 setup_call_cleanup(
169 http_open(URL, In,
170 [ authorization(basic(ClientID, ClientSecret)),
171 header(content_type, ContentType),
172 status_code(Code)
173 ]),
174 read_reply(Code, ContentType, In, Info0),
175 close(In)),
176 me_info(Info0, Info).
177
178me_info(Info, Me) :-
179 [Me] = Info.get(items),
180 !.
181me_info(Info, Info).
182
183
184read_reply(Code, ContentType, In, Dict) :-
185 debug(oauth, '/me returned ~p ~p', [Code, ContentType]),
186 http_parse_header_value(content_type, ContentType, Parsed),
187 read_reply2(Code, Parsed, In, Dict).
188
194
195read_reply2(200, media(application/json, _Attributes), In, Dict) :- !,
196 json_read_dict(In, Dict).
197read_reply2(Code, media(application/json, _Attributes), In,
198 error{code:Code, details:Details}) :- !,
199 json_read_dict(In, Details).
200read_reply2(Code, Type, In,
201 error{code:Code, message:Reply}) :-
202 debug(oauth(token), 'Got code ~w, type ~q', [Code, Type]),
203 read_string(In, _, Reply).
204
205
209
210stackexchange_logout(_Request) :-
211 catch(session_remove_user_data, _, true),
212 reply_logged_out([]).
213
217
218swish_config:user_info(_Request, stackexchange, UserInfo) :-
219 http_in_session(_SessionID),
220 http_session_data(user_info(stackexchange, UserInfo)).
221
225
226map_user_info(Dict0, Dict) :-
227 dict_pairs(Dict0, Tag, Pairs0),
228 maplist(map_user_field, Pairs0, Pairs),
229 http_link_to_id(stackexchange_logout, [], LogoutURL),
230 dict_pairs(Dict, Tag,
231 [ identity_provider-stackexchange,
232 auth_method-oauth2,
233 logout_url-LogoutURL
234 | Pairs
235 ]).
236
237map_user_field(display_name-Name, name-Name) :- !.
238map_user_field(profile_image-URL, picture-URL) :- !.
239map_user_field(link-URL, profile_url-URL) :- !.
240map_user_field(user_id-Id, external_identity-SId) :- !,
241 format(string(SId), '~w', [Id]).
242map_user_field(Field, Field).
243
244session_remove_user_data :-
245 http_session_retractall(oauth2(_,_)),
246 http_session_retractall(user_info(_,_)).
247
248
249 252
256
257uri_extend(Base, Relative, Query, URI) :-
258 uri_resolve(Relative, Base, URI0),
259 uri_extend_query(URI0, Query, URI).
260
265
266uri_extend_query(URI0, Query, URI) :-
267 uri_components(URI0, Components0),
268 extend_query(Components0, Query, Query1),
269 uri_data(search, Components0, Query1, Components1),
270 uri_components(URI, Components1).
271
272extend_query(Components, QueryEx, Query) :-
273 uri_data(search, Components, Query0),
274 ( var(Query0)
275 -> uri_query_components(Query, QueryEx)
276 ; uri_query_components(Query0, Q0),
277 merge_components(Q0, QueryEx, Q),
278 uri_query_components(Query, Q)
279 ).
280
281merge_components([], Q, Q).
282merge_components([N=_|T0], Q1, Q) :-
283 memberchk(N=_, Q1), !,
284 merge_components(T0, Q1, Q).
285merge_components([H|T0], Q1, [H|Q]) :-
286 merge_components(T0, Q1, Q)