lib & contrib: update imports
[nit.git] / contrib / shibuqam / examples / shibuqamoauth.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Server that implements an OAuth2-like authentication bound to a `shibuqam` valid server.
16 #
17 # The protocol is based on [OAuth2](https://tools.ietf.org/html/rfc6749),
18 # especially the [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1).
19 #
20 # # Use as a web service
21 #
22 # * User: the human user and its user-agent (browser) that use the client website.
23 # * Client: the 3rd party web site that need authentication. It owns `https://client.example.com`
24 # * Server: the public OAuth server. It owns `https://server.example.com`
25 #
26 # From the user & client point of view, the process is the following:
27 #
28 # 1. The client redirects the user to the server.
29 # 2. The user authenticates on the server.
30 # 3. The server redirects the user to the client with a token.
31 # 4. The client sends the token to the server.
32 # 5. The server responds with the user information.
33 #
34 # ## 1. User Request
35 #
36 # Two GET fields
37 #
38 # * `redirect_uri`: A full URI that will be used to redirect the user once the authentication is done.
39 # * `state`: A temporary string used to check that the callback is legitimate.
40 #
41 # On request, the service authenticate the user then redirect to the callback with the user informations.
42 #
43 # Example:
44 #
45 # ~~~raw
46 # GET https://server.example.com/login?redirect_uri=https://client.example.com/callback&state=secret
47 # ~~~
48 #
49 # ## 3. Response
50 #
51 # After the authentication, the response is a 303 redirection to `redirect_uri`
52 # with the following GET arguments:
53 #
54 # * `code:` a temporary token to send-back to the server
55 # * `state`: the same state to check that the response is not forged.
56 #
57 # Example:
58 #
59 # ~~~raw
60 # HTTP/1.1 303 See Other
61 # Location: https://client.example.com/callback?code=something&state=secret
62 # ~~~
63 #
64 # ## 3b. Errors
65 #
66 # If the request is badly formed or if the `redirect_uri` is not authorized,
67 # then there is no redirection and a text error message is send to the user.
68 #
69 # If there is a problem during the authentication, then there is a redirection
70 # but the GET fields are only `state` and `error=access_denied`.
71 #
72 # ## 4. Client request
73 #
74 # A single POST field
75 #
76 # * `code`: The one you get as the user response.
77 #
78 # Example:
79 #
80 # ~~~raw
81 # POST https://server.example.com/info
82 # code=something
83 # ~~~
84 #
85 # ## 5. Response
86 #
87 # The response is a JSON object that is the plain serialization of a `User` instance.
88 #
89 # ~~~json
90 # HTTP/1.1 200 OK
91 # Content-Type: application/json;charset=UTF-8
92 #
93 # {
94 # "id": "jdoe",
95 # "given_name": "John",
96 # "display_name": "John Doe",
97 # "email": "doe.john@example.com",
98 # "avatar": "https://www.gravatar.com/avatar/4fe50a575e1c28773800a0aa03c62dbe?d=retro"
99 # }
100 # ~~~
101 #
102 # # To configure and execute
103 #
104 # ## Run the server
105 #
106 # shibuqamoauth authorized_list [host] [port]
107 #
108 # `authorized_list` is a textfile that registers the list of accepted `redirect_uri`.
109 #
110 # Example:
111 #
112 # ~~~raw
113 # https://example.com/foo
114 # https://other.example.com/
115 # ~~~
116 #
117 # To be authorized, a `redirect_uri` must have one of the line of `authorized_list` as a prefix.
118 # For instance, the previous `authorized_list` accepts `https://example.com/foo` and `https://example.com/foo/bar`
119 # but not `https://example.com/bar` nor `https://sub.example.com/foo`.
120 #
121 #
122 # ## Install as a server
123 #
124 # A correct `shibuqam` reverse proxy must be configured (see `shibuqam` for details).
125 # In a full scenario, the server is replaced by:
126 #
127 # * Proxy: the public reverse proxy that can do the genuine Shibboleth authentication.
128 # It owns `https://server.example.com`
129 # * Service: the private shibuqamoauth service. It owns a NATed `https://shibuqamoauth.example.com/`
130 #
131 # On the first access to the server (user request):
132 #
133 # 1. Proxy gets the request
134 # 2. Proxy does the Shibboleth authentication.
135 # 3. Proxy enhances the request header.
136 # 4. Proxy forwards the request to service.
137 # 5. Service checks that `redir` is authorized.
138 # 6. Service digs in the enhanced request header to generate a token and associate it to the user.
139 # 7. Redirect the user to client with the callback and the token.
140 #
141 # On the second access to the server (client request):
142 #
143 # 1. Proxy gets the request
144 # 2. Proxy forwards the request to service without authentification (it is not a user).
145 # 3. Service checks that the token existe and is not expired.
146 # 4. Service serialize and send the user as in the
147 #
148 # The Proxy should only do the authentication on the user request.
149 # The simplest way is to configure two routes that reverse proxy on the same server.
150 #
151 # # Why not using real OAuth2?
152 #
153 # OAuth2 is centered about *access_tokens* that allow clients to performs
154 # a (possibly scoped) set of actions/queries on the behalf of an authenticated user.
155 #
156 # In our scenario, there is no action/queries to do once an user is authenticated.
157 # So we do not delivers *access_tokens* since there is nothing to access once the user is known.
158 #
159 # We need only the third-party authentication part of the protocol.
160 #
161 # Since we do not have the same goals than the RFC, we also have a simplified protocol.
162 # Noways, OAuth is mainly a framework and the implementations are very diverse and unfortunately no
163 # not interoperable anyway.
164 #
165 # Here is the specific changes we have:
166 #
167 # * no `response_type`: there is a single kind of grant and the two steps are identified by the http method (GET or POST) and the configured routes on the server.
168 # * no `client_id`: because we do not want to code a db of clients. The `redirect_uri` can be seen as a simple client_id. We have also no way to present the client to the user since we do not control the authorization page of shibboleth since is done by the reverse proxy.
169 # * no `scope` and no `authorisation_code` since since there is nothing to access. We just get minimal information after the successful shibboleth login. Thus we have nothing more to authorise once the user is authenticated.
170 # * no `client_secret`: the user information are already public. There is no need to make the code more complex to protect public information.
171 module shibuqamoauth
172
173 import popcorn
174 import shibuqam
175 import json
176
177 redef class HttpRequest
178 # percent decoded get or post parameter.
179 fun dec_param(name: String): nullable String
180 do
181 var res = get_args.get_or_null(name)
182 if res == null then res = post_args.get_or_null(name)
183 if res == null then return null
184 return res.from_percent_encoding
185 end
186 end
187
188 class AuthHandler
189 super Handler
190
191 # List of prefixes of authorized redirections.
192 var authorized: Array[String]
193
194 # Check is `redir` is an authorized redirection (client)
195 fun is_authorized(redir: String): Bool
196 do
197 for r in authorized do if redir.has_prefix(r) then return true
198 return false
199 end
200
201
202 # Associate each `code` issued to the user, to the info intended to the client.
203 var infos = new HashMap[String, Info]
204
205 # Remove expired keys
206 fun expiration
207 do
208 var now = get_time
209 for k, i in infos do
210 if i.expiration < now then
211 infos.keys.remove(k)
212 print "{i.user.id} -> expired"
213 end
214 end
215 end
216
217 # Generate a new usable token
218 #
219 # Not thread safe!
220 fun new_token: String
221 do
222 var token
223 loop
224 token = generate_token
225 if not infos.has_key(token) then break
226 end
227 return token
228 end
229
230 redef fun get(http_request, response)
231 do
232 # GET means a Authorization Request from the user-agent
233
234 # Get the `redirect_uri` parameter, we use it to identify the client
235 var redir = http_request.dec_param("redir")
236 if redir == null then redir = http_request.dec_param("redirect_uri")
237 if redir == null then
238 response.send("No redirect_uri.", 400)
239 return
240 end
241
242 # Check if the client is authorized
243 if not is_authorized(redir) then
244 response.send("Site not authorized.", 403)
245 return
246 end
247
248 # Get the state, we use it to avoid CSRF attacks
249 var state = http_request.dec_param("state")
250 var res = redir + "?"
251
252 # If we are here, the reverse proxy did the authentication
253 # Is there an user?
254 var user = http_request.user
255 if user == null then
256 print "no user -> {redir}"
257 res += "error=access_denied"
258 else
259 # The user is authenticated.
260 print "{user.id} -> {redir}"
261
262 # We prepare a token (code) that the client will use to get the information.
263 expiration
264 var token = new_token
265
266 res += "code={token.to_percent_encoding}"
267
268 var ttl = 10*60*60 # 10 minutes
269 var info = new Info(get_time + ttl, user)
270 infos[token] = info
271 end
272 if state != null then res += "&state={state.to_percent_encoding}"
273 response.redirect res
274 end
275
276 redef fun post(http_request, response)
277 do
278 # POST means an Access Token Request from the client.
279 # Unfortunately, we have no access to grant, only informations.
280 print http_request.post_args.join(" ; ", ": ")
281
282 expiration
283
284 var code = http_request.dec_param("code")
285 if code == null then
286 print "POST: no code"
287 return
288 end
289 var info = infos.get_or_null(code)
290 if info == null then
291 print "POST: bad code {code}"
292 return
293 end
294
295 print "{info.user.id} -> retrieved"
296
297 # Drop the code as it is a single use
298 infos.keys.remove(code)
299
300 # Send the requested information.
301 response.json(info.user)
302 end
303 end
304
305 redef class User
306 super Jsonable
307 end
308
309 # Information about an authenticated used stored on the server to be given to the client.
310 class Info
311 # Time to live
312 var expiration: Int
313
314 # The identified user
315 var user: User
316 end
317
318 var host = "localhost"
319 var port = 3000
320
321 if args.is_empty then
322 print "usage: shibuqamoauth authorized_list [host] [port]"
323 return
324 end
325
326 var list = args[0]
327 if args.length > 1 then host = args[1]
328 if args.length > 2 then port = args[2].to_i
329
330 var authorized = list.to_path.read_lines
331 if authorized.is_empty then
332 print_error "{list}: not found or empty"
333 exit 1
334 end
335
336 var app = new App
337 app.use("/*", new AuthHandler(authorized))
338 app.listen(host, port)