2f84be74d45eea2f0c0f5d323fa7b23ce8e5ce50
[nit.git] / lib / popcorn / pop_auth.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Authentification handlers.
18 #
19 # For now, only Github OAuth is provided.
20 #
21 # See https://developer.github.com/v3/oauth/.
22 #
23 # This module provide 4 base classes that can be used together to implement a
24 # Github OAuth handshake.
25 #
26 # Here an example of application using the Github Auth as login mechanism.
27 #
28 # There is 4 available routes:
29 # * `/login`: redirects the user to the Github OAuth login page (see `GithubLogin`)
30 # * `/profile`: shows the currently logged in user (see `Profile Handler`)
31 # * `/logout`: logs out the user by destroying the entry from the session (see `GithubLogout`)
32 # * `/oauth`: callback url for Github service after player login (see `GithubOAuthCallBack`)
33 #
34 # Routes redirection are handled at the OAuth service registration. Please see
35 # https://developer.github.com/v3/oauth/#redirect-urls for more niformation on how
36 # to configure your service to provide smouth redirections beween your routes.
37 #
38 # ~~~
39 # import popcorn
40 # import popcorn::pop_auth
41 #
42 # class ProfileHandler
43 # super Handler
44 #
45 # redef fun get(req, res) do
46 # var session = req.session
47 # if session == null then
48 # res.send "No session :("
49 # return
50 # end
51 # var user = session.user
52 # if user == null then
53 # res.send "Not logged in"
54 # return
55 # end
56 # res.send "<h1>Hello {user.login}</h1>"
57 # end
58 # end
59 #
60 # var client_id = "github client id"
61 # var client_secret = "github client secret"
62 #
63 # var app = new App
64 # app.use("/*", new SessionInit)
65 # app.use("/login", new GithubLogin(client_id))
66 # app.use("/oauth", new GithubOAuthCallBack(client_id, client_secret))
67 # app.use("/logout", new GithubLogout)
68 # app.use("/profile", new ProfileHandler)
69 # app.listen("localhost", 3000)
70 # ~~~
71 #
72 # Optionaly, you can use the `GithubUser` handler to provide access to the
73 # Github user stored in session:
74 #
75 # ~~~
76 # app.use("/api/user", new GithubUser)
77 # ~~~
78 module pop_auth
79
80 import pop_json
81 import pop_sessions
82 import github
83
84 # Github OAuth login handler.
85 #
86 # See https://developer.github.com/v3/oauth/.
87 class GithubLogin
88 super Handler
89
90 # Client ID delivered by GitHub for your application.
91 #
92 # See https://github.com/settings/applications/new.
93 var client_id: String is writable
94
95 # The URL in your application where users will be sent after authorization.
96 #
97 # If `null`, the URL used in application registration will be used.
98 #
99 # See https://developer.github.com/v3/oauth/#redirect-urls.
100 var redirect_uri: nullable String = null is writable
101
102 # A space delimited list of scopes.
103 #
104 # See https://developer.github.com/v3/oauth/#scopes.
105 var scope: nullable String = null is writable
106
107 # An optional and unguessable random string.
108 #
109 # It is used to protect against cross-site request forgery attacks.
110 var state: nullable String = null is writable
111
112 # Allow signup at login.
113 #
114 # Whether or not unauthenticated users will be offered an option to sign up
115 # for GitHub during the OAuth flow. The default is true.
116 #
117 # Use false in the case that a policy prohibits signups.
118 var allow_signup = true is writable
119
120 # Github OAuth login URL.
121 var auth_url = "https://github.com/login/oauth/authorize" is writable
122
123 # Build Github URL to OAuth service.
124 fun build_auth_redirect: String do
125 var url = "{auth_url}?client_id={client_id}&allow_signup={allow_signup}"
126 var redirect_uri = self.redirect_uri
127 if redirect_uri != null then url = "{url}&redirect_uri={redirect_uri}"
128 var scope = self.scope
129 if scope != null then url = "{url}&scope={scope}"
130 var state = self.state
131 if state != null then url = "{url}&state={state}"
132 return url
133 end
134
135 redef fun get(req, res) do res.redirect build_auth_redirect
136 end
137
138 # Get the authentification code and translate it to an access token.
139 class GithubOAuthCallBack
140 super Handler
141
142 # The client ID delivered by GitHub for your application.
143 #
144 # See https://github.com/settings/applications/new.
145 var client_id: String is writable
146
147 # The client secret you received from Github when your registered your application.
148 var client_secret: String is writable
149
150 # The URL in your application where users will be sent after authorization.
151 #
152 # If `null`, the URL used in application registration will be used.
153 #
154 # See https://developer.github.com/v3/oauth/#redirect-urls.
155 var redirect_uri: nullable String is writable
156
157 # An optional and unguessable random string.
158 #
159 # It is used to protect against cross-site request forgery attacks.
160 var state: nullable String is writable
161
162 # Github OAuth token URL.
163 var token_url = "https://github.com/login/oauth/access_token" is writable
164
165 # Header map sent with the OAuth token request.
166 var headers: HeaderMap do
167 var map = new HeaderMap
168 map["Accept"] = "application/json"
169 return map
170 end
171
172 # Build the OAuth post data.
173 fun build_auth_body(code: String): HeaderMap do
174 var map = new HeaderMap
175 map["client_id"] = client_id
176 map["client_secret"] = client_secret
177 map["code"] = code
178 var redirect_uri = self.redirect_uri
179 if redirect_uri != null then map["redirect_uri"] = redirect_uri
180 var state = self.state
181 if state != null then map["state"] = state
182 return map
183 end
184
185 redef fun get(req, res) do
186 # Get OAuth code
187 var code = req.string_arg("code")
188 if code == null then
189 res.error 401
190 return
191 end
192
193 # Exchange it for an access token
194 var access_token = request_access_token(code)
195 if access_token == null then
196 res.error 401
197 return
198 end
199
200 # FIXME reinit curl before next request to avoid weird 404
201 curl = new Curl
202
203 # Load github user
204 var gh_api = new GithubAPI(access_token)
205 var user = gh_api.load_auth_user
206 if user == null then
207 res.error 401
208 return
209 end
210 # Set session and redirect to user page
211 var session = req.session
212 if session == null then
213 res.error 500
214 return
215 end
216 session.user = user
217 res.redirect redirect_uri or else "/"
218 end
219
220 # Request an access token from an access `code`.
221 private fun request_access_token(code: String): nullable String do
222 var request = new CurlHTTPRequest(token_url)
223 request.headers = headers
224 request.data = build_auth_body(code)
225 var response = request.execute
226 return parse_token_response(response)
227 end
228
229 # Parse the Github access_token response and extract the access_token.
230 private fun parse_token_response(response: CurlResponse): nullable String do
231 if response isa CurlResponseFailed then
232 print "Request to Github OAuth failed"
233 print "Requested URI: {token_url}"
234 print "Error code: {response.error_code}"
235 print "Error msg: {response.error_msg}"
236 return null
237 else if response isa CurlResponseSuccess then
238 var obj = response.body_str.parse_json
239 if not obj isa JsonObject then
240 print "Error: Cannot parse json response"
241 print response.body_str
242 return null
243 end
244 var access_token = obj.get_or_null("access_token")
245 if not access_token isa String then
246 print "Error: No `access_token` key in response"
247 print obj.to_json
248 return null
249 end
250 return access_token
251 end
252 return null
253 end
254 end
255
256 # Destroy user session and redirect to homepage.
257 class GithubLogout
258 super Handler
259
260 # The URL in your application where users will be sent after logout.
261 #
262 # If `null`, the root uri `/` will be used.
263 var redirect_uri: nullable String is writable
264
265 redef fun get(req, res) do
266 var session = req.session
267 if session != null then
268 session.user = null
269 end
270 res.redirect redirect_uri or else "/"
271 end
272 end
273
274 # AuthHandler allows access to session user
275 #
276 # Inherit this handler to access to session user from your custom handler.
277 #
278 # For example, you need a profile handler that checks if the user is logged
279 # before returning it in json format.
280 # ~~~
281 # import popcorn::pop_auth
282 #
283 # class ProfileHandler
284 # super AuthHandler
285 #
286 # redef fun get(req, res) do
287 # var user = check_session_user(req, res)
288 # if user == null then return
289 # res.json user
290 # end
291 # end
292 # ~~~
293 #
294 # By using `check_session_user`, we delegate to the `AuthHandler` the responsability
295 # to set the HTTP 403 error.
296 # We then check is the user is not null before pursuing.
297 abstract class AuthHandler
298 super Handler
299
300 # Returns `user` from `req.session` or null if no user is authenticated.
301 fun session_user(req: HttpRequest): nullable User do
302 var session = req.session
303 if session == null then return null
304 var user = session.user
305 return user
306 end
307
308 # Check the session for user and return it.
309 #
310 # If no `user` can be found in session, set res as a HTTP 403 error and return `null`.
311 fun check_session_user(req: HttpRequest, res: HttpResponse): nullable User do
312 var user = session_user(req)
313 if user == null then
314 res.error 403
315 end
316 return user
317 end
318 end
319
320 # Get the currently logged in user from session.
321 class GithubUser
322 super AuthHandler
323
324 redef fun get(req, res) do
325 var user = check_session_user(req, res)
326 if user == null then return
327 res.json user
328 end
329 end
330
331 redef class Session
332
333 # Github user if logged in.
334 var user: nullable User = null is writable
335 end