#include "jsmn/jsmn.h" #include #include #include #include #include #include // #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; } /// /// Данная функция принимает логин и токен, проверяет на внешнем сервере и одобряет или не одобряет аутентификацию. /// 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 и соответствия их значениям. /// Если все поля соответствуют то авторизация успешна. /// /// /// /// /// /// 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; }