AuthenticatorOATH.java revision a35224ef1ee8c02d389ffeeb676b4de432294fb6
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2012-2015 ForgeRock AS.
*/
/*
* Portions Copyrighted 2014-2015 Nomura Research Institute, Ltd.
*/
/**
* Implements the OATH specification. OATH uses a OTP to authenticate
* a token to the server. This class implements two of OATH's protocols for OTP
* generation and authentication; HMAC-based One Time Password (HOTP) and
* Time-based One Time Password (TOTP).
*/
public class AuthenticatorOATH extends AMLoginModule {
//debug log name
//static attribute names
private static final int NUM_CODES = 10;
private static final String PASSWORD_LENGTH =
"iplanet-am-auth-fr-oath-password-length";
private static final String WINDOW_SIZE =
"iplanet-am-auth-fr-oath-hotp-window-size";
private static final String TRUNCATION_OFFSET =
"iplanet-am-auth-fr-oath-truncation-offset";
private static final String TOTP_TIME_STEP =
"iplanet-am-auth-fr-oath-size-of-time-step";
private static final String TOTP_STEPS_IN_WINDOW =
"iplanet-am-auth-fr-oath-steps-in-window";
private static final String MIN_SECRET_KEY_LENGTH =
"iplanet-am-auth-fr-oath-min-secret-key-length";
//module attribute holders
private String issuerName;
private int userConfiguredSkippable = 0;
private boolean isOptional;
private int passLen = 0;
private int minSecretKeyLength = 0;
private int windowSize = 0;
private int truncationOffset = -1;
private boolean checksum = false;
private int totpTimeStep = 0;
private int totpStepsInWindow = 0;
private long time = 0;
private int totpMaxClockDrift = 0;
private static final int HOTP = 0;
private static final int TOTP = 1;
private static final int ERROR = 2;
private int algorithm = 0;
private static final int LOGIN_OPTIONAL = 2;
private static final int LOGIN_NO_DEVICE = 3;
private static final int LOGIN_SAVED_DEVICE = 4;
private static final int REGISTER_DEVICE = 5;
private static final int RECOVERY_USED = 6;
private static final int LOGIN_OPT_DEVICE = 7;
private static final int REGISTER_DEVICE_OPTION_VALUE_INDEX = 1;
private static final int SKIP_OATH_INDEX = 2;
private static final int OPT_DEVICE_SKIP_INDEX = 1;
private static final int SCRIPT_OUTPUT_CALLBACK_INDEX = 1;
private AuthenticatorOathService realmOathService;
private AMIdentity id;
/**
* Standard constructor sets-up the debug logging module.
*/
public AuthenticatorOATH() {
amAuthOATH = "amAuthAuthenticatorOATH";
}
/**
* Returns the principal for this module. This class is overridden from
* AMLoginModule.
*
* @return Principal of the authenticated user.
*/
return new OATHPrincipal(userId);
}
return new OATHPrincipal(userName);
}
return null;
}
/**
* Initializes the authentication module. This function gets the modules
* settings, and the username from the previous authentication module in
* the chain.
*
* @param subject For whom this module is initializing.
* @param sharedState Previously chained module data.
* @param options Configuration for this module.
*/
if (debug.messageEnabled()) {
}
//get username from previous authentication
try {
//gets skippable name from the realm's service and stores it
id = getIdentity();
try {
} catch (NumberFormatException e) {
passLen = 0;
}
try {
} catch (NumberFormatException e) {
}
} else {
}
//set authentication level
try {
} catch (Exception e) {
if (debug.errorEnabled()) {
}
}
}
if (debug.errorEnabled()) {
}
}
}
/**
* Processes the OTP input by the user. Checks the OTP for validity, and
* resynchronizes the server as needed.
*
* @param callbacks Incoming from the UI.
* @param state State of the module to process this access.
* @return -1 for success; 0 for failure, any other int to move to that state.
* @throws AuthLoginException upon any errors.
*/
try {
//check for session and get username and UUID
// session upgrade case. Need to find the user ID from the old
}
if (debug.messageEnabled()) {
}
}
}
try {
} catch (Exception e) {
}
int selectedIndex;
//fall-throughs are INTENTIONAL
switch (state) {
case LOGIN_START:
return ISAuthConstants.LOGIN_SUCCEED;
return LOGIN_OPTIONAL;
} else {
return LOGIN_NO_DEVICE;
} else {
return LOGIN_SAVED_DEVICE;
}
}
//process callbacks
//callback[0] = Name CallBack (OTP)
//callback[2] = Configure account to skip OATH
case LOGIN_OPTIONAL:
}
if (selectedIndex == SKIP_OATH_INDEX) {
return ISAuthConstants.LOGIN_SUCCEED;
}
case LOGIN_NO_DEVICE:
}
return REGISTER_DEVICE;
}
case LOGIN_OPT_DEVICE:
}
if (selectedIndex == OPT_DEVICE_SKIP_INDEX) {
return ISAuthConstants.LOGIN_SUCCEED;
}
case LOGIN_SAVED_DEVICE:
}
//get OTP
}
//get Arrival time of the OTP
return RECOVERY_USED;
if (isOptional) { //if it's optional and you log in, config not skippable
}
return ISAuthConstants.LOGIN_SUCCEED;
} else {
//the OTP is out of the window or incorrect
}
case REGISTER_DEVICE:
if (isOptional) {
return LOGIN_OPT_DEVICE;
} else {
return LOGIN_SAVED_DEVICE;
}
case RECOVERY_USED:
if (isOptional) { //if it's optional and you log in, config not skippable
}
return ISAuthConstants.LOGIN_SUCCEED;
default:
}
}
}
return settings;
}
//check settings aren't null
}
return true;
}
return false;
}
/**
* Sets the state of this OATH module such that it has all necessary information to proceed, informed
*
* First checks the state of the auth module.
*
* If auth module is not optional, then default flow.
* If auth module is optional, then read the state of the user's selection (via the userActivatedAttrName attr.)
* If they have it activated, then default flow (this is required).
* If they have it blank, then default flow (this is required).
* If they have it set to 'can ignore' (e.g. userCanIgnoreAttrName's value is set to "true"), then ignore.
*/
private void detectNecessity(AMIdentity identity) throws AuthLoginException, IdRepoException, SSOException {
//not optional if they haven't selected anywhere to save the user's preference
isOptional = false;
}
//value is stored as: 0 (not chosen), 1 (skippable) or 2 (not skippable)
if (isOptional) {
}
}
}
private void paintRegisterDeviceCallback(AMIdentity id, OathDeviceSettings settings) throws AuthLoginException {
replaceCallback(REGISTER_DEVICE, SCRIPT_OUTPUT_CALLBACK_INDEX, createQRCodeCallback(settings, id, SCRIPT_OUTPUT_CALLBACK_INDEX));
}
/**
* There is a hack here to reverse a hack in RESTLoginView.js. Implementing the code properly in RESTLoginView.js so
* as to remove this hack will take too long at present, and stands in the way of completion of this module's
* QR code additions. I have opted to simply reverse the hack in this singular case.
*
* In the below code returning the ScriptTextOutputCallback, the String used in its construction is
* defined as follows:
*
* createQRDomElementJS
* Adds the DOM element, in this case a div, in which the QR code will appear.
* QRCodeGenerationUtilityFunctions.
* getQRCodeGenerationJavascriptForAuthenticatorAppRegistration(authenticatorAppRegistrationUri)
* Adds a specific call to the Javascript library code, sending the app registration url as the
* text to encode as a QR code. This QR code will then appear in the previously defined DOM
* element (which must have an id of 'qr').
* hideButtonHack
* A hack to reverse a hack in RESTLoginView.js. See more detailed comment above.*
*/
private Callback createQRCodeCallback(OathDeviceSettings settings, AMIdentity id, int callbackIndex) throws AuthLoginException {
try {
return new ScriptTextOutputCallback(
GenerationUtils.getQRCodeGenerationJavascriptForAuthenticatorAppRegistration(callback, authenticatorAppRegistrationUri));
} catch (IOException e) {
}
}
private String getAuthenticatorAppRegistrationUri(OathDeviceSettings settings, AMIdentity id) throws
//check settings aren't null
}
try {
} else {
}
} catch (DecoderException de) {
debug.error("OATH .getCreateQRDomElementJS() : Could not decode secret key from hex to plain text", de);
}
}
/**
* Called to cleanup the class level variables.
*/
public void destroyModuleState() {
}
/**
* Called to cleanup the class level variables that won't be used again.
*/
public void nullifyUsedVars() {
amAuthOATH = null;
}
/**
* Checks the input OTP.
*
* @param otp The OTP to verify.
* @param id The user for whom to verify the OTP.
* @param settings With which the OTP was configured.
* @return true if the OTP is valid; false if the OTP is invalid, or out of
* sync with server.
* @throws AuthLoginException on any error
*/
private boolean checkOTP(String otp, AMIdentity id, OathDeviceSettings settings) throws AuthLoginException {
//check settings aren't null
}
if (minSecretKeyLength <= 0) {
}
//check size of key
}
//make sure secretkey is not smaller than minSecretKeyLength
if (debug.errorEnabled()) {
}
}
//convert secretkey hex string to hex.
//check password length MUST be 6 or higher according to RFC
if (passLen < 6) {
}
try {
/*
* HOTP check section
*/
//test the counter in the lookahead window
for (int i = 0; i <= windowSize; i++) {
//OTP is correct set the counter value to counter+i (+1 for having been successful)
return true;
}
}
/*
* TOTP check section
*/
//get Last login time
//Check TOTP values for validity
if (lastLoginTimeStep < 0) {
}
//must be greater than 0 or we get divide by 0, and cant be negative
if (totpTimeStep <= 0) {
}
if (totpStepsInWindow < 0) {
}
//get Time Step
boolean sameWindow = false;
//check if we are in the time window to prevent 2 logins within the window using the same OTP
if (debug.messageEnabled()) {
}
sameWindow = true;
}
return true;
}
for (int i = 1; i <= totpStepsInWindow; i++) {
//check time step after current time
return true;
}
//check time step before current time
"than the current times OTP");
return false;
return true;
}
}
} else {
}
} catch (AuthLoginException e) {
// Re-throw to avoid the catch-all block below that would log and lose the error message.
throw e;
} catch (Exception e) {
}
return false;
}
/**
* Returns the first in the set of OATH device settings, or null if no
* device settings were returned.
*/
//get data from the DAO
}
//get rid of white space in string (messes with the data converter)
//convert secretKey to lowercase
//make sure secretkey is even length
}
return secretKey;
}
/**
* Gets the AMIdentity of a user with username equal to userName.
*
* @return The AMIdentity of user with username equal to userName.
*/
private AMIdentity getIdentity() {
idsc.setRecursive(true);
idsc.setAllReturnAttributes(true);
// search for the identity
try {
if (searchResults != null) {
}
} else {
}
} catch (IdRepoException e) {
} catch (SSOException e) {
}
return theID;
}
/**
* Sets the HOTP counter for a user.
*
* @param id The user id to set the counter for.
* @param counter The counter value to set the attribute too.
* @param settings The settings to store the value in.
*/
}
/**
* Sets the last login time of a user.
*
* @param id The id of the user to set the attribute of.
* @param time The time <strong>step</strong> to set the attribute to.
* @param settings The settings to store the value in.
*/
// Update the observed time-step drift for resynchronisation
}
}
}