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