2FA в Oracle ApEx

    Предлагаю Вашему вниманию реализацию 2FA в Oracle Application Express. В качестве второго фактора будет использовано решение от Google с установленным на телефон приложением Authenticator.



    Данная реализация не претендует на присвоение себе статуса best practice, цель написания этой статьи — поделиться этим решением и получить рекомендации по улучшению, повышению безопасности используемого кода.

    Как только я приступил к этой задаче, я обнаружил, что встроенная процедура apex_authentication.login принимает на вход только два параметра, возможность модификации данной процедуры под свои нужды даже не рассматривал, поскольку это могло повлиять на другие веб-приложения. В данной реализации перед вызовом встроенной процедуры аутентификации, проверяющей пару login & password, вызывается самописная процедура Preauth, проверяющая пару login & one time password.

    Исходные данные: аутентификация в веб-приложение основана на проверке пары логина и пароля, хранимых в таблице users соответствующей схемы (у каждого веб-приложения своя схема). В эту таблицу нужно добавить столбец, в котором будут храниться secret keys для второго фактора. Для большей безопасности можно создать отдельную таблицу с ограниченными правами доступа, в которой будут храниться id пользователей и ключи зашифрованном виде.

    Ключ представляет собой base32-строку из 16 символов в верхнем регистре. Для того, чтобы секретный ключ сохранить в приложении Google Authenticator, можно сгенерировать QR-код, к примеру, воспользовавшись этим решением.

    На странице Login page в конструкторе ApEx как правило есть только два поля: username и password. Необходимо добавить дополнительное поле, к примеру с именем P101_TOTP, сюда пользователь будет вводить 6 цифр из приложения Google Authenticator. На этой же странице добавляется процесс, инициируемый по событию After Submit, запускающий процедуру Preauth. Выглядит это вот так:



    Процедура Preauth сравнивает one time password с кодом, который генерируется на сервере с тем же самым секретным ключом:

    Код процедуры
    create or replace PROCEDURE "PREAUTH" 
    (p_username IN VARCHAR2
    ,p_totp IN VARCHAR2) 
    AS
      l_value          NUMBER;
      usersToken    VARCHAR2(20);
      tempToken    VARCHAR2(20);
      l_current_sid number;
    BEGIN
        SELECT token INTO usersToken FROM users WHERE upper(users.login) = upper(p_username);
        IF usersToken != '0' THEN  
           BEGIN
              tempToken := TOTP(cSecret => usersToken);
              SELECT 1 INTO l_value FROM users
                 WHERE 1 = 1
                 AND upper(users.login) = upper(p_username)
                 AND USERS.IS_LOCKED = 0
                 AND p_totp = tempToken;
              EXCEPTION
                 WHEN no_data_found
                   OR too_many_rows THEN
                   l_value := 0;
                 WHEN OTHERS THEN
                   l_value := 0;
           END;
        END IF;  
    l_current_sid := apex_custom_auth.get_session_id_from_cookie;
    IF l_value = 0 THEN
         raise_application_error (-20000,'Please, try again');
         apex_util.set_authentication_result(4);
         APEX_AUTHENTICATION.LOGOUT(
                    p_session_id => l_current_sid,
                    p_app_id => v('APP_ID'));
    END IF;
    END PREAUTH;

    Как видим, процедура обращается к функции с именем TOTP, автор функции опубликовал её здесь.

    Код функции TOTP
    
    create or replace FUNCTION  "TOTP" 
    (cSecret IN VARCHAR2) RETURN VARCHAR IS
      cBASE32 CONSTANT VARCHAR2(32) := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';  
      szBits VARCHAR2(500) := '';  
      szTmp VARCHAR2(500) := '';  
      szTmp2 VARCHAR2(500) := '';  
      nPos NUMBER;  
      nEpoch NUMBER(38);  
      szEpoch VARCHAR2(16);  
      rHMAC RAW(100);  
      nOffSet NUMBER;  
      nPart1 NUMBER;  
      nPart2 NUMBER := 2147483647;
      nPart3 NUMBER;
      l_obfuscated_password users.pass%TYPE;
      calculatedCode VARCHAR2(6);
    
    FUNCTION to_binary(inNum NUMBER) RETURN VARCHAR2  
    IS  
      szBin VARCHAR2(8);  
      nRem NUMBER := inNum;  
    BEGIN  
      IF inNum = 0 THEN  
          RETURN '0';  
      END IF;  
      WHILE nRem > 0  
      LOOP  
          szBin := MOD(nRem, 2) || szBin;  
          nRem  := TRUNC(nRem / 2 );  
      END LOOP;  
    RETURN szBin;  
    END to_binary; 
    
    BEGIN
      FOR c IN 1..LENGTH(cSecret)  
      LOOP  
      nPos := INSTR( cBASE32, SUBSTR(cSecret, c, 1))-1;  
      szBits := szBits || LPAD( to_binary(nPos), 5, '0');  
      END LOOP;  
      
      nPos := 1;  
      WHILE nPos < LENGTH(szBits)  
      LOOP  
      SELECT LTRIM(TO_CHAR(BIN_TO_NUM( TO_NUMBER(SUBSTR(szBits, nPos, 1)), TO_NUMBER(SUBSTR(szBits, nPos+1, 1)), TO_NUMBER(SUBSTR(szBits, nPos+2, 1)), TO_NUMBER(SUBSTR(szBits, nPos+3, 1)) ), 'x'))  
      INTO szTmp2  
      FROM dual;  
      szTmp := szTmp || szTmp2;  
      nPos := nPos + 4;  
      END LOOP;  
      
      SELECT EXTRACT(DAY FROM (CURRENT_TIMESTAMP-TIMESTAMP '1970-01-01 00:00:00 +00:00'))*86400+  
        EXTRACT(HOUR FROM (CURRENT_TIMESTAMP-TIMESTAMP '1970-01-01 00:00:00 +00:00'))*3600+  
        EXTRACT(MINUTE FROM (CURRENT_TIMESTAMP-TIMESTAMP '1970-01-01 00:00:00 +00:00'))*60+  
        EXTRACT(SECOND FROM (CURRENT_TIMESTAMP-TIMESTAMP '1970-01-01 00:00:00 +00:00')) n  
      INTO nEpoch  
      FROM dual;  
      
      SELECT LPAD(LTRIM(TO_CHAR( FLOOR(nEpoch/30), 'xxxxxxxxxxxxxxxx' )), 16, '0')  
      INTO szEpoch  
      FROM dual;  
      
      rHMAC := DBMS_CRYPTO.MAC( src => hextoraw(szEpoch), typ => DBMS_CRYPTO.HMAC_SH1, key => hextoraw(szTmp) );  
      
      nOffSet := TO_NUMBER( SUBSTR( RAWTOHEX(rHMAC), -1, 1), 'x');  
      
      nPart1 := TO_NUMBER( SUBSTR( RAWTOHEX(rHMAC), nOffSet*2+1, 8), 'xxxxxxxx');  
      calculatedCode := SUBSTR(BITAND( nPart1, nPart2), -6, 6);
      RETURN calculatedCode;
    END "TOTP";
    


    Эта функция работала как положено, за исключением тех случаев, когда генерируемый one time password имел цифру ноль в старшем разряде — возвращаемое значение имело тип Number, и этот ноль игнорировался Oracle. Поэтому я внёс в неё изменения, чтобы возвращался тип VARCHAR2.

    Важное замечание: процедура Preauth проверяет наличие секретного ключа для пользователя, осуществляющего попытку входа. Это сделано намеренно, для того, чтобы «плавно» включить использование второго фактора в веб-приложении, т.е. не у всех пользователей сразу. Для выключения этой проверки достаточно закомментировать строку:

    IF usersToken != '0' THEN  

    Спасибо за внимание.
    Поделиться публикацией

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое