1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Authentification handlers.
19 # For now, only Github OAuth is provided.
21 # See https://developer.github.com/v3/oauth/.
23 # This module provide 4 base classes that can be used together to implement a
24 # Github OAuth handshake.
26 # Here an example of application using the Github Auth as login mechanism.
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`)
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.
40 # import popcorn::pop_auth
42 # class ProfileHandler
45 # redef fun get(req, res) do
46 # var session = req.session
47 # if session == null then
48 # res.send "No session :("
51 # var user = session.user
52 # if user == null then
53 # res.send "Not logged in"
56 # res.send "<h1>Hello {user.login}</h1>"
60 # var client_id = "github client id"
61 # var client_secret = "github client secret"
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)
72 # Optionaly, you can use the `GithubUser` handler to provide access to the
73 # Github user stored in session:
76 # app.use("/api/user", new GithubUser)
83 # Github OAuth login handler.
85 # See https://developer.github.com/v3/oauth/.
89 # Client ID delivered by GitHub for your application.
91 # See https://github.com/settings/applications/new.
92 var client_id
: String is writable
94 # The URL in your application where users will be sent after authorization.
96 # If `null`, the URL used in application registration will be used.
98 # See https://developer.github.com/v3/oauth/#redirect-urls.
99 var redirect_uri
: nullable String = null is writable
101 # A space delimited list of scopes.
103 # See https://developer.github.com/v3/oauth/#scopes.
104 var scope
: nullable String = null is writable
106 # An optional and unguessable random string.
108 # It is used to protect against cross-site request forgery attacks.
109 var state
: nullable String = null is writable
111 # Allow signup at login.
113 # Whether or not unauthenticated users will be offered an option to sign up
114 # for GitHub during the OAuth flow. The default is true.
116 # Use false in the case that a policy prohibits signups.
117 var allow_signup
= true is writable
119 # Github OAuth login URL.
120 var auth_url
= "https://github.com/login/oauth/authorize" is writable
122 # Build Github URL to OAuth service.
123 fun build_auth_redirect
: String do
124 var url
= "{auth_url}?client_id={client_id}&allow_signup={allow_signup}"
125 var redirect_uri
= self.redirect_uri
126 if redirect_uri
!= null then url
= "{url}&redirect_uri={redirect_uri}"
127 var scope
= self.scope
128 if scope
!= null then url
= "{url}&scope={scope}"
129 var state
= self.state
130 if state
!= null then url
= "{url}&state={state}"
134 redef fun get
(req
, res
) do res
.redirect build_auth_redirect
137 # Get the authentification code and translate it to an access token.
138 class GithubOAuthCallBack
141 # The client ID delivered by GitHub for your application.
143 # See https://github.com/settings/applications/new.
144 var client_id
: String is writable
146 # The client secret you received from Github when your registered your application.
147 var client_secret
: String is writable
149 # The URL in your application where users will be sent after authorization.
151 # If `null`, the URL used in application registration will be used.
153 # See https://developer.github.com/v3/oauth/#redirect-urls.
154 var redirect_uri
: nullable String is writable
156 # An optional and unguessable random string.
158 # It is used to protect against cross-site request forgery attacks.
159 var state
: nullable String is writable
161 # Github OAuth token URL.
162 var token_url
= "https://github.com/login/oauth/access_token" is writable
164 # Header map sent with the OAuth token request.
165 var headers
: HeaderMap do
166 var map
= new HeaderMap
167 map
["Accept"] = "application/json"
171 # Build the OAuth post data.
172 fun build_auth_body
(code
: String): HeaderMap do
173 var map
= new HeaderMap
174 map
["client_id"] = client_id
175 map
["client_secret"] = client_secret
177 var redirect_uri
= self.redirect_uri
178 if redirect_uri
!= null then map
["redirect_uri"] = redirect_uri
179 var state
= self.state
180 if state
!= null then map
["state"] = state
184 redef fun get
(req
, res
) do
186 var code
= req
.string_arg
("code")
192 # Exchange it for an access token
193 var access_token
= request_access_token
(code
)
194 if access_token
== null then
199 # FIXME reinit curl before next request to avoid weird 404
203 var gh_api
= new GithubAPI(access_token
)
204 var user
= gh_api
.load_auth_user
209 # Set session and redirect to user page
210 var session
= req
.session
211 if session
== null then
216 res
.redirect redirect_uri
or else "/"
219 # Request an access token from an access `code`.
220 private fun request_access_token
(code
: String): nullable String do
221 var request
= new CurlHTTPRequest(token_url
)
222 request
.headers
= headers
223 request
.data
= build_auth_body
(code
)
224 var response
= request
.execute
225 return parse_token_response
(response
)
228 # Parse the Github access_token response and extract the access_token.
229 private fun parse_token_response
(response
: CurlResponse): nullable String do
230 if response
isa CurlResponseFailed then
231 print
"Request to Github OAuth failed"
232 print
"Requested URI: {token_url}"
233 print
"Error code: {response.error_code}"
234 print
"Error msg: {response.error_msg}"
236 else if response
isa CurlResponseSuccess then
237 var obj
= response
.body_str
.parse_json
238 if not obj
isa JsonObject then
239 print
"Error: Cannot parse json response"
240 print response
.body_str
243 var access_token
= obj
.get_or_null
("access_token")
244 if not access_token
isa String then
245 print
"Error: No `access_token` key in response"
255 # Destroy user session and redirect to homepage.
259 # The URL in your application where users will be sent after logout.
261 # If `null`, the root uri `/` will be used.
262 var redirect_uri
: nullable String is writable
264 redef fun get
(req
, res
) do
265 var session
= req
.session
266 if session
!= null then
269 res
.redirect redirect_uri
or else "/"
273 # AuthHandler allows access to session user
275 # Inherit this handler to access to session user from your custom handler.
277 # For example, you need a profile handler that checks if the user is logged
278 # before returning it in json format.
280 # import popcorn::pop_auth
282 # class ProfileHandler
285 # redef fun get(req, res) do
286 # var user = check_session_user(req, res)
287 # if user == null then return
293 # By using `check_session_user`, we delegate to the `AuthHandler` the responsability
294 # to set the HTTP 403 error.
295 # We then check is the user is not null before pursuing.
296 abstract class AuthHandler
299 # Returns `user` from `req.session` or null if no user is authenticated.
300 fun session_user
(req
: HttpRequest): nullable User do
301 var session
= req
.session
302 if session
== null then return null
303 var user
= session
.user
307 # Check the session for user and return it.
309 # If no `user` can be found in session, set res as a HTTP 403 error and return `null`.
310 fun check_session_user
(req
: HttpRequest, res
: HttpResponse): nullable User do
311 var user
= session_user
(req
)
319 # Get the currently logged in user from session.
323 redef fun get
(req
, res
) do
324 var user
= check_session_user
(req
, res
)
325 if user
== null then return
332 # Github user if logged in.
333 var user
: nullable User = null is writable