examples: annotate examples
[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 is example
172
173 import popcorn
174 import popcorn::pop_json
175 import shibuqam
176 import json
177
178 class AuthHandler
179 super Handler
180
181 # List of prefixes of authorized redirections.
182 var authorized: Array[String]
183
184 # Check is `redir` is an authorized redirection (client)
185 fun is_authorized(redir: String): Bool
186 do
187 for r in authorized do if redir.has_prefix(r) then return true
188 return false
189 end
190
191
192 # Associate each `code` issued to the user, to the info intended to the client.
193 var infos = new HashMap[String, Info]
194
195 # Remove expired keys
196 fun expiration
197 do
198 var now = get_time
199 for k, i in infos do
200 if i.expiration < now then
201 infos.keys.remove(k)
202 print "{i.user.id} -> expired"
203 end
204 end
205 end
206
207 # Generate a new usable token
208 #
209 # Not thread safe!
210 fun new_token: String
211 do
212 var token
213 loop
214 token = generate_token
215 if not infos.has_key(token) then break
216 end
217 return token
218 end
219
220 redef fun get(http_request, response)
221 do
222 # GET means a Authorization Request from the user-agent
223
224 # Get the `redirect_uri` parameter, we use it to identify the client
225 var redir = http_request.string_arg("redir")
226 if redir == null then redir = http_request.string_arg("redirect_uri")
227 if redir == null then
228 response.send("No redirect_uri.", 400)
229 return
230 end
231
232 # Check if the client is authorized
233 if not is_authorized(redir) then
234 response.send("Site not authorized.", 403)
235 return
236 end
237
238 # Get the state, we use it to avoid CSRF attacks
239 var state = http_request.string_arg("state")
240 var res = redir + "?"
241
242 # If we are here, the reverse proxy did the authentication
243 # Is there an user?
244 var user = http_request.user
245 if user == null then
246 print "no user -> {redir}"
247 res += "error=access_denied"
248 else
249 # The user is authenticated.
250 print "{user.id} -> {redir}"
251
252 # We prepare a token (code) that the client will use to get the information.
253 expiration
254 var token = new_token
255
256 res += "code={token.to_percent_encoding}"
257
258 var ttl = 10*60*60 # 10 minutes
259 var info = new Info(get_time + ttl, user)
260 infos[token] = info
261 end
262 if state != null then res += "&state={state.to_percent_encoding}"
263 response.redirect res
264 end
265
266 redef fun post(http_request, response)
267 do
268 # POST means an Access Token Request from the client.
269 # Unfortunately, we have no access to grant, only informations.
270 print http_request.post_args.join(" ; ", ": ")
271
272 expiration
273
274 var code = http_request.string_arg("code")
275 if code == null then
276 print "POST: no code"
277 return
278 end
279 var info = infos.get_or_null(code)
280 if info == null then
281 print "POST: bad code {code}"
282 return
283 end
284
285 print "{info.user.id} -> retrieved"
286
287 # Drop the code as it is a single use
288 infos.keys.remove(code)
289
290 # Send the requested information.
291 response.json(info.user)
292 end
293 end
294
295 redef class User
296 super Serializable
297 end
298
299 # Information about an authenticated used stored on the server to be given to the client.
300 class Info
301 # Time to live
302 var expiration: Int
303
304 # The identified user
305 var user: User
306 end
307
308 var host = "localhost"
309 var port = 3000
310
311 if args.is_empty then
312 print "usage: shibuqamoauth authorized_list [host] [port]"
313 return
314 end
315
316 var list = args[0]
317 if args.length > 1 then host = args[1]
318 if args.length > 2 then port = args[2].to_i
319
320 var authorized = list.to_path.read_lines
321 if authorized.is_empty then
322 print_error "{list}: not found or empty"
323 exit 1
324 end
325
326 var app = new App
327 app.use("/*", new AuthHandler(authorized))
328 app.listen(host, port)