Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ 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
else
pam_google_authenticator_la_SOURCES += src/pam_no_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
20 changes: 20 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,26 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[

AC_LANG_POP(C)

AC_CHECK_LIB([curl], [curl_global_init], [have_curl=yes])
AC_CHECK_LIB([jansson], [json_loads], [have_jansson=yes])

AC_ARG_ENABLE([authy],
[AC_HELP_STRING([--disable-authy],
[enable Authy push authentcation [default=auto]])],
[:], [enable_authy=check])

AS_IF([test "x${enable_authy}" = "xcheck"], [
AS_IF([test "x${have_jansson}" = "xyes" && test "x${have_curl}" = "xyes"],
[enable_authy=yes], [enable_authy=no])
])

AM_CONDITIONAL([ENABLE_AUTHY], [test x${enable_authy} = xyes])
AS_IF([test "x${enable_authy}" = "xyes"], [
AS_IF([test "x${have_jansson}" != "xyes"], [AC_MSG_ERROR([libjannson is missing])])
AS_IF([test "x${have_curl}" != "xyes"], [AC_MSG_ERROR([libcurl is missing])])
AC_DEFINE(ENABLE_AUTHY, [1], [Define to 1 to enable Authy extension.])
LIBS="$LIBS -ljansson -lcurl"
])

AC_SEARCH_LIBS([dlopen], [dl])

Expand All @@ -81,6 +100,7 @@ AC_OUTPUT

echo "
$PACKAGE_NAME version $PACKAGE_VERSION
Authy..........: $enable_authy
Prefix.........: $prefix
Debug Build....: $debug
C Compiler.....: $CC $CFLAGS $CPPFLAGS
Expand Down
266 changes: 266 additions & 0 deletions src/pam_authy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*
* Google authenticator extension for Authy push notifications
*
* Copyright 2020 Krzysztof Olejarczyk
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#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_approval(pam_handle_t *pamh, char *api_key, char *uuid)
{
json_t *payload = NULL, *jt = NULL;
mblock_t buffer = {0};
authy_rc_t rc;

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

char url[120];
snprintf(url, sizeof(url),
"https://api.authy.com/onetouch/json/approval_requests/%s",
uuid);

char xheader[64];
snprintf(xheader, sizeof(xheader), "X-Authy-API-Key: %s", api_key);

struct curl_slist *headers = NULL;
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);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);

CURLcode res;
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
log_message(LOG_ERR, pamh, "authy_err: curl call failed: %d (%s)",
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");
rc = AUTHY_CONN_ERROR;
goto exit_err;
}

char *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:
free(buffer.memory);
free(jt);
free(payload);

if (curl)
curl_easy_cleanup(curl);

return rc;
}

static authy_rc_t authy_post_approval(pam_handle_t *pamh, long authy_id, char *api_key, int timeout, char **uuid)
{
json_t *payload = NULL, *jt = NULL;
mblock_t buffer = {0};
authy_rc_t rc;

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

const char *username;
if (pam_get_user(pamh, &username, NULL) != PAM_SUCCESS ||
!username ||
!*username) {
log_message(LOG_ERR, pamh, "pam_get_user() failed to get a user name");
rc = AUTHY_LIB_ERROR;
goto exit_err;
}

char hostname[128] = {0};
if (gethostname(hostname, sizeof(hostname)-1)) {
strcpy(hostname, "unix");
}

char url[80];
snprintf(url, sizeof(url),
"https://api.authy.com/onetouch/json/users/%ld/approval_requests",
authy_id);

char xheader[64];
snprintf(xheader, sizeof(xheader), "X-Authy-API-Key: %s", api_key);

char data[200];
snprintf(data, sizeof(data), "message=Login authentication&" \
"details=%s at %s&seconds_to_expire=%d",
username, hostname, timeout);

struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, xheader);
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);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);

CURLcode res;
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
log_message(LOG_ERR, pamh, "authy_err: curl call failed: %d (%s)",
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");
rc = AUTHY_CONN_ERROR;
goto exit_err;
}
*uuid = (char *)json_string_value(json_object_get(jt, "uuid"));
if (!*uuid) {
log_message(LOG_ERR, pamh, "authy_err: 'uuid' field missing");
rc = AUTHY_CONN_ERROR;
goto exit_err;
}

exit_err:
free(buffer.memory);
free(jt);
free(payload);

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)
{
authy_rc_t rc;
char *uuid = NULL;
char *err_str = NULL;

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

log_message(LOG_INFO, pamh, "authy_dbg: Waiting for Authy authentication approval");
struct timespec start_time, now;
clock_gettime(CLOCK_MONOTONIC, &start_time);
do {
rc = authy_check_approval(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");
goto exit_err;
default:
break;
}
sleep(1);
clock_gettime(CLOCK_MONOTONIC, &now);
} while ((start_time.tv_sec + timeout + 5) > now.tv_sec);
rc = AUTHY_EXPIRED;
err_str = "expired (pam timeout)";

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

free(uuid);

return rc;
}
Loading