[Node.js] NICE 본인인증

Dragon_Tack·2024년 2월 4일
0

프로젝트내에 실명인증형 서비스 도입이 필요하여, NICE API연동에 대해 사용했던 히스토리를 기록하고자 한다. 나중에 쓸것같아서,, ㅎ

0. Access Token 요청

먼저 나이스 본인인증 API를 사용하기 전 나이스 API 접근 권한을 확인하기 위한 Access Token이 필요하며, Access Token 요청 API를 호출하여 발급받아야 합니다.

 async getNiceAccessToken() {
    const Authorization = Buffer.from(
      this.client_id + ':' + this.client_secret,
    ).toString('base64');

    const url =
      'https://svc.niceapi.co.kr:22001/digital/niceid/oauth/oauth/token';
    const headers = {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: `Basic ${Authorization}`,
    };
    const dataBody = {
      scope: 'default',
      grant_type: 'client_credentials',
    };
    try {
      const tokenData = await firstValueFrom(
        this.httpService.post(url, dataBody, { headers: headers }).pipe(
          map((response) => {
            return response.data;
          }),
          (error) => error,
        ),
      );

      console.log('tokenData- ', tokenData);

      return tokenData;
    } catch (e) {
      console.log('tokenError Data', e);
    }
  }

1. 암호화 Token 요청

클라이언트에서 본인인증 요청이 들어오면 서버에서 나이스 암호화 Token 요청 API를 호출하여 암호화 Token을 요청합니다.

 //1. 암호화 Token요청
  async getEncryptionToken(access_token, reqDtim, currentTimestamp, reqNo) {
    try {
      console.log(access_token, currentTimestamp, this.client_id);
      const url =
        'https://svc.niceapi.co.kr:22001/digital/niceid/api/v1.0/common/crypto/token';
      const authorization = Buffer.from(
        access_token + ':' + currentTimestamp + ':' + this.client_id,
      ).toString('base64');

      console.log(authorization);
      const data = {
        dataHeader: {
          CNTY_CD: 'ko', // 이용언어 : ko, en, cn
        },
        dataBody: {
          req_dtim: reqDtim, // 요청일시
          req_no: reqNo, // 요청고유번호
          enc_mode: '1', // 사용할 암복호화 구분 1 : AES128/CBC/PKCS7
        },
      };
      const headers = {
        'Content-Type': 'application/json',
        Authorization: `bearer ${authorization}`,
        client_id: this.client_id,
        productID: this.productID,
      };

      const response = await firstValueFrom(
        this.httpService.post(url, data, { headers: headers }).pipe(
          map((response) => {
            return response;
          }),
          (error) => error,
        ),
      );
      const resData = response.data;
      // P000 성공, 이외 모두 오류
      if (
        resData.dataHeader.GW_RSLT_CD !== '1200' &&
        resData.dataBody.rsp_cd !== 'P000'
      ) {
        console.log(
          'Failed to request crypto token',
          response.data.dataBody.rsp_cd,
        );
        throw new HttpException('Failed to request crypto token', 400);
      }
      console.log(resData);
      // throw new HttpException('qqq', 400);
      // 사이트 코드, 서버 토큰 값, 서버 토큰 버전 반환

      return {
        siteCode: resData.dataBody.site_code,
        tokenVal: resData.dataBody.token_val,
        tokenVersionId: resData.dataBody.token_version_id,
      };
    } catch (error) {
      throw new HttpException('암호화 토큰 발급요청 실패', 400);

      console.log('Failed to get encryption token', error);
    }
  }

2. 대칭키 생성

암호화 Token 요청 API 요청일시와 요청시간, 응답받은 토큰 값의 조합으로 요청 데이터를 암호화할 대칭키와 무결성키를 생성해 줍니다. 그리고 대칭키는 나이스로부터 반환될 인증 결과를 복호화 하는데 다시 사용되기 때문에 세션에 저장해 줍니다.

 async generateSymmetricKey(reqDtim, reqNo, tokenVal) {
    if (!reqDtim || !reqNo || !tokenVal) {
      throw new Error('Empty parameter');
    }
    const value = reqDtim.trim() + reqNo.trim() + tokenVal.trim();
    // SHA256 암호화 후 Base64 encoding
    const hash = createHash('sha256').update(value).digest('base64');
    const key = hash.slice(0, 16); // 앞에서부터 16byte
    const iv = hash.slice(-16); // 뒤에서부터 16byte
    const hmacKey = hash.slice(0, 32); // 앞에서부터 32byte
    return {
      key,
      iv,
      hmacKey,
    };
  }

3-1. 요청 데이터 암호화

나이스 본인인증 화면을 호출하기 위한 요청 값을 위에서 생성한 대칭키로 암호화해줍니다. returnurl와 methodtype 경우 나이스 본인인증 처리 결과를 리다이렉트 받을 url과 http method로 해당 값으로 인증 결과를 처리할 API 생성이 필요합니다.

 async encryptData(data, key, iv) {
    try {
      if (!data || !key || !iv) {
        throw new Error('Empty parameter');
      }
      const strData = JSON.stringify(data).trim();

      // const iconvs = iconv.encode(strData, 'euc-kr');
      //얘를 암호화해

      // AES128/CBC/PKCS7 암호화
      const cipher = createCipheriv(
        'aes-128-cbc',
        Buffer.from(key),
        Buffer.from(iv),
      );
      let encrypted = cipher.update(strData, 'utf-8', 'base64');
      encrypted += cipher.final('base64');

      return encrypted;
    } catch (e) {
      console.log('Failed to Encrypt data', e);
    }
  }

3-2. Hmac 무결성 체크값 생성
나이스 본인인증 화면을 호출하기 위한 요청 값으로 무결성 체크값 또한 생성해줍니다.

async hmac256(data, hmacKey) {
    try {
      if (!data || !hmacKey) {
        throw new Error('Empty parameter');
      }
      const hmac = createHmac('sha256', Buffer.from(hmacKey));
      hmac.update(Buffer.from(data));
      const integrityValue = hmac.digest().toString('base64');
      return integrityValue;
    } catch (error) {
      console.log('Failed to generate HMACSHA256 encrypt', error);
    }
  }

4. 나이스 표준창서비스 호출

암호화 토큰 요청 API에서 반환받은 토큰 버전 아이디, 암호화한 요청 데이터, 무결성 값을 클라이언트에 반환합니다. 그러면 클라이언트에서는 해당 데이터를 가지고 pass 본인인증 서비스를 팝업 형태로 호출합니다.

 async getNiceForm(query: NiceFormDto, user) {
    //[1] - 나이스 본인인증요청

    //1.암호화 Token 요청 -- 이건 매번요청떄마다 하는것?
    const nowDate = new Date();
    const reqDtim = this.formatDate(nowDate);
    const currentTimestamp = Math.floor(nowDate.getTime()     / 1000);
    const reqNo = v4().substring(0, 30);

    try {
      const secert_token = await this.getEncryptionToken(
        this.access_token,
        reqDtim,
        currentTimestamp,
        reqNo,
      );

      if (!secert_token) {
        throw new HttpException('암호화 토큰 발급요청 실패', 400);
      }

      //2.대칭키 생성
      const hmacInfo: any = await this.generateSymmetricKey(
        reqDtim,
        reqNo,
        secert_token.tokenVal,
      );
     
      const key = hmacInfo.key;
      const iv = hmacInfo.iv;
      const hmacKey = hmacInfo.hmacKey;
      // key ,iv 는 DB또는 세션에서 저장해서 갖고있어야함 <<<<<--

      const set = await this.cacheService.set(
        'symmetric:' + secert_token.tokenVersionId,
        { key: key, iv: iv },
        1000 * 60 * 60,
      );
      console.log('set key - ', secert_token.tokenVersionId, 'value - ', {
        key,
        iv,
      });

      //어떤유저의 어떤요청이 시작되었는지..?

      //3.1 요청 데이터 암호화
      const requestno = reqNo; // 서비스 요청 고유 번호(*)
      const returnurl = this.appUrl + this.callback_url; // 인증결과를 받을 url(*)
      const sitecode = secert_token.siteCode; // 암호화토큰요청 API 응답 site_code(*) ??
      const authtype = 'M'; // 인증수단 고정(M:휴대폰인증,C:카드본인확인인증,X:인증서인증,U:공동인증서인증,F:금융인증서인증,S:PASS인증서인증)
      const mobileco = ''; // 이통사 우선 선택
      const bussinessno = ''; // 사업자번호(법인인증인증에 한함)
      const methodtype = 'get'; // 결과 url 전달 시 http method 타입
      const popupyn = 'Y'; //

      //휴대폰인증
      //핀번호
      //회원탈퇴
      let receivedata: any = {
        user_idx: user.user_idx,
        req_type: query.req_type,
      }; // 인증 후 전달받을 데이터 세팅
      receivedata = JSON.stringify(receivedata);

      const reqData = {
        requestno: requestno,
        returnurl: returnurl,
        sitecode: sitecode,
        authtype: authtype,
        methodtype: methodtype,
        popupyn: popupyn,
        receivedata: receivedata,
      };
      const encData = await this.encryptData(reqData, key, iv);

      //3.2 Hmac 무결성 체크값 생성(?)
      const integrityValue = await this.hmac256(encData, hmacKey);

      //클라이언트에 뿌려줌
      return {
        tokenVersionId: secert_token.tokenVersionId,
        encData: encData,
        integrityValue: integrityValue,
      };
      //[6] - NICE 표준창 호출
    } catch (e) {
      console.log(e);
      throw new HttpException(e.response, 400);
    }
  }

5. 인증결과 확인

본인인증 서비스를 마치면 나이스는 인증결과를 나이스 본인인증 화면을 호출하기 위한 요청 값에서 입력한 returnurl(/api/auth/nice/callback)로 반환해 줍니다.

 @Get('/api/auth/nice/callback')
  async getNiceCallBack(@Query() query: NiceCallbackDto, @Res() res) {
    try {
      // Process your data or perform necessary operations
      // Redirect after successful processing
      const result = await this.niceService.getNiceCallBack(query);
      const queryString = '?' + new URLSearchParams(result).toString();
      return res.redirect(this.front_url + this.success_url + queryString);
    } catch (error) {
      // Redirect in case of an error
      const queryString = `?errorCode=${error.response}`;
      return res.redirect(this.front_url + this.fail_url + queryString);
    }
  }

위와 같은 프로세스로 비즈니스 로직에 필요한 부분을 암호화 생성시에 쿼리스트링으로 말아주고 인증결과 확인시에 추가적으로 필요한 결과값이나 그에 따른 요청처리를 진행해주면 됩니다.

로그인 폼을 띄울때 암호키의 생명주기가 결정되며, 대칭키와 일치한지의 검증이 필요하기에 분산서버 로직에서는 in-memory가아닌 redis 캐시서버에 적절한 해시키값을 매칭해서 관리해주면 좋을듯합니다.

profile
고민의 흔적을 늘여놓는 공간

0개의 댓글