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