Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ google_authenticator_SOURCES = \

pam_google_authenticator_la_SOURCES = \
src/pam_google_authenticator.c \
src/pam_util.c \
$(CORE_SRC)
if ENABLE_AUTHY
pam_google_authenticator_la_SOURCES += src/pam_authy.c
endif
pam_google_authenticator_la_LIBADD = -lpam
pam_google_authenticator_la_CFLAGS = $(AM_CFLAGS)
pam_google_authenticator_la_LDFLAGS = $(AM_LDFLAGS) $(MODULES_LDFLAGS) -export-symbols-regex "pam_sm_(setcred|open_session|authenticate)"
Expand Down
65 changes: 65 additions & 0 deletions README.authy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Authy push authentication for Google Authenticator PAM module
This extension uses Authy (https://authy.com/) to authenticate
using push notifications.

## Build, install, setup
See https://github.com/google/google-authenticator-libpam

Authy extension requires the following extra dependencies:
- libjansson
- libcurl

If running Debian-based distro, do:
```
sudo apt install libjansson-dev libcurl4-gnutls-dev
```

## Extra PAM module options
### enable_authy
If set, Authy extension is enabled. PAM module sends push notification
to the Authy authenticator on user login. If authentication passes,
login is granted. On failure, the classic Google OTP is used.

## Extra .google_authenticator fields
To setup Authy authentication, generate .google_authenticator file as described
in https://github.com/google/google-authenticator-libpam and add the following
extra fields.

### AUTHY_ID
See https://support.authy.com/hc/en-us/articles/360016449054-Find-your-Authy-ID
how to find out your Authy ID.

Example:
```
" AUTHY_ID 123456789"
```
### AUTHY_API_KEY
See: https://www.twilio.com/docs/authy/twilioauth-sdk/quickstart/obtain-authy-api-key

Obtaining an Authy API Key:
1. Create a Twilio account: https://www.twilio.com/try-twilio
2. Create an Authy application in the Twilio Console.
3. Once you've created a new Authy application, copy the API Key for
Production available in the Settings page of your Authy application.

Example:
```
" AUTHY_API_KEY aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP
```

### .google_authenticator example
```
00000000000000000000000000
" RESETTING_TIME_SKEW 52968321+21 52968328+21 52968508+20
" RATE_LIMIT 3 30 1589055237
" WINDOW_SIZE 17
" TOTP_AUTH
" AUTHY_ID 000000000
" AUTHY_API_KEY xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
11111111
22222222
33333333
"
```

### TODO: add Authy fields generation in google-authenticator tool
33 changes: 33 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,38 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[

AC_LANG_POP(C)

AC_ARG_ENABLE([authy],
[AC_HELP_STRING([--enable-authy],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not enable it by default if the library is found?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I'm going to rewrite configure.ac

[enable Authy push authentcation [default=no]])],
[
xenable_authy=$enableval
AC_DEFINE(ENABLE_AUTHY, [1], [Enable Authy extension])
],
[xenable_authy=no])
AM_CONDITIONAL([ENABLE_AUTHY], [false])

AC_CHECK_LIB([curl], [curl_global_init],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an autoconf expert, but why not just AC_CHECK_LIB([curl], [curl_global_init]) ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I'm going to rewrite configure.ac

[
LIBS="$LIBS -lcurl"
have_curl=yes
],[ have_curl=no ],[])
AC_CHECK_HEADERS([curl/curl.h])

AC_CHECK_LIB([jansson], [json_loads],
[
LIBS="$LIBS -ljansson"
have_jansson=yes
],[ have_jansson=no ],[])
AC_CHECK_HEADERS([jansson.h])

if test "x${xenable_authy}" == "x${enableval}"; then
if test "x${have_jansson}" == "xno"; then
AC_MSG_ERROR([libjannson is missing])
fi
if test "x${have_jansson}" == "xno" || test "x${have_curl}" == "xno"; then
AC_MSG_ERROR([libcurl is missing])
fi
fi


AC_SEARCH_LIBS([dlopen], [dl])
Expand All @@ -81,6 +113,7 @@ AC_OUTPUT

echo "
$PACKAGE_NAME version $PACKAGE_VERSION
Authy..........: $xenable_authy
Prefix.........: $prefix
Debug Build....: $debug
C Compiler.....: $CC $CFLAGS $CPPFLAGS
Expand Down
255 changes: 255 additions & 0 deletions src/pam_authy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
#define _GNU_SOURCE

#include "config.h"
#include <stdio.h>
#include <unistd.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <syslog.h>
#include <jansson.h>
#include <curl/curl.h>

#include <security/pam_appl.h>
#include <security/pam_modules.h>

#include "pam_util.h"
#include "pam_authy.h"

typedef struct {
char *memory;
size_t size;
} mblock_t;

static size_t ctrl_curl_receive(void *content, size_t size, size_t nmemb,
void *user_mem)
{
size_t realsize = size * nmemb;
mblock_t *mem = (mblock_t *)user_mem;

mem->memory = realloc(mem->memory, mem->size + realsize + 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look integer overflow safe.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the reference example from libcurl documentation: (https://curl.haxx.se/libcurl/c/CURLOPT_WRITEFUNCTION.htm)
I have to take a closer look how to make it more safe.

if (mem->memory == NULL) {
return -ENOMEM;
}

memcpy(&(mem->memory[mem->size]), content, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0;

return realsize;
}

static authy_rc_t authy_check_aproval(pam_handle_t *pamh, char *api_key, char *uuid)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"approval"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

{
CURL *curl = NULL;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define variables later. E.g. instead of what's here now, do:

CURL *curl = curl_easy_init();

That way the scope of variables is reduced making it clearer where they are set.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, scope will be reduced

CURLcode res;
authy_rc_t rc;
mblock_t buffer = {0};
struct curl_slist *headers = NULL;
char *url = NULL, *xheader = NULL, *str = NULL;
json_t *payload = NULL, *jt = NULL;

curl = curl_easy_init();
if (!curl) {
log_message(LOG_ERR, pamh, "authy_err: curl init failed\n");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for newline.
Applies to all calls to log_message

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

rc = AUTHY_LIB_ERROR;
goto exit_err;
}

asprintf(&url, "https://api.authy.com/onetouch/json/approval_requests/%s",
uuid);
asprintf(&xheader, "X-Authy-API-Key: %s", api_key);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asprintf is a GNU extension, so I think this might reduce the portability.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, asprintfs will be removed

headers = curl_slist_append(headers, xheader);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, ctrl_curl_receive);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&buffer);

res = curl_easy_perform(curl);
if (res != CURLE_OK) {
log_message(LOG_ERR, pamh, "authy_err: curl call failed: %d (%s)\n",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove newline

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

res, curl_easy_strerror(res));
rc = AUTHY_CONN_ERROR;
goto exit_err;
}
rc = AUTHY_OK;

payload = json_loads(buffer.memory, JSON_DECODE_ANY, NULL);
jt = json_object_get(payload, "approval_request");
if (!jt) {
log_message(LOG_ERR, pamh, "authy_err: 'approval_request' field missing\n");
rc = AUTHY_CONN_ERROR;
goto exit_err;
}

str = (char *)json_string_value(json_object_get(jt, "status"));
if (!str) {
rc = AUTHY_CONN_ERROR;
goto exit_err;
}

if (!strcmp(str, "pending")) {
rc = AUTHY_PENDING;
} else if (!strcmp(str, "expired")) {
rc = AUTHY_EXPIRED;
} else if (!strcmp(str, "denied")) {
rc = AUTHY_DENIED;
} else if (!strcmp(str, "approved")) {
rc = AUTHY_APPROVED;
}

exit_err:
if (buffer.memory)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Freeing null pointers is safe. No need to check if they're null.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, null pointer checking before free will be removed

free(buffer.memory);

if (jt)
free(jt);

if (str)
free(str);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


if (payload)
free(payload);

if (url)
free(url);

if (curl)
curl_easy_cleanup(curl);

return rc;
}

static authy_rc_t authy_post_aproval(pam_handle_t *pamh, long authy_id, char *api_key, int timeout, char **uuid)
{
CURL *curl = NULL;
CURLcode res;
authy_rc_t rc;
mblock_t buffer = {0};
struct curl_slist *headers = NULL;
char *url = NULL, *xheader = NULL, *str = NULL;
json_t *payload = NULL, *jt = NULL;
char *data = NULL;
char hostname[128] = { 0 };
const char *username;

curl = curl_easy_init();
if (!curl) {
log_message(LOG_ERR, pamh, "authy_err: curl init failed\n");
rc = AUTHY_LIB_ERROR;
goto exit_err;
}

pam_get_user(pamh, &username, NULL);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always check for errors.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, pam_get_user will check the result the same way as in pam_google_authenticator.c

if (gethostname(hostname, sizeof(hostname)-1)) {
strcpy(hostname, "unix");
}

asprintf(&url, "https://api.authy.com/onetouch/json/users/%ld/approval_requests",
authy_id);
asprintf(&xheader, "X-Authy-API-Key: %s", api_key);
headers = curl_slist_append(headers, xheader);
asprintf(&data, "message=Login authentication");
asprintf(&data, "%s&details=%s at %s", data, username, hostname);
asprintf(&data, "%s&seconds_to_expire=%d", data, timeout);
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(data));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a client-side timeout in case the server hangs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, CURLOPT_TIMEOUT will be added to the curl call

curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, ctrl_curl_receive);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&buffer);

res = curl_easy_perform(curl);
if (res != CURLE_OK) {
log_message(LOG_ERR, pamh, "authy_err: curl call failed: %d (%s)\n",
res, curl_easy_strerror(res));
rc = AUTHY_CONN_ERROR;
goto exit_err;
}
rc = AUTHY_OK;

payload = json_loads(buffer.memory, JSON_DECODE_ANY, NULL);
jt = json_object_get(payload, "approval_request");
if (!jt) {
log_message(LOG_ERR, pamh, "authy_err: 'approval_request' field missing\n");
rc = AUTHY_CONN_ERROR;
goto exit_err;
}
str = (char *)json_string_value(json_object_get(jt, "uuid"));
if (!str) {
log_message(LOG_ERR, pamh, "authy_err: 'uuid' field missing\n");
rc = AUTHY_CONN_ERROR;
goto exit_err;
}
asprintf(uuid, "%s", str);

exit_err:
if (buffer.memory)
free(buffer.memory);

if (jt)
free(jt);

if (str)
free(str);

if (payload)
free(payload);

if (url)
free(url);

if (data)
free(data);

if (curl)
curl_easy_cleanup(curl);

return rc;
}

authy_rc_t authy_login(pam_handle_t *pamh, long authy_id, char *api_key, int timeout)
{
time_t start_time;
authy_rc_t rc;
char *uuid = NULL;
char *err_str = NULL;

log_message(LOG_INFO, pamh, "authy_dbg: Sending Authy authentication push request\n");
rc = authy_post_aproval(pamh, authy_id, api_key, 30, &uuid);
if (rc != AUTHY_OK) {
log_message(LOG_ERR, pamh, "authy_err: Push Authentication request failed\n");
goto exit_err;
}

log_message(LOG_INFO, pamh, "authy_dbg: Waiting for Authy authentication approval\n");
start_time = time(NULL);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use clock_gettime() instead, so that this doesn't break if login happens when clock is set.

https://blog.habets.se/2010/09/gettimeofday-should-never-be-used-to-measure-time.html

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

do {
rc = authy_check_aproval(pamh, api_key, uuid);
switch (rc) {
case AUTHY_DENIED:
err_str = "denied";
goto exit_err;
case AUTHY_EXPIRED:
err_str = "expired";
goto exit_err;
case AUTHY_APPROVED:
log_message(LOG_INFO, pamh, "authy_dbg: Authentication approved\n");
goto exit_err;
default:
break;
}
sleep(1);
} while ((start_time + timeout + 5) > time(NULL));
rc = AUTHY_EXPIRED;
err_str = "expired (pam timeout)";

exit_err:
if (err_str)
log_message(LOG_ERR, pamh, "authy_err: Authentication %s\n", err_str);

if (uuid)
free(uuid);

return rc;
}
16 changes: 16 additions & 0 deletions src/pam_authy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#ifndef AUTHY_H
#define AUTHY_H

typedef enum {
AUTHY_OK = 0,
AUTHY_LIB_ERROR,
AUTHY_CONN_ERROR,
AUTHY_APPROVED,
AUTHY_DENIED,
AUTHY_PENDING,
AUTHY_EXPIRED,
} authy_rc_t;

authy_rc_t authy_login(pam_handle_t *pamh, long authy_id, char *api_key, int timeout);

#endif /* AUTHY_H */
Loading