/* * This program is is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ /** * $Id$ * @file rlm_securid.c * @brief Supports auth against SecurID servers using OTP h/w tokens. * * Supports "next-token code" and "new-pin" modes. * * @copyright 2012 The FreeRADIUS server project * @copyright 2012 Alan DeKok */ #include #include #include #include "rlm_securid.h" typedef enum { RC_SECURID_AUTH_SUCCESS = 0, RC_SECURID_AUTH_FAILURE = -3, RC_SECURID_AUTH_ACCESS_DENIED_FAILURE = -4, RC_SECURID_AUTH_INVALID_SERVER_FAILURE = -5, RC_SECURID_AUTH_CHALLENGE = -17 } SECURID_AUTH_RC; static const CONF_PARSER module_config[] = { { "timer_expire", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_securid_t, timer_limit), "600" }, { "max_sessions", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_securid_t, max_sessions), "2048" }, { "max_trips_per_session", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_securid_t, max_trips_per_session), NULL }, { "max_round_trips", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_securid_t, max_trips_per_session), "6" }, CONF_PARSER_TERMINATOR }; static SD_CHAR empty_pin[] = ""; /* comparison function to find session in the tree */ static int securid_session_cmp(void const *a, void const *b) { int rcode; SECURID_SESSION const *one = a; SECURID_SESSION const *two = b; rad_assert(one != NULL); rad_assert(two != NULL); rcode = fr_ipaddr_cmp(&one->src_ipaddr, &two->src_ipaddr); if (rcode != 0) return rcode; return memcmp(one->state, two->state, sizeof(one->state)); } static SECURID_AUTH_RC securidAuth(void *instance, REQUEST *request, char const *username, char const *passcode, char *replyMsgBuffer, size_t replyMsgBufferSize) { rlm_securid_t *inst = (rlm_securid_t *) instance; int acm_ret; SD_PIN pin_params; char new_pin[10]; char format[30]; SECURID_SESSION *securid_session = NULL; int rc = -1; SD_CHAR *securid_user, *securid_pass; if (!username) { ERROR("SecurID username is NULL"); return RC_SECURID_AUTH_FAILURE; } if (!passcode) { ERROR("SecurID passcode is NULL for %s user", username); return RC_SECURID_AUTH_FAILURE; } memcpy(&securid_user, &username, sizeof(securid_user)); memcpy(&securid_pass, &passcode, sizeof(securid_pass)); *replyMsgBuffer = '\0'; securid_session = securid_sessionlist_find(inst, request); if (!securid_session) { /* securid session not found */ SDI_HANDLE sdiHandle = SDI_HANDLE_NONE; acm_ret = SD_Init(&sdiHandle); if (acm_ret != ACM_OK) { ERROR("Cannot communicate with the ACE/Server"); return -1; } acm_ret = SD_Lock(sdiHandle, securid_user); if (acm_ret != ACM_OK) { ERROR("SecurID: Access denied. Name [%s] lock failed", username); return -2; } acm_ret = SD_Check(sdiHandle, securid_pass, securid_user); switch (acm_ret) { case ACM_OK: /* we are in now */ RDEBUG("SecurID authentication successful for %s", username); SD_Close(sdiHandle); return RC_SECURID_AUTH_SUCCESS; case ACM_ACCESS_DENIED: /* not this time */ RDEBUG("SecurID Access denied for %s", username); SD_Close(sdiHandle); return RC_SECURID_AUTH_ACCESS_DENIED_FAILURE; case ACM_INVALID_SERVER: ERROR("SecurID: Invalid ACE server"); return RC_SECURID_AUTH_INVALID_SERVER_FAILURE; case ACM_NEW_PIN_REQUIRED: RDEBUG2("SecurID new pin required for %s", username); /* create a new session */ securid_session = securid_session_alloc(); securid_session->sdiHandle = sdiHandle; /* save ACE handle for future use */ securid_session->securidSessionState = NEW_PIN_REQUIRED_STATE; securid_session->identity = strdup(username); /* Get PIN requirements */ (void) AceGetPinParams(sdiHandle, &pin_params); /* If a system-generated PIN is required */ if (pin_params.Selectable == CANNOT_CHOOSE_PIN) { /* Prompt user to accept a system generated PIN */ snprintf(replyMsgBuffer, replyMsgBufferSize, "\r\nAre you prepared to accept a new system-generated PIN [y/n]?"); securid_session->securidSessionState = NEW_PIN_SYSTEM_ACCEPT_STATE; } else if (pin_params.Selectable == USER_SELECTABLE) { //may be returned by AM 6.x servers. snprintf(replyMsgBuffer, replyMsgBufferSize, "\r\nPress 'y' to generate a new PIN\r\nOR\r\n'n'to enter a new PIN yourself [y/n]"); securid_session->securidSessionState = NEW_PIN_USER_SELECT_STATE; } else { if (pin_params.Alphanumeric) { strcpy(format, "alphanumeric characters"); } else { strcpy(format, "digits"); } snprintf(replyMsgBuffer, replyMsgBufferSize, " \r\n Enter your new PIN of %d to %d %s, \r\n or\r\n to cancel the New PIN procedure:", pin_params.Min, pin_params.Max, format); } /* insert new session in the session list */ securid_sessionlist_add(inst, request, securid_session); return RC_SECURID_AUTH_CHALLENGE; case ACM_NEXT_CODE_REQUIRED: RDEBUG2("Next securid token code required for %s", username); /* create a new session */ securid_session = securid_session_alloc(); securid_session->sdiHandle = sdiHandle; securid_session->securidSessionState = NEXT_CODE_REQUIRED_STATE; securid_session->identity = strdup(username); /* insert new session in the session list */ securid_sessionlist_add(inst, request, securid_session); strlcpy(replyMsgBuffer, "\r\nPlease Enter the Next Code from Your Token:", replyMsgBufferSize); return RC_SECURID_AUTH_CHALLENGE; default: ERROR("SecurID: Unexpected error from ACE/Agent API acm_ret=%d", acm_ret); securid_session_free(inst, request, securid_session); return RC_SECURID_AUTH_FAILURE; } } else { /* existing session found */ RDEBUG("Continuing previous session found for user [%s]", username); /* continue previous session */ switch (securid_session->securidSessionState) { case NEXT_CODE_REQUIRED_STATE: DEBUG2("Securid NEXT_CODE_REQUIRED_STATE: User [%s]", username); /* next token code mode */ acm_ret = SD_Next(securid_session->sdiHandle, securid_pass); if (acm_ret == ACM_OK) { INFO("Next SecurID token accepted for [%s].", securid_session->identity); rc = RC_SECURID_AUTH_SUCCESS; } else { INFO("SecurID: Next token rejected for [%s].", securid_session->identity); rc = RC_SECURID_AUTH_FAILURE; } /* deallocate session */ securid_session_free(inst, request, securid_session); return rc; case NEW_PIN_REQUIRED_STATE: RDEBUG2("SecurID NEW_PIN_REQUIRED_STATE for %s", username); /* save the previous pin */ if (securid_session->pin) { free(securid_session->pin); securid_session->pin = NULL; } securid_session->pin = strdup(passcode); strlcpy(replyMsgBuffer, "\r\n Please re-enter new PIN:", replyMsgBufferSize); /* set next state */ securid_session->securidSessionState = NEW_PIN_USER_CONFIRM_STATE; /* insert the updated session in the session list */ securid_sessionlist_add(inst, request, securid_session); return RC_SECURID_AUTH_CHALLENGE; case NEW_PIN_USER_CONFIRM_STATE: RDEBUG2("SecurID NEW_PIN_USER_CONFIRM_STATE: User [%s]", username); /* compare previous pin and current pin */ if (!securid_session->pin || strcmp(securid_session->pin, passcode)) { RDEBUG2("Pin confirmation failed. Pins do not match [%s] and [%s]", SAFE_STR(securid_session->pin), securid_pass); /* pins do not match */ /* challenge the user again */ AceGetPinParams(securid_session->sdiHandle, &pin_params); if (pin_params.Alphanumeric) { strcpy(format, "alphanumeric characters"); } else { strcpy(format, "digits"); } snprintf(replyMsgBuffer, replyMsgBufferSize, " \r\n Pins do not match--Please try again.\r\n Enter your new PIN of %d to %d %s, \r\n or\r\n to cancel the New PIN procedure:", pin_params.Min, pin_params.Max, format); securid_session->securidSessionState = NEW_PIN_REQUIRED_STATE; /* insert the updated session in the session list */ securid_sessionlist_add(inst, request, securid_session); rc = RC_SECURID_AUTH_CHALLENGE; } else { /* pins match */ RDEBUG2("Pin confirmation succeeded. Pins match"); acm_ret = SD_Pin(securid_session->sdiHandle, securid_pass); if (acm_ret == ACM_NEW_PIN_ACCEPTED) { RDEBUG("New SecurID pin accepted for %s.", securid_session->identity); securid_session->securidSessionState = NEW_PIN_AUTH_VALIDATE_STATE; /* insert the updated session in the session list */ securid_sessionlist_add(inst, request, securid_session); rc = RC_SECURID_AUTH_CHALLENGE; strlcpy(replyMsgBuffer, " \r\n\r\nWait for the code on your card to change, then enter new PIN and TokenCode\r\n\r\nEnter PASSCODE:", replyMsgBufferSize); } else { RDEBUG("SecurID: New SecurID pin rejected for %s.", securid_session->identity); SD_Pin(securid_session->sdiHandle, &empty_pin[0]); /* cancel PIN */ rc = RC_SECURID_AUTH_FAILURE; /* deallocate session */ securid_session_free(inst, request, securid_session); } } return rc; case NEW_PIN_AUTH_VALIDATE_STATE: acm_ret = SD_Check(securid_session->sdiHandle, securid_pass, securid_user); if (acm_ret == ACM_OK) { RDEBUG("New SecurID passcode accepted for %s.", securid_session->identity); rc = RC_SECURID_AUTH_SUCCESS; } else { INFO("SecurID: New passcode rejected for [%s].", securid_session->identity); rc = RC_SECURID_AUTH_FAILURE; } /* deallocate session */ securid_session_free(inst, request, securid_session); return rc; case NEW_PIN_SYSTEM_ACCEPT_STATE: if (!strcmp(passcode, "y")) { AceGetSystemPin(securid_session->sdiHandle, new_pin); /* Save the PIN for the next session * continuation */ if (securid_session->pin) { free(securid_session->pin); securid_session->pin = NULL; } securid_session->pin = strdup(new_pin); snprintf(replyMsgBuffer, replyMsgBufferSize, "\r\nYour new PIN is: %s\r\nDo you accept this [y/n]?", new_pin); securid_session->securidSessionState = NEW_PIN_SYSTEM_CONFIRM_STATE; /* insert the updated session in the * session list */ securid_sessionlist_add(inst, request, securid_session); rc = RC_SECURID_AUTH_CHALLENGE; } else { SD_Pin(securid_session->sdiHandle, &empty_pin[0]); //Cancel new PIN /* deallocate session */ securid_session_free(inst, request, securid_session); rc = RC_SECURID_AUTH_FAILURE; } return rc; case NEW_PIN_SYSTEM_CONFIRM_STATE: acm_ret = SD_Pin(securid_session->sdiHandle, (SD_CHAR*)securid_session->pin); if (acm_ret == ACM_NEW_PIN_ACCEPTED) { strlcpy(replyMsgBuffer, " \r\n\r\nPin Accepted. Wait for the code on your card to change, then enter new PIN and TokenCode\r\n\r\nEnter PASSCODE:", replyMsgBufferSize); securid_session->securidSessionState = NEW_PIN_AUTH_VALIDATE_STATE; /* insert the updated session in the session list */ securid_sessionlist_add(inst, request, securid_session); rc = RC_SECURID_AUTH_CHALLENGE; } else { SD_Pin(securid_session->sdiHandle, &empty_pin[0]); //Cancel new PIN strlcpy(replyMsgBuffer, " \r\n\r\nPin Rejected. Wait for the code on your card to change, then try again.\r\n\r\nEnter PASSCODE:", replyMsgBufferSize); /* deallocate session */ securid_session_free(inst, request, securid_session); rc = RC_SECURID_AUTH_FAILURE; } return rc; /* USER_SELECTABLE state should be implemented to preserve compatibility with AM 6.x servers, which can return this state */ case NEW_PIN_USER_SELECT_STATE: if (!strcmp(passcode, "y")) { /* User has opted for a system-generated PIN */ AceGetSystemPin(securid_session->sdiHandle, new_pin); snprintf(replyMsgBuffer, replyMsgBufferSize, "\r\nYour new PIN is: %s\r\nDo you accept this [y/n]?", new_pin); securid_session->securidSessionState = NEW_PIN_SYSTEM_CONFIRM_STATE; /* insert the updated session in the session list */ securid_sessionlist_add(inst, request, securid_session); rc = RC_SECURID_AUTH_CHALLENGE; } else { /* User has opted for a user-defined PIN */ AceGetPinParams(securid_session->sdiHandle, &pin_params); if (pin_params.Alphanumeric) { strcpy(format, "alphanumeric characters"); } else { strcpy(format, "digits"); } snprintf(replyMsgBuffer, replyMsgBufferSize, " \r\n Enter your new PIN of %d to %d %s, \r\n or\r\n to cancel the New PIN procedure:", pin_params.Min, pin_params.Max, format); securid_session->securidSessionState = NEW_PIN_REQUIRED_STATE; /* insert the updated session in the session list */ securid_sessionlist_add(inst, request, securid_session); rc = RC_SECURID_AUTH_CHALLENGE; } return rc; default: ERROR("rlm_securid: Invalid session state %d for user [%s]", securid_session->securidSessionState, username); break; } } return 0; } /******************************************/ static int mod_detach(void *instance) { rlm_securid_t *inst = (rlm_securid_t *) instance; /* delete session tree */ if (inst->session_tree) { rbtree_free(inst->session_tree); inst->session_tree = NULL; } pthread_mutex_destroy(&(inst->session_mutex)); return 0; } static int mod_instantiate(UNUSED CONF_SECTION *conf, void *instance) { rlm_securid_t *inst = instance; /* * Lookup sessions in the tree. We don't free them in * the tree, as that's taken care of elsewhere... */ inst->session_tree = rbtree_create(NULL, securid_session_cmp, NULL, 0); if (!inst->session_tree) { ERROR("rlm_securid: Cannot initialize session tree"); return -1; } pthread_mutex_init(&(inst->session_mutex), NULL); return 0; } /* * Authenticate the user via one of any well-known password. */ static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request) { int rcode; rlm_securid_t *inst = instance; char buffer[MAX_STRING_LEN]=""; char const *username=NULL, *password=NULL; VALUE_PAIR *vp; /* * We can only authenticate user requests which HAVE * a User-Name attribute. */ if (!request->username) { AUTH("rlm_securid: Attribute \"User-Name\" is required for authentication"); return RLM_MODULE_INVALID; } if (!request->password) { RAUTH("Attribute \"Password\" is required for authentication"); return RLM_MODULE_INVALID; } /* * Clear-text passwords are the only ones we support. */ if (request->password->da->attr != PW_USER_PASSWORD) { RAUTH("Attribute \"User-Password\" is required for authentication. Cannot use \"%s\".", request->password->da->name); return RLM_MODULE_INVALID; } /* * The user MUST supply a non-zero-length password. */ if (request->password->vp_length == 0) { REDEBUG("Password should not be empty"); return RLM_MODULE_INVALID; } /* * shortcuts */ username = request->username->vp_strvalue; password = request->password->vp_strvalue; if (RDEBUG_ENABLED3) { RDEBUG3("Login attempt with password \"%s\"", password); } else { RDEBUG("Login attempt with password"); } rcode = securidAuth(inst, request, username, password, buffer, sizeof(buffer)); switch (rcode) { case RC_SECURID_AUTH_SUCCESS: rcode = RLM_MODULE_OK; break; case RC_SECURID_AUTH_CHALLENGE: /* reply with Access-challenge message code (11) */ /* Generate Prompt attribute */ vp = fr_pair_afrom_num(request->reply, PW_PROMPT, 0); rad_assert(vp != NULL); vp->vp_integer = 0; /* no echo */ fr_pair_add(&request->reply->vps, vp); /* Mark the packet as a Acceess-Challenge Packet */ request->reply->code = PW_CODE_ACCESS_CHALLENGE; RDEBUG("Sending Access-Challenge"); rcode = RLM_MODULE_HANDLED; break; case RC_SECURID_AUTH_FAILURE: case RC_SECURID_AUTH_ACCESS_DENIED_FAILURE: case RC_SECURID_AUTH_INVALID_SERVER_FAILURE: default: rcode = RLM_MODULE_REJECT; break; } if (*buffer) pair_make_reply("Reply-Message", buffer, T_OP_EQ); return rcode; } /* * The module name should be the only globally exported symbol. * That is, everything else should be 'static'. * * If the module needs to temporarily modify it's instantiation * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE. * The server will then take care of ensuring that the module * is single-threaded. */ extern module_t rlm_securid; module_t rlm_securid = { .magic = RLM_MODULE_INIT, .name = "securid", .type = RLM_TYPE_HUP_SAFE, .inst_size = sizeof(rlm_securid_t), .config = module_config, .instantiate = mod_instantiate, .detach = mod_detach, .methods = { [MOD_AUTHENTICATE] = mod_authenticate }, };