#include "jsmn/jsmn.h"
|
#include <curl/curl.h>
|
#include <security/pam_ext.h>
|
#include <security/pam_modules.h>
|
#include <stdlib.h>
|
#include <string.h>
|
#include <syslog.h>
|
|
// #define LOG_INTO_SYSLOG
|
// #define LOG_INTO_FILE
|
// #define DISABLE_SSL_CHECKS // Полезно для дебага если у вас кривые сертификаты
|
|
// Для логгирования в файл надо создать файл /1/1.log с правами на запись для всех
|
|
struct response
|
{
|
char* ptr;
|
size_t len;
|
};
|
|
struct check_tokens
|
{
|
const char* key;
|
int key_len;
|
const char* value;
|
int value_len;
|
int match;
|
};
|
|
static void log_into_file( const char* message, ... )
|
{
|
va_list args;
|
va_start( args, message );
|
|
FILE* fptr;
|
fptr = fopen( "/1/1.log", "a" );
|
|
fprintf( fptr, message, args );
|
|
fclose( fptr );
|
|
va_end( args );
|
}
|
|
static void omp_log( const char* message, ... )
|
{
|
va_list args;
|
va_start( args, message );
|
va_list args2;
|
va_copy( args2, args );
|
|
#ifdef LOG_INTO_SYSLOG
|
syslog( LOG_AUTH | LOG_DEBUG, message, args );
|
#endif
|
|
#ifdef LOG_INTO_FILE
|
log_into_file( message, args2 );
|
#endif
|
|
va_end( args );
|
va_end( args2 );
|
}
|
|
static size_t writefunc( void* ptr, size_t size, size_t nmemb, struct response* r )
|
{
|
size_t data_size = size * nmemb;
|
size_t new_len = r->len + data_size;
|
char* new_ptr = realloc( r->ptr, new_len + 1 );
|
|
if( new_ptr == NULL )
|
{
|
omp_log( "OmegaOAuth2Pam: memory allocation failed" );
|
return 0;
|
}
|
|
r->ptr = new_ptr;
|
|
memcpy( r->ptr + r->len, ptr, data_size );
|
r->ptr[ r->len = new_len ] = '\0';
|
|
return data_size;
|
}
|
|
static int skip_object( const jsmntok_t* t, const int count )
|
{
|
int i;
|
if( count <= 0 )
|
return 0; /* should not happen */
|
|
if( t->type == JSMN_PRIMITIVE || t->type == JSMN_STRING )
|
{
|
return 1;
|
}
|
else if( t->type == JSMN_OBJECT )
|
{
|
int ret = 1;
|
for( i = 0; i < t->size; ++i )
|
{
|
ret += skip_object( t + ret, count - ret );
|
ret += skip_object( t + ret, count - ret );
|
}
|
return ret;
|
}
|
else if( t->type == JSMN_ARRAY )
|
{
|
int ret = 1;
|
for( i = 0; i < t->size; ++i )
|
ret += skip_object( t + ret, count - ret );
|
return ret;
|
}
|
else
|
return 0;
|
}
|
|
static int check_response( const struct response token_info, struct check_tokens* ct )
|
{
|
const char* const response_data = token_info.ptr;
|
struct check_tokens* cti;
|
int r, i = 1;
|
jsmn_parser p;
|
jsmntok_t t[ 128 ]; /* We expect no more than 128 tokens */
|
|
jsmn_init( &p );
|
if( ( r = jsmn_parse( &p, response_data, token_info.len, t, sizeof( t ) / sizeof( t[ 0 ] ) ) ) < 0 )
|
{
|
omp_log( "OmegaOAuth2Pam: Failed to parse tokeninfo JSON response" );
|
return PAM_AUTHINFO_UNAVAIL;
|
}
|
|
/* Assume the top-level element is an object */
|
if( r-- < 1 || t[ 0 ].type != JSMN_OBJECT )
|
{
|
omp_log( "OmegaOAuth2Pam: tokeninfo response: JSON Object expected" );
|
return PAM_AUTHINFO_UNAVAIL;
|
}
|
|
while( r > 0 )
|
{
|
if( t[ i ].type == JSMN_STRING )
|
{
|
--r;
|
/* try to find "interesting" keys in the top-level element object */
|
for( cti = ct; cti->key != NULL; ++cti )
|
{
|
if( cti->key_len == t[ i ].end - t[ i ].start &&
|
strncmp( response_data + t[ i ].start, cti->key, cti->key_len ) == 0 )
|
{
|
++i;
|
if( t[ i ].type == JSMN_STRING && cti->value_len == t[ i ].end - t[ i ].start &&
|
strncmp( response_data + t[ i ].start, cti->value, cti->value_len ) == 0 )
|
{
|
++i;
|
--r;
|
cti->match = 1;
|
break;
|
}
|
else
|
{
|
omp_log( "OmegaOAuth2Pam: '%.*s' value doesn't meet expectation: '%.*s' != '%.*s'",
|
cti->key_len, cti->key, t[ i ].end - t[ i ].start, response_data + t[ i ].start, cti->value_len, cti->value );
|
return PAM_AUTH_ERR;
|
}
|
}
|
}
|
|
/* skip value, because key was not interesting for us */
|
if( cti->key == NULL )
|
{
|
int skipped = skip_object( t + ++i, r );
|
r -= skipped;
|
i += skipped;
|
}
|
}
|
else
|
{
|
int skipped = skip_object( t + i, r );
|
r -= skipped;
|
i += skipped;
|
skipped = skip_object( t + i, r );
|
r -= skipped;
|
i += skipped;
|
}
|
}
|
|
r = PAM_SUCCESS;
|
for( cti = ct; cti->key != NULL; ++cti )
|
{
|
if( cti->match == 0 )
|
{
|
omp_log( "OmegaOAuth2Pam: can't find '%.*s' field in the tokeninfo JSON response object",
|
cti->key_len, cti->key );
|
|
if( cti == ct )
|
{ /* login token field always come first */
|
r = PAM_USER_UNKNOWN;
|
}
|
else if( r != PAM_USER_UNKNOWN )
|
{
|
r = PAM_AUTH_ERR;
|
}
|
}
|
}
|
|
if( r == PAM_SUCCESS )
|
{
|
omp_log( "OmegaOAuth2Pam: successfully authenticated '%.*s'", ct->value_len, ct->value );
|
}
|
else
|
{
|
omp_log( "OmegaOAuth2Pam: unsuccessfully authenticated '%.*s'", ct->value_len, ct->value );
|
}
|
|
return r;
|
}
|
|
static int query_token_info( const char* const tokeninfo_url, const char* const authtok, long* response_code, struct response* token_info )
|
{
|
int ret = 1;
|
struct curl_slist* headers = NULL;
|
char* authorization_header;
|
|
if( ( authorization_header = malloc( strlen( "Authorization: Bearer " ) + strlen( authtok ) + 1 ) ) )
|
{
|
strcpy( authorization_header, "Authorization: Bearer " );
|
strcat( authorization_header, authtok );
|
}
|
else
|
{
|
omp_log( "OmegaOAuth2Pam: authorization : memory allocation failed" );
|
return ret;
|
}
|
|
headers = curl_slist_append( headers, "Cache-Control: no-cache" );
|
if( !headers )
|
{
|
curl_slist_free_all( headers );
|
free( authorization_header );
|
return ret;
|
}
|
|
headers = curl_slist_append( headers, authorization_header );
|
if( !headers )
|
{
|
curl_slist_free_all( headers );
|
free( authorization_header );
|
return ret;
|
}
|
|
CURL* session = curl_easy_init();
|
|
if( !session )
|
{
|
omp_log( "OmegaOAuth2Pam: can't initialize curl" );
|
curl_slist_free_all( headers );
|
free( authorization_header );
|
return ret;
|
}
|
|
curl_easy_setopt( session, CURLOPT_URL, tokeninfo_url );
|
curl_easy_setopt( session, CURLOPT_HTTPHEADER, headers );
|
|
omp_log( tokeninfo_url );
|
|
curl_easy_setopt( session, CURLOPT_WRITEFUNCTION, writefunc );
|
curl_easy_setopt( session, CURLOPT_WRITEDATA, token_info );
|
|
// Полезно для дебага если у вас кривые сертификаты
|
#ifdef DISABLE_SSL_CHECKS
|
curl_easy_setopt( session, CURLOPT_SSL_VERIFYPEER, 0 );
|
curl_easy_setopt( session, CURLOPT_SSL_VERIFYHOST, 0 );
|
#endif
|
|
if( curl_easy_perform( session ) == CURLE_OK &&
|
curl_easy_getinfo( session, CURLINFO_RESPONSE_CODE, response_code ) == CURLE_OK )
|
{
|
ret = 0;
|
}
|
else
|
{
|
omp_log( "OmegaOAuth2Pam: failed to perform curl request" );
|
}
|
|
free( authorization_header );
|
|
curl_easy_cleanup( session );
|
|
return ret;
|
}
|
|
static int oauth2_authenticate( const char* const tokeninfo_url, const char* const authtok, struct check_tokens* ct )
|
{
|
struct response token_info;
|
long response_code = 0;
|
int ret;
|
|
if( ( token_info.ptr = malloc( 1 ) ) == NULL )
|
{
|
omp_log( "OmegaOAuth2Pam: memory allocation failed" );
|
return PAM_AUTHINFO_UNAVAIL;
|
}
|
token_info.ptr[ token_info.len = 0 ] = '\0';
|
|
if( query_token_info( tokeninfo_url, authtok, &response_code, &token_info ) != 0 )
|
{
|
ret = PAM_AUTHINFO_UNAVAIL;
|
}
|
else if( response_code == 200 )
|
{
|
ret = check_response( token_info, ct );
|
}
|
else
|
{
|
omp_log( "OmegaOAuth2Pam: authentication failed with response_code=%li", response_code );
|
|
ret = PAM_AUTH_ERR;
|
}
|
|
free( token_info.ptr );
|
|
return ret;
|
}
|
|
|
/// <summary>
|
/// Данная функция принимает логин и токен, проверяет на внешнем сервере и одобряет или не одобряет аутентификацию.
|
/// Url сервера должен приходить как argv[ 0 ].
|
/// В ответе сервера должно содержаться поле логина для проверки соответствия логина юзера пришедшего на проверку и логина юзера пришедшего из ответа сервера по токену.
|
/// Наименование поля логина берётся из argv[ 1 ].
|
/// Функции pam_get_user и pam_get_authtok выдают нам логин и токен на проверку.
|
/// Агрументы с идексом более 1 (argv[2, 3, 4, ...]) используются для проверки полей ответа сервера например если argv[2] = "role=user" то в json ответе сервера по Url из argv[ 0 ] должно содержаться поле "role" = "user".
|
/// Принцип работы: из агрументов получаюся url, login_field и поля на проверку, из pam_get_user и pam_get_authtok получаються логин и пароль.
|
/// Из этих данных формируется check_tokens ct из полей на проверку, ct[0] соответствует login_field и user_login из pam_get_user, последующие check_tokens это поля на проверку.
|
/// Формируется curl get запрос на url с auth_token из pam_get_authtok в header-е запроса.
|
/// Если ответ пришёл он анализируется на наличие всех полей из check_tokens ct и соответствия их значениям.
|
/// Если все поля соответствуют то авторизация успешна.
|
/// </summary>
|
/// <param name="pamh"></param>
|
/// <param name="flags"></param>
|
/// <param name="argc"></param>
|
/// <param name="argv"></param>
|
/// <returns></returns>
|
|
PAM_EXTERN int pam_sm_authenticate( pam_handle_t* pamh, int flags, int argc, const char** argv )
|
{
|
const char * tokeninfo_url = NULL, *authtok = NULL;
|
struct check_tokens ct[ argc ];
|
int i, ct_len = 1;
|
ct->key = ct->value = NULL;
|
|
if( argc > 0 )
|
tokeninfo_url = argv[ 0 ];
|
if( argc > 1 )
|
ct[ 0 ].key = argv[ 1 ];
|
|
if( tokeninfo_url == NULL || *tokeninfo_url == '\0' )
|
{
|
omp_log( "OmegaOAuth2Pam: tokeninfo_url is not defined or invalid" );
|
return PAM_AUTHINFO_UNAVAIL;
|
}
|
|
if( ct->key == NULL || *ct->key == '\0' )
|
{
|
omp_log( "OmegaOAuth2Pam: login_field is not defined or empty" );
|
return PAM_AUTHINFO_UNAVAIL;
|
}
|
|
if( pam_get_user( pamh, &ct->value, NULL ) != PAM_SUCCESS || ct->value == NULL || *ct->value == '\0' )
|
{
|
omp_log( "OmegaOAuth2Pam: can't get user login" );
|
return PAM_AUTHINFO_UNAVAIL;
|
}
|
|
if( pam_get_authtok( pamh, PAM_AUTHTOK, &authtok, NULL ) != PAM_SUCCESS || authtok == NULL || *authtok == '\0' )
|
{
|
omp_log( "OmegaOAuth2Pam: can't get authtok" );
|
return PAM_AUTHINFO_UNAVAIL;
|
}
|
|
ct->key_len = strlen( ct->key );
|
ct->value_len = strlen( ct->value );
|
ct->match = 0;
|
|
for( i = 2; i < argc; ++i )
|
{
|
const char* value = strchr( argv[ i ], '=' );
|
if( value != NULL )
|
{
|
ct[ ct_len ].key = argv[ i ];
|
ct[ ct_len ].key_len = value - argv[ i ];
|
ct[ ct_len ].value = value + 1;
|
ct[ ct_len ].value_len = strlen( value + 1 );
|
ct[ ct_len++ ].match = 0;
|
}
|
}
|
ct[ ct_len ].key = NULL;
|
|
return oauth2_authenticate( tokeninfo_url, authtok, ct );
|
}
|
|
PAM_EXTERN int pam_sm_chauthtok( pam_handle_t* pamh, int flags, int argc, const char** argv )
|
{
|
return PAM_SUCCESS;
|
}
|
|
PAM_EXTERN int pam_sm_open_session( pam_handle_t* pamh, int flags, int argc, const char** argv )
|
{
|
return PAM_SUCCESS;
|
}
|
|
PAM_EXTERN int pam_sm_close_session( pam_handle_t* pamh, int flags, int argc, const char** argv )
|
{
|
return PAM_SUCCESS;
|
}
|
|
PAM_EXTERN int pam_sm_setcred( pam_handle_t* pamh, int flags, int argc, const char** argv )
|
{
|
return PAM_CRED_UNAVAIL;
|
}
|
|
PAM_EXTERN int pam_sm_acct_mgmt( pam_handle_t* pamh, int flags, int argc, const char** argv )
|
{
|
return PAM_SUCCESS;
|
}
|