Tag Archives: google

Android In-App Billing 서버사이드 보안 완벽 정리

google_developers_logo

이번에는 안드로이드 어플리케이션의 앱내 구매(In-App Billing)를 사용한 상품 구매시에 해킹 여부를 서버에서 검증하는 방법에 대해 정리해보겠습니다. 기존에 인터넷 상에 최종 사용자(End User) 기준의 구글 API 사용 방법에 대한 레퍼런스는 많은편인것 같으니 좀 더 사용이 용이한 서비스 계정(Service Account)를 이용한 결제 검증을 하는 방법에 대해 알아보겠습니다. 이 문서는 [Android In-App Billing 보안 완벽 정리]를 먼저 읽으신 다음에 읽으시길 권장드립니다.

여기서 사용하려는 방식은 구글 사용자의 계정 로그인 후 엑세스/리프레시 토큰을 관리 유지하면서 API를 사용하지 않고 구글이 제공하는 여러 인증 방식중에 하나인 서비스 계정으로 API를 바로 사용하는 방법입니다. 이를 위해서는 먼서 서비스 계정을 만들 필요가 있습니다. 해당 부분은 [Server to Server 어플리케이션을 위한 Google OAuth 2.0]에 정리해 두었으니 이 내용을 참고하셔서 서비스 계정을 우선 준비하시기 바랍니다. 이후의 내용은 이 서비스 계정이 준비된 상태라고 판단하고 진행을 하겠습니다.

서비스 계정 발급을 통해 준비가 되어야 하는 것은 크게 두가지 입니다.

  • 유니크한 구글이 발급한 이메일 주소
  • 구글이 생성한 P12 방식 비밀키 파일

구글 개발자 센터에서 서비스 계정 연결하기

준비에 문제가 없다면 구글 개발자 콘솔에 접속하여 현재 설정 상태가 구글 API를 호출하기에 문제가 없는지 점검을 해보겠습니다. 우선 설정 – API 액세스에 들어가 봅니다.

google_validation_iab_01

오른쪽에 나오는 정보중에 마지막으로 서비스 계정이라는 항목이 있습니다. 여기에 미리 추가했던 서비스 계정이 보여지게 됩니다. 엑세스 권한 부여 버튼을 눌러 해당 서비스 계정으로 안드로이드 관련 API를 호출 할 수 있도록 해 줍니다. 이미 권한이 부여되었다면 권한 보기 버튼이 있을 것입니다.

google_validation_iab_03

여기서 중요한것은 구매내역 및 영수증 검증을 하기 위해서는 재무 보고서 보기 권한이 필요하다는 부분입니다. 역할을 금융으로 선택해 주면 해당 권한이 자동으로 선택됩니다. 즉 영수증 검증을 위해서는 금융 역할을 갖는 서비스 계정이 필요합니다.

google_validation_iab_04

정상적으로 권한 설정이 된것을 확인할 수 있습니다. 이것으로 Google 쪽에서 필요한 작업은 모두 끝났습니다. 이과정은 하나의 안드로이드 개발자 당 한번만 수행하면 됩니다. 앱을 추가할때마다 하실 필요는 없습니다. 이제 코드를 보도록 하겠습니다.

구글 클라이언트 라이브러리를 이용하여 API 호출하기

복잡하게 토큰을 관리하며 HTTP/REST API를 사용할것 없이 구글이 제공하는 클라이언트 라이브러리를 사용하도록 합시다. [이곳]에서 자신의 언어에 맞는 라이브러리를 찾으실 수 있습니다. 여기서는 Java로 진행하도록 하겠습니다. Java 기준 필요한 라이브러리는 두가지입니다. google-api-client 와 google-api-services-androidpublisher 입니다. 저의 경우에는 gradle을 사용하여 예제 프로젝트를 만들어보았습니다. 다음과 같이 dependencies를 추가 해 주면 됩니다.

dependencies {
    compile 'com.google.api-client:google-api-client:1.21.0'
    compile 'com.google.apis:google-api-services-androidpublisher:v2-rev22-1.21.0'
}

이제 다음과 같은 방법으로 GoogleCredential을 생성해주도록 합시다. 여기서는 기존에 서비스 계정 발급을 통해 받은 이메일주소와 P12 비밀키 파일을 잘 지정해 주셔야 합니다.

String emailAddress = "my-new-service-account@api-project-000000.iam.gserviceaccount.com";

JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
GoogleCredential credential = new GoogleCredential.Builder()
     .setTransport(httpTransport)
     .setJsonFactory(JSON_FACTORY)
     .setServiceAccountId(emailAddress)
     .setServiceAccountPrivateKeyFromP12File(new File("src/GooglePlayAndroidDeveloperPrivateKey.p12"))
     .setServiceAccountScopes(Collections.singleton("https://www.googleapis.com/auth/androidpublisher"))
     .build();

이제 특정 구글 앱내 구매를 처리하기 위해서 다음과 같은 간단한 코드만으로 API를 호출하고 결과를 확인할 수 있습니다.

String packageName = "pe.kr.theeye.trivialdrive";
String productId = "gas";
String purchaseToken = "njnbhmfid...AuSkyASqY";

AndroidPublisher publisher = new AndroidPublisher.Builder(httpTransport, JSON_FACTORY, credential)
	.setApplicationName(packageName)
	.build();

AndroidPublisher.Purchases.Products.Get get = publisher.purchases().products().get(packageName, productId, purchaseToken);
ProductPurchase productPurchase = get.execute();
System.out.println(productPurchase.toPrettyString());

// 인앱 상품의 소비 상태. 0 아직 소비 안됨(Yet to be consumed) / 1 소비됨(Consumed)
Integer consumptionState = productPurchase.getConsumptionState();

// 개발자가 지정한 임의 문자열 정보
String developerPayload = productPurchase.getDeveloperPayload();

// 구매 상태. 0 구매완료 / 1 취소됨
Integer purchaseState = productPurchase.getPurchaseState();

// 상품이 구매된 시각. 타임스탬프 형태
Long purchaseTimeMillis = productPurchase.getPurchaseTimeMillis();

받을 수 있는 결과의 예시는 다음과 같습니다. 정상적인 상품이라면 HTTP 200 OK가 떨어지며 다음과 같이 정상적인 상품의 정보와 consumptionState가 1이고 purchaseState가 0이게 됩니다.  consumptionState가 0이라면 개발자의 실수라던가 구매 과정에서 앱이 크래시되거나 다양한 이유가 있겠지만 어쨌든 결제 과정에서 오류가 발생했다고 보시면 될 것 같습니다.

{
  "consumptionState" : 1,	// 0 아직 컨슘 안됨, 1 컨슘됨
  "developerPayload" : "",
  "kind" : "androidpublisher#productPurchase",
  "purchaseState" : 0,	// 0 구매완료, 1 취소
  "purchaseTimeMillis" : "1454502702978"
}

purchaseState의 경우 결제 완료 상태의 경우 0으로 오게 되며 개발자가 환불을 해주거나 유저가 임의로 환불을 한 상태라면 1로 바뀌게 됩니다. 이게 즉시 바뀌지는 않고 몇분정도 딜레이가 있는것 같습니다.

만약에 위변조된 구매 토큰을 가지고 검증 요청을 하였다면 다음과 같은 오류 메시지를 받게 됩니다. HTTP 400번대의 오류코드가 떨어지게 됩니다. 인증관련 오류는 401이 존재하지 않는 구매 영수증의 경우 404가 떨어집니다.

{
  "code" : 404,
  "errors" : [ {
    "domain" : "global",
    "location" : "token",
    "locationType" : "parameter",
    "message" : "The purchase token was not found.",
    "reason" : "purchaseTokenNotFound"
  } ],
  "message" : "The purchase token was not found."
}

이런 오류의 경우 get.execute() 호출 할 때에 GoogleJsonResponseException 예외가 발생하니 관련하여 적절한 처리를 해주시면 됩니다.

참고 사항

Google Purchase API 버전별 차이점 (v1.1 / v2)

기존에 v1.1은 권한과 상관없이 아무앱의 결제 정보를 영수증값과 패키지명 등을 이용하여 조회가 가능했다고 들었지만 실제로 테스트 해본 결과 다른 앱의 정보를 조회하는것은 불가능했습니다. v1.1 및 v2 모두 다음과 같이 권한 오류가 발생하였습니다.

{
  "error": {
    "errors": [
      {
        "domain": "androidpublisher",
        "reason": "permissionDenied",
        "message": "The current user has insufficient permissions to perform the requested operation."
      }
    ],
    "code": 401,
    "message": "The current user has insufficient permissions to perform the requested operation."
  }
}

purchaseState의 값을 조회하면 현재 구매 완료된 상태인지 취소된 상태인지 여부를 확인할 수 있습니다. 하지만 v1.1에서는 무조건 0으로 나오는 등 정상적으로 값을 조회할 수 없었습니다. 결제 취소 여부를 조회할 필요가 있으시다면 필히 v2로 구현을 하시기를 추천드립니다.

쿼터 제약 사항

구글플레이 개발자 API를 사용하는 어플리케이션들은 초기 사용 할당량으로 하루에 (하나의 어플리케이션 당) 20만건을 부여받습니다. 이 정도의 수량이면 대부분의 일반적인 어플리케이션들에는 충분한 양이지만 만약에 더 높은 제한 조건이 필요할 경우 구글 개발자 콘솔의 “Request more” 링크를 사용해 주시기 바랍니다.

참고 :

https://stackoverflow.com/questions/11115381/unable-to-get-the-subscription-information-from-google-play-android-developer-ap
https://developers.google.com/android-publisher/api-ref/purchases/products

Server to Server 어플리케이션을 위한 Google OAuth 2.0

google_developers_logo

Google OAuth 2.0 시스템은 웹 어플리케이션과 구글 서비스간의 통신과 같은 서버간 통신을 지원합니다. 이러한 시나리오를 위해서는 개별 최종 사용자 대신에 당신의 어플리케이션에 속하는 서비스 계정(Service Account)가 필요합니다. 당신의 어플리케이션은 서비스 계정을 이용하여 구글 API를 호출할 수 있으며 이 경우 사용자가 직접적으로 연관되지 않습니다. 이 시나리오는 “Two-Legged OAuth” 또는 “2LO”로 불립니다. (비슷한 용어로 “Three-Legged OAuth”의 경우에는 최종 사용자가 구글 API를 호출하는 경우를 말하며 이때에 권한 요청 화면이 뜰 수 있습니다)

일반적으로, 어플리케이션이 구글 API를 호출할 때 사용자의 정보보다는 어플리케이션 자체의 정보를 가지고 작업할 때 서비스 계정을 사용합니다. 예를 들어 데이터를 영구적으로 저장하기 위해 Google Cloud Datastore를 사용하는 어플리케이션이 있다면 서비스 계정을 사용하여 인증하여 사용할 수 있습니다.

Google Apps의 도에인 관리자를 이용하면 도메인의 유저들을 대표하여 사용자 정보에 접근할 수 있는 전체 도메인 권한을 부여할 수 있습니다. 이 문서에서는 어플리케이션이 Google APIs 클라이언트 라이브러리 또는 HTTP를 이용하여 OAuth 2.0 서버간(Server to Server) 호출을 하는 방법에 대해 정리합니다.

개요

서버간 상호작용을 지원하기 위해서 먼저 개발자 콘솔에 있는 당신의 프로젝트의 서비스 계정을 만들어야 합니다. 만약 당신이 Google Apps 도메인 내의 사용자 정보에 접근하고자 한다면 도메인 전체(domain-wide) 접근 권한을 서비스 어카운트에 부여해야 합니다.

그다음 당신의 어플리케이션이 OAuth 2.0 인증 서버로부터 엑세스토큰을 요청하기 위해  서비스 계정 자격을 이용하여 인증 API 요청을 만듭니다.

마지막으로  당신의 어플리케이션은 구글 API를 호출할 수 있는 엑세스토큰을 사용할 수 있게 됩니다.

추천 : 당신의 어플리케이션은 이러한 작업을 당신이 사용하는 언어에 적절한 구글 API 클라이언트 라이브러리를 사용하거나 HTTP를 직접 이용하여 OAuth 2.0 시스템과 통신하는 방법을 사용할 수 있습니다. 하지만 서버간 통신을 통한 인증 과정에서 암호화 사인이 된 JSON Web Tokens (JWTs)를 만들어 사용하게 되고 이 부분에서 당신의 어플리케이션의 보안에 치명적인 영향을 끼칠 수 있는 심각한 에러가 발생하기 쉽습니다.

이런 이유로 우리는 구글 API 클라이언트 라이브러리를 사용하여 당신의 어플리케이션 코드에서 암호화 관련 처리를 신경쓰지 않기를 강력하게 추천합니다.

서비스 계정 만들기

서비스 계정 자격은 유니크하게 생성된 이메일주소(email address)와 최소한 한개의 공개/비밀 키쌍(public/private key pair)이 포함됩니다. 만약에 도메인 전체 위임을 활성화 한다면 클라이언트 아이디(client id) 역시 서비스 계정 자격에 포함되어야 합니다.

당신의 어플리케이션이 구글 앱 엔진(Google App Engine)에서 돌아간다면 서비스 계정은 당신의 프로젝트가 생성될 때 자동으로 설정됩니다.

만약 당신의 어플리케이션이 구글 컴퓨트 엔진(Google Compute Engine)에서 돌아간다면 프로젝트 생성시 서비스 계정은 자동으로 설정되지만 어플리케이션이 접근하고자 하는 접근 범위(Scope)를 정의해 주어야 합니다. [참고]

당신의 어플리케이션이 구글 앱엔진 또는 구글 컴퓨트엔진 에서 동작하지 않는다면 구글 개발자 콘솔에서 이러한 자격을 획득하여야 합니다. 서비스 계정 자격을 생성하기 위해서, 혹은 이미 생성된 공개된 자격을 보기 위해서는 다음을 수행합니다.

  1. 개발자 콘솔 권한(Permission) 페이지의 서비스 계정 섹션(Service accounts section)에 진입합니다.

google_api_oauth_01

2. 서비스 계정 만들기 버튼을 누릅니다.

google_api_oauth_02

3. 서비스 계정 만들기 창에서 서비스 계정의 이름을 입력하고, 새 비공개 키 제공을 선택합니다. 만약 구글 앱스 도메인 전체 인증을 허용하려면 Google Apps 도메인 전체 위임 사용을 체크하시면 됩니다. 그리고 만들기를 클릭합니다.

google_api_oauth_03

당신의 새로운 공개/비공개 키 쌍이 생성되며 당신의 컴퓨터에 다운로드 됩니다. 이것은 단 하나의 복사본이기에 당신은 안전하게 이 키를 보관할 의무가 있습니다.

당신은 언제든지 이 개발자 콘솔에 돌아와서 이메일 주소, 키 ID와 같은 정보를 확인할 수 있습니다. 또는 추가로 공개/비공개 키를 생성할 수 있습니다. 개발자 콘솔에서의 서비스 계정 자격에 대한 더 자세한 내용은 [참고]를 확인해주세요.

서비스 계정의 이메일 주소를 적어놓고 서비스 계정의 P12 비밀키 파일을 당신의 어플리케이션이 접근할 수 있는 위치에 저장하십시오. 당신의 어플리케이션이 인증 API 호출을 만드는데 이것들이 필요합니다.

참고 : 당신은 개발 환경과 프로덕션 환경 모두에서 이 비밀키를 안전하게 보관하고 관리해야 합니다. 구글은 당신의 비밀키의 복사본을 보관하지 않습니다. 단지 공개키만을 가지고 있습니다.

인증 API 호출을 준비하기

구글에서는 구글 API 호출을 위한 라이브러리로 Java를 비롯한 다양한 언어를 지원하고 있습니다. [참고]를 확인해주세요.

구글 개발자 콘솔에서 이메일 주소와 비밀키를 획득한 뒤 JAVA용 구글 API 클라이언트 라이브러리를 이용하여 서비스 계정 자격 및 어플리케이션이 엑세스를 원하는 범위를 정의하는 GoogleCredential을 생성합니다.

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.sqladmin.SQLAdminScopes;

// ...

String emailAddress = "123456789000-abc123def456@developer.gserviceaccount.com";
JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
GoogleCredential credential = new GoogleCredential.Builder()
    .setTransport(httpTransport)
    .setJsonFactory(JSON_FACTORY)
    .setServiceAccountId(emailAddress)
    .setServiceAccountPrivateKeyFromP12File(new File("MyProject.p12"))
    .setServiceAccountScopes(Collections.singleton(SQLAdminScopes.SQLSERVICE_ADMIN))
    .build();

만약 도메인 전체 위임을 포함한 서비스 어카운트이고 특정 유저로 가장하여 사용하고 싶을 경우 GoogleCredential 팩토리의 setServiceAccountUser 메소드를 사용하여 사용자 계정의 이메일 주소를 특정해 주시기 바랍니다.

GoogleCredential credential = new GoogleCredential.Builder()
    .setTransport(httpTransport)
    .setJsonFactory(JSON_FACTORY)
    .setServiceAccountId(emailAddress)
    .setServiceAccountPrivateKeyFromP12File(new File("MyProject.p12"))
    .setServiceAccountScopes(Collections.singleton(SQLAdminScopes.SQLSERVICE_ADMIN))
    .setServiceAccountUser("user@example.com")
    .build();

이제 이렇게 만들어진 GoogleCredential 객체를 사용하여 당신의 어플리케이션에서 구글 API를 호출할 수 있습니다.

구글 API 호출

GoogleCredential 객체를 사용하여 구글 API를 다음과 같은 방법으로 호출할 수 있습니다.

1. GoogleCredential 객체를 사용하여 당신이 사용하고자 하는 API의 서비스 객체를 생성합니다.

SQLAdmin sqladmin =
    new SQLAdmin.Builder(httpTransport, JSON_FACTORY, credential).build();

2. 서비스 객체가 제공하는 인터페이스를 사용하여 원하는 API의 요청을 생성합니다. 예를 들어 exciting-example-123 프로젝트의 클라우드 SQL 데이터베이스에 존재하는 리스트를 불러오는 코드는 다음과 같습니다.

SQLAdmin.Instances.List instances =
    sqladmin.instances().list("exciting-example-123").execute();

참고 : Using OAuth 2.0 to Access Google APIs