---
title: SDK 인증과 App Links
description: 시스템 브라우저 로그인과 PKCE callback을 Android 앱에 연결하는 방법입니다.
---

# 인증과 App Links

SDK는 시스템 브라우저에서 OpenTester 로그인을 진행합니다. 사용자가 앱 연결을 승인하면 callback으로 받은 일회용 코드를 PKCE 검증과 함께 앱 범위 토큰으로 교환합니다. 비밀번호와 Google 로그인 토큰을 호스트 앱에 전달하지 않습니다.

```text
Android 앱 → OpenTester 브라우저 로그인 → 앱 참여 승인 확인
          ← 등록된 callback + code + state
Android SDK → 코드 교환 → 앱 범위 토큰 → 권한 확인 / 보고서 제출
```

## 1. callback 등록

개발자 콘솔에 다음과 같은 HTTPS callback을 등록하고 `OpenTesterConfig.redirectUri`에 같은 값을 지정합니다.

```text
https://your-app.example.com/opentester/callback
```

서버에 등록된 주소와 정확히 일치해야 합니다. SDK는 scheme·authority·path·state를 검사하며 query나 fragment가 있는 **설정값**을 허용하지 않습니다. callback 응답에는 서버가 `code`와 `state` query를 추가합니다.

## 2. Android App Links 설정

[시작하기](/sdk/)의 debug 빌드 예제에서는 아래 설정을 `app/src/debug/AndroidManifest.xml`에 둡니다. 기존 앱 설정에 필요한 부분을 합치고 실제 패키지명·도메인으로 변경합니다.

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:name="com.example.app.TesterApplication">
        <activity
            android:name="com.example.app.TesterActivity"
            android:exported="true"
            android:launchMode="singleTask">
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="http" />
                <data android:scheme="https" />
                <data android:host="your-app.example.com"
                    android:path="/opentester/callback" />
            </intent-filter>
        </activity>
    </application>
</manifest>
```

Android의 링크 연결 선언에는 HTTP와 HTTPS scheme을 포함하지만, **OpenTester에 등록하는 callback은 HTTPS**입니다. SDK는 다른 scheme으로 들어오는 응답을 거부합니다. Android의 App Links 선언 방식은 [Android 공식 가이드](https://developer.android.com/training/app-links/add-applinks)를 참고하세요.

callback 도메인의 `/.well-known/assetlinks.json`에는 설치되는 앱의 application ID와 서명 인증서 SHA-256 지문을 등록합니다. 예시의 지문 문자열은 실제 값으로 바꿔야 합니다.

```json
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": ["YOUR_SIGNING_CERTIFICATE_SHA256"]
    }
  }
]
```

Play App Signing을 사용한다면 사용자에게 설치되는 앱의 서명 인증서 지문을 사용합니다. debug 빌드를 검증할 때는 debug application ID와 인증서도 해당 테스트 도메인에 맞게 설정합니다. 파일 제공과 인증서 확인 방법은 [웹사이트 연결 구성 가이드](https://developer.android.com/training/app-links/configure-assetlinks)에 설명되어 있습니다.

## 3. 로그인과 callback 처리

다음 Activity는 [시작하기](/sdk/)의 `TesterApplication`을 사용합니다. `app/src/debug/java/com/example/app/TesterActivity.kt`에 두고 테스트 화면에서 이 Activity를 엽니다. SDK 의존성이 제공하는 Kotlin Coroutines를 사용합니다.

```kotlin
package com.example.app

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import dev.opentester.sdk.OpenTesterException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

class TesterActivity : Activity() {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
    private val tester get() = (application as TesterApplication).tester
    private lateinit var status: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        status = TextView(this)
        val login = Button(this).apply {
            text = "테스터 로그인"
            setOnClickListener { tester.authenticate(this@TesterActivity) }
        }
        setContentView(LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(login)
            addView(status)
        })
        consumeCallback(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        consumeCallback(intent)
    }

    private fun consumeCallback(incoming: Intent) {
        val uri = incoming.data ?: return
        incoming.data = null // Activity 재생성 시 같은 callback을 다시 처리하지 않습니다.
        scope.launch {
            try {
                val grant = tester.handleCallback(uri)
                status.text = "${grant.user.name}님, 테스트를 시작하세요."
            } catch (e: CancellationException) {
                throw e
            } catch (e: OpenTesterException) {
                status.text = e.message
            }
        }
    }

    override fun onDestroy() {
        scope.cancel()
        super.onDestroy()
    }
}
```

직접 시스템 브라우저를 열어야 한다면 `createAuthenticationUri()`의 결과를 사용합니다. 이 메서드를 다시 부르면 새 로그인 시도가 시작되므로 URL 확인용으로 반복 호출하지 마세요. 로그인 화면을 WebView에 넣지 않습니다.

## 세션 수명과 재로그인

| 항목               | 동작                                     |
| ------------------ | ---------------------------------------- |
| 인증 대기 상태     | SDK 메모리에서 최대 15분 유지            |
| 서버 일회용 코드   | 발급 후 2분 유효, 한 번만 교환           |
| 앱 범위 토큰       | 발급 후 1시간 유효                       |
| 프로세스 종료      | 토큰과 PKCE 상태가 사라져 재로그인 필요  |
| 새로운 로그인 시작 | 기존 SDK 세션을 지우고 새 PKCE 상태 생성 |
| `logout()`         | SDK의 토큰과 진행 중 로그인 제거         |

토큰 자동 갱신은 지원하지 않습니다. `handleCallback()`에서 네트워크 오류가 발생한 경우에도 새 로그인부터 시작하세요. `logout()`은 브라우저의 로그인 쿠키나 이미 발급된 서버 토큰을 즉시 폐기하지 않습니다.

## 참여 권한 확인

앱의 테스트 화면에 재진입할 때 coroutine에서 다음을 호출할 수 있습니다.

```kotlin
val grant = tester.checkAccess()
```

이 함수는 서버의 현재 승인 상태를 확인합니다. 보고서를 제출할 때도 서버가 다시 검사하며, 참여 권한이 철회되면 요청을 거부합니다. `AccessGrant`를 저장해 오프라인에서 승인된 것으로 판단하지 마세요.

SDK 인증은 OpenTester의 앱 참여 권한을 나타냅니다. 호스트 앱 자체의 서버 데이터 접근은 해당 서버에서 별도로 인증·인가해야 합니다. Google Play 테스트 참여 여부나 연속 참여 일수는 SDK가 판정하지 않습니다.
