Keycloak PAM Module Development Tutorial [Step by Step]

Keycloak is an identity provider, which can take the task for managing and authenticating the users for an application. Web applications can be integrated with keycloak with OIDC. Linux uses PAM modules for authentication activities and NSS module for user lookup activities. By default Linux will have his own database in the machine which holds the details of the users and groups. For keycloak to work as an identity provider, we will have to create a custom PAM module, who is capable of connecting to keycloak and to consume and authenticate users .
In this article, we will see how to develop PAM module for using keycloak as Identity provider for Rocky Linux. We can write the PAM, in multiple languages, C is choose for this article

Setup Details

Pre-Requisites

  • keycloak should be installed and running
  • A new realm apart from master realm need to be created ( for this article, i have created a realm called uma)
  • A user with credentials should be created in the newly created realm
  • While setting the credentials for the new users, make sure the flag Temporary is turned off
  • For the sake of this article, i will be using http rather than https, you can  enable it going to  realm (uma)→  Realm Settings → Login → Require SSL (select None from drop down)

PAM module development

I'm assuming, you have some prior knowledge on PAM development. If you don't have initial understanding, you can go through PAM development tutorials. Below contains the C code which contain PAM module development

#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <errno.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>

static int Keycloak_connect(const char *,const char *);
PAM_EXTERN int pam_sm_authenticate( pam_handle_t *pamh, int flags,int argc, const char **argv ) {
  	int retval;
    struct passwd pwd;
    const char * password=NULL;
    struct passwd *result;
    char *buf;
    size_t bufsize;
    int s;
const char* pUsername;
	retval = pam_get_user(pamh, &pUsername, "Username: ");
    	if (retval != PAM_SUCCESS) {
		return retval;
	}

	printf("Welcome %s\n", pUsername);

  retval = pam_get_authtok(pamh, PAM_AUTHTOK, &password, "PASSWORD: ");



return Keycloak_connect(pUsername,password);
}
  CURLcode ret;
  CURL *hnd;


static int Keycloak_connect(const char *a, const char *b){

  char prefix[500];
  sprintf(prefix,"client_id=admin-cli&username=%s&password=%s&grant_type=password&client_secret=",a,b);
  
size_t my_dummy_write(char *ptr, size_t size, size_t nmemb, void *userdata)
{
   return size * nmemb;
}
  hnd = curl_easy_init();
  curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
  // The realm name can be taken as argument
  curl_easy_setopt(hnd, CURLOPT_URL, "http://192.168.56.102:8080/realms/uma/protocol/openid-connect/token");
  curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
  curl_easy_setopt(hnd, CURLOPT_NOPROXY, "*");
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, prefix);
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)86);
  curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.61.1");
  curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
  curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
  curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);
  curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
  curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
  curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, &my_dummy_write);

  /* Here is a list of options the curl code used that cannot get generated
     as source easily. You may select to either not use them or implement
     them yourself.

  CURLOPT_WRITEDATA set to a objectpointer
  CURLOPT_INTERLEAVEDATA set to a objectpointer
  CURLOPT_WRITEFUNCTION set to a functionpointer
  CURLOPT_READDATA set to a objectpointer
  CURLOPT_READFUNCTION set to a functionpointer
  CURLOPT_SEEKDATA set to a objectpointer
  CURLOPT_SEEKFUNCTION set to a functionpointer
  CURLOPT_ERRORBUFFER set to a objectpointer
  CURLOPT_STDERR set to a objectpointer
  CURLOPT_HEADERFUNCTION set to a functionpointer
  CURLOPT_HEADERDATA set to a objectpointer

  */

  ret = curl_easy_perform(hnd);
  long http_code = 0;
  curl_easy_getinfo (hnd, CURLINFO_RESPONSE_CODE, &http_code);
  //  if(ret != CURLE_OK)
    if (http_code == 200 && ret != CURLE_ABORTED_BY_CALLBACK)
    {
      return PAM_SUCCESS;
    }
    else 
    {
      // fprintf(stderr, "curl_easy_perform() failed: %s\n",curl_easy_strerror(ret));
      return PAM_PERM_DENIED;
    }


  curl_easy_cleanup(hnd);
  hnd = NULL;

}
/**** End of sample code ****/

Step By Step Explanations

  • The pam_sm_authenticate function is the function which gets called, when a pam aware application calls the pam module for authentication
  • We will writing all the required custom code under this function
PAM_EXTERN int pam_sm_authenticate( pam_handle_t *pamh, int flags,int argc, const char **argv ) {
    int retval;
    struct passwd pwd;
    const char * password=NULL;
    struct passwd *result;
  • Below function pam_get_user will get the user name from the user
retval = pam_get_user(pamh, &pUsername, "Username: ");
  •  Below function pam_get_authok will get the password as input from the user
  retval = pam_get_authtok(pamh, PAM_AUTHTOK, &password, "PASSWORD: ");
  • Below function keycloak is the heart of the PAM , where all the connection between machine and the keycloak happens
 static int Keycloak_connect(const char *a, const char *b){
  • In the function keycloak_connect, we will be passing the user name and password which we got from the user to the this function
  • We are using curl library in C to get the the response from keycloak GUI
  • We are using password grant flow to check if the entered username and password is correct in the keycloak
  • Below snippet shows the creation of POST FIELD required for the curl command
sprintf(prefix,"client_id=admin-cli&username=%s&password=%s&grant_type=password&client_secret=",a,b);
  • Below curl command will be send to the keycloak via library available in C (libcurl.h)
curl --noproxy '*' -k -d client_id=admin-cli  -d username=<username> -d password=<password> -d grant_type=password -d client_secret= http://192.168.56.102:8080/realms/uma_protection/protocol/openid-connect/token
  • Below snippet, shows the checking of the curl response status code, if the status code is 200. we will be responding with PAM_SUCCESS else PAM_PERM_DENIED
curl_easy_getinfo (hnd, CURLINFO_RESPONSE_CODE, &http_code);
  //  if(ret != CURLE_OK)
    if (http_code == 200 && ret != CURLE_ABORTED_BY_CALLBACK)
    {
      return PAM_SUCCESS;
    }
    else
    {
     // fprintf(stderr, "curl_easy_perform() failed: %s\n",curl_easy_strerror(ret));
      return PAM_PERM_DENIED;
    }

Compilation and Testing

  • We have to compile the code to create shared-object file
  • I have saved my code as example.c file, and will be generated a so file with name pam_test.so
gcc -shared -o pam_test.so -I . -DPAM -fPIC example.c -lcurl -lpam
  • Copy the so file to /usr/lib64/security/ (for rocky linux)
  • Create a pam file in /etc/pam.d with name of the application in our case it will be test. And update the below content to the file
[root@LDH ~]# cat /etc/pam.d/test
auth required pam_test.so
account required pam_test.so
session required pam_limits.so
  • We will using pamtester for testing the pam, I have already created user called ldhuser  under realm uma and have assigned password for the user. Below code snippet shows a successful authentication
[root@localhost ~]# pamtester /etc/pam.d/test ldhuser authenticate
Welcome ldhuser
PASSWORD: 
pamtester: successfully authenticated

Above explained code is a simple one, in actual keycloak PAM, we will have to consider multiple edge cases to make it more robust, such as, missing users, locked users etc

Search on LinuxDataHub

Leave a Comment