/
openid_controller.rb
341 lines (288 loc) · 9.72 KB
/
openid_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
require 'pathname'
require 'openid'
require 'openid/consumer/discovery'
require 'openid/extensions/sreg'
require 'openid/extensions/pape'
require 'openid/store/filesystem'
class OpenidController < ApplicationController
# protect_from_forgery :except => [:index]
include OpenidHelper
include OpenID::Server
layout nil
def index
begin
permitted_params = params.permit(
'authenticity_token', 'back_to',
'commit',
'open_id', 'openid.assoc_handle',
'openid.op_endpoint',
'openid.response_nonce',
'openid.sig', 'openid.signed',
'openid.sreg.email',
'openid.sreg.fullname', # fullname contains both status and role
'openid.sreg.nickname',
'return_to', 'openid.claimed_id',
'openid.identity', 'openid.mode',
'openid.ns', 'openid.ns.sreg',
'openid.realm', 'openid.return_to',
'openid.sreg.required',
'openid.trust_root',
'openid.id_select',
'openid.immediate',
'openid.cancel_url'
).to_h
oidreq = if params['openid.mode']
server.decode_request(permitted_params)
else
server.decode_request(Rack::Utils.parse_query(request.env['ORIGINAL_FULLPATH'].split('?')[1]))
end
rescue ProtocolError => e
# invalid openid request, so just display a page with an error message
render plain: e.to_s, status: 500
return
end
# no openid.mode was given
unless oidreq
render plain: 'This is an OpenID server endpoint.'
return
end
requested_credentials = ''
requested_username = ''
provider = nil
if request.env['ORIGINAL_FULLPATH']&.split('?')[1]
request.env['ORIGINAL_FULLPATH'].split('?')[1].split('&').each do |param|
requested_credentials = param.split('=')[1].split('%2F') if param.split('=')[0] == 'openid.claimed_id'
end
end
# ORIGINAL_FULLPATH will be like https://publiclab.org/openid/:username(/:provider)
# so we need to get the username for sure and the provider if it exists
# requested_credentials contains array of the ORIGINAL_FULLPATH striped with '/' symbol
if requested_credentials && requested_credentials[-3] == 'openid'
requested_username = requested_credentials[-2]
provider = requested_credentials[-1]
else
provider = nil
requested_username = requested_credentials[-1]
end
if current_user.nil? && params['openid.mode'] != 'check_authentication'
session[:openid_return_to] = request.env['ORIGINAL_FULLPATH']
flash[:warning] = 'Please log in first.'
if provider
# authentication through the provider
redirect_to '/auth/' + provider
else
# form based authentication
redirect_to '/login'
end
return
else
if oidreq
oidresp = nil
if oidreq.is_a?(CheckIDRequest)
identity = oidreq.identity
if oidreq.id_select
if oidreq.immediate
oidresp = oidreq.answer(false)
elsif session[:username]
# The user hasn't logged in.
# show_decision_page(oidreq) # this doesnt make sense... it was in the example though
session[:openid_return_to] = request.env['ORIGINAL_FULLPATH']
if provider
# provider based authentication
redirect_to '/auth/' + provider
else
# form based authentication
redirect_to '/login'
end
else
# Else, set the identity to the one the user is using.
identity = url_for_user
end
end
if oidresp
nil
elsif is_authorized(identity, oidreq.trust_root)
oidresp = oidreq.answer(true, nil, identity)
# add the sreg response if requested
add_sreg(oidreq, oidresp)
# ditto pape
add_pape(oidreq, oidresp)
elsif oidreq.immediate
server_url = url_for action: 'index'
oidresp = oidreq.answer(false, server_url)
else
session[:last_oidreq] = oidreq
@oidreq = oidreq
redirect_to action: 'decision'
return
end
else
oidresp = server.handle_request(oidreq)
end
render_response(oidresp)
else
session[:openid_return_to] = request.env['ORIGINAL_FULLPATH']
if provider
# provider based authentication
redirect_to '/auth/' + provider
else
# form based authentication
redirect_to '/login'
end
end
end
end
def resume
if session[:openid_return_to] # for openid login, redirects back to openid auth process
return_to = session[:openid_return_to]
session[:openid_return_to] = nil
session[:openid_requester] = nil
redirect_to return_to
end
end
def show_decision_page(oidreq, message = '')
session[:last_oidreq] = oidreq
@oidreq = oidreq
flash.now[:notice] = message if message
render template: 'openid/decide'
end
def user_page
# Yadis content-negotiation: we want to return the xrds if asked for.
accept = request.env['HTTP_ACCEPT']
# This is not technically correct, and should eventually be updated
# to do real Accept header parsing and logic. Though I expect it will work
# 99% of the time.
if accept&.include?('application/xrds+xml')
user_xrds
return
end
# content negotiation failed, so just render the user page
xrds_url = url_for(controller: 'user', action: params[:username]) + '/xrds'
identity_page = <<~HTML
<html><head>
<meta http-equiv="X-XRDS-Location" content="#{xrds_url}" />
<link rel="openid.server" href="#{url_for action: 'index'}" />
</head><body><p>OpenID identity page for #{params[:username]}</p>
</body></html>
HTML
# Also add the Yadis location header, so that they don't have
# to parse the html unless absolutely necessary.
response.headers['X-XRDS-Location'] = xrds_url
render plain: identity_page
end
def user_xrds
types = [
OpenID::OPENID_2_0_TYPE,
OpenID::OPENID_1_0_TYPE,
OpenID::SREG_URI
]
render_xrds(types)
end
def idp_xrds
types = [
OpenID::OPENID_IDP_2_0_TYPE
]
render_xrds(types)
end
def decision
oidreq = session[:last_oidreq]
session[:last_oidreq] = nil
id_to_send = params[:id_to_send]
identity = oidreq&.identity
if oidreq.id_select
if id_to_send && (id_to_send != '')
session[:username] = id_to_send
session[:approvals] = []
identity = url_for_user
else
msg = 'You must enter a username to in order to send ' \
'an identifier to the Relying Party.'
show_decision_page(oidreq, msg)
return
end
else
session[:username] = current_user.username
end
if session[:approvals]
session[:approvals] << oidreq.trust_root
else
session[:approvals] = [oidreq.trust_root]
end
oidresp = oidreq.answer(true, nil, identity)
add_sreg(oidreq, oidresp)
add_pape(oidreq, oidresp)
return render_response(oidresp)
end
protected
def server
if @server.nil?
server_url = url_for action: 'index', only_path: false
dir = Pathname.new(request.host).join('db').join('openid-store')
store = OpenID::Store::Filesystem.new(dir)
@server = Server.new(store, server_url)
end
@server
end
def approved(trust_root)
return false if session[:approvals].nil?
session[:approvals].member?(trust_root)
end
def is_authorized(identity_url, trust_root)
(session[:username] && (identity_url == url_for_user) && approved(trust_root))
end
def render_xrds(types)
type_str = ''
types.each do |uri|
type_str += "<Type>#{uri}</Type>\n "
end
yadis = <<~XML
<?xml version="1.0" encoding="UTF-8"?>
<xrds:XRDS
xmlns:xrds="xri://$xrds"
xmlns="xri://$xrd*($v*2.0)">
<XRD>
<Service priority="0">
#{type_str}
<URI>#{url_for(controller: 'openid', only_path: false)}</URI>
</Service>
</XRD>
</xrds:XRDS>
XML
response.headers['content-type'] = 'application/xrds+xml'
render plain: yadis
end
def add_sreg(oidreq, oidresp)
# check for Simple Registration arguments and respond
sregreq = OpenID::SReg::Request.from_openid_request(oidreq)
return if sregreq.nil?
# In a real application, this data would be user-specific,
# and the user should be asked for permission to release
# it.
sreg_data = {
'nickname' => current_user.username, # session[:username],
'email' => current_user.email,
'fullname' => "status=" + current_user.status.to_s + ":role=" + current_user.role # fullname contains both status and role
}
sregresp = OpenID::SReg::Response.extract_response(sregreq, sreg_data)
oidresp.add_extension(sregresp)
end
def add_pape(oidreq, oidresp)
papereq = OpenID::PAPE::Request.from_openid_request(oidreq)
return if papereq.nil?
paperesp = OpenID::PAPE::Response.new
paperesp.nist_auth_level = 0 # we don't even do auth at all!
oidresp.add_extension(paperesp)
end
def render_response(oidresp)
server.signatory.sign(oidresp) if oidresp.needs_signing
web_response = server.encode_response(oidresp)
case web_response.code
when HTTP_OK
render plain: web_response.body, status: 200
when HTTP_REDIRECT
redirect_to web_response.headers['location']
else
render plain: web_response.body, status: 400
end
end
end