본문 바로가기
프로그래밍/Android, iOS

[Android] 2차 인증(2 factor authentication)

by YuminK 2022. 3. 30.

최근에 회사 내 어플에서 2차 인증 쪽 로직을 추가해서 관련 글을 적어보려고 한다.

일단 흐름은 로그인을 이용하여 우리 어플을 사용하는 유저들이 2차 인증을 통과한 이후에만 서비스를 이용할 수 있도록 하는 기능이다. 2차 인증의 방식에는 Email / OTP 인증이 있는데 google OTP를 주로 많이 사용하는 듯하다.

이메일 같은 경우에는 모바일 앱에서 이메일에 대한 요청을 보내고 인증 코드를 입력하면 유효성을 확인하는 통신 처리가 필요하다. 이거는 흔히 사용하는 방식이니까 굳이 더 자세한 설명은 필요 없을 것 같다.

Google OTP의 경우에는 구글에서 따로 제공하는 어플이 있는데 우리가 키를 등록하면 30초를 주기로 6자리 인증 코드를 생성해 준다. 우리가 어플에서 인증을 받는 상황에서는 서버에서 확인하는 로직 없이 로컬에서 처리할 수 있도록 되어 있다. 또한 동일한 키를 이용하여 계정을 등록한 경우에는 동일한 코드가 생성이 된다.

1. 초기 상태에서는 OTP Key를 등록하여 서버에 저장을 해놓는다.

(모바일에 OTP 앱을 설치하고 해당 키를 이용하여 OTP 계정을 등록한 상태이다.)

2. 유저가 로그인을 하는 상황에서 해당 키를 얻고 이 키와 OTP 앱에서 알려주는 인증 코드를 입력하여 2차 인증 과정을 거친다.

번외적으로 OTP 앱이 존재하지 않거나 서버에 키를 등록을 하지 않은 경우라면 OTP라는 인증 수단을 이용할 수 없도록 할 수 있다. OTP 기능 활성화 여부가 존재한다면 설정하지 않은 경우에도 막아두는 것이 가능하다.

필요한 추가 정보(로그인 상황)

1. OTP 활성화 여부

2. OTP 암호화된 키 정보

필요 API

1. OTP 인증을 위한 otp Key 등록 API

2. OTP 활성화 여부를 바꾸는 API

3. 이메일을 통해서 인증을 하는 경우에 입력한 코드가 맞는 정보인지 확인하는 API

4. 이메일 인증 요청 API

제 경우에는 이러한 부분이 필요했는데 기획에 따라 구현하시면 됩니다.

라이브러리 경로에 jar 파일 추가합니다.

commons-codec-1.9.jar
0.25MB

 

build.gradle 파일에 jar 파일을 추가합니다.

implementation files('libs/commons-codec-1.9.jar')

구글 OTP 관련 코드는 https://lasdri.tistory.com/793 블로그에서 가져왔습니다.

 

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Random;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base32;

// https://lasdri.tistory.com/793
public class GoogleOTP {

    public HashMap<String, String> generate(String userName, String hostName) {
        HashMap<String, String> map = new HashMap<String, String>();
        byte[] buffer = new byte[5 + 5 * 5];
        new Random().nextBytes(buffer);
        Base32 codec = new Base32();
        byte[] secretKey = Arrays.copyOf(buffer, 10);
        byte[] bEncodedKey = codec.encode(secretKey);

        String encodedKey = new String(bEncodedKey);
        String url = getQRBarcodeURL(userName, hostName, encodedKey);

        map.put("encodedKey", encodedKey);
        map.put("url", url);
        return map;
    }

    public boolean checkCode(String userCode, String otpkey) {
        long otpnum = Integer.parseInt(userCode); // Google OTP 앱에 표시되는 6자리 숫자
        long wave = new Date().getTime() / 30000; // Google OTP의 주기는 30초
        boolean result = false;
        try {
            Base32 codec = new Base32();
            byte[] decodedKey = codec.decode(otpkey);
            int window = 3;
            for (int i = -window; i <= window; ++i) {
                long hash = verify_code(decodedKey, wave + i);
                if (hash == otpnum) result = true;
            }
        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return result;
    }

    private static int verify_code(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
        byte[] data = new byte[8];
        long value = t;
        for (int i = 8; i-- > 0; value >>>= 8) {
            data[i] = (byte) value;
        }

        SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(signKey);
        byte[] hash = mac.doFinal(data);

        int offset = hash[20 - 1] & 0xF;

        // We're using a long because Java hasn't got unsigned int.
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
            truncatedHash <<= 8;
            // We are dealing with signed bytes:
            // we just keep the first byte.
            truncatedHash |= (hash[offset + i] & 0xFF);
        }

        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;

        return (int) truncatedHash;
    }

    public static String getQRBarcodeURL(String user, String host, String secret) {
        String format = "http://chart.apis.google.com/chart?cht=qr&chs=200x200&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s&chld=H|0";
        return String.format(format, user, host, secret);
    }
}

 

사용 예시(Kotlin)

val googleOtp by lazy { GoogleOTP() } 

// OTP 키 생성
// 만들어준 OTP 키를 이용하여 OTP 앱에 직접 등록하여 사용
val map = googleOtp.generate("name", "host")
strEncodedKey = map["encodedKey"]!!

// OTP 앱에서 생성된 코드와 등록된 키를 가지고 OTP 인증을 시도한다.
// strEncodedKey : 로그인 상황에서 서버에 등록된 키를 받아와서 사용
if(googleOtp.checkCode(strPinCode, strEncodedKey))
{
  // 인증 성공
}
else
{
   // 인증 실패
}

댓글