Skip to content

Commit

Permalink
Add cache_per_rhost configuration flag (#98)
Browse files Browse the repository at this point in the history
This allows to cache successful logins per remote host, i.e. per remote
IP address.

When active, the pair 'user_id,rhost' will be cached instead of just
'user_id'. This means that if the user connects from a different IP
address, a new web-based authentication is required.

Use case:

We use SSH with public_key authentication followed by
keyboard-interactive via pam-weblogin. The idea is that a cached web
login effectively results in the usual scriptable public_key
authentication that users are accustomed to.

Setting the cache duration to several hours means that users have to go
through the additional web login typically only once per workday.
However, a stolen key pair might then be enough to access the system
since an active user will have a cached web login most of the time.

Restricting the caching to the remote IP address solves this problem: a
connection from a new IP address requires reauthentication.

---------

Co-authored-by: Martin Lambers <[email protected]>
  • Loading branch information
marlam and Martin Lambers authored Oct 18, 2024
1 parent 147e2ec commit d8e0923
Show file tree
Hide file tree
Showing 9 changed files with 38 additions and 11 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,15 @@ token = <replace with SRAM API TOKEN for your service>
retries = 3
attribute = email
cache_duration = 30
#cache_per_rhost
verify = /etc/ssl/ca.crt
```

- `url` is the pam-weblogin endpoint of the weblogin server
- `token` is the complete HTTP `Authorization` header, including `Bearer`
- `retries` is the number of verification code retries allowed
- `cache_duration` is the time the server should respond with a cached answer instead of reauthenticating the user, in seconds
- `cache_per_rhost`, if activated, signals that caching should take place per remote host, so that connecting from a different IP address requires reauthentication
- `verify` alternative SSL CA, for debug purposes

## Locking yourself out
Expand Down
1 change: 1 addition & 0 deletions pam-weblogin.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ token = Bearer <replace with SRAM API TOKEN for your service>
retries = 3
attribute = email
cache_duration = 30
#cache_per_rhost
verify = /etc/ssl/ca.crt
17 changes: 11 additions & 6 deletions server/weblogin_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ def pop_auth(session_id):
auths.pop(session_id, None)


def pop_cached(user_id):
logging.debug(f"pop cached {user_id}")
cached.pop(user_id, None)
def pop_cached(cache_id):
logging.debug(f"pop cached {cache_id}")
cached.pop(cache_id, None)


def session_id(length=8):
Expand Down Expand Up @@ -133,13 +133,16 @@ def start():
logging.debug(f"/pam-weblogin/start\n <- {data}")

user_id = data.get('user_id')
rhost = data.get('rhost')
cache_per_rhost = data.get('cache_per_rhost')
cache_id = (user_id, rhost) if cache_per_rhost else (user_id, None)
attribute = data.get('attribute')
cache_duration = data.get('cache_duration', 0)
new_session_id = session_id()
url = os.environ.get("URL", config['url']).rstrip('/')
session_url = url + "/pam-weblogin/login/" + new_session_id
qr_code = create_qr(session_url)
cache = cached.get(user_id, False)
cache = cached.get(cache_id, False)
displayname = user_id or 'weblogin'

# The Smart Shell testcase
Expand All @@ -158,6 +161,7 @@ def start():
}
auths[new_session_id]['user_id'] = user_id
auths[new_session_id]['attribute'] = attribute
auths[new_session_id]['cache_id'] = cache_id
auths[new_session_id]['code'] = new_code
auths[new_session_id]['cache_duration'] = cache_duration
Timer(timeout, pop_auth, [new_session_id]).start()
Expand Down Expand Up @@ -198,6 +202,7 @@ def check_pin():
attribute = this_auth.get('attribute')
code = this_auth.get('code')
cache_duration = this_auth.get('cache_duration')
cache_id = this_auth.get('cache_id')
if rcode == code:
reply = {
'result': 'SUCCESS',
Expand Down Expand Up @@ -244,9 +249,9 @@ def check_pin():
],
'info': f'User {user_id} has authenticated successfully ({attribute})'
}
cached[user_id] = True
cached[cache_id] = True
pop_auth(session_id)
Timer(int(cache_duration), pop_cached, [user_id]).start()
Timer(int(cache_duration), pop_cached, [cache_id]).start()
else:
reply = {
'result': 'FAIL',
Expand Down
8 changes: 7 additions & 1 deletion src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Config *getConfig(const char *filename)
cfg->token = NULL;
cfg->attribute = NULL;
cfg->cache_duration = DEFAULT_CACHE_DURATION;
cfg->cache_per_rhost = false;
cfg->retries = DEFAULT_RETRIES;
cfg->pam_user = false;

Expand Down Expand Up @@ -138,12 +139,17 @@ Config *getConfig(const char *filename)
char *val = strchr(key, '=');
if (val == NULL)
{
/* Check for bare pam_user config */
/* Check for bare boolean flags in config */
if (!strcmp(key, "pam_user"))
{
cfg->pam_user = true;
log_message(LOG_DEBUG, "pam_user");
}
else if (!strcmp(key, "cache_per_rhost"))
{
cfg->cache_per_rhost = true;
log_message(LOG_DEBUG, "cache_per_rhost");
}
else
{
log_message(LOG_ERR, "Configuration line: %d: missing '=' symbol, skipping line", lineno);
Expand Down
1 change: 1 addition & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ typedef struct
char *attribute;
bool pam_user;
unsigned int cache_duration;
bool cache_per_rhost;
unsigned int retries;
} Config;

Expand Down
16 changes: 12 additions & 4 deletions src/pam_weblogin.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, UNUSED int flags, int arg
log_message(LOG_ERR, "Error getting user");
return PAM_SYSTEM_ERR;
}
/* Get RHOST. This should always be valid since this PAM module is used with SSH. */
const char *rhost;
if (pam_get_item(pamh, PAM_RHOST, (const void **)(&rhost)) != PAM_SUCCESS || !rhost)
{
log_message(LOG_ERR, "Error getting rhost");
return PAM_SYSTEM_ERR;
}

/* Check if debug argument was given */
if (argc == 2 && strcmp(argv[1], "debug") == 0)
Expand Down Expand Up @@ -79,6 +86,7 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, UNUSED int flags, int arg
log_message(LOG_INFO, "cfg->cache_duration: '%d'\n", cfg->cache_duration);
log_message(LOG_INFO, "cfg->retries: '%d'\n", cfg->retries);
*/
log_message(LOG_INFO, "Starting for user %s from %s", username, rhost);

authorization = str_printf("Authorization: %s", cfg->token);

Expand All @@ -90,12 +98,12 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, UNUSED int flags, int arg
char *data = NULL;
if (!cfg->pam_user) // We check local user against remote user based on attribute
{
data = str_printf("{\"user_id\":\"%s\",\"attribute\":\"%s\",\"cache_duration\":\"%d\",\"GIT_COMMIT\":\"%s\",\"JSONPARSER_GIT_COMMIT\":\"%s\"}",
username, cfg->attribute, cfg->cache_duration, TOSTR(GIT_COMMIT), TOSTR(JSONPARSER_GIT_COMMIT));
data = str_printf("{\"user_id\":\"%s\",\"attribute\":\"%s\",\"rhost\":\"%s\",\"cache_duration\":\"%d\",\"cache_per_rhost\":\"%s\",\"GIT_COMMIT\":\"%s\",\"JSONPARSER_GIT_COMMIT\":\"%s\"}",
username, cfg->attribute, rhost, cfg->cache_duration, cfg->cache_per_rhost ? "true" : "false", TOSTR(GIT_COMMIT), TOSTR(JSONPARSER_GIT_COMMIT));
} else // We get local user from remote user based on attribute
{
data = str_printf("{\"attribute\":\"%s\",\"cache_duration\":\"%d\",\"GIT_COMMIT\":\"%s\",\"JSONPARSER_GIT_COMMIT\":\"%s\"}",
cfg->attribute, cfg->cache_duration, TOSTR(GIT_COMMIT), TOSTR(JSONPARSER_GIT_COMMIT));
data = str_printf("{\"attribute\":\"%s\",\"rhost\":\"%s\",\"cache_duration\":\"%d\",\"cache_per_rhost\":\"%s\",\"GIT_COMMIT\":\"%s\",\"JSONPARSER_GIT_COMMIT\":\"%s\"}",
cfg->attribute, rhost, cfg->cache_duration, cfg->cache_per_rhost ? "true" : "false", TOSTR(GIT_COMMIT), TOSTR(JSONPARSER_GIT_COMMIT));
}

/* Request auth session_id/challenge */
Expand Down
1 change: 1 addition & 0 deletions tests/pam-weblogin.conf_1
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ retries = 3
attribute = uid
# cache_duration=60
cache_duration=60
#cache_per_rhost
#pam_user
pam_user
nonsense
Expand Down
1 change: 1 addition & 0 deletions tests/pam-weblogin.conf_2
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ retries = 3
attribute = uid
# cache_duration=60
cache_duration=0
cache_per_rhost
#pam_user
pam_user
nonsense
Expand Down
2 changes: 2 additions & 0 deletions tests/test_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ START_TEST (test_getConfig_1)
ck_assert_int_eq(cfg->retries, 3);
ck_assert_str_eq(cfg->attribute, "uid");
ck_assert_int_eq(cfg->cache_duration, 60);
ck_assert(!cfg->cache_per_rhost);
ck_assert(cfg->pam_user);
}
END_TEST
Expand All @@ -35,6 +36,7 @@ START_TEST (test_getConfig_2)
ck_assert_int_eq(cfg->retries, 3);
ck_assert_str_eq(cfg->attribute, "uid");
ck_assert_int_eq(cfg->cache_duration, 0);
ck_assert(cfg->cache_per_rhost);
ck_assert(cfg->pam_user);
}
END_TEST
Expand Down

0 comments on commit d8e0923

Please sign in to comment.