326f0bf7981bd3182e0a48d2816a583ca0ef1b0f
[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 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)