Next: Properties, Previous: Preparation, Up: Top
Your application's use of the library can be roughly modeled into the following steps: initialize the library, optionally specify the callback, perform the authentication, and finally clean up. The following image illustrate this.
The third step may look the most complex, but for a simple client it will actually not involve any code. If your application need to handle several concurrent clients, or if it is a server that need to serve many clients simultaneous, things do get a bit more complicated.
For illustration, we will write a simple client. Writing a server
would be similar, the only difference is that, later on, instead of
supplying username or passwords, you need to decide whether someone
should be allowed to log in or not. The code for what we have
discussed so far make up our main
function in our client
(see Example 1):
int main (int argc, char *argv[]) { Gsasl *ctx = NULL; int rc; if ((rc = gsasl_init (&ctx)) != GSASL_OK) { printf ("Cannot initialize libgsasl (%d): %s", rc, gsasl_strerror (rc)); return 1; } client (ctx); gsasl_done (ctx); return 0; }
Here, the call to the function client
correspond to the third
step in the image above.
For a more complicated application, that have several clients running
simultaneous, instead of simply calling client
, it may have
created new threads for each session, and call client
within
each thread. The library is thread safe.
An actual authentication session is more complicated than what we have seen so far. The steps that make up it are: decide which mechanism to use, start the session, optionally specify the callback, optionally set any properties, perform the authentication loop, and clean up. Naturally, your application will start to talk its own protocol (e.g., SMTP or IMAP) after these steps have concluded.
The authentication loop is based on sending tokens (typically short messages encoded in base 64) back and forth between the client and server. It continue until authentication succeeds or there is an error. The format of the data to transfer, the number of iterations in the loop, and other details are specified by each mechanism. The goal of the library is to isolate your application from the details of all different mechanisms.
Note that the library do not send data to the server itself, but return it in an buffer. You must send it to the server yourself, according to an application protocol profile. For example, the SASL application protocol profile for SMTP is described in RFC 2554.
The following image illustrate the steps we have been talking about.
We will now show the implementation of the client
function used
before.
void client (Gsasl *ctx) { Gsasl_session *session; const char *mech = "PLAIN"; int rc; /* Create new authentication session. */ if ((rc = gsasl_client_start (ctx, mech, &session)) != GSASL_OK) { printf ("Cannot initialize client (%d): %s\n", rc, gsasl_strerror (rc)); return; } /* Set username and password in session handle. This info will be lost when this session is deallocated below. */ gsasl_property_set (session, GSASL_AUTHID, "jas"); gsasl_property_set (session, GSASL_PASSWORD, "secret"); /* Do it. */ client_authenticate (ctx, session); /* Cleanup. */ gsasl_finish (session); }
This function is responsible for deciding which mechanism to use. In
this case, the `PLAIN' mechanism is hard coded, but you will see
later how this can be made more flexible. The function create a new
session, store the username and password in the session handle, then
call another function client_authenticate
to handle the
authentication loop, and end by cleaning up. Let's continue with the
implementation of client_authenticate
.
void client_authenticate (Gsasl * ctx, Gsasl_session * session) { char buf[BUFSIZ] = ""; char *p; int rc; /* This loop mimic a protocol where the server get to send data first. */ do { printf ("Input base64 encoded data from server:\n"); fgets (buf, sizeof (buf) - 1, stdin); if (buf[strlen (buf) - 1] == '\n') buf[strlen (buf) - 1] = '\0'; rc = gsasl_step64 (session, buf, &p); if (rc == GSASL_NEEDS_MORE || rc == GSASL_OK) { printf ("Output:\n%s\n", p); free (p); } } while (rc == GSASL_NEEDS_MORE); printf ("\n"); if (rc != GSASL_OK) { printf ("Authentication error (%d): %s\n", rc, gsasl_strerror (rc)); return; } /* The client is done. Here you would typically check if the server let the client in. If not, you could try again. */ printf ("If server accepted us, we're done.\n"); }
This last function need to be discussed in some detail. First, you should be aware that there are two versions of this function, that differ in a subtle way. The version above (see Example 2) is used for application profiles where the server send data first. For some mechanisms, this may waste a roundtrip, because the server need input from the client to proceed. Therefor, today the recommended approach is to permit client to send data first (see Example 1). Which version you should use depend on which application protocol you are implementing.
Further, you should realize that it is bad programming style to use a
fixed size buffer. On GNU systems, you may use the getline
functions instead of fgets
. However, in practice, there are
few mechanisms that use very large tokens. In typical configurations,
the mechanism with the largest tokens (GSSAPI) can use at least 500
bytes. A fixed buffer size of 8192 bytes may thus be sufficient for
now. But don't say I didn't warn you, when a future mechanism doesn't
work in your application, because of a fixed size buffer.
The gsasl_step64
(and of course also gasl_step
) return
two non-error return codes. GSASL_OK
is used for success,
indicating that the library consider the authentication finished.
That may include a successful server authentication, depending on the
mechanism. You must not let the client continue to the application
protocol part unless you receive GSASL_OK
from these functions.
In particular, don't be fooled into believing authentication were
successful if the server reply “OK” but these function has failed
with an error. The server may have been hacked, and could be tricking
you into sending confidential data, without having successfully
authenticated the server.
The non-error return code GSASL_NEEDS_MORE
is used to signal to
your application that you should send the output token to the peer,
and wait for a new token, and do another iteration. If the server
conclude the authentication process, with no data, you should call
gsasl_step64
(or gsasl_step
) specifying a zero-length
token.
If the functions (gsasl_step
and gsasl_step64
) return
any non-error code, the content of the output buffer is undefined.
Otherwise, it is the callers responsibility to deallocate the buffer,
by calling free
. Note that in some situations, where the
buffer is empty, NULL
is returned as the buffer value. You
should treat this as an empty buffer.
Our earlier code was hard coded to use a specific mechanism. This is
rarely a good idea. Instead, it is recommended to select the best
mechanism available from the list of mechanism supported by the
server. Note that without TLS or similar, the list may have been
maliciously altered, by an attacker. This means that you should abort
if you cannot find any mechanism that exceeds your minimum security
level. There is a function gsasl_client_suggest_mechanism
(see Global Functions) that will try to pick the “best”
available mechanism from a list of mechanisms. Our simple interactive
example client (see Example 3) include the following function to
decide which mechanism to use. Note that the code doesn't blindly use
what is returned from gsasl_client_suggest_mechanism
, but
rather let some logic (in this case the user, through an interactive
query) decide which mechanism is acceptable.
const char *client_mechanism (Gsasl *ctx) { static char mech[GSASL_MAX_MECHANISM_SIZE + 1] = ""; char mechlist[BUFSIZ] = ""; const char *suggestion; printf ("Enter list of mechanism that server support, separate by SPC:\n"); fgets (mechlist, sizeof (mechlist) - 1, stdin); suggestion = gsasl_client_suggest_mechanism (ctx, mechlist); if (suggestion) printf ("Library suggest use of `%s'.\n", suggestion); printf ("Enter mechanism to use:\n"); fgets (mech, sizeof (mech) - 1, stdin); mech[strlen (mech) - 1] = '\0'; return mech; }
When running this example code, it might look like in the following output.
Enter list of mechanism that server support, separate by SPC: CRAM-MD5 DIGEST-MD5 GSSAPI FOO BAR Library suggest use of `GSSAPI'. Enter mechanism to use: CRAM-MD5 Input base64 encoded data from server: Zm5vcmQ= Output: amFzIDkyY2U1NWE5MTM2ZTY4NzEyMTUyZTFjYmFmNjVkZjgx If server accepted us, we're done.
Our earlier code specified the username and password before the authentication loop, as in:
gsasl_property_set (ctx, GSASL_AUTHID, "jas"); gsasl_property_set (ctx, GSASL_PASSWORD, "secret");
This may work for simple mechanisms, that only ever need an username and a password. But some mechanism require more information, such as an authorization identity, a special PIN or passcode, a realm, a hostname, a service name, or an anonymous identifier. Querying the user for all that information, without knowing exactly which of it is really needed will result in a poor user interface. The user should not have to input private information, if it isn't required.
The approach is a bad idea for another reason. What if the server abort the authentication process? Then your application have already queried the user for a username and password. It would be better if you only asked the user for this information, annoying to input, when it is known to be needed.
A better approach to this problem is to use a callback. Then the mechanism may query your application whenever it need some information, like the username and password. It will only do this at the precise step in the authentication when the information is actually needed. Further, if the user abort, e.g., a password prompt, the mechanism is directly informed of this (because it invoked the callback), and could recover somehow.
Our final example (see Example 4) specify a callback function,
inside main
as below.
/* Set the callback handler for the library. */ gsasl_callback_set (ctx, callback);
The function itself is implemented as follows.
int callback (Gsasl * ctx, Gsasl_session * sctx, Gsasl_property prop) { char buf[BUFSIZ] = ""; int rc = GSASL_NO_CALLBACK; /* Get user info from user. */ printf ("Callback invoked, for property %d.\n", prop); switch (prop) { case GSASL_PASSCODE: printf ("Enter passcode:\n"); fgets (buf, sizeof (buf) - 1, stdin); buf[strlen (buf) - 1] = '\0'; gsasl_property_set (sctx, GSASL_PASSCODE, buf); rc = GSASL_OK; break; case GSASL_AUTHID: printf ("Enter username:\n"); fgets (buf, sizeof (buf) - 1, stdin); buf[strlen (buf) - 1] = '\0'; gsasl_property_set (sctx, GSASL_AUTHID, buf); rc = GSASL_OK; break; default: printf ("Unknown property! Don't worry.\n"); break; } return rc; }
Again, it is bad style to use a fixed size buffer. Mmm'kay.
Which properties you should handle is up to you. If you don't know
how to respond to a certain property, simply return
GSASL_NO_CALLBACK
. The basic properties to support are
authentication identity (GSASL_AUTHID
), authorization identity
(GSASL_AUTHZID
), and password (GSASL_PASSWORD
). See
See Properties, for the list of all properties, and what your
callback should (ideally) do for them, and which properties each
mechanism require in order to work.