1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Server that implements an OAuth2-like authentication bound to a `shibuqam` valid server.
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).
20 # # Use as a web service
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`
26 # From the user & client point of view, the process is the following:
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.
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.
41 # On request, the service authenticate the user then redirect to the callback with the user informations.
46 # GET https://server.example.com/login?redirect_uri=https://client.example.com/callback&state=secret
51 # After the authentication, the response is a 303 redirection to `redirect_uri`
52 # with the following GET arguments:
54 # * `code:` a temporary token to send-back to the server
55 # * `state`: the same state to check that the response is not forged.
60 # HTTP/1.1 303 See Other
61 # Location: https://client.example.com/callback?code=something&state=secret
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.
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`.
72 # ## 4. Client request
76 # * `code`: The one you get as the user response.
81 # POST https://server.example.com/info
87 # The response is a JSON object that is the plain serialization of a `User` instance.
91 # Content-Type: application/json;charset=UTF-8
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"
102 # # To configure and execute
106 # shibuqamoauth authorized_list [host] [port]
108 # `authorized_list` is a textfile that registers the list of accepted `redirect_uri`.
113 # https://example.com/foo
114 # https://other.example.com/
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`.
122 # ## Install as a server
124 # A correct `shibuqam` reverse proxy must be configured (see `shibuqam` for details).
125 # In a full scenario, the server is replaced by:
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/`
131 # On the first access to the server (user request):
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.
141 # On the second access to the server (client request):
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
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.
151 # # Why not using real OAuth2?
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.
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.
159 # We need only the third-party authentication part of the protocol.
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.
165 # Here is the specific changes we have:
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.
180 # List of prefixes of authorized redirections.
181 var authorized
: Array[String]
183 # Check is `redir` is an authorized redirection (client)
184 fun is_authorized
(redir
: String): Bool
186 for r
in authorized
do if redir
.has_prefix
(r
) then return true
191 # Associate each `code` issued to the user, to the info intended to the client.
192 var infos
= new HashMap[String, Info]
194 # Remove expired keys
199 if i
.expiration
< now
then
201 print
"{i.user.id} -> expired"
206 # Generate a new usable token
209 fun new_token
: String
213 token
= generate_token
214 if not infos
.has_key
(token
) then break
219 redef fun get
(http_request
, response
)
221 # GET means a Authorization Request from the user-agent
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)
231 # Check if the client is authorized
232 if not is_authorized
(redir
) then
233 response
.send
("Site not authorized.", 403)
237 # Get the state, we use it to avoid CSRF attacks
238 var state
= http_request
.string_arg
("state")
239 var res
= redir
+ "?"
241 # If we are here, the reverse proxy did the authentication
243 var user
= http_request
.user
245 print
"no user -> {redir}"
246 res
+= "error=access_denied"
248 # The user is authenticated.
249 print
"{user.id} -> {redir}"
251 # We prepare a token (code) that the client will use to get the information.
253 var token
= new_token
255 res
+= "code={token.to_percent_encoding}"
257 var ttl
= 10*60*60 # 10 minutes
258 var info
= new Info(get_time
+ ttl
, user
)
261 if state
!= null then res
+= "&state={state.to_percent_encoding}"
262 response
.redirect res
265 redef fun post
(http_request
, response
)
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
(" ; ", ": ")
273 var code
= http_request
.string_arg
("code")
275 print
"POST: no code"
278 var info
= infos
.get_or_null
(code
)
280 print
"POST: bad code {code}"
284 print
"{info.user.id} -> retrieved"
286 # Drop the code as it is a single use
287 infos
.keys
.remove
(code
)
289 # Send the requested information.
290 response
.json
(info
.user
)
298 # Information about an authenticated used stored on the server to be given to the client.
303 # The identified user
307 var host
= "localhost"
310 if args
.is_empty
then
311 print
"usage: shibuqamoauth authorized_list [host] [port]"
316 if args
.length
> 1 then host
= args
[1]
317 if args
.length
> 2 then port
= args
[2].to_i
319 var authorized
= list
.to_path
.read_lines
320 if authorized
.is_empty
then
321 print_error
"{list}: not found or empty"
326 app
.use
("/*", new AuthHandler(authorized
))
327 app
.listen
(host
, port
)