summaryrefslogtreecommitdiff
path: root/tests/lib/webkdc-login-t.c
blob: b82d5bdf8830db4e540f893b30622000a0b20e45 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
/*
 * Test WebKDC login support.
 *
 * Written by Russ Allbery <rra@stanford.edu>
 * Copyright 2011
 *     The Board of Trustees of the Leland Stanford Junior University
 *
 * See LICENSE for licensing terms.
 */

#include <config.h>
#include <portable/system.h>

#include <apr_pools.h>
#include <apr_strings.h>
#include <apr_tables.h>
#include <time.h>

#include <tests/tap/basic.h>
#include <tests/tap/kerberos.h>
#include <tests/tap/remctl.h>
#include <util/concat.h>
#include <webauth/basic.h>
#include <webauth/keys.h>
#include <webauth/tokens.h>
#include <webauth/webkdc.h>


int
main(void)
{
    apr_pool_t *pool = NULL;
    WEBAUTH_KEYRING *ring, *session;
    WEBAUTH_KEY *session_key;
    char key_data[WA_AES_128], username[BUFSIZ], password[BUFSIZ];
    int status;
    char *realm, *path, *keyring, *conf;
    time_t now;
    pid_t remctld;
    FILE *file;
    struct webauth_context *ctx;
    struct webauth_webkdc_config config;
    struct webauth_user_config user_config;
    struct webauth_webkdc_login_request request;
    struct webauth_webkdc_login_response *response;
    struct webauth_token *token, login, wkproxy, wkproxy2;
    struct webauth_token_request req;
    struct webauth_token_webkdc_proxy *pt;
    struct webauth_token_webkdc_service service;
    struct webauth_webkdc_proxy_data *pd;
    time_t expiration = 0;

    if (apr_initialize() != APR_SUCCESS)
        bail("cannot initialize APR");
    if (apr_pool_create(&pool, NULL) != APR_SUCCESS)
        bail("cannot create memory pool");
    if (webauth_context_init_apr(&ctx, pool) != WA_ERR_NONE)
        bail("cannot initialize WebAuth context");

    /* Load the precreated keyring that we'll use for token encryption. */
    keyring = test_file_path("data/keyring");
    status = webauth_keyring_read_file(keyring, &ring);
    if (status != WA_ERR_NONE)
        bail("cannot read %s: %s", keyring,
             webauth_error_message(NULL, status));
    test_file_path_free(keyring);

    /* Ensure we have a basic configuration available. */
    memset(&config, 0, sizeof(config));
    config.local_realms = apr_array_make(pool, 1, sizeof(const char *));
    config.permitted_realms = apr_array_make(pool, 1, sizeof(const char *));
    memset(&user_config, 0, sizeof(user_config));
    if (chdir(getenv("SOURCE")) < 0)
        bail("can't chdir to SOURCE");
    config.principal = kerberos_setup();
    if (config.principal == NULL)
        skip_all("Kerberos tests not configured");
    realm = strchr(config.principal, '@');
    if (realm == NULL)
        skip_all("Kerberos principal has no realm");
    realm++;
    config.keytab_path = test_file_path("data/test.keytab");
    if (config.keytab_path == NULL)
        skip_all("Kerberos tests not configured");

    /*
     * Ensure we have a username and password.
     *
     * FIXME: Ideally, we would skip the tests that don't require this and
     * still do the rest.
     */
    path = test_file_path("data/test.password");
    if (path == NULL)
        skip_all("Kerberos tests not configured");
    file = fopen(path, "r");
    if (file == NULL)
        sysbail("cannot open %s", path);
    if (fgets(username, sizeof(username), file) == NULL)
        bail("cannot read %s", path);
    if (fgets(password, sizeof(password), file) == NULL)
        bail("cannot read password from %s", path);
    fclose(file);
    if (username[strlen(username) - 1] != '\n')
        bail("no newline in %s", path);
    username[strlen(username) - 1] = '\0';
    if (password[strlen(password) - 1] != '\n')
        bail("username or password too long in %s", path);
    password[strlen(password) - 1] = '\0';
    test_file_path_free(path);

    /*
     * FIXME: Similarly, if we don't have remctl, we should skip tests instead
     * of skipping the whole thing.
     */
#ifndef PATH_REMCTLD
    skip_all("remctld not found");
#endif
    if (chdir(getenv("SOURCE")) < 0)
        bail("can't chdir to SOURCE");
    conf = concatpath(getenv("SOURCE"), "data/conf-webkdc");
    remctld = remctld_start(PATH_REMCTLD, config.principal, conf, NULL);

    plan(158);

    /* Provide basic configuration to the WebKDC code. */
    status = webauth_webkdc_config(ctx, &config);
    is_int(WA_ERR_NONE, status, "WebKDC configuration succeeded");

    /* Flesh out the absolute minimum required in the request. */
    now = time(NULL);
    memset(&request, 0, sizeof(request));
    memset(&service, 0, sizeof(service));
    service.subject = "krb5:webauth/example.com@EXAMPLE.COM";
    if (webauth_random_key(key_data, sizeof(key_data)) != WA_ERR_NONE)
        bail("cannot create random key");
    session_key = webauth_key_create(WA_AES_KEY, key_data, sizeof(key_data));
    status = webauth_keyring_from_key(ctx, session_key, &session);
    if (status != WA_ERR_NONE)
        bail("cannot create keyring from session key");
    service.session_key = session_key->data;
    service.session_key_len = session_key->length;
    service.creation = now;
    service.expiration = now + 60;
    request.service = &service;
    memset(&req, 0, sizeof(req));
    req.type = "id";
    req.auth = "webkdc";
    req.return_url = "https://example.com/";
    req.creation = now;
    request.request = &req;
    request.creds = apr_array_make(pool, 1, sizeof(struct token *));

    /*
     * Attempted login with now proxy or login tokens.  Should return an error
     * indicating that a proxy token is required.
     */
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Minimal login returns success");
    is_int(WA_PEC_PROXY_TOKEN_REQUIRED, response->login_error,
           "...with correct error code");
    is_string("need a proxy token", response->login_message,
              "...and correct error message");
    ok(response->factors_wanted == NULL, "...no factors wanted");
    ok(response->factors_configured == NULL, "...no factors configured");
    ok(response->proxies == NULL, "...no new webkdc-proxy tokens");
    is_string("https://example.com/", response->return_url,
              "...return URL is correct");
    is_string("krb5:webauth/example.com@EXAMPLE.COM", response->requester,
              "...requester is correct");
    is_string(NULL, response->subject, "...no subject");
    is_string(NULL, response->result, "...no result token");
    is_string(NULL, response->result_type, "...no result type");
    is_string(NULL, response->login_cancel, "...no login cancel token");
    ok(response->app_state == NULL, "...no app state");
    is_int(0, response->app_state_len, "...no app state length");
    ok(response->logins == NULL, "...no login information");

    /* Try again, but with a login cancel token requested. */
    req.options = "lc";
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Login w/cancel returns success");
    is_int(WA_PEC_PROXY_TOKEN_REQUIRED, response->login_error,
           "...with correct error code");
    is_string("need a proxy token", response->login_message,
              "...and correct error message");
    ok(response->login_cancel != NULL, "...and now a cancel token");
    status = webauth_token_decode(ctx, WA_TOKEN_ERROR, response->login_cancel,
                                  session, &token);
    is_int(WA_ERR_NONE, status, "...which decodes properly");
    if (status != WA_ERR_NONE)
        ok_block(3, 0, "...invalid error token");
    else {
        is_int(WA_PEC_LOGIN_CANCELED, token->token.error.code,
               "...with correct code");
        is_string("user canceled login", token->token.error.message,
                  "...and message");
        ok(token->token.error.creation - now < 3, "...and creation time");
    }

    /* Now add a login token and see if we can get an id token in response. */
    config.local_realms = apr_array_make(pool, 1, sizeof(const char *));
    APR_ARRAY_PUSH(config.local_realms, const char *) = "none";
    memset(&login, 0, sizeof(login));
    login.type = WA_TOKEN_LOGIN;
    login.token.login.username = username;
    login.token.login.password = password;
    login.token.login.creation = now;
    APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &login;
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Login w/password returns success");
    is_int(0, response->login_error, "...with no error");
    is_string(NULL, response->login_message, "...and no message");
    ok(response->proxies != NULL, "...and now we have proxy tokens");
    pt = NULL;
    if (response->proxies == NULL)
        ok_block(11, 0, "...no proxy tokens");
    else {
        is_int(1, response->proxies->nelts, "...one proxy token");
        pd = &APR_ARRAY_IDX(response->proxies, 0,
                            struct webauth_webkdc_proxy_data);
        is_string("krb5", pd->type, "...of type krb5");
        status = webauth_token_decode(ctx, WA_TOKEN_WEBKDC_PROXY, pd->token,
                                      ring, &token);
        is_int(WA_ERR_NONE, status, "...which decodes properly");
        pt = &token->token.webkdc_proxy;
        is_string(username, pt->subject, "...with correct subject");
        is_string("krb5", pt->proxy_type, "...and correct type");
        ok(strncmp("WEBKDC:krb5:", pt->proxy_subject, 12) == 0,
           "...and correct proxy subject prefix");
        ok(strcmp(config.principal, pt->proxy_subject + 12) == 0,
           "...and correct proxy subject identity");
        ok(pt->data != NULL, "...and data is not NULL");
        is_string("p", pt->initial_factors, "...and factors is password");
        ok(pt->creation - now < 3, "...and creation is okay");
        ok(pt->expiration > now, "...and expiration is sane");
    }
    is_string(username, response->subject, "...subject is correct");
    ok(response->result != NULL, "...there is a result token");
    is_string("id", response->result_type, "...which is an id token");
    status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result,
                                  session, &token);
    is_int(WA_ERR_NONE, status, "...result token decodes properly");
    if (status != WA_ERR_NONE)
        ok_block(7, 0, "...no result token: %s",
                 webauth_error_message(ctx, status));
    else {
        is_string(username, token->token.id.subject,
                  "...result subject is right");
        is_string("webkdc", token->token.id.auth,
                  "...result auth type is right");
        ok(token->token.id.auth_data == NULL, "...and there is no auth data");
        is_string("p", token->token.id.initial_factors,
                  "...result initial factors is right");
        is_int(0, token->token.id.loa, "...and no LoA");
        ok(token->token.id.creation - now < 3, "...and creation is sane");
        is_int(pt->expiration, token->token.id.expiration,
               "...and expiration matches the expiration of the proxy token");
    }

    /* Get an id token with a Kerberos authenticator and test forced auth. */
    req.options = "lc,fa";
    request.creds = apr_array_make(pool, 2, sizeof(struct webauth_token *));
    APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &login;
    service.subject = apr_pstrcat(pool, "krb5:", config.principal, NULL);
    req.auth = "krb5";
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Login for krb5 auth returns success");
    is_int(0, response->login_error, "...with no error");
    is_string(NULL, response->login_message, "...and no message");
    ok(response->login_cancel != NULL, "...and there is a cancel token");
    ok(response->result != NULL, "...there is a result token");
    is_string("id", response->result_type, "...which is an id token");
    status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result,
                                  session, &token);
    is_int(WA_ERR_NONE, status, "...result token decodes properly");
    if (status != WA_ERR_NONE)
        ok_block(3, 0, "...no result token: %s",
                 webauth_error_message(ctx, status));
    else {
        is_string(username, token->token.id.subject,
                  "...result subject is right");
        is_string("krb5", token->token.id.auth,
                  "...result auth type is right");
        ok(token->token.id.auth_data != NULL, "...and there is auth data");
    }

    /* Get a proxy token instead. */
    req.options = NULL;
    request.creds = apr_array_make(pool, 2, sizeof(struct webauth_token *));
    APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &login;
    req.type = "proxy";
    req.auth = NULL;
    req.proxy_type = "krb5";
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Login for proxy token returns success");
    is_int(0, response->login_error, "...with no error");
    is_string(NULL, response->login_message, "...and no message");
    ok(response->login_cancel == NULL, "...and there is no cancel token");
    is_string(username, response->subject, "...subject is correct");
    pt = NULL;
    if (response->proxies == NULL)
        ok_block(3, 0, "...no proxy tokens");
    else {
        is_int(1, response->proxies->nelts, "...one proxy token");
        pd = &APR_ARRAY_IDX(response->proxies, 0,
                            struct webauth_webkdc_proxy_data);
        is_string("krb5", pd->type, "...of type krb5");
        status = webauth_token_decode(ctx, WA_TOKEN_WEBKDC_PROXY, pd->token,
                                      ring, &token);
        is_int(WA_ERR_NONE, status, "...which decodes properly");
        expiration = token->token.webkdc_proxy.expiration;
    }
    ok(response->result != NULL, "...there is a result token");
    is_string("proxy", response->result_type, "...which is a proxy token");
    status = webauth_token_decode(ctx, WA_TOKEN_PROXY, response->result,
                                  session, &token);
    is_int(WA_ERR_NONE, status, "...result token decodes properly");
    if (status != WA_ERR_NONE)
        ok_block(6, 0, "...no result token: %s",
                 webauth_error_message(ctx, status));
    else {
        is_string(username, token->token.proxy.subject,
                  "...result subject is right");
        is_string("krb5", token->token.proxy.type,
                  "...result proxy type is right");
        is_string("p", token->token.proxy.initial_factors,
                  "...result initial factors is right");
        is_int(0, token->token.proxy.loa, "...and no LoA");
        ok(token->token.proxy.creation - now < 3, "...and creation is sane");
        is_int(expiration, token->token.proxy.expiration,
               "...and expiration matches the expiration of the proxy token");
        status = webauth_token_decode_raw(ctx, WA_TOKEN_WEBKDC_PROXY,
                                          token->token.proxy.webkdc_proxy,
                                          token->token.proxy.webkdc_proxy_len,
                                          ring, &token);
        is_int(WA_ERR_NONE, status, "...embedded webkdc-proxy token decodes");
        if (status != WA_ERR_NONE)
            ok_block(7, 0, "...no webkdc-proxy token: %s",
                     webauth_error_message(ctx, status));
        else {
            pt = &token->token.webkdc_proxy;
            is_string(username, pt->subject, "...with correct subject");
            is_string("krb5", pt->proxy_type, "...and correct type");
            ok(strcmp(request.service->subject, pt->proxy_subject) == 0,
               "...and correct proxy subject identity");
            ok(pt->data != NULL, "...and data is not NULL");
            is_string("p", pt->initial_factors, "...and factors is password");
            ok(pt->creation - now < 3, "...and creation is okay");
            ok(pt->expiration > now, "...and expiration is sane");
        }
    }

    /* Get an id token with a single sign-on webkdc-proxy token. */
    memset(&wkproxy, 0, sizeof(wkproxy));
    wkproxy.type = WA_TOKEN_WEBKDC_PROXY;
    wkproxy.token.webkdc_proxy.subject = "testuser";
    wkproxy.token.webkdc_proxy.proxy_type = "remuser";
    wkproxy.token.webkdc_proxy.proxy_subject = "WEBKDC:remuser";
    wkproxy.token.webkdc_proxy.data = "testuser";
    wkproxy.token.webkdc_proxy.data_len = strlen("testuser");
    wkproxy.token.webkdc_proxy.initial_factors = "x,x1";
    wkproxy.token.webkdc_proxy.loa = 3;
    wkproxy.token.webkdc_proxy.creation = now;
    wkproxy.token.webkdc_proxy.expiration = now + 60 * 60;
    request.creds = apr_array_make(pool, 2, sizeof(struct webauth_token *));
    APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &wkproxy;
    req.type = "id";
    req.auth = "webkdc";
    req.proxy_type = NULL;
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    if (status != WA_ERR_NONE)
        diag("error status: %s", webauth_error_message(ctx, status));
    is_int(WA_ERR_NONE, status, "Proxy auth for webkdc returns success");
    is_int(0, response->login_error, "...with no error");
    is_string(NULL, response->login_message, "...and no message");
    ok(response->proxies != NULL, "...and we have proxy tokens");
    if (response->proxies == NULL)
        ok_block(5, 0, "...no proxy tokens");
    else {
        is_int(1, response->proxies->nelts, "...one proxy token");
        pd = &APR_ARRAY_IDX(response->proxies, 0,
                            struct webauth_webkdc_proxy_data);
        is_string("remuser", pd->type, "...of type webkdc");
        status = webauth_token_decode(ctx, WA_TOKEN_WEBKDC_PROXY, pd->token,
                                      ring, &token);
        is_int(WA_ERR_NONE, status, "...which decodes properly");
        pt = &token->token.webkdc_proxy;
        is_string("testuser", pt->subject, "...with correct subject");
        is_string("x,x1", pt->initial_factors, "...and initial factors");
    }
    ok(response->result != NULL, "...there is a result token");
    is_string("id", response->result_type, "...which is an id token");
    status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result,
                                  session, &token);
    is_int(WA_ERR_NONE, status, "...result token decodes properly");
    if (status != WA_ERR_NONE)
        ok_block(6, 0, "...no result token: %s",
                 webauth_error_message(ctx, status));
    else {
        is_string("testuser", token->token.id.subject,
                  "...result subject is right");
        is_string("webkdc", token->token.id.auth,
                  "...result auth type is right");
        is_string("x,x1", token->token.proxy.initial_factors,
                  "...result initial factors is right");
        is_int(3, token->token.id.loa, "...result LoA is right");
        ok(token->token.id.creation - now < 3, "...and creation is sane");
        is_int(now + 60 * 60, token->token.id.expiration,
               "...and expiration matches the expiration of the proxy token");
    }

    /* Set forced authentication and try again. */
    req.options = "fa";
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Proxy auth w/forced login returns success");
    is_int(WA_PEC_LOGIN_FORCED, response->login_error,
           "...with the right error");
    is_string("forced authentication, need to login", response->login_message,
              "...and the right message");
    is_string("testuser", response->subject, "...but we do know the subject");

    /* Remove forced authentication but ask for a proxy token. */
    req.options = NULL;
    req.type = "proxy";
    req.auth = NULL;
    req.proxy_type = "krb5";
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Proxy auth for proxy returns success");
    is_int(WA_PEC_PROXY_TOKEN_REQUIRED, response->login_error,
           "...with the right error");
    is_string("need a proxy token", response->login_message,
              "...and the right message");
    is_string("testuser", response->subject, "...but we do know the subject");

    /* Now, add configuration for user information, and try this again. */
    user_config.protocol = WA_PROTOCOL_REMCTL;
    user_config.host = "localhost";
    user_config.port = 14373;
    user_config.identity = config.principal;
    user_config.command = "test";
    status = webauth_user_config(ctx, &user_config);
    is_int(WA_ERR_NONE, status, "User information config accepted");
    req.type = "id";
    req.auth = "webkdc";
    req.proxy_type = NULL;
    wkproxy.token.webkdc_proxy.subject = "mini";
    wkproxy.token.webkdc_proxy.data = "mini";
    wkproxy.token.webkdc_proxy.data_len = strlen("mini");
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    if (status != WA_ERR_NONE)
        diag("error status: %s", webauth_error_message(ctx, status));
    is_int(WA_ERR_NONE, status, "Proxy auth w/user config returns success");
    is_int(0, response->login_error, "...with no error");
    is_string(NULL, response->login_message, "...and no message");
    ok(response->result != NULL, "...there is a result token");
    is_string("id", response->result_type, "...which is an id token");
    status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result,
                                  session, &token);
    is_int(WA_ERR_NONE, status, "...result token decodes properly");
    if (status != WA_ERR_NONE)
        ok_block(4, 0, "...no result token: %s",
                 webauth_error_message(ctx, status));
    else {
        is_string("mini", token->token.id.subject,
                  "...result subject is right");
        is_string("webkdc", token->token.id.auth,
                  "...result auth type is right");
        is_string("x,x1", token->token.proxy.initial_factors,
                  "...result initial factors is right");
        is_int(1, token->token.id.loa, "...result LoA is right");
    }

    /*
     * Request an X.509 factor and try again.  This should still work even
     * though this user doesn't have password listed as a supported factor in
     * the metadata.
     */
    req.initial_factors = "x";
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    if (status != WA_ERR_NONE)
        diag("error status: %s", webauth_error_message(ctx, status));
    is_int(WA_ERR_NONE, status, "Multifactor with proxy returns success");
    is_int(0, response->login_error, "...with no error");
    is_string(NULL, response->login_message, "...and no message");
    ok(response->result != NULL, "...there is a result token");
    is_string("id", response->result_type, "...which is an id token");

    /*
     * Change the WebKDC proxy token to assert just a password factor and ask
     * for an OTP factor, and try again.  This should be rejected with
     * multifactor required.
     */
    wkproxy.token.webkdc_proxy.initial_factors = "p";
    req.initial_factors = "o";
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Multifactor without config returns success");
    is_int(WA_PEC_MULTIFACTOR_UNAVAILABLE, response->login_error,
           "...with the right error");
    is_string("multifactor required but not configured",
              response->login_message, "...and the right message");
    ok(response->result == NULL, "...and there is no result token");
    is_int(1, response->factors_wanted->nelts, "...and one factor is wanted");
    is_string("o", APR_ARRAY_IDX(response->factors_wanted, 0, const char *),
              "...which is the OTP factor");
    is_int(1, response->factors_configured->nelts,
           "...and one factor is configured");
    is_string("p",
              APR_ARRAY_IDX(response->factors_configured, 0, const char *),
              "...which is the password factor");

    /*
     * Try with a user who has multifactor configuration and forced
     * multifactor.
     */
    wkproxy.token.webkdc_proxy.subject = "full";
    wkproxy.token.webkdc_proxy.data = "full";
    wkproxy.token.webkdc_proxy.data_len = strlen("full");
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status, "Multifactor with config returns success");
    is_int(WA_PEC_MULTIFACTOR_REQUIRED, response->login_error,
           "...with the right error");
    is_string("multifactor login required", response->login_message,
              "...and the right message");
    ok(response->result == NULL, "...and there is no result token");
    is_int(2, response->factors_wanted->nelts,
           "...and two factors are wanted");
    is_string("o", APR_ARRAY_IDX(response->factors_wanted, 0, const char *),
              "...which is the OTP factor");
    is_int(5, response->factors_configured->nelts,
           "...and five factors are configured");
    is_string("p",
              APR_ARRAY_IDX(response->factors_configured, 0, const char *),
              "...which is the password factor");
    is_string("r",
              APR_ARRAY_IDX(response->factors_configured, 1, const char *),
              "...the random factor");
    is_string("m",
              APR_ARRAY_IDX(response->factors_configured, 2, const char *),
              "...the generic multifactor factor");
    is_string("o",
              APR_ARRAY_IDX(response->factors_configured, 3, const char *),
              "...the OTP factor");
    is_string("o3",
              APR_ARRAY_IDX(response->factors_configured, 4, const char *),
              "...and the OTP-3 factor");

    /*
     * Add a second webkdc-proxy token that repesents an OTP login.  This
     * login should then work.
     */
    wkproxy.token.webkdc_proxy.loa = 3;
    wkproxy2.type = WA_TOKEN_WEBKDC_PROXY;
    wkproxy2.token.webkdc_proxy.subject = "full";
    wkproxy2.token.webkdc_proxy.proxy_type = "remuser";
    wkproxy2.token.webkdc_proxy.proxy_subject = "WEBKDC:remuser";
    wkproxy2.token.webkdc_proxy.data = "full";
    wkproxy2.token.webkdc_proxy.data_len = strlen("full");
    wkproxy2.token.webkdc_proxy.initial_factors = "o,o3";
    wkproxy2.token.webkdc_proxy.loa = 2;
    wkproxy2.token.webkdc_proxy.creation = now;
    wkproxy2.token.webkdc_proxy.expiration = now + 30 * 60;
    APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &wkproxy2;
    status = webauth_webkdc_login(ctx, &request, &response, ring);
    is_int(WA_ERR_NONE, status,
           "Multifactor with two proxies returns success");
    is_int(0, response->login_error, "...with no error");
    is_string(NULL, response->login_message, "...and no error message");
    ok(response->result != NULL, "...there is a result token");
    is_string("id", response->result_type, "...which is an id token");
    status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result,
                                  session, &token);
    is_int(WA_ERR_NONE, status, "...result token decodes properly");
    if (status != WA_ERR_NONE)
        ok_block(5, 0, "...no result token: %s",
                 webauth_error_message(ctx, status));
    else {
        is_string("full", token->token.id.subject,
                  "...result subject is right");
        is_string("webkdc", token->token.id.auth,
                  "...result auth type is right");
        is_string("p,o,o3,m", token->token.proxy.initial_factors,
                  "...result initial factors is right");
        is_int(3, token->token.id.loa, "...result LoA is right");
        is_int(now + 30 * 60, token->token.id.expiration,
               "...and expiration matches the shorter expiration");
    }

    /* Clean up. */
    remctld_stop(remctld);
    kerberos_cleanup();
    apr_terminate();
    return 0;
}