src/pam/pam_oslogin_login.cc (188 lines of code) (raw):

// Copyright 2023 Google Inc. All Rights Reserved. // // Licensed 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 PAM_SM_ACCOUNT #include <security/pam_ext.h> #include <security/pam_modules.h> #include <security/_pam_types.h> #include <syslog.h> #include <cstddef> #include <cstdio> #include <map> #include <sstream> #include <string> #include <vector> #include "include/compat.h" #include "include/oslogin_utils.h" using oslogin_utils::AuthOptions; using oslogin_utils::ContinueSession; using oslogin_utils::GetUser; using oslogin_utils::ParseJsonToChallenges; using oslogin_utils::ParseJsonToKey; using oslogin_utils::ParseJsonToEmail; using oslogin_utils::StartSession; using oslogin_utils::ValidateUserName; extern "C" { // pm_sm_acct_mgmt is the account management PAM implementation for non-admin users (or users // without the proper loginAdmin policy). This account management module is intended for custom // configuration handling only, where users need a way to in their stack configurations to // differentiate a OS Login user. The Google Guest Agent will not manage the lifecycle of // this module, it will not add this to the stack as part of the standard/default configuration // set. PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t* pamh, int flags, int argc, const char** argv) { struct AuthOptions opts; const char *user_name; string user_response; if (pam_get_user(pamh, &user_name, NULL) != PAM_SUCCESS) { PAM_SYSLOG(pamh, LOG_INFO, "Could not get pam user."); return PAM_PERM_DENIED; } opts = {}; if (!AuthorizeUser(user_name, opts, &user_response)) { return PAM_PERM_DENIED; } return PAM_SUCCESS; } PAM_EXTERN int pam_sm_setcred(pam_handle_t* pamh, int flags, int argc, const char** argv) { return PAM_SUCCESS; } PAM_EXTERN int pam_sm_authenticate(pam_handle_t* pamh, int flags, int argc, const char** argv) { const char *user_name; if (pam_get_user(pamh, &user_name, NULL) != PAM_SUCCESS) { PAM_SYSLOG(pamh, LOG_INFO, "Could not get pam user."); return PAM_PERM_DENIED; } std::string str_user_name(user_name); if (!ValidateUserName(user_name)) { return PAM_PERM_DENIED; } std::string response; if (!(GetUser(str_user_name, &response))) { return PAM_PERM_DENIED; } // System accounts begin with the prefix `sa_`. std::string sa_prefix = "sa_"; if (str_user_name.compare(0, sa_prefix.size(), sa_prefix) == 0) { return PAM_SUCCESS; } std::string email; if (!ParseJsonToEmail(response, &email) || email.empty()) { return PAM_PERM_DENIED; } response = ""; if (!StartSession(email, &response)) { PAM_SYSLOG(pamh, LOG_ERR, "Bad response from the two-factor start session " "request: %s", response.empty() ? "empty response" : response.c_str()); return PAM_PERM_DENIED; } std::string status; if (!ParseJsonToKey(response, "status", &status)) { PAM_SYSLOG(pamh, LOG_ERR, "Failed to parse status from start session " "response"); return PAM_PERM_DENIED; } if (status == "NO_AVAILABLE_CHALLENGES") { PAM_SYSLOG(pamh, LOG_ERR, "User has no two-factor methods enabled."); return PAM_PERM_DENIED; // User is not two-factor enabled, deny login. } std::string session_id; if (!ParseJsonToKey(response, "sessionId", &session_id)) { return PAM_PERM_DENIED; } std::vector<oslogin_utils::Challenge> challenges; if (!ParseJsonToChallenges(response, &challenges)) { PAM_SYSLOG(pamh, LOG_ERR, "Failed to parse challenge values from " "JSON response"); return PAM_PERM_DENIED; } std::map<std::string,std::string> user_prompts; user_prompts[AUTHZEN] = "Google phone prompt"; user_prompts[TOTP] = "Security code from Google Authenticator application"; user_prompts[INTERNAL_TWO_FACTOR] = "Security code from security key"; user_prompts[IDV_PREREGISTERED_PHONE] = "Voice or text message verification code"; user_prompts[SECURITY_KEY_OTP] = "Security code from a security key"; oslogin_utils::Challenge challenge; if (challenges.size() > 1) { std::stringstream prompt; prompt << "Please choose from the available authentication methods: "; for(vector<oslogin_utils::Challenge>::size_type i = 0; i != challenges.size(); ++i) { prompt << "\n" << i+1 << ": " << user_prompts[challenges[i].type]; } prompt << "\n\nEnter the number for the authentication method to use: "; char *choice = NULL; if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &choice, "%s", prompt.str().c_str()) != PAM_SUCCESS) { pam_error(pamh, "Unable to get user input"); return PAM_PERM_DENIED; } int choicei; if (sscanf(choice, "%d", &choicei) != 1) { pam_error(pamh, "Error parsing user input"); return PAM_PERM_DENIED; } if (size_t(choicei) > challenges.size() || choicei <= 0) { pam_error(pamh, "Invalid option"); return PAM_PERM_DENIED; } challenge = challenges[choicei - 1]; } else { challenge = challenges[0]; } if (challenge.status != "READY") { // Call continueSession with the START_ALTERNATE flag. if (!ContinueSession(true, email, "", session_id, challenge, &response)) { PAM_SYSLOG(pamh, LOG_ERR, "Bad response from two-factor continue session " "request: %s", response.empty() ? "empty response" : response.c_str()); return PAM_PERM_DENIED; } } char *user_token = NULL; if (challenge.type == INTERNAL_TWO_FACTOR) { if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, "Enter your security code: ") != PAM_SUCCESS) { pam_error(pamh, "Unable to get user input"); return PAM_PERM_DENIED; } } else if (challenge.type == SECURITY_KEY_OTP) { if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, "Enter your security code by visiting https://g.co/sc: ") != PAM_SUCCESS) { pam_error(pamh, "Unable to get user input"); return PAM_PERM_DENIED; } } else if (challenge.type == TOTP) { if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, "Enter your one-time password: ") != PAM_SUCCESS) { pam_error(pamh, "Unable to get user input"); return PAM_PERM_DENIED; } } else if (challenge.type == AUTHZEN) { if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, "A login prompt has been sent to your enrolled device. " "Press enter to continue") != PAM_SUCCESS) { pam_error(pamh, "Unable to get user input"); return PAM_PERM_DENIED; } } else if (challenge.type == IDV_PREREGISTERED_PHONE) { if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, "A security code has been sent to your phone. " "Enter code to continue: ") != PAM_SUCCESS) { pam_error(pamh, "Unable to get user input"); return PAM_PERM_DENIED; } } else { PAM_SYSLOG(pamh, LOG_ERR, "Unsupported challenge type %s", challenge.type.c_str()); return PAM_PERM_DENIED; } if (!ContinueSession(false, email, user_token, session_id, challenge, &response)) { PAM_SYSLOG(pamh, LOG_ERR, "Bad response from two-factor continue " "session request: %s", response.empty() ? "empty response" : response.c_str()); return PAM_PERM_DENIED; } if (!ParseJsonToKey(response, "status", &status) || status != "AUTHENTICATED") { if (ParseJsonToKey(response, "rejectionReason", &status) && !status.empty()) { pam_error(pamh, status.c_str()); } return PAM_PERM_DENIED; } return PAM_SUCCESS; } }