# 인증

팝빌 API를 사용하기 위한 공통 정의 사항과 인증 토큰 발급 및 사용 방법을 안내합니다.

## 1. 공통 정의 사항

팝빌 API 규격은 다음과 같이 정의합니다.

- RESTful API
- HTTPS 통신 / TLS 1.3, 1.2 지원
- JSON 메시지 포맷

> API 호출과 응답에 필요한 문자 인코딩은 모두 UTF-8 Character Set 을 사용합니다.

팝빌 API에서 사용하는 Base URL은 다음과 같습니다.

| 환경            | Base URL                            | ServiceID     |
| ------------- | ----------------------------------- | ------------- |
| 팝빌 인증 서버      | https\://auth.linkhub.co.kr         | -             |
| 팝빌 API 테스트 서버 | https\://popbill-test.linkhub.co.kr | POPBILL\_TEST |
| 팝빌 API 운영 서버  | https\://popbill.linkhub.co.kr      | POPBILL       |

※ `ServiceID`는 팝빌 API 테스트/운영을 구별하는 고유식별정보입니다.

위 도메인의 IP는 가변적으로 변경됩니다.\
방화벽으로 인해 고정 IP가 필요한 경우에는 아래의 Outbound 정보를 등록해야 합니다. 고정 IP를 설정한 뒤에는 아래의 안내된 도메인으로 호출해야 합니다.

| 환경            | Base URL                                   | IP                          | PORT |
| ------------- | ------------------------------------------ | --------------------------- | ---- |
| 팝빌 인증 서버      | https\://static-auth.linkhub.co.kr         | 52.78.164.186 13.124.222.90 | 443  |
| 팝빌 API 테스트 서버 | https\://static-popbill-test.linkhub.co.kr | 52.78.164.186 13.124.222.90 | 443  |
| 팝빌 API 운영 서버  | https\://static-popbill.linkhub.co.kr      | 52.78.164.186 13.124.222.90 | 443  |

## 2. 인증 방식

팝빌 인증 방식은 팝빌 인증 서버로부터 토큰을 발급받고, 이후 모든 API 호출 시 발급받은 토큰을 함께 전송하는 방식입니다.

```mermaid
sequenceDiagram
    participant 고객사
    participant POPBiLL

    고객사->>POPBiLL: Bearer Token 발급요청
    POPBiLL-->>고객사: Bearer Token 발급
    고객사->>POPBiLL: Bearer Token 이용한 API 호출
    POPBiLL-->>고객사: API 응답
```

1. 팝빌 인증 서버에서 토큰을 발급받으려면 HTTP `Authorization` Header에 다음 형식으로 인증 정보를 기재해야 합니다.

```text
Authorization: LINKHUB {LinkID} {Signature}
```

`LinkID`는 파트너를 식별하기 위한 값으로 팝빌에서 API Key 발급 시 제공됩니다. `Signature`는 요청 본문의 변조 방지를 위한 서명 값으로, API Key 발급 시 함께 제공되는 `SecretKey`를 이용해 HMAC-SHA256 알고리즘으로 생성합니다.

`Signature` 생성 방법은 다음과 같습니다.

```java
// 팝빌에서 발급받은 API Key의 LinkID
String linkID = "TESTER";


// 팝빌에서 발급받은 API key의 SecretKey
String secretKey = "SwWxqU+0TErBXy/9TVjIPEnI0VTUMMSQZtJf3Ed8q3I=";


// 팝빌회원 사업자번호
String accessID = "1234567890";


// 토큰 사용 허용 IP -> 요청 헤더 X-LH-Forwarded 에 사용
String forwardedIP = "*";


// API 버전 -> 요청 헤더 X-LH-Version 에 사용
String apiVersion = "2.0";


// 테스트 환경: POPBILL_TEST, 운영 환경 : POPBILL
String serviceID = "POPBILL_TEST";


// Request Body
String requestBody = "{\"access_id\":\"" + accessID + "\", \"scope\":[\"110\", \"member\"]}";
byte[] bodyBytes = requestBody.getBytes(StandardCharsets.UTF_8);


// BodyDigest (SHA-256 -> Base64 encode)
byte[] sha256Bytes = MessageDigest.getInstance("SHA-256").digest(bodyBytes);
String bodyDigest = Base64.getEncoder().encodeToString(sha256Bytes);


// 요청 일시(UTC) -> 요청 헤더 X-LH-Date 에 사용
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String requestDT = sdf.format(new Date());


// 요청 URI
String resourceURI = "/" + serviceID + "/Token";


// StringToSign 구성
String stringToSign = "POST"     + "\n"
                            + bodyDigest     + "\n"
                            + requestDT     + "\n"
                            + forwardedIP    + "\n"
                            + apiVersion    + "\n"
                            + resourceURI;


// Signature
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(Base64.getDecoder().decode(secretKey), "HmacSHA256"));


byte[] hmacBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
String signature = Base64.getEncoder().encodeToString(hmacBytes);


// Authorization Header
String authorization = "LINKHUB" + " " + linkID + " " + signature;
```

- `bodyDigest` : 요청 본문(Request Body)의 SHA-256 해시값을 Base64로 인코딩한 문자열입니다.
- `requestDT` : 요청 일시로, 반드시 UTC 시간으로 기재해야 합니다.
- `forwardedIP` : 토큰 사용을 허용할 IP를 의미하며, 미입력 시 인증 서버에 토큰 발급 요청을 보낸 IP에서만 사용할 수 있습니다. 모든 IP에서 API 호출을 허용하려면 \*로 입력합니다.
- `apiVersion` : 팝빌 API 버전으로 2.0을 입력합니다.
- `resourceURI` : 토큰을 발급받을 API의 요청 URI로, /{serviceID}/Token 형식으로 구성합니다.
- `stringToSign` : HTTP Method, `bodyDigest`, `requestDT`, `forwardedIP`, `apiVersion`, `resourceURI`를 줄바꿈(\n)으로 연결한 문자열입니다.

`stringToSign`을 `secretKey`로 HMAC-SHA256 서명하고 Base64로 인코딩하면 `signature`가 생성됩니다. 생성된 `signature`를 `Authorization` Header에 입력하여 인증 토큰 발급을 요청합니다.

2. 발급받은 인증 토큰은 팝빌 API를 호출할 때 권한을 검증하는데 사용됩니다. 토큰은 발급 시점부터 30분간 유효하며, 유효 기간이 지난 후에는 새 토큰을 재발급받아 사용해야 합니다.

팝빌 API를 호출할 때 Bearer 방식으로 `Authorization` Header에 토큰 값을 입력합니다.

```text
Authorization: Bearer {session_token}
```

## 3. 인증 토큰 발급

| HTTP Method | 테스트(Sandbox)                                    | 운영(Production)                             |
| ----------- | ----------------------------------------------- | ------------------------------------------ |
| POST        | <https://auth.linkhub.co.kr/POPBILL_TEST/Token> | <https://auth.linkhub.co.kr/POPBILL/Token> |

- 팝빌 API를 사용하기 위한 인증 토큰을 발급합니다.

> 인증 토큰은 사업자번호별로 별도 발급이 필요합니다.\
> 토큰은 발급 시점부터 30분 동안만 유효하며, 만료시에는 새 토큰을 재발급받아야 합니다.

### Request

**요청 헤더**

- Authorization `required: Y` `description: 인증 토큰`
  - [\[참고\] 인증](https://developers.popbill.com/api-reference/htcashbill/getting-started/authorization#authorization)
- Content-Type `required: Y` `description: 요청 본문 형식`
  - `application/json`
- X-LH-Version `required: Y` `description: API 버전`
  - `2.0`
- X-LH-Date `required: Y` `description: 요청 일시(UTC)`
  - 형식 : yyyy-MM-ddTHH:mm:ssZ
- X-LH-Forwarded `required: N` `description: 토큰 사용 허용 IP`
  - `*`: 모든 IP 허용
  - 미입력: 토큰 발급을 요청한 IP에서만 토큰 사용 가능

**요청 본문**

- access\_id `type: string` `length: 10` `required: Y` `description: 팝빌회원 사업자번호`
- scope `type: array` `length: -` `required: Y` `description: API 접근 권한`
  - `110`: 전자세금계산서
  - `140`: 현금영수증
  - `121`: 전자명세서 - 거래명세서
  - `122`: 전자명세서 - 청구서
  - `123`: 전자명세서 - 견적서
  - `124`: 전자명세서 - 발주서
  - `125`: 전자명세서 - 입금표
  - `126`: 전자명세서 - 영수증
  - `111`: 홈택스수집 - 전자세금계산서
  - `141`: 홈택스수집 - 현금영수증
  - `170`: 사업자등록상태조회
  - `171`: 기업정보조회
  - `182`: 예금주조회 - 성명조회
  - `183`: 예금주조회 - 실명조회
  - `180`: 계좌조회
  - `153`: 카카오톡 - 알림톡
  - `156`: 카카오톡 - 브랜드 메시지(I)
  - `157`: 카카오톡 - 브랜드 메시지(N)
  - `158`: 카카오톡 - 브랜드 메시지(M)
  - `150`: 문자 - SMS
  - `151`: 문자 - LMS
  - `152`: 문자 - MMS
  - `160`: 팩스 - 일반망
  - `161`: 팩스 - 지능망
  - `member`: 공통

**요청 예시**

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.TimeZone;


public class Main {


    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException, IOException {


        // 팝빌에서 발급받은 API Key의 LinkID
        String linkID = "TESTER";


        // 팝빌에서 발급받은 API Key의 SecretKey
        String secretKey = "SwWxqU+0TErBXy/9TVjIPEnI0VTUMMSQZtJf3Ed8q3I=";


        // 팝빌회원 사업자번호
        String accessID = "1234567890";


        // 토큰 사용 허용 IP -> 요청 헤더 X-LH-Forwarded에 사용
        String forwardedIP = "*";


        // API 버전 -> 요청 헤더 X-LH-Version에 사용
        String apiVersion = "2.0";


        // 테스트 환경: POPBILL_TEST, 운영 환경 : POPBILL
        String serviceID = "POPBILL_TEST";


        // Request Body
        String requestBody = "{\"access_id\":\"" + accessID + "\", \"scope\":[\"110\", \"member\"]}";
        byte[] bodyBytes = requestBody.getBytes(StandardCharsets.UTF_8);


        // BodyDigest (SHA-256 -> Base64 encode)
        byte[] sha256Bytes = MessageDigest.getInstance("SHA-256").digest(bodyBytes);
        String bodyDigest = Base64.getEncoder().encodeToString(sha256Bytes);


        // 요청 일시(UTC) -> 요청 헤더 X-LH-Date에 사용
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
        String requestDT = sdf.format(new Date());


        // 요청 URI
        String resourceURI = "/" + serviceID + "/Token";


        // StringToSign 구성
        String stringToSign = "POST"     + "\n"
                                    + bodyDigest     + "\n"
                                    + requestDT     + "\n"
                                    + forwardedIP    + "\n"
                                    + apiVersion    + "\n"
                                    + resourceURI;


        // Signature
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(Base64.getDecoder().decode(secretKey), "HmacSHA256"));


        byte[] hmacBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        String signature = Base64.getEncoder().encodeToString(hmacBytes);


        // Authorization Header
        String authorization = "LINKHUB" + " " + linkID + " " + signature;


        URL url = new URL("https://auth.linkhub.co.kr" + resourceURI);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setRequestProperty("Authorization", authorization);
        conn.setRequestProperty("x-lh-forwarded", forwardedIP);
        conn.setRequestProperty("x-lh-date", requestDT);
        conn.setRequestProperty("x-lh-version", apiVersion);


        try (OutputStream os = conn.getOutputStream()) {
            os.write(bodyBytes);
        }


        int status = conn.getResponseCode();
        InputStream is = (status == HttpURLConnection.HTTP_OK) ? conn.getInputStream() : conn.getErrorStream();


        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buf = new byte[8192];
        int n;
        while ((n = is.read(buf)) != -1) {
            baos.write(buf, 0, n);
        }
        is.close();


        String responseBody = new String(baos.toByteArray(), StandardCharsets.UTF_8);


        System.out.println("Body : " + responseBody);
    }
}
```

### Response

**응답 본문**

- session\_token `type: string` `length: -` `description: 인증 토큰`
- serviceID `type: string` `length: -` `description: 테스트/운영 환경 구분 고유식별정보`
  - `POPBILL_TEST`: 테스트
  - `POPBILL`: 운영
- linkID `type: string` `length: -` `description: LinkID`
- userID `type: string` `length: -` `description: 팝빌회원 사업자번호`
- partnerCode `type: string` `length: -` `description: 파트너코드`
- usercode `type: string` `length: -` `description: 팝빌회원 코드`
- scope `type: array` `length: -` `description: API 접근 권한`
  - `110`: 전자세금계산서
  - `140`: 현금영수증
  - `121`: 전자명세서 - 거래명세서
  - `122`: 전자명세서 - 청구서
  - `123`: 전자명세서 - 견적서
  - `124`: 전자명세서 - 발주서
  - `125`: 전자명세서 - 입금표
  - `126`: 전자명세서 - 영수증
  - `111`: 홈택스수집 - 전자세금계산서
  - `141`: 홈택스수집 - 현금영수증
  - `170`: 사업자등록상태조회
  - `171`: 기업정보조회
  - `182`: 예금주조회 - 성명조회
  - `183`: 예금주조회 - 실명조회
  - `180`: 계좌조회
  - `153`: 카카오톡 - 알림톡
  - `156`: 카카오톡 - 브랜드 메시지(I)
  - `157`: 카카오톡 - 브랜드 메시지(N)
  - `158`: 카카오톡 - 브랜드 메시지(M)
  - `150`: 문자 - SMS
  - `151`: 문자 - LMS
  - `152`: 문자 - MMS
  - `160`: 팩스 - 일반망
  - `161`: 팩스 - 지능망
  - `member`: 공통
- ipaddress `type: string` `length: -` `description: 토큰 사용 허용 IP`
- expiration `type: string` `length: -` `description: 토큰 만료일시(UTC)`
  - 형식 : yyyy-MM-ddTHH:mm:ssZ

**응답 예시**

```json
{
    "session_token": "dGfY0osTAoWNdlNwmugjEDTu...4MuWhVplgLrlQ7FzenX98qiZYDSqQ0ISEOJP",
    "serviceID": "POPBILL_TEST",
    "linkID": "TESTER",
    "userID": "1234567890",
    "partnerCode": "014040000003",
    "usercode": "025070002467",
    "scope": [ "member", "110" ],
    "ipaddress": "*",
    "expiration": "2025-11-19T03:41:17.839Z"
}
```
