> 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" %}
**请注意**

* 此步骤 **客户端（迷你应用）** 只负责获取授权码。
* 获取授权码之后的 **token 交换 / 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
* Method : `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
* Method : `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                                 | 无法找到接入登录服务的用户 key 值                                                             |
| 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
* Method : `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.
