/* Attempting to refresh an access token without a refresh token. */
MH_OAUTH_NO_REFRESH,
+
+ /* requested user not in cred file */
+ MH_OAUTH_CRED_USER_NOT_FOUND,
+
/* error loading serialized credentials */
MH_OAUTH_CRED_FILE
} mh_oauth_err_code;
* On error, return FALSE.
*/
boolean
-mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred);
+mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred, const char *user);
/*
* Load OAuth tokens from file.
* On error, return NULL.
*/
mh_oauth_cred *
-mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx);
+mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx, const char *user);
/*
* Return null-terminated SASL client response for XOAUTH2 from access token.
/* Ignoring token_type ([1] 7.1) because
* https://developers.google.com/accounts/docs/OAuth2InstalledApp says
* "Currently, this field always has the value Bearer". */
+
+ /* only filled while loading cred files, otherwise NULL */
+ char *user;
};
struct mh_oauth_ctx {
adios(fn, "failed to lock");
}
- if ((cred = mh_oauth_cred_load(fp, ctx)) == NULL) {
+ if ((cred = mh_oauth_cred_load(fp, ctx, user)) == NULL) {
adios(NULL, mh_oauth_get_err_string(ctx));
}
}
fseek(fp, 0, SEEK_SET);
- if (!mh_oauth_cred_save(fp, cred)) {
+ if (!mh_oauth_cred_save(fp, cred, user)) {
adios(NULL, mh_oauth_get_err_string(ctx));
}
}
case MH_OAUTH_NO_REFRESH:
base = "no refresh token";
break;
+ case MH_OAUTH_CRED_USER_NOT_FOUND:
+ base = "user not found in cred file";
+ break;
case MH_OAUTH_CRED_FILE:
base = "error loading cred file";
break;
return ctx->cred_fn = result;
}
-boolean
-mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred)
+/* for loading multi-user cred files */
+struct user_creds {
+ mh_oauth_cred *creds;
+
+ /* number of allocated mh_oauth_cred structs above points to */
+ size_t alloc;
+
+ /* number that are actually filled in and used */
+ size_t len;
+};
+
+/* If user has an entry in user_creds, return pointer to it. Else allocate a
+ * new struct in user_creds and return pointer to that. */
+static mh_oauth_cred *
+find_or_alloc_user_creds(struct user_creds user_creds[], const char *user)
{
- int fd = fileno(fp);
- if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) goto err;
- if (ftruncate(fd, 0) < 0) goto err;
- if (cred->access_token != NULL) {
- if (fprintf(fp, "access: %s\n", cred->access_token) < 0) goto err;
- }
- if (cred->refresh_token != NULL) {
- if (fprintf(fp, "refresh: %s\n", cred->refresh_token) < 0) goto err;
+ mh_oauth_cred *creds = user_creds->creds;
+ size_t i;
+ for (i = 0; i < user_creds->len; i++) {
+ if (strcmp(creds[i].user, user) == 0) {
+ return &creds[i];
+ }
}
- if (cred->expires_at > 0) {
- if (fprintf(fp, "expire: %ld\n", (long)cred->expires_at) < 0) goto err;
+ if (user_creds->alloc == user_creds->len) {
+ user_creds->alloc *= 2;
+ user_creds->creds = mh_xrealloc(user_creds->creds, user_creds->alloc);
}
- return TRUE;
+ creds = user_creds->creds+user_creds->len;
+ user_creds->len++;
+ creds->user = getcpy(user);
+ creds->access_token = creds->refresh_token = NULL;
+ creds->expires_at = 0;
+ return creds;
+}
- err:
- set_err(cred->ctx, MH_OAUTH_CRED_FILE);
- return FALSE;
+static void
+free_user_creds(struct user_creds *user_creds)
+{
+ mh_oauth_cred *cred;
+ size_t i;
+ cred = user_creds->creds;
+ for (i = 0; i < user_creds->len; i++) {
+ free(cred[i].user);
+ free(cred[i].access_token);
+ free(cred[i].refresh_token);
+ }
+ free(user_creds->creds);
+ free(user_creds);
}
static boolean
-parse_cred(char **access, char **refresh, char **expire, FILE *fp,
- mh_oauth_ctx *ctx)
+load_creds(struct user_creds **result, FILE *fp, mh_oauth_ctx *ctx)
{
- boolean result = FALSE;
+ boolean success = FALSE;
char name[NAMESZ], value_buf[BUFSIZ];
int state;
m_getfld_state_t getfld_ctx = 0;
+ struct user_creds *user_creds = mh_xmalloc(sizeof *user_creds);
+ user_creds->alloc = 4;
+ user_creds->len = 0;
+ user_creds->creds = mh_xmalloc(user_creds->alloc * sizeof *user_creds->creds);
+
for (;;) {
- int size = sizeof value_buf;
+ size_t size = sizeof value_buf;
switch (state = m_getfld(&getfld_ctx, name, value_buf, &size, fp)) {
case FLD:
case FLDPLUS: {
- char **save;
- if (strcmp(name, "access") == 0) {
- save = access;
- } else if (strcmp(name, "refresh") == 0) {
- save = refresh;
- } else if (strcmp(name, "expire") == 0) {
- save = expire;
+ char **save, *expire;
+ time_t *expires_at = NULL;
+ if (strncmp(name, "access-", 7) == 0) {
+ const char *user = name + 7;
+ mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
+ user);
+ save = &creds->access_token;
+ } else if (strncmp(name, "refresh-", 8) == 0) {
+ const char *user = name + 8;
+ mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
+ user);
+ save = &creds->refresh_token;
+ } else if (strncmp(name, "expire-", 7) == 0) {
+ const char *user = name + 7;
+ mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
+ user);
+ expires_at = &creds->expires_at;
+ save = &expire;
} else {
set_err_details(ctx, MH_OAUTH_CRED_FILE, "unexpected field");
break;
*save = trimcpy(tmp);
free(tmp);
}
+ if (expires_at != NULL) {
+ errno = 0;
+ *expires_at = strtol(expire, NULL, 10);
+ free(expire);
+ if (errno != 0) {
+ set_err_details(ctx, MH_OAUTH_CRED_FILE,
+ "invalid expiration time");
+ break;
+ }
+ expires_at = NULL;
+ }
continue;
}
case BODY:
case FILEEOF:
- result = TRUE;
+ success = TRUE;
break;
default:
break;
}
m_getfld_state_destroy(&getfld_ctx);
- return result;
+
+ if (success) {
+ *result = user_creds;
+ } else {
+ free_user_creds(user_creds);
+ }
+
+ return success;
+}
+
+static boolean
+save_user(FILE *fp, const char *user, const char *access, const char *refresh,
+ long expires_at)
+{
+ if (access != NULL) {
+ if (fprintf(fp, "access-%s: %s\n", user, access) < 0) return FALSE;
+ }
+ if (refresh != NULL) {
+ if (fprintf(fp, "refresh-%s: %s\n", user, refresh) < 0) return FALSE;
+ }
+ if (expires_at > 0) {
+ if (fprintf(fp, "expire-%s: %ld\n", user, (long)expires_at) < 0) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+boolean
+mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred, const char *user)
+{
+ struct user_creds *user_creds;
+ int fd = fileno(fp);
+ size_t i;
+
+ /* Load existing creds if any. */
+ if (!load_creds(&user_creds, fp, cred->ctx)) {
+ return FALSE;
+ }
+
+ if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) goto err;
+ if (ftruncate(fd, 0) < 0) goto err;
+ if (fseek(fp, 0, SEEK_SET) < 0) goto err;
+
+ /* Write all creds except for this user. */
+ for (i = 0; i < user_creds->len; i++) {
+ mh_oauth_cred *c = &user_creds->creds[i];
+ if (strcmp(c->user, user) == 0) continue;
+ if (!save_user(fp, c->user, c->access_token, c->refresh_token,
+ c->expires_at)) {
+ goto err;
+ }
+ }
+
+ /* Write updated creds for this user. */
+ if (!save_user(fp, user, cred->access_token, cred->refresh_token,
+ cred->expires_at)) {
+ goto err;
+ }
+
+ free_user_creds(user_creds);
+
+ return TRUE;
+
+ err:
+ free_user_creds(user_creds);
+ set_err(cred->ctx, MH_OAUTH_CRED_FILE);
+ return FALSE;
}
mh_oauth_cred *
-mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx)
+mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx, const char *user)
{
- mh_oauth_cred *result;
- time_t expires_at = 0;
- char *access, *refresh, *expire;
-
- access = refresh = expire = NULL;
- if (!parse_cred(&access, &refresh, &expire, fp, ctx)) {
- free(access);
- free(refresh);
- free(expire);
+ mh_oauth_cred *creds, *result = NULL;
+ struct user_creds *user_creds;
+ size_t i;
+
+ if (!load_creds(&user_creds, fp, ctx)) {
return NULL;
}
- if (expire != NULL) {
- errno = 0;
- expires_at = strtol(expire, NULL, 10);
- free(expire);
- if (errno != 0) {
- set_err_details(ctx, MH_OAUTH_CRED_FILE, "invalid expiration time");
- free(access);
- free(refresh);
- return NULL;
+ /* Search user_creds for this user. If we don't find it, return NULL.
+ * If we do, free fields of all structs except this one, moving this one to
+ * the first struct if necessary. When we return it, it just looks like one
+ * struct to the caller, and the whole array is freed later. */
+ creds = user_creds->creds;
+ for (i = 0; i < user_creds->len; i++) {
+ if (strcmp(creds[i].user, user) == 0) {
+ result = creds;
+ if (i > 0) {
+ result->access_token = creds[i].access_token;
+ result->refresh_token = creds[i].refresh_token;
+ result->expires_at = creds[i].expires_at;
+ }
+ } else {
+ free(creds[i].access_token);
+ free(creds[i].refresh_token);
}
+ free(creds[i].user);
+ }
+
+ if (result == NULL) {
+ set_err_details(ctx, MH_OAUTH_CRED_USER_NOT_FOUND, user);
+ return NULL;
}
- result = mh_xmalloc(sizeof *result);
result->ctx = ctx;
- result->access_token = access;
- result->refresh_token = refresh;
- result->expires_at = expires_at;
+ result->user = NULL;
return result;
}
# it against our "correct" output.
f="${MHTMPDIR}/oauth-test"
- sed 's/^expire:.*/expire:/' "$f" > "$f".notime
+ sed 's/^\(expire.*:\).*/\1/' "$f" > "$f".notime
check "$f".notime "${MHTMPDIR}/$$.expected-creds"
rm "$f"
}
start_test 'access token ready, pop server accepts message'
fake_creds <<EOF
-access: test-access
-expire: 2000000000
+access-nobody@example.com: test-access
+expire-nobody@example.com: 2000000000
EOF
start_pop_xoauth
start_test 'expired access token, refresh works, pop server accepts message'
fake_creds <<EOF
-access: old-access
-refresh: test-refresh
-expire: 1414303986
+access-nobody@example.com: old-access
+refresh-nobody@example.com: test-refresh
+expire-nobody@example.com: 1414303986
EOF
expect_http_post_refresh
start_test 'refresh gets proper error from http'
fake_creds <<EOF
-access: test-access
-refresh: test-refresh
+access-nobody@example.com: test-access
+refresh-nobody@example.com: test-refresh
EOF
expect_http_post_refresh
start_test 'pop server rejects token'
fake_creds <<EOF
-access: wrong-access
-expire: 2000000000
+access-nobody@example.com: wrong-access
+expire-nobody@example.com: 2000000000
EOF
start_pop_xoauth
start_test "pop server doesn't support oauth"
fake_creds <<EOF
-access: test-access
-expire: 2000000000
+access-nobody@example.com: test-access
+expire-nobody@example.com: 2000000000
EOF
start_pop testuser testpass
test_mhlogin() {
start_fakehttp
- run_test 'eval echo code | mhlogin -saslmech xoauth2 -authservice test' \
+ run_test 'eval echo code | mhlogin -saslmech xoauth2 -authservice test -user nobody@example.com' \
"Load the following URL in your browser and authorize nmh to access test:
http://127.0.0.1:${http_port}/oauth/auth?response_type=code&client_id=test-id&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=test-scope
EOF
expect_creds <<EOF
-access: test-access
-expire:
+access-nobody@example.com: test-access
+expire-nobody@example.com:
EOF
test_mhlogin
EOF
expect_creds <<EOF
-access: test-access
+access-nobody@example.com: test-access
EOF
test_mhlogin
EOF
expect_creds <<EOF
-access: test-access
-refresh: refresh-token
-expire:
+access-nobody@example.com: test-access
+refresh-nobody@example.com: refresh-token
+expire-nobody@example.com:
EOF
test_mhlogin
EOF
expect_creds <<EOF
-refresh: refresh-token
+refresh-nobody@example.com: refresh-token
EOF
test_mhlogin
EOF
expect_creds <<EOF
-access: test-access
-refresh: refresh-token
-expire:
+access-nobody@example.com: test-access
+refresh-nobody@example.com: refresh-token
+expire-nobody@example.com:
EOF
test_mhlogin
+# TEST
+start_test 'mhlogin multiple users'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+ "access_token": "user3-access",
+ "refresh_token": "user3-refresh",
+ "expires_in": 3600,
+ "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access-nobody@example.com: user1-access
+refresh-nobody@example.com: user1-refresh
+expire-nobody@example.com:
+access-nobody2@example.com: user2-access
+refresh-nobody2@example.com: user2-refresh
+expire-nobody2@example.com:
+access-nobody3@example.com: user3-access
+refresh-nobody3@example.com: user3-refresh
+expire-nobody3@example.com:
+EOF
+
+fake_creds <<EOF
+access-nobody@example.com: user1-access
+refresh-nobody@example.com: user1-refresh
+expire-nobody@example.com: 100
+access-nobody2@example.com: user2-access
+refresh-nobody2@example.com: user2-refresh
+expire-nobody2@example.com: 100
+EOF
+
+start_fakehttp
+run_test 'eval echo code | mhlogin -saslmech xoauth2 -authservice test -user nobody3@example.com' \
+ "Load the following URL in your browser and authorize nmh to access test:
+
+http://127.0.0.1:${http_port}/oauth/auth?response_type=code&client_id=test-id&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=test-scope
+
+Enter the authorization code: $1"
+check_http_req
+check_creds_private
+check_creds
+
+#
+# fail cases
+#
+
# TEST
start_test 'mhlogin user enters bad code'
test_mhlogin 'Code rejected; try again? '
-#
-# fail cases
-#
-
# TEST
start_test 'mhlogin response has no content-type'
start_test 'mhlogin -browser'
run_test "eval echo code | mhlogin -saslmech xoauth2 -authservice test\
- -browser 'echo \$@ > ${MHTMPDIR}/$$.browser'" \
+ -user nobody@example.com -browser 'echo \$@ > ${MHTMPDIR}/$$.browser'" \
"Follow the prompts in your browser to authorize nmh to access test.
Enter the authorization code: mhlogin: error exchanging code for OAuth2 token
mhlogin: error making HTTP request to OAuth2 authorization endpoint: Failed to connect to 127.0.0.1 port ${http_port}: Connection refused"
setup_draft
fake_creds <<EOF
-access: test-access
-refresh: test-refresh
-expire: 2000000000
+access-nobody@example.com: test-access
+refresh-nobody@example.com: test-refresh
+expire-nobody@example.com: 2000000000
EOF
start_fakesmtp
setup_draft
fake_creds <<EOF
-access: old-access
-refresh: test-refresh
-expire: 1414303986
+access-nobody@example.com: old-access
+refresh-nobody@example.com: test-refresh
+expire-nobody@example.com: 1414303986
EOF
expect_http_post_refresh
EOF
expect_creds <<EOF
-access: test-access
-refresh: test-refresh
-expire:
+access-nobody@example.com: test-access
+refresh-nobody@example.com: test-refresh
+expire-nobody@example.com:
EOF
test_send
setup_draft
fake_creds <<EOF
-access: old-access
-refresh: old-refresh
-expire: 1414303986
+access-nobody@example.com: old-access
+refresh-nobody@example.com: old-refresh
+expire-nobody@example.com: 1414303986
EOF
expect_http_post_old_refresh
EOF
expect_creds <<EOF
-access: test-access
-refresh: test-refresh
+access-nobody@example.com: test-access
+refresh-nobody@example.com: test-refresh
EOF
test_send
setup_draft
fake_creds <<EOF
-access: old-access
-refresh: test-refresh
+access-nobody@example.com: old-access
+refresh-nobody@example.com: test-refresh
EOF
expect_http_post_refresh
EOF
expect_creds <<EOF
-access: test-access
-refresh: test-refresh
+access-nobody@example.com: test-access
+refresh-nobody@example.com: test-refresh
EOF
test_send
setup_draft
fake_creds <<EOF
-refresh: test-refresh
+refresh-nobody@example.com: test-refresh
EOF
expect_http_post_refresh
EOF
expect_creds <<EOF
-access: test-access
-refresh: test-refresh
+access-nobody@example.com: test-access
+refresh-nobody@example.com: test-refresh
EOF
test_send
fake_creds < /dev/null
-test_send_no_servers 'send: no valid credentials -- run mhlogin -saslmech xoauth2 -authservice test'
+test_send_no_servers 'send: user not found in cred file: nobody@example.com'
# TEST
start_test 'garbage creds file'
fake_creds <<EOF
bork: bork
-access: test-access
+access-nobody@example.com: test-access
EOF
test_send_no_servers 'send: error loading cred file: unexpected field'
start_test 'garbage expiration time'
fake_creds <<EOF
-access: test-access
-expire: 99999999999999999999999999999999
+access-nobody@example.com: test-access
+expire-nobody@example.com: 99999999999999999999999999999999
EOF
test_send_no_servers 'send: error loading cred file: invalid expiration time'
start_test 'refresh response has no access token'
fake_creds <<EOF
-refresh: test-refresh
+refresh-nobody@example.com: test-refresh
EOF
expect_http_post_refresh
start_test 'expired access token, no refresh token -- tell user to mhlogin'
fake_creds <<EOF
-access: test-access
-expire: 1414303986
+access-nobody@example.com: test-access
+expire-nobody@example.com: 1414303986
EOF
test_send_no_servers 'send: no valid credentials -- run mhlogin -saslmech xoauth2 -authservice test'
start_test 'access token has no expiration, no refresh token -- tell user to mhlogin'
fake_creds <<EOF
-access: test-access
+access-nobody@example.com: test-access
EOF
test_send_no_servers 'send: no valid credentials -- run mhlogin -saslmech xoauth2 -authservice test'
start_test 'refresh finds no http server'
fake_creds <<EOF
-access: test-access
-refresh: test-refresh
+access-nobody@example.com: test-access
+refresh-nobody@example.com: test-refresh
EOF
cat > "${testname}.expected-send-output" <<EOF
start_test 'refresh gets response too big'
fake_creds <<EOF
-refresh: test-refresh
+refresh-nobody@example.com: test-refresh
EOF
expect_http_post_refresh
XOAUTH='not-that-one'
fake_creds <<EOF
-access: test-access
-expire: 2000000000
+access-nobody@example.com: test-access
+expire-nobody@example.com: 2000000000
EOF
test_send_only_fakesmtp 'post: problem initializing server; [BHST] Not no way, not no how!
EOF
expect_creds <<EOF
-access: test-access
-expire:
+access-nobody@example.com: test-access
+expire-nobody@example.com:
EOF
start_fakehttp
-run_test 'eval echo code | mhlogin -saslmech xoauth2 -authservice test' \
+run_test 'eval echo code | mhlogin -user nobody@example.com -saslmech xoauth2 -authservice test' \
"Load the following URL in your browser and authorize nmh to access test:
http://127.0.0.1:${http_port}/oauth/auth?response_type=code&client_id=test-id&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=test-scope
start_test 'inc refreshes'
fake_creds <<EOF
-access: old-access
-refresh: test-refresh
-expire: 1414303986
+access-nobody@example.com: old-access
+refresh-nobody@example.com: test-refresh
+expire-nobody@example.com: 1414303986
EOF
expect_http_post_refresh
start_test 'msgchck refreshes'
fake_creds <<EOF
-access: old-access
-refresh: test-refresh
-expire: 1414303986
+access-nobody@example.com: old-access
+refresh-nobody@example.com: test-refresh
+expire-nobody@example.com: 1414303986
EOF
expect_http_post_refresh
start_test 'send refreshes'
fake_creds <<EOF
-access: old-access
-refresh: test-refresh
-expire: 1414303986
+access-nobody@example.com: old-access
+refresh-nobody@example.com: test-refresh
+expire-nobody@example.com: 1414303986
EOF
expect_http_post_refresh
* complete copyright information.
*/
+#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <h/oauth.h>
#define MHLOGIN_SWITCHES \
+ X("user username", 0, USERSW) \
X("saslmech", 0, SASLMECHSW) \
X("authservice", 0, AUTHSERVICESW) \
X("browser", 0, BROWSERSW) \
}
static int
-do_login(const char *svc, const char *browser, int snoop)
+do_login(const char *svc, const char *user, const char *browser, int snoop)
{
char *fn, *code;
mh_oauth_ctx *ctx;
adios(NULL, "missing -authservice switch");
}
+ if (user == NULL) {
+ adios(NULL, "missing -user switch");
+ }
+
if (!mh_oauth_new(&ctx, svc)) {
adios(NULL, mh_oauth_get_err_string(ctx));
}
adios(NULL, mh_oauth_get_err_string(ctx));
}
- cred_file = lkfopendata(fn, "w", &failed_to_lock);
+ cred_file = lkfopendata(fn, "r+", &failed_to_lock);
+ if (cred_file == NULL && errno == ENOENT) {
+ cred_file = lkfopendata(fn, "w+", &failed_to_lock);
+ }
if (cred_file == NULL || failed_to_lock) {
adios(fn, "oops");
}
- if (!mh_oauth_cred_save(cred_file, cred)) {
+ if (!mh_oauth_cred_save(cred_file, cred, user)) {
adios(NULL, mh_oauth_get_err_string(ctx));
}
if (lkfclosedata(cred_file, fn) != 0) {
main(int argc, char **argv)
{
char *cp, **argp, **arguments;
- const char *saslmech = NULL, *svc = NULL, *browser = NULL;
+ const char *user = NULL, *saslmech = NULL, *svc = NULL, *browser = NULL;
int snoop = 0;
if (nmh_init(argv[0], 1)) { return 1; }
print_version(invo_name);
done (0);
+ case USERSW:
+ if (!(user = *argp++) || *user == '-')
+ adios (NULL, "missing argument to %s", argp[-2]);
+ continue;
+
case SASLMECHSW:
if (!(saslmech = *argp++) || *saslmech == '-')
adios (NULL, "missing argument to %s", argp[-2]);
free(arguments);
#ifdef OAUTH_SUPPORT
- return do_login(svc, browser, snoop);
+ return do_login(svc, user, browser, snoop);
#else
NMH_UNUSED(svc);
NMH_UNUSED(browser);