Asynchronous SIP Authentication
Available since 2.17 (#4816).
Tip
This page covers only the asynchronous-challenge path. For the underlying digest mechanism — selecting MD5 vs SHA-256 vs SHA-512/256, advertising multiple credentials per realm, server-side challenge issuance, AKA, and build prerequisites — see SIP Digest Authentication.
Overview
By default, PJSIP handles 401/407 authentication challenges synchronously: when a challenge arrives, the library looks up pre-configured credentials, builds the Authorization header, and resends the request — all within the transaction callback.
The asynchronous authentication API lets applications intercept the challenge and supply credentials later. This is useful when credentials are not available immediately, for example:
Prompting the user for a password
Fetching credentials from a vault or keychain
Performing an OAuth token exchange
Applying rate-limiting or policy checks before retrying
The feature is available at three API levels:
Layer |
Entry Point |
Async Mechanism |
|---|---|---|
PJSIP |
Token-based callback; call |
|
PJSUA-LIB |
|
Set |
PJSUA2 |
|
Call |
The feature is fully backward-compatible. If the callback is not set or does not handle the challenge, the library falls back to the existing synchronous credential-based authentication.
PJSUA2 API
Implement the virtual method pj::Account::onAuthChallenge().
Inside the callback you can:
Respond immediately with the account’s current credentials.
Respond with new credentials supplied at challenge time.
Defer the decision and respond (or abandon) asynchronously.
Responding immediately
void onAuthChallenge(OnAuthChallengeParam &prm) override
{
// Respond using credentials already configured on the account
prm.challenge.respond();
}
Responding with new credentials
void onAuthChallenge(OnAuthChallengeParam &prm) override
{
AuthCredInfoVector creds;
creds.push_back(AuthCredInfo("digest", "*",
"alice", 0, "secret123"));
prm.challenge.respond(creds);
}
Warning
respond(creds) sets the credentials on the account’s shared auth
session, permanently replacing the account’s credentials. All
subsequent authentication for the account will use these new credentials.
If you only want to authenticate a single request, prefer the PJSIP-level
API where you can set credentials on a non-shared session.
Deferring the response
defer() returns a heap-allocated AuthChallenge* that you own.
Pass it to another thread, a timer, or any asynchronous workflow, then call
respond() or abandon() when ready.
void onAuthChallenge(OnAuthChallengeParam &prm) override
{
AuthChallenge *deferred = prm.challenge.defer();
// Hand off to async credential-fetching logic.
// When credentials are available:
AuthCredInfoVector creds;
creds.push_back(AuthCredInfo("digest", "*",
"alice", 0, "secret123"));
deferred->respond(creds);
delete deferred;
}
Note
On a deferred AuthChallenge, respond(), abandon(), and
destruction all call into PJSIP and therefore must be invoked from a
pjlib-registered thread. If a worker thread fetches credentials,
register it with pj_thread_register() (or hand the result
back to a registered thread) before calling respond()/abandon().
Warning
If respond() or abandon() is never called, the destructor will
auto-abandon the challenge — but in garbage-collected languages (Java,
Python) the destructor/finalizer typically runs on an unregistered GC
thread, which will trip PJLIB thread-registration assertions. Always
explicitly call respond(), abandon(), or delete on the
deferred object from a pjlib-registered thread. Do not rely on GC
to clean it up. See Problems with Garbage Collection.
Python example
import pjsua2 as pj
class MyAccount(pj.Account):
def onAuthChallenge(self, prm):
deferred = prm.challenge.defer()
# ... fetch credentials asynchronously (from a pjlib-registered
# thread, or marshal the result back to one) ...
creds = pj.AuthCredInfoVector()
creds.append(pj.AuthCredInfo("digest", "*",
"alice", 0, "secret123"))
deferred.respond(creds)
# Must explicitly consume the deferred object — do not rely on
# Python GC to abandon it (finalizer runs on an unregistered
# thread). See the GC warning above.
PJSUA-LIB API
Set the on_auth_challenge callback in pjsua_callback:
pjsua_config cfg;
pjsua_config_default(&cfg);
cfg.cb.on_auth_challenge = &on_auth_challenge;
The callback receives a pjsua_on_auth_challenge_param with the
auth session, opaque token, original request (tdata), and the
challenge response (rdata). To handle asynchronously:
Clone
rdataand add a reference totdata(they become invalid after the callback returns).Set
param->handled = PJ_TRUE.Later, call
pjsip_auth_clt_reinit_req()to build the authenticated request, thenpjsip_auth_clt_async_send_req()to send it (orpjsip_auth_clt_async_abandon()to give up).
struct auth_ctx {
pjsip_auth_clt_sess *auth_sess;
void *token;
pjsip_tx_data *tdata;
pjsip_rx_data *rdata_clone;
pjsua_acc_id acc_id;
};
/* Timer callback — runs after credentials are available */
static void on_creds_ready(pj_timer_heap_t *th, pj_timer_entry *te)
{
struct auth_ctx *ctx = (struct auth_ctx *)te->user_data;
pjsip_cred_info cred;
pjsip_tx_data *new_req = NULL;
pj_status_t status;
PJ_UNUSED_ARG(th);
/* Set credentials on the auth session */
pj_bzero(&cred, sizeof(cred));
cred.scheme = pj_str("digest");
cred.realm = pj_str("*");
cred.username = pj_str("alice");
cred.data = pj_str("secret123");
cred.data_type = PJSIP_CRED_DATA_PLAIN_PASSWD;
pjsip_auth_clt_set_credentials(ctx->auth_sess, 1, &cred);
/* Build and send the authenticated request */
status = pjsip_auth_clt_reinit_req(ctx->auth_sess,
ctx->rdata_clone,
ctx->tdata, &new_req);
if (status == PJ_SUCCESS && new_req) {
pjsip_auth_clt_async_send_req(ctx->auth_sess,
ctx->token, new_req);
} else {
pjsip_auth_clt_async_abandon(ctx->auth_sess, ctx->token);
}
pjsip_rx_data_free_cloned(ctx->rdata_clone);
pjsip_tx_data_dec_ref(ctx->tdata);
free(ctx);
}
static void on_auth_challenge(pjsua_on_auth_challenge_param *prm)
{
struct auth_ctx *ctx;
pj_timer_entry *te;
pj_time_val delay = {1, 0};
ctx = (struct auth_ctx *)malloc(sizeof(*ctx));
ctx->auth_sess = prm->auth_sess;
ctx->token = prm->token;
ctx->tdata = prm->tdata;
ctx->acc_id = prm->acc_id;
pjsip_rx_data_clone(prm->rdata, 0, &ctx->rdata_clone);
pjsip_tx_data_add_ref(ctx->tdata);
prm->handled = PJ_TRUE;
te = (pj_timer_entry *)malloc(sizeof(*te));
pj_timer_entry_init(te, 0, ctx, &on_creds_ready);
pjsip_endpt_schedule_timer(pjsua_get_pjsip_endpt(), te, &delay);
}
PJSIP API
At the lowest level, configure async auth on any
pjsip_auth_clt_sess:
pjsip_auth_clt_async_setting async_opt;
pj_bzero(&async_opt, sizeof(async_opt));
async_opt.cb = &my_on_challenge;
async_opt.user_data = my_context;
pjsip_auth_clt_async_configure(&auth_sess, &async_opt);
The callback signature:
static pj_bool_t my_on_challenge(
pjsip_auth_clt_sess *sess,
void *token,
const pjsip_auth_clt_async_on_chal_param *param)
{
/* Return PJ_TRUE to handle the challenge asynchronously.
* Later call pjsip_auth_clt_async_send_req() or
* pjsip_auth_clt_async_abandon().
*
* Return PJ_FALSE to fall back to synchronous auth.
*/
}
The opaque token carries per-challenge state (allocated from the
transaction pool with a group-lock reference to keep the transaction alive).
After calling async_send_req() or async_abandon(), the token is
invalidated and must not be reused.
Design Notes
Token lifetime
Each challenge allocates a token from the transaction’s pool and takes a
group-lock reference on the transaction. This keeps the transaction alive
until the application calls async_send_req() or async_abandon(),
at which point the reference is released and the token is invalidated.
Sync fallback
If the callback returns PJ_FALSE (or is not set), the library falls
through to the existing synchronous pjsip_auth_clt_reinit_req() path.
This makes the feature fully backward-compatible.