Filter is successfully called, still needs to do job properly.
[mod_auth_kerb.cvs/.git] / mod_auth_gssweb.c
1 /*
2  * Copyright (c) 2012, 2013, 2014 JANET(UK)
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  *
16  * 3. Neither the name of JANET(UK) nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24  * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
25  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
29  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
31  * OF THE POSSIBILITY OF SUCH DAMAGE.
32  *
33  * NOTE: Some code in this module was derived from code in
34  * mod_auth_gssapi.c which is copyrighted by CESNET.  See that file
35  * for full copyright details.
36  *
37  * NOTE: Portions of the code in this file were derived from example
38  * code distributed under the Apache 2.0 license:
39  *     http://www.apache.org/licenses/LICENSE-2.0
40  * 
41  * This module implements the Apache server side of the GSSWeb
42  * authentiction type which allows Moonshot to be used for
43  * authentication in web applications.  The module consists of two
44  * components: the hook function (gssweb_authenticate_user) that does
45  * most of the work, and an output filter (gssweb_authenticate_filter)
46  * that is registered by the hook function to send the output token
47  * back to the client in a json message that wraps the original
48  * response content.
49  *
50  * This module uses a simple protocol between the client and server
51  * to exchange GSS tokens and nonce information.  The protocol is 
52  * described in the protocol.txt file included with module source.
53  */
54
55 #include "mod_auth_gssweb.h"
56
57 module AP_MODULE_DECLARE_DATA auth_gssweb_module;
58
59 #define command(name, func, var, type, usage)           \
60   AP_INIT_ ## type (name, (void*) func,                 \
61         (void*)APR_OFFSETOF(gss_auth_config, var),      \
62         OR_AUTHCFG | RSRC_CONF, usage)
63
64 static const command_rec gssweb_config_cmds[] = {
65     command("GSSServiceName", ap_set_string_slot, service_name,
66             TAKE1, "Service name used for Apache authentication."),
67
68     { NULL }
69 };
70   
71 #define DEFAULT_ENCTYPE         "application/x-www-form-urlencoded"
72 #define GSS_MAX_TOKEN_SIZE      4096    //TBD -- check this value
73
74 /* gssweb_read_req() -- reads the request data into a buffer 
75  */
76 static int gssweb_read_req(request_rec *r, const char **rbuf, apr_off_t *size)
77 {
78   int rc = OK;
79
80   if((rc = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR))) {
81     return(rc);
82   }
83   
84   if(ap_should_client_block(r)) {
85
86     char         argsbuffer[HUGE_STRING_LEN];
87     apr_off_t    rsize, len_read, rpos = 0;
88     apr_off_t length = r->remaining;
89
90     *rbuf = (const char *) apr_pcalloc(r->pool, (apr_size_t) (length + 1));
91     *size = length;
92     while((len_read = ap_get_client_block(r, argsbuffer, sizeof(argsbuffer))) > 0) {
93       if((rpos + len_read) > length) {
94         rsize = length - rpos;
95       }
96       else {
97         rsize = len_read;
98       }
99       
100       memcpy((char *) *rbuf + rpos, argsbuffer, (size_t) rsize);
101       rpos += rsize;
102     }
103   }
104   return(rc);
105 }
106
107 /* gssweb_get_post_data() -- Gets the token and nonce from the request
108  * data.
109  */
110 static int gssweb_get_post_data(request_rec *r, int *nonce, gss_buffer_desc *input_token)
111 {
112   const char *data;
113   apr_off_t datalen;
114   const char *key, *val, *type;
115   int rc = 0;
116
117   *nonce = 0;
118   input_token->length = 0;
119   input_token->value = NULL;
120
121   if(r->method_number != M_POST) {
122     gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: Request data is not a POST, declining.");
123     return DECLINED;
124   }
125
126   type = apr_table_get(r->headers_in, "Content-Type");
127   if(strcasecmp(type, DEFAULT_ENCTYPE) != 0) {
128     gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: Unexpected content type, declining.");
129     return DECLINED;
130   }
131
132   if((rc = gssweb_read_req(r, &data, &datalen)) != OK) {
133     gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: Data read error, rc = %d", rc);
134     return rc;
135   }
136
137   while(*data && (val = ap_getword(r->pool, &data, '&'))) { 
138     gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: obtained val from ap_getword() (%s)", val);
139     key = ap_getword(r->pool, &val, '=');
140     ap_unescape_url((char*)key);
141     ap_unescape_url((char*)val);
142     if (0 == strcasecmp(key, "token")) {
143       gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: found token (%s)", val);
144       input_token->value = malloc(strlen(val));
145       input_token->length = apr_base64_decode(input_token->value, val);
146       gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: Token successfully decoded.");
147     }
148     else if (0 == strcasecmp(key, "nonce")) {
149       gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: found nonce (%s)", val);
150       *nonce = atoi(val);
151     }
152     else {
153       gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: unknown key (%s)", key);
154     }
155   }
156   if ((0 == *nonce) || (0 == input_token->length)) {
157     gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: nonce (%d) or token len (%d) is 0, declining", *nonce, input_token->length);
158     return DECLINED;
159   }
160   else {
161     gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_get_post_data: returning nonce (%d) and token (%d bytes)", *nonce, input_token->length);
162     return OK;
163   }
164 }
165
166 /* gssweb_authenticate_filter() -- Output filter for gssweb authentication.
167  * Wraps original response in JSON -- adding JSON to the beginning of the 
168  * response, escapes double quotes in the original response, and adds JSON
169  * to the end of the response.  Handles responses that involve more than
170  * one filter call by maintaining state until an EOS bucket is received.
171  */
172 static apr_status_t gssweb_authenticate_filter (ap_filter_t *f,
173                                         apr_bucket_brigade *brig_in)
174 {
175   gss_log(APLOG_MARK, APLOG_DEBUG, 0, f->r, "Entering GSSWeb filter");
176
177   request_rec *r = f->r;
178   conn_rec *c = r->connection;
179   apr_bucket_brigade *brig_out;
180   apr_bucket *bkt_in = NULL;
181   apr_bucket *bkt_out = NULL;
182   apr_bucket *bkt_eos = NULL;
183   const char *data = NULL;
184   apr_size_t len = 0;
185   char *buf = NULL;
186   apr_size_t n = 0;
187   apr_status_t ret = 0;
188
189   /* get the context from the request */
190
191   /* if this is the first call for this response, send JSON block */
192
193   if (first call) {
194     if (NULL = (brig_out = apr_brigade_create(r->pool, c->bucket_alloc))) {
195       // indicate processing error
196       apr_brigade_cleanup(brig_in);
197       return ??;
198     }
199     
200     //create and send opening JSON block
201     
202     if (?? != (ret = ap_pass_brigade(f->next,pbbOut))) {
203       // indicate a processing error
204       apr_brigade_cleanup(brig_in);
205       apr_brigade_cleanup(brig_out);
206       return ret;
207     }
208   }
209
210   /* loop through the buckets, escaping and sending each one */
211   for (bkt_in = APR_BRIGADE_FIRST(brig_in);
212        bkt_in != APR_BRIGADE_SENTINEL(brig_in);
213        bkt_in = APR_BUCKET_NEXT(bkt_in))
214     {
215       brig_out = apr_brigade_create(r->pool, c->bucket_alloc);
216
217       /* if this is an EOS, send the JSON closing block */
218       if(APR_BUCKET_IS_EOS(bkt_in))
219         {
220           // create and send the JSON closing block
221
222           // flag that the next filter call will be a new response 
223
224           /* set EOS in the outbound brigade */
225           bkt_eos = apr_bucket_eos_create(c->bucket_alloc);
226           APR_BRIGADE_INSERT_TAIL (brig_out, bkt_eos);
227
228           /* pass the brigade */
229           if (?? != (ret = ap_pass_brigade(f->next, bkt_out))) {
230             // indicate a processing error
231             apr_brigade_cleanup(brig_in);
232             apr_brigade_cleanup(brig_out);
233             return ret;
234           }
235           continue;
236         }
237
238       /* read */
239       apr_bucket_read(bkt_in, &data, &len, APR_BLOCK_READ);
240
241       /* write */
242       buf = apr_bucket_alloc(len, c->bucket_alloc);
243       bkt_out = apr_bucket_heap_create(buf, len, apr_bucket_free,
244                                        c->bucket_alloc);
245
246       for(n=0 ; n < len ; ++n) {
247         // escape quotes
248       }
249
250       APR_BRIGADE_INSERT_TAIL(brig_out, bkt_out);
251       if (?? == (ret = ap_pass_brigade(f->next, bkt_out))) {
252         apr_brigade_cleanup(brig_in);
253         apr_brigade_cleanup(brig_out);
254         return ret;
255       }
256     }
257
258   apr_brigade_cleanup(brig_in);
259   return ret;
260 }
261
262 /* gssweb_add_filter() -- Hook to add our output filter to the request
263  * (r). Called for all error responses through the
264  * gssweb_insert_error_filter hook.
265  */
266 static void
267 gssweb_add_filter(request_rec *r) 
268 {
269   gss_conn_ctx conn_ctx = NULL;
270
271   /* Get the context for this request */
272   conn_ctx = gss_get_conn_ctx(r);
273   if (conn_ctx == NULL) {
274     gss_log(APLOG_MARK, APLOG_ERR, 0, r, "gssweb_add_filter: Failed to find or create internal context.");
275     return;
276   }
277
278   /* Add the output filter */
279   ap_add_output_filter("gssweb_auth_filter", (void *)conn_ctx, r, r->connection);
280   return;
281 }
282
283 /* gssweb_authenticate_user() -- Hook to perform actual user
284  * authentication.  Will be called once for each round trip in the GSS
285  * authentication loop.  Reads the tokend from the request, calls
286  * gss_accept_sec_context(), and stores the output token and context
287  * in the user data areas.  Adds output filter to send the GSS
288  * output token back to the client.
289  */
290 static int
291 gssweb_authenticate_user(request_rec *r) 
292 {
293   gss_auth_config *conf = 
294     (gss_auth_config *) ap_get_module_config(r->per_dir_config,
295                                                 &auth_gssweb_module);
296   const char *auth_line = NULL;
297   const char *type = NULL;
298   char *auth_type = NULL;
299   char *negotiate_ret_value = NULL;
300   gss_conn_ctx conn_ctx = NULL;
301   int ret;
302   OM_uint32 major_status, minor_status, minor_status2;
303   gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER;
304   gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
305   gss_name_t client_name = GSS_C_NO_NAME;
306   gss_cred_id_t delegated_cred = GSS_C_NO_CREDENTIAL;
307   gss_cred_id_t server_creds = GSS_C_NO_CREDENTIAL;
308   OM_uint32 ret_flags = 0;
309   unsigned int nonce;
310
311   gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "Entering GSSWeb authentication");
312    
313   /* Check if this is for our auth type */
314   type = ap_auth_type(r);
315   if (type == NULL || strcasecmp(type, "GSSWeb") != 0) {
316         gss_log(APLOG_MARK, APLOG_DEBUG, 0, r,
317                 "gssweb_authenticate_user: AuthType '%s' is not GSSWeb, bailing out",
318                 (type) ? type : "(NULL)");
319
320         return DECLINED;
321   }
322
323   /* Set up a GSS context for this request, if there isn't one already */
324   conn_ctx = gss_get_conn_ctx(r);
325   if (conn_ctx == NULL) {
326     gss_log(APLOG_MARK, APLOG_ERR, 0, r, "gssweb_authenticate_user: Failed to create internal context");
327     return HTTP_INTERNAL_SERVER_ERROR;
328   }
329
330   /* Read the token and nonce from the POST */
331   if (0 != gssweb_get_post_data(r, &nonce, &input_token)) {
332     ret = HTTP_UNAUTHORIZED;
333     gss_log(APLOG_MARK, APLOG_ERR, 0, r, "gssweb_authenticate_user: Unable to read nonce or input token.");
334     goto end;
335   }
336   gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_authenticate_user: GSSWeb nonce value = %u.", nonce);
337    
338   /* If the nonce is set and doesn't match, start over */
339   if ((0 != conn_ctx->nonce) && (conn_ctx->nonce != nonce)) {
340     if (GSS_C_NO_CONTEXT != conn_ctx->context) {
341       gss_delete_sec_context(&minor_status, &conn_ctx->context, GSS_C_NO_BUFFER);
342     }
343     conn_ctx->context = GSS_C_NO_CONTEXT;
344     conn_ctx->state = GSS_CTX_EMPTY;
345     conn_ctx->user = NULL;
346     if (0 != conn_ctx->output_token.length) {
347       gss_release_buffer(&minor_status, &(conn_ctx->output_token));
348     }
349   }
350  
351   /* If the output filter reported an internal server error, return it */
352   if (GSS_CTX_ERROR == conn_ctx->state) {
353     ret = HTTP_INTERNAL_SERVER_ERROR;
354     gss_log(APLOG_MARK, APLOG_ERR, 0, r,
355             "gssweb_authenticate_user: Output filter returned internal server error, reporting.");
356     goto end;
357   }
358
359   /* Set-up the output filter (TBD -- register this once?) */
360   ap_register_output_filter("gssweb_auth_filter", gssweb_authenticate_filter, NULL, AP_FTYPE_RESOURCE);
361   ap_add_output_filter("gssweb_auth_filter", (void *)conn_ctx, r, r->connection);
362
363   /* Acquire server credentials (TBD -- do this once?) */
364   ret = get_gss_creds(r, conf, &server_creds);
365   if (ret)
366     goto end;
367   gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_authenticate_user: Server credentials acquired.");
368     
369   /* Call gss_accept_sec_context */
370   major_status = gss_accept_sec_context(&minor_status,
371                                         &conn_ctx->context,
372                                         server_creds,
373                                         &input_token,
374                                         GSS_C_NO_CHANNEL_BINDINGS,
375                                         NULL,
376                                         NULL,
377                                         &output_token,
378                                         &ret_flags,
379                                         NULL,
380                                         &delegated_cred);
381   gss_log(APLOG_MARK, APLOG_DEBUG, 0, r,
382           "gssweb_authenticate_user: Client %s us their credential",
383           (ret_flags & GSS_C_DELEG_FLAG) ? "delegated" : "didn't delegate");
384
385   if (GSS_ERROR(major_status)) {
386     gss_log(APLOG_MARK, APLOG_ERR, 0, r,
387             "%s", get_gss_error(r, major_status, minor_status,
388                                 "gssweb_authenticate_user: Failed to establish authentication"));
389     gss_delete_sec_context(&minor_status, &conn_ctx->context, GSS_C_NO_BUFFER);
390     conn_ctx->context = GSS_C_NO_CONTEXT;
391     conn_ctx->state = GSS_CTX_EMPTY;
392     ret = HTTP_UNAUTHORIZED;
393     goto end;
394     gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_authenticate_user: Decoding ouput token.");
395   }
396
397   gss_log(APLOG_MARK, APLOG_DEBUG, 0, r, "gssweb_authenticate_user: Got sec context, storing nonce and output token.");
398
399   /* Store the nonce & ouput token in the stored context */
400   conn_ctx->nonce = nonce;
401   conn_ctx->output_token = output_token;
402     
403   /* If we aren't done yet, go around again */
404   if (major_status & GSS_S_CONTINUE_NEEDED) {
405     conn_ctx->state = GSS_CTX_IN_PROGRESS;
406     ret = HTTP_UNAUTHORIZED;
407     goto end;
408   }
409
410   conn_ctx->state = GSS_CTX_ESTABLISHED;
411         r->user = apr_pstrdup(r->pool, conn_ctx->user);
412         r->ap_auth_type = "GSSWeb";
413   ret = OK;
414
415  end:
416   if (delegated_cred)
417     gss_release_cred(&minor_status, &delegated_cred);
418   
419   if (output_token.length) 
420     gss_release_buffer(&minor_status, &output_token);
421     
422   if (client_name != GSS_C_NO_NAME)
423     gss_release_name(&minor_status, &client_name);
424
425   if (server_creds != GSS_C_NO_CREDENTIAL)
426     gss_release_cred(&minor_status, &server_creds);
427
428   return ret;
429 }
430
431 static void
432 gssweb_register_hooks(apr_pool_t *p)
433 {
434     ap_hook_check_user_id(gssweb_authenticate_user, NULL, NULL, APR_HOOK_MIDDLE);
435     ap_hook_insert_error_filter(gssweb_add_filter, NULL, NULL, APR_HOOK_MIDDLE);
436 }
437
438 module AP_MODULE_DECLARE_DATA auth_gssweb_module = {
439     STANDARD20_MODULE_STUFF,
440     gss_config_dir_create,
441     NULL,
442     NULL,
443     NULL,
444     gssweb_config_cmds,
445     gssweb_register_hooks
446 };