> For the complete documentation index, see [llms.txt](https://developers-apps-in-toss.toss.im/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-zh/common/authentication/toss-login.md).

# Toss 登录

服务介绍和控制台设置方法请参考 [Toss 登录介绍文档](https://developers-apps-in-toss.toss.im/guide/authentication/intro#undefined-4)。

### 基本信息

| 项目           | 值                                  |
| ------------ | ---------------------------------- |
| Base URL     | `https://apps-in-toss-api.toss.im` |
| 服务器认证        | mTLS（客户端证书）                        |
| Content-Type | `application/json`                 |

{% hint style="info" %}
**服务器间通信需要 mTLS 证书**

Toss 登录 API 是从合作方服务器调用到 Apps in Toss 服务器的服务器间通信。为了安全，请先在服务器上配置 mTLS 证书后再调用。证书发放方法是 [mTLS 证书发放方法](https://developers-apps-in-toss.toss.im/documentation/integration/getting-started)。
{% endhint %}

***

### 1. 获取授权码

**SDK 函数：** `appLogin`

`appLogin`使用 Toss App 的认证流程执行登录，登录成功后授权码(`authorizationCode`）会被返回。

{% hint style="info" %}
**请注意**

* 此步骤 **客户端（迷你应用）** 只负责获取授权码。
* 在获取授权码之后的 **令牌交换 / AccessToken 签发 / 用户信息查询**必须由 **服务器**处理。
* 授权码有效期为 **10分钟**。
* 授权码是 **一次性**，重复使用会失败。
* `authorizationCode`请不要在客户端长期保存。
* 敏感信息（`AccessToken`, `RefreshToken` 等）应 **在服务器上安全保存**。
  {% endhint %}

**首次进行 Toss 登录时** `appLogin` 调用该函数后会打开 Toss 登录窗口，并显示在 Apps in Toss 控制台中注册的条款同意页面。用户同意必需条款后会返回授权码。

**已完成 Toss 登录时** `appLogin` 调用该函数后会直接返回授权码，无需单独的登录窗口。

**签名**

```typescript
function appLogin(): Promise<{
  authorizationCode: string;
  referrer: 'DEFAULT' | 'SANDBOX';
}>;
```

**返回值**

* authorizationCode string

  这是在用户认证完成后签发的授权码。传递到服务器后会交换为 AccessToken。
* referrer string

  表示登录请求发生在哪个环境中。DEFAULT : 实际 Toss App 环境，SANDBOX : 沙箱环境

**示例：通过 Toss 认证进行登录的示例**

{% tabs %}
{% tab title="js" %}

```js
import { appLogin } from '@apps-in-toss/web-framework';

async function handleLogin() {
  const { authorizationCode, referrer } = await appLogin();

  // 将获取到的授权码（`authorizationCode`）和 `referrer` 传递给服务器。
}
```

{% endtab %}

{% tab title="React" %}

```tsx
import { appLogin } from '@apps-in-toss/web-framework';
import { Button } from '@toss/tds-mobile';

function Page() {
  async function handleLogin() {
    const { authorizationCode, referrer } = await appLogin();

    // 将获取到的授权码（`authorizationCode`）和 `referrer` 传递给服务器。
  }

  return (
    <Button size="medium" onClick={handleLogin}>
      登录
    </Button>
  );
}
```

{% endtab %}

{% tab title="React Native" %}

```tsx
import { appLogin } from '@apps-in-toss/framework';
import { Button } from '@toss/tds-react-native';

function Page() {
  async function handleLogin() {
    const { authorizationCode, referrer } = await appLogin();

    // 将获取到的授权码（`authorizationCode`）和 `referrer` 传递给服务器。
  }

  return <Button onPress={handleLogin}>登录</Button>;
}
```

{% endtab %}
{% endtabs %}

**体验示例应用**

[apps-in-toss-examples](https://github.com/toss/apps-in-toss-examples) 在仓库中 [with-app-login](https://github.com/toss/apps-in-toss-examples/tree/main/with-app-login) 下载代码并体验吧。

***

### 2. 获取 AccessToken

用于调用用户信息查询 API 的 **签发访问令牌。**

* Content-Type: `application/json`
* Method: `POST`
* URL: `/api-partner/v1/apps-in-toss/user/oauth2/generate-token`

{% hint style="info" %}
**请参考**

AccessToken 的有效期为 1 小时。
{% endhint %}

**请求**

| 名称                | 类型     | 是否必填 | 说明       |
| ----------------- | ------ | ---- | -------- |
| authorizationCode | string | Y    | 授权码      |
| referrer          | string | Y    | referrer |

**成功响应**

| 名称           | 类型     | 是否必填 | 说明             |
| ------------ | ------ | ---- | -------------- |
| tokenType    | string | Y    | 固定为 bearer     |
| accessToken  | string | Y    | accessToken    |
| refreshToken | string | Y    | refreshToken   |
| expiresIn    | string | Y    | 过期时间（秒）        |
| scope        | string | Y    | 已授权的 scope（分隔） |

```json
{
  "resultType": "SUCCESS",
  "success": {
    "accessToken": "eyJraWQiOiJjZXJ0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJtMHVmMmhaUmpJTnNEQTdLNHVuVHhMb3IwcWNSa2JNPSIsImF1ZCI6IjNlenQ2ZTF0aDg2b2RheTlwOWN1eTg0dTRvdm5nNnNzIiwibmJmIjoxNzE4MjU0ODM2LCJzY29wZSI6WyJ1c2VyX2NpIiwidXNlcl9iaXJ0aGRheSIsInVzZXJfbmF0aW9uYWxpdHkiLCJ1c2VyX25hbWUiLCJ1c2VyX3Bob25lIiwidXNlcl9nZW5kZXIiXSwiaXNzIjoiaHR0cHM6Ly9jZXJ0LnRvc3MuaW0iLCJleHAiOjE3MTgyNTg0MzYsImlhdCI6MTcxODI1NDgzNiwianRpIjoiMTJkYjYwZjYtMjEzYS00NWQ3LTllOTItODBjMzBdseY2JkMGQ3In0.W1cjoeMN8pd3Jqgh6h8YzSVQ1PUNldulJJgy6bgH1AoDbv5xFTlBLzz9Slb_u52zUpyZbhglwblQmNJs7GT6-us7XtfxSGxTUY3ORqIhF_PPGQ6soi_Qgsi-hmX165CCAilf8cltSTTuTt8xOiEbLuSTY-cecxo7SkPUonQ_0v4_Ik0kwOiOBuYZyuch3KmlYQZTqsJmxlwJAPB8M9tZTtDpLOv9MEPU35YS7CZyN0l7lwn1EKrDHJdzA5CnstqEdz2I0eREmMgZoG9mSEybgD4NtPmVJos6AJerUGgSmzP_TwwlybVATuGpnAUmH1idaZJ-MHZJhUhR82z4zTn3bw",
    "refreshToken": "xNEYPASwWw0n1AxZUHU9KeGj8BitDyYo4wi8rpfkUcJwByVxpAdUzwtIaWGVL6vHdrXLCxIlHAQRPF9hHnFleTsHkqUXzc-_78sD_r1Uh5Ff9UCYfArx8LTn1Vk99dDb",
    "scope": "user_ci user_birthday user_nationality user_name user_phone user_gender",
    "tokenType": "Bearer",
    "expiresIn": 3599
  }
}
```

**失败响应** 当授权码已过期，或使用同一授权码重复请求 AccessToken 时

```json
{
  "error": "invalid_grant"
}
```

```json
{
  "resultType": "FAIL",
  "error": {
    "errorCode": "INTERNAL_ERROR",
    "reason": "处理请求时发生了问题。"
  }
}
```

### 3. 重新获取 AccessToken

重新签发用于调用用户信息查询 API 的访问令牌。

* Content-type : application/json
* 方法： `POST`
* URL： `/api-partner/v1/apps-in-toss/user/oauth2/refresh-token`

{% hint style="info" %}
**请参考**

refreshToken 的有效期为 14 天。
{% endhint %}

**请求**

| 名称           | 类型     | 是否必填 | 说明                |
| ------------ | ------ | ---- | ----------------- |
| refreshToken | string | Y    | 已签发的 RefreshToken |

**成功响应**

| 名称           | 类型     | 是否必填 | 说明             |
| ------------ | ------ | ---- | -------------- |
| tokenType    | string | Y    | 固定为 bearer     |
| accessToken  | string | Y    | accessToken    |
| refreshToken | string | Y    | refreshToken   |
| expiresIn    | string | Y    | 过期时间（秒）        |
| scope        | string | Y    | 已授权的 scope（分隔） |

**失败响应**

| 名称        | 类型     | 是否必填 | 说明   |
| --------- | ------ | ---- | ---- |
| errorCode | string | Y    | 错误代码 |
| reason    | string | Y    | 错误消息 |

### 4. 获取用户信息

查询用户信息。 `DI`是 `null`会返回，且可无限次调用。为保护个人信息，所有个人信息都以 **加密形式**提供。

* Content-type : application/json
* 方法： `GET`
* URL： `/api-partner/v1/apps-in-toss/user/oauth2/login-me`

{% hint style="info" %}
**`scope` 中 `user_key` 值将会追加**

`scope` 参数为 **在控制台中选择的项目里，仅会返回用户同意的值** 会返回。 **自 2026 年 1 月 2 日起 `scope` 在值中 `user_key` 会添加项目。** 由于新增 scope **可能包含之前未定义的值，** 请注意在处理 scope 时不要发生异常。
{% endhint %}

**请求头**

| 名称            | 类型     | 是否必填 | 说明                                                           |
| ------------- | ------ | ---- | ------------------------------------------------------------ |
| Authorization | string | Y    | 使用 AccessToken 发起认证请求 `Authorization: Bearer ${AccessToken}` |

**成功响应**

| 名称          | 类型     | 是否必填 | 是否加密 | 说明                                                 |
| ----------- | ------ | ---- | ---- | -------------------------------------------------- |
| userKey     | number | Y    | N    | 这是仅能在该应用中使用的用户唯一识别值。即使是同一用户，若应用不同，userKey 也可能不同。   |
| scope       | string | Y    | N    | 这是已授权的 scope 列表。包含控制台中选择的项目里用户同意的值和 `user_key` 项目。 |
| agreedTerms | list   | Y    | N    | 这是用户同意的条款列表。                                       |
| name        | string | N    | Y    | 这是用户姓名。                                            |
| phone       | string | N    | Y    | 这是用户手机号码。                                          |
| birthday    | string | N    | Y    | 这是用户出生日期。（yyyyMMdd）                                |
| ci          | string | N    | Y    | 这是用户的 CI 值。                                        |
| di          | string | N    | Y    | 请始终提供 `null` 会以值形式返回。                              |
| gender      | string | N    | Y    | 这是用户性别信息。（MALE/FEMALE）                             |
| nationality | string | N    | Y    | 这是用户是否为本国人/外国人信息。（LOCAL/FOREIGNER）                 |
| email       | string | N    | Y    | 这是用户的邮箱信息。该值未经过占有认证。                               |

{% hint style="info" %}
**userKey 按应用单独签发**

userKey 是仅在该应用内有效的标识符。即使是同一用户，应用不同也会签发不同的 userKey。
{% endhint %}

```json
{
  "resultType": "SUCCESS",
  "success": {
    "userKey": 443731104,
    "scope": "user_ci,user_birthday,user_nationality,user_name,user_phone,user_gender, user_key",
    "agreedTerms": ["terms_tag1", "terms_tag2"],
    "name": "ENCRYPTED_VALUE",
    "phone": "ENCRYPTED_VALUE",
    "birthday": "ENCRYPTED_VALUE",
    "ci": "ENCRYPTED_VALUE",
    "di": null,
    "gender": "ENCRYPTED_VALUE",
    "nationality": "ENCRYPTED_VALUE",
    "email": null
  }
}
```

**失败响应** 使用无效令牌时，请确认当前使用中的 access\_token 的有效期，并重新签发。

```json
{
  "error": "invalid_grant"
}
```

**服务器错误响应示例**

| errorCode                                             | 说明                                                                               |
| ----------------------------------------------------- | -------------------------------------------------------------------------------- |
| INTERNAL\_ERROR                                       | 内部服务器错误                                                                          |
| USER\_KEY\_NOT\_FOUND                                 | 无法找到接入登录服务的用户键值                                                                  |
| USER\_NOT\_FOUND                                      | 无法找到 Toss 用户信息                                                                   |
| BAD\_REQUEST\_RETRIEVE\_CERT\_RESULT\_EXCEEDED\_LIMIT | 超出可查询次数，使用同一令牌 `/api/login/user/me/without-di` 调用 API 查询时可正常查询，但 di 字段会返回 null 值 |

```json
{
  "resultType": "FAIL",
  "error": {
    "errorCode": "INTERNAL_ERROR",
    "reason": "处理请求时发生了问题。"
  }
}
```

### 5. 解密用户信息

通过控制台以电子邮件收到的 `解密密钥`和 `AAD（Additional Authenticated DATA）` 请按此进行。

**加密算法**

* AES 对称密钥加密
* 密钥长度：256 位
* 模式：GCM
* AAD：会和解密密钥一起通过电子邮件发送给您。

**数据交换方式**

* 加密数据的前部包含 IV（NONCE）。
* 解密时需要从密文中提取 IV 并使用，才能正常解密。

**解密示例代码**

<details>

<summary>Kotlin 示例</summary>

```kotlin
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

class Test {
    fun decrypt(
        encryptedText: String,
        base64EncodedAesKey: String,
        add: String,
    ): String {
        val IV_LENGTH = 12
        val decoded = Base64.getDecoder().decode(encryptedText)
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val keyByteArray = Base64.getDecoder().decode(base64EncodedAesKey)
        val key = SecretKeySpec(keyByteArray, "AES")
        val iv = decoded.copyOfRange(0, IV_LENGTH)
        val nonceSpec = GCMParameterSpec(16 * Byte.SIZE_BITS, iv)

        cipher.init(Cipher.DECRYPT_MODE, key, nonceSpec)
        cipher.updateAAD(add.toByteArray())

        return String(cipher.doFinal(decoded, IV_LENGTH, decoded.size - IV_LENGTH))
    }
}
```

</details>

<details>

<summary>PHP 示例</summary>

```php
<?php

class Test {
    public function decrypt($encryptedText, $base64EncodedAesKey, $add) {
        $IV_LENGTH = 12;
        $decoded = base64_decode($encryptedText);
        $keyByteArray = base64_decode($base64EncodedAesKey);
        $iv = substr($decoded, 0, $IV_LENGTH);
        $ciphertext = substr($decoded, $IV_LENGTH);

        $tag = substr($ciphertext, -16);
        $ciphertext = substr($ciphertext, 0, -16);

        $decrypted = openssl_decrypt(
            $ciphertext,
            'aes-256-gcm',
            $keyByteArray,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            $add
        );

        return $decrypted;
    }
}


// 使用示例
$test = new Test();
$encryptedText = "Encrypted Text"; // 输入 Encrypted Text
$base64EncodedAesKey = "Key"; // 输入 Key
$add = "TOSS";

$result = $test->decrypt($encryptedText, $base64EncodedAesKey, $add);
echo $result;

?>
```

</details>

<details>

<summary>JAVA 示例</summary>

```java
public class Test {
    public String decrypt(
        String encryptedText,
        String base64EncodedAesKey,
        String add
    ) throws Exception {
        final int IV_LENGTH = 12;
        byte[] decoded = Base64.getDecoder().decode(encryptedText);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        byte[] keyByteArray = Base64.getDecoder().decode(base64EncodedAesKey);
        SecretKeySpec key = new SecretKeySpec(keyByteArray, "AES");
        byte[] iv = new byte[IV_LENGTH];
        System.arraycopy(decoded, 0, iv, 0, IV_LENGTH);
        GCMParameterSpec nonceSpec = new GCMParameterSpec(16 * Byte.SIZE, iv);

        cipher.init(Cipher.DECRYPT_MODE, key, nonceSpec);
        cipher.updateAAD(add.getBytes());

        byte[] decrypted = cipher.doFinal(decoded, IV_LENGTH, decoded.length - IV_LENGTH);
        return new String(decrypted);
    }
}
```

</details>

### 6. 断开登录

如果不再使用已签发的 AccessToken，或需要根据用户请求使令牌失效，请删除（使其失效）该令牌。

* Content-type : application/json
* 方法： `POST`
* URL：
  * 使用 accessToken 断开连接： `/api-partner/v1/apps-in-toss/user/oauth2/access/remove-by-access-token`
  * 使用 userKey 断开连接： `/api-partner/v1/apps-in-toss/user/oauth2/access/remove-by-user-key`

**使用 AccessToken 断开登录连接**

```
// 格式
curl --request POST 'https://apps-in-toss-api.toss.im/api-partner/v1/apps-in-toss/user/oauth2/access/remove-by-access-token' \\
--header 'Content-Type: application/json' \\
--header 'Authorization: Bearer $access_token'

// 示例
curl --request POST 'https://apps-in-toss-api.toss.im/api-partner/v1/apps-in-toss/user/oauth2/access/remove-by-access-token' \\
--header 'Content-Type: application/json' \\
--header 'Authorization: Bearer eyJraWQiOiJjZXJ0IizzYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJtMHVmMmhaUmpJTnNEQTdLNHVuVHhMb3IwcWNSa2JNPSIsImF1ZCI6IjNlenQ2ZTF0aDg2b2RheTlwOWN1eTg0dTRvdm5nNnNzIiwibmJmIjoxNzE4MjU0ODM2LCJzY29wZSI6WyJ1c2VyX2NpIiwidXNlcl9iaXJ0aGRheSIsInVzZXJfbmF0aW9uYWxpdHkiLCJ1c2VyX25hbWUiLCJ1c2VyX3Bob25lIiwidXNlcl9nZW5kZXIiXSwiaXNzIjoiaHR0cHM6Ly9jZXJ0LnRvc3MuaW0iLCJleHAiOjE3MTgyNTg0MzYsImlhdCI6MTcxODI1NDgzNiwianRpIjoiMTJkYjYwZjYtMjEzYS00NWQ3LTllOTItODBjMzBmY2JkMGQ3In0.W1cjoeMN8pd3Jqgh6h8YzSVQ1PUNldulJJgy6bgH1AoDbv5xFTlBLwk9Slb_u52zUpyZbhglwblQmNJs7GT6-us7XtfxSGxTUY3ORqIhF_PPGQ6soi_Qgsi-hmX165CCAilf8cltSTTuTt8xOiEbLuSTY-cecxo7SkPUonQ_0v4_Ik0kwOiOBuYZyuch3KmlYQZTqsJmxlwJAPB8M9tZTtDpLOv9MEPU35YS7CZyN0l7lwn1EKrDHJdzA5CnstqEdz2I0eREmMgZoG9mSEybgD4NtPmVJos6AJerUGgSmzP_TwwlybVATuGpnAUmH1idaZJ-MHZJhUhR82z4zTn3bw'
```

**使用 userKey 断开登录连接**

{% hint style="info" %}
**请参考**

当一个 userKey 关联的 AccessToken 很多时 **readTimeout(3秒)** 可能会发生。此时不要重试请求，请在一段时间后再试。
{% endhint %}

```
// 格式
curl --request POST 'https://apps-in-toss-api.toss.im/api-partner/v1/apps-in-toss/user/oauth2/access/remove-by-user-key' \\
--header 'Content-Type: application/json' \\
--header 'Authorization: Bearer $access_token' \\
--data '{"userKey": $user_key}'

// 示例
curl --request POST 'https://apps-in-toss-api.toss.im/api-partner/v1/apps-in-toss/user/oauth2/access/remove-by-user-key' \\
--header 'Content-Type: application/json' \\
--data '{"userKey": 443731103}'
```

```json
{
  "resultType": "SUCCESS",
  "success": {
    "userKey": 443731103
  }
}
```

### 7. 通过回调断开登录

如果用户在 Toss App 内解除与服务的连接，我们会通知商户服务器。如果需要处理已断开连接的用户，可使用此功能。可在控制台中填写接收回调的 URL 和 basic Auth 头。

{% hint style="info" %}
**请务必确认**

如果服务直接调用断开登录连接 API， **则不会调用回调。**
{% endhint %}

**GET 方式**

* 在请求 requestParam 中 `userKey`和 `referrer`包含。

```
// 格式
curl --request GET '$callback_url?userKey=$userKey&referrer=$referrer'

// 示例
curl --request GET '$callback_url?userKey=443731103&referrer=UNLINK'
```

**POST 方式**

* 在请求 body 中 `userKey`和 `referrer`包含。

```
// 格式
curl --request POST '$callback_url' \\
--header 'Content-Type: application/json' \\
--data '{"userKey": $user_key, "referrer": $referrer}'

// 示例
curl --request POST '$callback_url' \\
--header 'Content-Type: application/json' \\
--data '{"userKey": 443731103, "referrer": "UNLINK"}'
```

referrer 是断开连接请求的路径。

| referrer           | 说明                                                                                       |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `UNLINK`           | 用户在 Toss App 中直接断开连接时会调用。（路径：Toss App → 设置 → 认证与安全 → 通过 Toss 登录的服务 → "断开连接"）             |
| `WITHDRAWAL_TERMS` | 用户撤回登录服务条款同意时会调用。（路径：Toss App → 设置 → 法律信息及其他 → 条款与个人信息处理同意 → 各服务同意内容："Toss 登录" → "撤回同意"） |
| `WITHDRAWAL_TOSS`  | 用户注销 Toss 会员时会调用。                                                                        |

### 故障排查

**本地开发中发生认证错误时**

在本地开发时发生认证错误的原因主要有两种。

1. 认证令牌已过期 之前签发的认证令牌可能已过期。请签发新令牌后再试。
2. 开发者未登录 你可能尚未使用开发者账号登录沙箱环境。请参考沙箱应用下载，在登录后再重新尝试。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-zh/common/authentication/toss-login.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
