chore: remove connectors (#1880)
|
@ -9,7 +9,7 @@
|
|||
"bootstrap": "lerna bootstrap",
|
||||
"prepare": "if test \"$NODE_ENV\" != \"production\" && test \"$CI\" != \"true\" ; then husky install ; fi",
|
||||
"prepack": "lerna run --stream prepack",
|
||||
"dev": "lerna run --stream prepack && lerna --ignore=@logto/integration-test run --parallel dev",
|
||||
"dev": "lerna run --stream prepack -- --incremental && lerna --ignore=@logto/integration-test run --parallel dev",
|
||||
"start": "cd packages/core && NODE_ENV=production node . --from-root",
|
||||
"ci:build": "lerna run --stream build",
|
||||
"ci:lint": "lerna run --parallel lint",
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
* **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector-alipay-native:** add Alipay Native connector ([#873](https://github.com/logto-io/logto/issues/873)) ([9589aea](https://github.com/logto-io/logto/commit/9589aeafec8592531aa1dfe598ca6cec7325eded))
|
||||
* **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
* **connectors:** handle authorization callback parameters in each connector respectively ([#1166](https://github.com/logto-io/logto/issues/1166)) ([097aade](https://github.com/logto-io/logto/commit/097aade2e2e1b1ea1531bcb4c1cca8d24961a9b9))
|
||||
* **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
* **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
* **native-connectors:** pass random state to native connector sdk ([#922](https://github.com/logto-io/logto/issues/922)) ([9679620](https://github.com/logto-io/logto/commit/96796203dd4247d7ecdee044f13f3d57f04ca461))
|
||||
* remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* alipay native ([895a24b](https://github.com/logto-io/logto/commit/895a24b41eafddde82c94668d742613a333b6991))
|
||||
* **connector-alipay-native:** fix data guard ([#992](https://github.com/logto-io/logto/issues/992)) ([2dc50d6](https://github.com/logto-io/logto/commit/2dc50d65318dfc7d64034bd3c501cec8feb5dde1))
|
|
@ -1,268 +0,0 @@
|
|||
# Alipay Native
|
||||
|
||||
The official Logto connector for Alipay social sign-in in mobile-device native apps.
|
||||
|
||||
支付宝原生应用社交登录官方 Logto 连接器 [中文文档](#支付宝原生连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Alipay Native](#alipay-native)
|
||||
- [Get started](#get-started)
|
||||
- [Register Alipay developer account](#register-alipay-developer-account)
|
||||
- [Create and configure Alipay app](#create-and-configure-alipay-app)
|
||||
- [Set up the Logto Alipay Native connector settings](#set-up-the-logto-alipay-native-connector-settings)
|
||||
- [Config types](#config-types)
|
||||
- [Enable Alipay native sign-in in your app](#enable-alipay-native-sign-in-in-your-app)
|
||||
- [iOS](#ios)
|
||||
- [Android](#android)
|
||||
- [Test Alipay native connector](#test-alipay-native-connector)
|
||||
- [References](#references)
|
||||
- [支付宝原生连接器](#支付宝原生连接器)
|
||||
- [开始上手](#开始上手)
|
||||
- [注册支付宝开发者账号](#注册支付宝开发者账号)
|
||||
- [在支付宝开放平台上创建并且配置应用](#在支付宝开放平台上创建并且配置应用)
|
||||
- [设置支付宝原生连接器](#设置支付宝原生连接器)
|
||||
- [配置类型](#配置类型)
|
||||
- [在你的应用中启用支付宝原生登录](#在你的应用中启用支付宝原生登录)
|
||||
- [iOS](#ios-1)
|
||||
- [Android](#android-1)
|
||||
- [测试支付宝原生连接器](#测试支付宝原生连接器)
|
||||
- [参考](#参考)
|
||||
|
||||
## Get started
|
||||
|
||||
Alipay Native connector works closely with Logto SDK on mobile platforms. It takes advantage of Alipay's OAuth 2.0 authentication workflow and enables Alipay users to sign in to other Apps using public Alipay user profiles without going through a troublesome register process.
|
||||
|
||||
## Register Alipay developer account
|
||||
|
||||
[Register an Alipay developer account](https://certifyweb.alipay.com/certify/reg/guide#/) if you don't have one.
|
||||
|
||||
## Create and configure Alipay app
|
||||
|
||||
1. Sign in to the [Alipay console](https://open.alipay.com/) with the account you have just registered.
|
||||
2. Go to "Web & Mobile Apps" (网页&移动应用) tab in "My Application" (我的应用) panel.
|
||||
3. Click "Create an App" (立即创建) button to start configuring your application.
|
||||
4. Name your application in "Application Name" (应用名称) following the naming conventions and upload your "Application Icon" (应用图标), make sure you choose "mobile application" (移动应用) as "App type" (应用类型). For building iOS App, a unique "Bundle ID" is required. Also, "application signature" (应用签名) and "application package name" (应用包名) are required for Android apps.
|
||||
5. After finishing creating the application, we come to the Overview page, where we should click "add ability" (添加能力) to add "Third-party application authorization" (第三方应用授权), "Get member information" (获取会员信息) and "App Alipay login" (App 支付宝登录) before enabling Alipay sign-in.
|
||||
6. Go to [Alipay Customer Center](https://b.alipay.com/index2.htm), and sign in with the Alipay developer account. Click "Account Center" (账号中心) on the topbar and go to "APPID binding" (APPID 绑定), whose entrance can be found at the bottom of the sidebar. "Add binding" (添加绑定) by type in the APPID of the mobile application you just created in step 4.
|
||||
7. Click on "Sign" button of "App Alipay login", and finish signing process following the guide. After finishing this step, you are expected to find abilities you have just added in step 5 kicks in.
|
||||
8. Come back to Alipay open platform console page, and you can find "Interface signing method" (接口加签方式(密钥/证书)) in "development information" (开发信息) section. Click "set up" (设置) button, and you can find yourself on a page setting signing method. "Public Key" (公钥) is the preferred signing mode, and fill in contents from the public key file you have generated in the text input box.
|
||||
9. Set up "Authorization Redirect URI" (授权回调地址) by clicking "set up" (设置) button on the bottom of the Alipay console page. `${your_logto_origin}/callback/alipay-native` is the default redirect URI used in Logto core.
|
||||
10. After finishing all these steps, go back to the top right corner of Alipay console page, and click "Submit for review" (提交审核). Once the review is approved, you are good to go with a smooth Alipay sign-in flow.
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> You can use _openssl_ to generate key pairs on your local machine by executing following code snippet in terminal.
|
||||
>
|
||||
> ```bash
|
||||
> openssl genrsa -out private.pem 2048
|
||||
> openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||
> ```
|
||||
>
|
||||
> When filling in the public key on the Alipay app setup website, you need to remove the header and footer of `public.pem`, delete all newline characters, and paste the rest of the contents into the text input box for "public key".
|
||||
|
||||
## Set up the Logto Alipay Native connector settings
|
||||
|
||||
1. In [the Alipay console workspace](https://open.alipay.com/dev/workspace) go to "My application" (我的应用) panel and click "Web & Mobile Apps" (网页&移动应用) tab, you can find APPID of all applications.
|
||||
2. In step 7 of the previous part, you have already generated a key pair including a private key and a public key.
|
||||
3. Fill out the Logto connector settings:
|
||||
- Fill out the `appId` field with APPID you've got from step 1.
|
||||
- Fill out the `privateKey` field with contents from the private key file mentioned in step 2. Please MAKE SURE to use '\n' to replace all newline characters. You don't need to remove header and footer in private key file.
|
||||
- Fill out the `signType` filed with 'RSA2' due to the `Public key` signing mode we chose in step 7 of "Create And Configure Alipay Apps".
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type | Enum values |
|
||||
|------------|-------------|-----------------|
|
||||
| appId | string | N/A |
|
||||
| privateKey | string | N/A |
|
||||
| signType | enum string | 'RSA' \| 'RSA2' |
|
||||
|
||||
## Enable Alipay native sign-in in your app
|
||||
|
||||
### iOS
|
||||
|
||||
We assume you have integrated [Logto iOS SDK](https://docs.logto.io/docs/recipes/integrate-logto/ios) in your app. In this case, things are pretty simple, and you don't even need to read the Alipay SDK doc:
|
||||
|
||||
**1. Add `LogtoSocialPluginAlipay` to your Xcode project**
|
||||
|
||||
Add the framework:
|
||||
|
||||

|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> The plugin includes Alipay "minimalist SDK" (极简版 SDK). You can directly use `import AFServiceSDK` once imported the plugin.
|
||||
|
||||
**2. Add the plugin to your `LogtoClient` init options**
|
||||
|
||||
```swift
|
||||
let logtoClient = LogtoClient(
|
||||
useConfig: config,
|
||||
socialPlugins: [LogtoSocialPluginAlipay(callbackScheme: "your-scheme")]
|
||||
)
|
||||
```
|
||||
|
||||
Where `callbackScheme` is one of the [custom URL Schemes](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) that can navigate to your app.
|
||||
|
||||
### Android
|
||||
|
||||
We assume you have integrated [Logto Android SDK](https://docs.logto.io/docs/recipes/integrate-logto/android) in your app. In this case, things are pretty simple, and you don't even need to read the Alipay SDK doc:
|
||||
|
||||
**1. Download the Alipay "minimalist SDK" and add it to your project**
|
||||
|
||||
Download the Alipay "minimalist SDK" (极简版 SDK) from [Logto 3rd-party Social SDKs](https://github.com/logto-io/social-sdks/blob/master/alipay/android/alipaySdk-15.7.9-20200727142846.aar) to your project's `app/libs` folder:
|
||||
|
||||
```bash
|
||||
project-path/app/libs/alipaySdk-15.7.9-20200727142846.aar
|
||||
```
|
||||
|
||||
**2. Add the Alipay "minimalist SDK" as a dependency**
|
||||
|
||||
Open your `build.gradle` file:
|
||||
|
||||
```bash
|
||||
project-path/app/build.gradle
|
||||
```
|
||||
|
||||
Add the dependency:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// ...
|
||||
implementation(files("./libs/alipaySdk-15.7.9-20200727142846.aar")) // kotlin-script
|
||||
// or
|
||||
implementation files('./libs/alipaySdk-15.7.9-20200727142846.aar') // groovy-script
|
||||
}
|
||||
```
|
||||
|
||||
### Test Alipay native connector
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in#enable-connector-in-sign-in-experience).
|
||||
|
||||
Once Alipay native connector is enabled, you can build and run your app to see if it works.
|
||||
|
||||
## References
|
||||
|
||||
- [Alipay Docs - Access Preparation - How to create an app](https://opendocs.alipay.com/support/01rau6)
|
||||
- [Alipay Docs - Web & Mobile Apps - Create an app](https://opendocs.alipay.com/open/200/105310)
|
||||
|
||||
# 支付宝原生连接器
|
||||
|
||||
## 开始上手
|
||||
|
||||
支付宝原生连接器与 Logto 所提供的原生平台上的 SDK 紧密搭配使用。它利用支付宝所提供的 OAuth 2.0 身份认证服务,使支付宝用户无需繁琐的注册流程,即可直接用其在支付宝上公开的身份信息登录其他应用。
|
||||
|
||||
## 注册支付宝开发者账号
|
||||
|
||||
如果你还没有支付宝开发者账号,参考链接:[注册一个支付宝开发者账号](https://certifyweb.alipay.com/certify/reg/guide#/)
|
||||
|
||||
## 在支付宝开放平台上创建并且配置应用
|
||||
|
||||
1. 使用你所创建的支付宝开发者账号登录[支付宝开放平台控制台](https://open.alipay.com/)。
|
||||
2. 在「我的应用」中选择「网页&移动应用」标签页。
|
||||
3. 点击「立即创建」开始创建并且配置你的应用
|
||||
4. 根据平台的命名规则通过「应用名称」字段给你的应用命名;在「应用图标」中上传应用图标;将「应用类型」设定为「移动应用」。当要创建一个 iOS 应用时, 需要提供「Bundle ID」。当创建的是 Android 应用时,则需要提供「应用签名」和「应用名」。
|
||||
5. 当应用创建成功后,我们进入到了「概览」页面,接下来我们在「能力列表」中点击「+ 添加能力」,将「App 支付宝登录」、「获取会员信息」、「第三方应用授权」添加到能力列表中。
|
||||
6. 使用开发者账号登录[支付宝商家中心](https://b.alipay.com/index2.htm)后,从顶栏菜单的进入「账号中心」,然后选择从左侧的菜单栏底部进入「APPID 绑定」页面。点击「+ 添加绑定」,之后输入你在步骤 4 中所创建的应用的 APPID。
|
||||
7. 点按「App 支付宝登录」旁边「签约」按钮,并按照提示完成签约。当此步骤完成后,步骤 5 中所添加的各种「能力」即可生效。
|
||||
8. 回到「支付宝开放平台控制台」中第 5 步所创建的应用的「概览」页面, 在该页面的「开发信息」中点击「接口加签方式(密钥/证书)」的「设置」链接,将「选择加签模式」设定为「公钥」,然后将你生成的公钥填入下方「填写公钥字符」的文本编辑框中。
|
||||
9. 点击「授权回调地址」的「设置链接」,选择你所需要的「回调地址类型」,将 Logto Core 默认使用的 `${your_logto_origin}/callback/alipay-native` 设置为「回调地址」。
|
||||
10. 当设置完以上的所有步骤,点击「概览」页面上方的「提交审核」,当审核通过后,你将可以顺利地使用支付宝登录自己的应用。
|
||||
|
||||
> ℹ️ **注意**
|
||||
>
|
||||
> 你可以用 _openssl_ 来在本地机器上,用下面这一段代码在终端里生成一个密钥对。
|
||||
>
|
||||
> ```bash
|
||||
> openssl genrsa -out private.pem 2048
|
||||
> openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||
> ```
|
||||
>
|
||||
> 在支付宝应用设置网页上填写公钥时,需要把生成的 `public.pem` 文件中内容的文件头和文件尾去掉,同时删除所有的换行符,再把剩下的内容粘贴到填写公钥的文本框中。
|
||||
|
||||
## 设置支付宝原生连接器
|
||||
|
||||
1. 在[支付宝开放平台控制台](https://open.alipay.com/dev/workspace)中,点击「我的应用」面板中的「网页&移动应用」,获取应用的 APPID。
|
||||
2. 获取你在上一部分的第 7 个步骤中生成的密钥对。
|
||||
3. 配置你的应用的支付宝原生连接器:
|
||||
- 将你在第 1 步中获取的 APPID 填入 `appId` 字段。
|
||||
- 将你在第 2 步中获得的密钥对的私钥填入 `privateKey` 字段。请保留私钥文件内容中的文件头和文件尾,并 **确保** 使用 '\n' 替换了所有换行符。
|
||||
- 将你在第 2 步中所获得的密钥的签名模式 'RSA2' 填入 `signType` 字段。
|
||||
|
||||
### 配置类型
|
||||
|
||||
| 名称 | 类型 | 枚举值 |
|
||||
|------------|-------------|-----------------|
|
||||
| appId | string | N/A |
|
||||
| privateKey | string | N/A |
|
||||
| signType | enum string | 'RSA' \| 'RSA2' |
|
||||
|
||||
## 在你的应用中启用支付宝原生登录
|
||||
|
||||
### iOS
|
||||
|
||||
我们假设你已经在你的应用中集成了 [Logto iOS SDK](https://docs.logto.io/zh-cn/docs/recipes/integrate-logto/ios/)。之后的流程很简单,你甚至不需要阅读支付宝 SDK 文档:
|
||||
|
||||
**1. 添加 `LogtoSocialPluginAlipay` 到你的 Xcode 工程**
|
||||
|
||||
添加 framework:
|
||||
|
||||

|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> 该插件已包含支付宝极简 SDK。在引入插件后你可以直接使用 `import AFServiceSDK`。
|
||||
|
||||
**2. 将插件添加至 `LogtoClient` 的初始化项**
|
||||
|
||||
```swift
|
||||
let logtoClient = LogtoClient(
|
||||
useConfig: config,
|
||||
socialPlugins: [LogtoSocialPluginAlipay(callbackScheme: "your-scheme")]
|
||||
)
|
||||
```
|
||||
|
||||
其中 `callbackScheme` 是 [custom URL Schemes](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) 中之一,它将被用来在登录成功后跳转回应用。
|
||||
|
||||
### Android
|
||||
|
||||
我们假设你已经在你的应用中集成了 [Logto Android SDK](https://docs.logto.io/docs/recipes/integrate-logto/android)。之后的流程很简单,你甚至不需要阅读支付宝 SDK 文档:
|
||||
|
||||
**1. 下载支付宝极简版 SDK 到你的项目中**
|
||||
|
||||
从 [Logto 3rd-party Social SDKs](https://github.com/logto-io/social-sdks/blob/master/alipay/android/alipaySdk-15.7.9-20200727142846.aar) 下载支付宝极简版 SDK 到项目的 `app/libs` 目录下:
|
||||
|
||||
```bash
|
||||
project-path/app/libs/alipaySdk-15.7.9-20200727142846.aar
|
||||
```
|
||||
|
||||
**2. 添加支付宝极简版 SDK 为项目依赖项**
|
||||
|
||||
打开 `build.gradle` 文件:
|
||||
|
||||
```bash
|
||||
project-path/app/build.gradle
|
||||
```
|
||||
|
||||
添加依赖:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// ...
|
||||
implementation(files("./libs/alipaySdk-15.7.9-20200727142846.aar")) // kotlin-script
|
||||
// or
|
||||
implementation files('./libs/alipaySdk-15.7.9-20200727142846.aar') // groovy-script
|
||||
}
|
||||
```
|
||||
|
||||
## 测试支付宝原生连接器
|
||||
|
||||
大功告成。别忘了 [在登录体验中启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8)。
|
||||
|
||||
在支付宝原生连接器启用后,你可以构建并运行你的应用看看是否生效。
|
||||
|
||||
## 参考
|
||||
|
||||
- [支付宝文档中心 - 接入准备 - 如何创建应用](https://opendocs.alipay.com/support/01rau6)
|
||||
- [支付宝文档中心 - 网页&移动应用 - 创建应用](https://opendocs.alipay.com/open/200/105310)
|
Before Width: | Height: | Size: 32 KiB |
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"appId": "<app-id>",
|
||||
"signType": "<signing-algorithm-either-RSA-or-RSA2>",
|
||||
"privateKey": "<private-key>"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { Config, merge } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = merge({
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error'],
|
||||
});
|
||||
|
||||
export default config;
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.797 2H5.20356C3.43362 2 2 3.44594 2 5.2314V18.9389C2 20.7228 3.43362 22.1698 5.20356 22.1698H18.797C20.5669 22.1698 21.9995 20.7228 21.9995 18.9389V5.2314C21.9995 3.44594 20.5669 2 18.797 2Z" fill="#1677FF"/>
|
||||
<path d="M7.40468 17.5112C4.29324 17.5112 3.37319 15.0391 4.91124 13.6875C5.42428 13.2301 6.362 13.0073 6.86165 12.957C8.70977 12.7728 10.4208 13.484 12.4398 14.4779C11.0206 16.3448 9.21318 17.5112 7.40468 17.5112ZM18.4672 14.6653C17.6666 14.3949 16.5928 13.9815 15.397 13.545C16.1146 12.2854 16.6887 10.8513 17.0657 9.29287H13.1236V7.86086H17.9531V7.06077H13.1236V4.67444H11.1529C10.8069 4.67444 10.8069 5.01825 10.8069 5.01825V7.06131H5.92286V7.86086H10.8069V9.29287H6.77436V10.0919H14.5953C14.3194 11.0548 13.9418 11.9856 13.4691 12.8686C10.9306 12.0241 8.22298 11.3397 6.52159 11.7606C5.43392 12.0311 4.73291 12.5136 4.32162 13.0191C2.43173 15.338 3.78716 18.8591 7.77741 18.8591C10.1364 18.8591 12.4098 17.5326 14.1711 15.346C16.7985 16.6206 22.0006 18.8071 22.0006 18.8071V15.6903C22.0006 15.6903 21.3473 15.6379 18.4672 14.6653Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,64 +0,0 @@
|
|||
{
|
||||
"name": "@logto/connector-alipay-native",
|
||||
"version": "1.0.0-beta.8",
|
||||
"description": "Alipay Native implementation.",
|
||||
"main": "./lib/index.js",
|
||||
"exports": "./lib/index.js",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"license": "MPL-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"docs",
|
||||
"logo.svg",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "rm -rf lib/ && ncc build src/index.ts -o lib",
|
||||
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/connector-core": "^1.0.0-beta.8",
|
||||
"@silverhand/essentials": "^1.2.0",
|
||||
"@silverhand/jest-config": "1.0.0",
|
||||
"dayjs": "^1.10.5",
|
||||
"got": "^11.8.2",
|
||||
"iconv-lite": "0.6.3",
|
||||
"snakecase-keys": "^5.1.0",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^28.1.3",
|
||||
"@shopify/jest-koa-mocks": "^5.0.0",
|
||||
"@silverhand/eslint-config": "1.0.0",
|
||||
"@silverhand/ts-config": "1.0.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/lodash.pick": "^4.4.6",
|
||||
"@types/node": "^16.3.1",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"eslint": "^8.21.0",
|
||||
"jest": "^28.1.3",
|
||||
"jest-matcher-specific-error": "^1.0.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"nock": "^13.2.2",
|
||||
"prettier": "^2.7.1",
|
||||
"supertest": "^6.2.2",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import { ConnectorMetadata, ConnectorPlatform } from '@logto/connector-core';
|
||||
|
||||
export const authorizationEndpoint = 'alipay://'; // This is used to arouse the native Alipay App
|
||||
export const alipayEndpoint = 'https://openapi.alipay.com/gateway.do';
|
||||
export const methodForAccessToken = 'alipay.system.oauth.token';
|
||||
export const methodForUserInfo = 'alipay.user.info.share';
|
||||
|
||||
export const alipaySigningAlgorithmMapping = {
|
||||
RSA: 'RSA-SHA1',
|
||||
RSA2: 'RSA-SHA256',
|
||||
} as const;
|
||||
export const alipaySigningAlgorithms = ['RSA', 'RSA2'] as const;
|
||||
|
||||
export const invalidAccessTokenCode = ['20001'];
|
||||
|
||||
export const invalidAccessTokenSubCode = ['isv.code-invalid'];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'alipay-native',
|
||||
target: 'alipay',
|
||||
platform: ConnectorPlatform.Native,
|
||||
name: {
|
||||
en: 'Alipay',
|
||||
'zh-CN': '支付宝',
|
||||
'tr-TR': 'Alipay',
|
||||
'ko-KR': 'Alipay',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Alipay is a third-party mobile and online payment platform.',
|
||||
'zh-CN': '支付宝是一个第三方支付平台。',
|
||||
'tr-TR': 'Alipay, üçüncü şahıslara ait bir mobil ve çevrimiçi ödeme platformudur.',
|
||||
'ko-KR': 'Alipay는 서드파티 모바일 및 온라인 결제 플랫폼 입니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
configTemplate: './docs/config-template.json',
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
||||
|
||||
export const timestampFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
@ -1,242 +0,0 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-core';
|
||||
import nock from 'nock';
|
||||
|
||||
import createConnector, { getAccessToken } from '.';
|
||||
import { alipayEndpoint } from './constant';
|
||||
import { mockedAlipayNativeConfigWithValidPrivateKey } from './mock';
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedAlipayNativeConfigWithValidPrivateKey);
|
||||
|
||||
describe('getAuthorizationUri', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get a valid uri by state', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
const authorizationUri = await connector.getAuthorizationUri({
|
||||
state: 'dummy-state',
|
||||
redirectUri: 'dummy-redirect-uri',
|
||||
});
|
||||
expect(authorizationUri).toBe('alipay://?app_id=2021000000000000&state=dummy-state');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
|
||||
it('should get an accessToken by exchanging with code', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const response = await getAccessToken('code', mockedAlipayNativeConfigWithValidPrivateKey);
|
||||
const { accessToken } = response;
|
||||
expect(accessToken).toEqual('access_token');
|
||||
});
|
||||
|
||||
it('throw General error if auth_code not provided in input', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
error_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid code',
|
||||
sub_code: 'isv.code-invalid ',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({})).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, '{}')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when accessToken is empty', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: '',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
await expect(
|
||||
getAccessToken('code', mockedAlipayNativeConfigWithValidPrivateKey)
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
});
|
||||
|
||||
it('should fail with wrong code', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
error_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid code',
|
||||
sub_code: 'isv.code-invalid ',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
await expect(
|
||||
getAccessToken('wrong_code', mockedAlipayNativeConfigWithValidPrivateKey)
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid code')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.once()
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
|
||||
it('should get userInfo with accessToken', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '10000',
|
||||
msg: 'Success',
|
||||
user_id: '2088000000000000',
|
||||
nick_name: 'PlayboyEric',
|
||||
avatar: 'https://www.alipay.com/xxx.jpg',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
const { id, name, avatar } = await connector.getUserInfo({ auth_code: 'code' });
|
||||
expect(id).toEqual('2088000000000000');
|
||||
expect(name).toEqual('PlayboyEric');
|
||||
expect(avatar).toEqual('https://www.alipay.com/xxx.jpg');
|
||||
});
|
||||
|
||||
it('should throw SocialAccessTokenInvalid with code 20001', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid auth token',
|
||||
sub_code: 'aop.invalid-auth-token',
|
||||
sub_msg: '无效的访问令牌',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'Invalid auth token')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw SocialAuthCodeInvalid with sub_code `isv.code-invalid`', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '40002',
|
||||
msg: 'Invalid auth code',
|
||||
sub_code: 'isv.code-invalid',
|
||||
sub_msg: '授权码 (auth_code) 错误、状态不对或过期',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid auth code')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw General error with other response error codes', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '40002',
|
||||
msg: 'Invalid parameter',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'Invalid parameter',
|
||||
code: '40002',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with right accessToken but empty userInfo', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '10000',
|
||||
msg: 'Success',
|
||||
user_id: undefined,
|
||||
nick_name: 'PlayboyEric',
|
||||
avatar: 'https://www.alipay.com/xxx.jpg',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with other request errors', async () => {
|
||||
nock(alipayEndpointUrl.origin).post(alipayEndpointUrl.pathname).query(true).reply(500);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' })).rejects.toThrow();
|
||||
});
|
||||
});
|
|
@ -1,193 +0,0 @@
|
|||
/**
|
||||
* The Implementation of OpenID Connect of Alipay Web Open Platform.
|
||||
* https://opendocs.alipay.com/open/218/105325
|
||||
* https://opendocs.alipay.com/open/218/105327
|
||||
*
|
||||
* https://opendocs.alipay.com/open/204/105295/
|
||||
* https://opendocs.alipay.com/open/204/105296/
|
||||
*/
|
||||
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
GetConnectorConfig,
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
} from '@logto/connector-core';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import got from 'got';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
alipayEndpoint,
|
||||
authorizationEndpoint,
|
||||
methodForAccessToken,
|
||||
methodForUserInfo,
|
||||
defaultMetadata,
|
||||
defaultTimeout,
|
||||
timestampFormat,
|
||||
invalidAccessTokenCode,
|
||||
invalidAccessTokenSubCode,
|
||||
} from './constant';
|
||||
import {
|
||||
alipayNativeConfigGuard,
|
||||
AlipayNativeConfig,
|
||||
accessTokenResponseGuard,
|
||||
userInfoResponseGuard,
|
||||
ErrorHandler,
|
||||
} from './types';
|
||||
import { signingParameters } from './utils';
|
||||
|
||||
export type { AlipayNativeConfig } from './types';
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state }) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AlipayNativeConfig>(config, alipayNativeConfigGuard);
|
||||
|
||||
const { appId } = config;
|
||||
|
||||
const queryParameters = new URLSearchParams({ app_id: appId, state });
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (code: string, config: AlipayNativeConfig) => {
|
||||
const initSearchParameters = {
|
||||
method: methodForAccessToken,
|
||||
format: 'JSON',
|
||||
timestamp: dayjs().format(timestampFormat),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
charset: 'utf8',
|
||||
...config,
|
||||
};
|
||||
const signedSearchParameters = signingParameters(initSearchParameters);
|
||||
|
||||
const httpResponse = await got.post(alipayEndpoint, {
|
||||
searchParams: signedSearchParameters,
|
||||
timeout: defaultTimeout,
|
||||
});
|
||||
|
||||
const result = accessTokenResponseGuard.safeParse(JSON.parse(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
const { error_response, alipay_system_oauth_token_response } = result.data;
|
||||
|
||||
const { msg, sub_msg } = error_response ?? {};
|
||||
|
||||
assert(
|
||||
alipay_system_oauth_token_response,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg ?? sub_msg)
|
||||
);
|
||||
const { access_token: accessToken } = alipay_system_oauth_token_response;
|
||||
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
|
||||
return { accessToken };
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data) => {
|
||||
const { auth_code } = await authorizationCallbackHandler(data);
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
|
||||
validateConfig<AlipayNativeConfig>(config, alipayNativeConfigGuard);
|
||||
|
||||
const { accessToken } = await getAccessToken(auth_code, config);
|
||||
|
||||
assert(
|
||||
accessToken && config,
|
||||
new ConnectorError(ConnectorErrorCodes.InsufficientRequestParameters)
|
||||
);
|
||||
|
||||
const initSearchParameters = {
|
||||
method: methodForUserInfo,
|
||||
format: 'JSON',
|
||||
timestamp: dayjs().format(timestampFormat),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
auth_token: accessToken,
|
||||
biz_content: JSON.stringify({}),
|
||||
charset: 'utf8',
|
||||
...config,
|
||||
};
|
||||
const signedSearchParameters = signingParameters(initSearchParameters);
|
||||
|
||||
const httpResponse = await got.post(alipayEndpoint, {
|
||||
searchParams: signedSearchParameters,
|
||||
timeout: defaultTimeout,
|
||||
});
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(JSON.parse(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
const { alipay_user_info_share_response } = result.data;
|
||||
|
||||
errorHandler(alipay_user_info_share_response);
|
||||
|
||||
const { user_id: id, avatar, nick_name: name } = alipay_user_info_share_response;
|
||||
|
||||
if (!id) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse);
|
||||
}
|
||||
|
||||
return { id, avatar, name };
|
||||
};
|
||||
|
||||
const errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => {
|
||||
if (invalidAccessTokenCode.includes(code)) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
|
||||
}
|
||||
|
||||
if (sub_code) {
|
||||
assert(
|
||||
!invalidAccessTokenSubCode.includes(sub_code),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: msg,
|
||||
code,
|
||||
sub_code,
|
||||
sub_msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const dataGuard = z.object({ auth_code: z.string() });
|
||||
|
||||
const result = dataGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAlipayNativeConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: alipayNativeConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAlipayNativeConnector;
|
|
@ -1,25 +0,0 @@
|
|||
import { AlipayNativeConfig } from './types';
|
||||
|
||||
export const mockedTimestamp = '2022-02-22 22:22:22';
|
||||
|
||||
export const mockedAlipayNativeConfig: AlipayNativeConfig = {
|
||||
appId: '2021000000000000',
|
||||
signType: 'RSA2',
|
||||
privateKey: '<private-key>',
|
||||
};
|
||||
|
||||
export const mockedAlipayNativeConfigWithValidPrivateKey: AlipayNativeConfig = {
|
||||
appId: '2021000000000000',
|
||||
signType: 'RSA2',
|
||||
privateKey:
|
||||
'-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC52SvnlRfzJJDR\nA1h4MX2JWV7Yt1j+1gvtQuLh0RYbE0AgRyz8CXFcJegO8gNyUQ05vrc1RMVzvNh8\njfjLpIX8an88KE4FyoG5P8NWrwPw5ZXOnzdvNxAV8QWOU+rT4WAdCsx4++mLlb5v\nGL18R77f3WLgY23bFtcGr9q7/qOaLzNxEe4idX1eLf7Ba/gQRY0awA55/Epd1Mi7\nLqTfxTd11PoBZQPe0vnuChp3P2l1MNpIJ5G1eQ4RXgI4UMClEbGRlBN7GUlXy5p7\ng6RtvOcwmBNoE4i0/HbvaanY3u7oenST3iSzEXa2hXMjnZPvg0G4Y5mq/V6XJPTh\nJrFc9XzFAgMBAAECggEAXfmNtN10LdN4kugBLU3BL9mMF0Om8b1kbIXc2djzN5+l\nVm0HNy7DLphQXnZL/ds0N9XTKFFtEpgUU+8qNjcsNTXYvp+WzGDY9cZjTQrUkFRX\nSxLBYjBSpvWoHI8ceCVHh4f1Wtvu/VEr6Vt2PUi+IM7+d35vh1BmTJBRp6wcKBMH\nXdfjWIi5z37pTXD3OTfUjBCtzA2DX0vY6UTsmD9UI0Mb6IJdT6qugiGODFdlsduA\nWJoZlXV1VbHcvGt7DoeQgzA45sr5siUnm+ntTVBHOR/hoZQrr0DY/O/MLKYUj/+r\nZMKKpx/7VHnWfMia2EOHfjW8vUlnraUzI+5E2/FzIQKBgQDgi7S7pfRux8YONGP2\nRtHPkF8d0YllsfKedhqF3cQlJ1dhxzVqHOi1IFn6ttuuYy5UsP5apYa2kj2UUPCa\nZGGi19Vnc+RHThpR4K6/OGFrpbINAgiVJLj7F8GXzqeA7W2ZHMp1R+oB+oTxih6t\nU0dbeTP01kbBV1/7+ZUKPhLE6QKBgQDT4cMgq01F/WIGGd1GUHZQjH5bqtNiJpIf\n2Q2OTw/gn1DVnwDXpHuXPxtC3NRoaRW/dTqsF6AAkMja3voPM3sYJurGBdU8pZPC\nquc9mqqu6TR5gX3KL1lSESvMBEgfLUy/f0gI3JNw1mG17pIhnXmOB2be3HfZPcj3\nwKWlluY/fQKBgDLll97c3A3sPGll2K6vGMmqmNTCdRlW/36JmLN1NAuT4kuoguP9\nj4XWwm6A2kSp+It73vue/20MsuaWfiMQ08y8jYO4kirTekXK3vE7D2H+GeC28EkW\nHNPVa61ES1V++9Oz4fQ5i8JNDatOOmvhL5B9ZZh+pWUXsAsGZJEAxvJZAoGAMPHO\n5GYN1KQil6wz3EFMA3Fg4wYEDIFCcg7uvbfvwACtaJtxU18QmbCfOIPQoUndFzwa\nUJSohljrvPuTIh3PSpX618GTL45EIszd2/I1iXAfig3qo+DqLjX/OwKmMmWBfB8H\n4dwqRv+O1LsGkLNS2AdHsSWWnd1S5kBfQ3AnQfUCgYACM8ldXZv7uGt9uZBmxile\nB0Hg5w7F1v9VD/m9ko+INAISz8OVkD83pCEoyHwlr20JjiF+yzAakOuq6rBi+l/V\n1veSiTDUcZhciuq1G178dFYepJqisFBu7bAM+WBS4agTTtxdSLZkHeS4VX+H3DOc\ntri43NXw6QS7uQ5/+2TsEw==\n-----END PRIVATE KEY-----',
|
||||
};
|
||||
|
||||
export const mockedAlipayNativePublicParameters = {
|
||||
format: 'JSON',
|
||||
grantType: 'authorization_code',
|
||||
timestamp: mockedTimestamp,
|
||||
version: '1.0',
|
||||
charset: 'utf8',
|
||||
method: '<method-placeholder>',
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { alipaySigningAlgorithms } from './constant';
|
||||
|
||||
export const alipayNativeConfigGuard = z.object({
|
||||
appId: z.string().max(16),
|
||||
privateKey: z.string(),
|
||||
signType: z.enum(alipaySigningAlgorithms),
|
||||
});
|
||||
|
||||
export type AlipayNativeConfig = z.infer<typeof alipayNativeConfigGuard>;
|
||||
|
||||
// `error_response` and `alipay_system_oauth_token_response` are mutually exclusive.
|
||||
export const errorResponseGuard = z.object({
|
||||
code: z.string(),
|
||||
msg: z.string(), // To know `code` and `msg` details, see: https://opendocs.alipay.com/common/02km9f
|
||||
sub_code: z.string().optional(),
|
||||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
export const alipaySystemOauthTokenResponseGuard = z.object({
|
||||
user_id: z.string(), // Unique Alipay ID, 16 digits starts with '2088'
|
||||
access_token: z.string(),
|
||||
expires_in: z.number(), // In seconds (is string type in docs which is not true)
|
||||
refresh_token: z.string(),
|
||||
re_expires_in: z.number(), // Expiration timeout of refresh token, in seconds (is string type in docs which is not true)
|
||||
});
|
||||
|
||||
export const accessTokenResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
error_response: z.optional(errorResponseGuard),
|
||||
alipay_system_oauth_token_response: z.optional(alipaySystemOauthTokenResponseGuard),
|
||||
});
|
||||
|
||||
export type AccessTokenResponse = z.infer<typeof accessTokenResponseGuard>;
|
||||
|
||||
export const alipayUserInfoShareResponseGuard = z.object({
|
||||
user_id: z.string().optional(), // String of digits with max length of 16
|
||||
avatar: z.string().optional(), // URL of avatar
|
||||
province: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
nick_name: z.string().optional(),
|
||||
gender: z.string().optional(), // Enum type: 'F' for female, 'M' for male
|
||||
code: z.string(),
|
||||
msg: z.string(), // To know `code` and `msg` details, see: https://opendocs.alipay.com/common/02km9f
|
||||
sub_code: z.string().optional(),
|
||||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
type AlipayUserInfoShareResponseGuard = z.infer<typeof alipayUserInfoShareResponseGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
alipay_user_info_share_response: alipayUserInfoShareResponseGuard,
|
||||
});
|
||||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
export type ErrorHandler = (response: AlipayUserInfoShareResponseGuard) => void;
|
|
@ -1,60 +0,0 @@
|
|||
import { methodForAccessToken } from './constant';
|
||||
import {
|
||||
mockedAlipayNativeConfigWithValidPrivateKey,
|
||||
mockedAlipayNativePublicParameters,
|
||||
} from './mock';
|
||||
import { signingParameters } from './utils';
|
||||
|
||||
const listenJSONParse = jest.spyOn(JSON, 'parse');
|
||||
const listenJSONStringify = jest.spyOn(JSON, 'stringify');
|
||||
|
||||
describe('signingParameters', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const testingParameters = {
|
||||
...mockedAlipayNativePublicParameters,
|
||||
...mockedAlipayNativeConfigWithValidPrivateKey,
|
||||
method: methodForAccessToken,
|
||||
code: '7ffeb112fbb6495c9e7dfb720380DD39',
|
||||
};
|
||||
|
||||
it('should return exact signature with the given parameters (functionality check)', () => {
|
||||
const decamelizedParameters = signingParameters(testingParameters);
|
||||
expect(decamelizedParameters.sign).toBe(
|
||||
'jqVzRnwdvBEIocvKGZlZ4X3CK0pEsm8HpRWL9FtGS+P8ZRehh+Wvb3lmXWf0fhTIHmcZahQMAnLFO3OmqcwlUrs4PuRgPVmLG6mK087tkw/GP18hlstnD1hN3DS98eZZQsn8psxdHQ1qtzuik1fM0hiZvR7d/Pr72yNhIzgzWa66wBXJGYc6cmSQzB7g5hFg7L/SC55Xk205tkXkenPO9ti2TY8+bWOEZ4hAteWGftwCROz+1ne3EVrt2e/LpQQvRmDPhMIRVEShmcGTNj0ovnjN2K4Uo/YB7+hPLJkrGpYBV4hDEV91KQ9RybmE927xgIzXl7xbiHvK+BayFGNzFA=='
|
||||
);
|
||||
});
|
||||
|
||||
it('should return exact signature with the given parameters (with empty property in testingParameters)', () => {
|
||||
const decamelizedParameters = signingParameters({
|
||||
...testingParameters,
|
||||
emptyProperty: '',
|
||||
});
|
||||
expect(decamelizedParameters.sign).toBe(
|
||||
'jqVzRnwdvBEIocvKGZlZ4X3CK0pEsm8HpRWL9FtGS+P8ZRehh+Wvb3lmXWf0fhTIHmcZahQMAnLFO3OmqcwlUrs4PuRgPVmLG6mK087tkw/GP18hlstnD1hN3DS98eZZQsn8psxdHQ1qtzuik1fM0hiZvR7d/Pr72yNhIzgzWa66wBXJGYc6cmSQzB7g5hFg7L/SC55Xk205tkXkenPO9ti2TY8+bWOEZ4hAteWGftwCROz+1ne3EVrt2e/LpQQvRmDPhMIRVEShmcGTNj0ovnjN2K4Uo/YB7+hPLJkrGpYBV4hDEV91KQ9RybmE927xgIzXl7xbiHvK+BayFGNzFA=='
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call JSON.parse() when biz_content is empty', () => {
|
||||
signingParameters(testingParameters);
|
||||
expect(listenJSONParse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call JSON.parse() when biz_content is not empty', () => {
|
||||
signingParameters({
|
||||
...testingParameters,
|
||||
biz_content: JSON.stringify({ AB: 'AB' }),
|
||||
});
|
||||
expect(listenJSONParse).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call JSON.stringify() when some value is object string', () => {
|
||||
signingParameters({
|
||||
...testingParameters,
|
||||
testObject: JSON.stringify({ AB: 'AB' }),
|
||||
});
|
||||
expect(listenJSONStringify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
import * as crypto from 'crypto';
|
||||
|
||||
import * as iconv from 'iconv-lite';
|
||||
import snakeCaseKeys from 'snakecase-keys';
|
||||
|
||||
import { alipaySigningAlgorithmMapping } from './constant';
|
||||
import { AlipayNativeConfig } from './types';
|
||||
|
||||
export type SigningParameters = (
|
||||
parameters: AlipayNativeConfig & Record<string, string | undefined>
|
||||
) => Record<string, string>;
|
||||
|
||||
// Reference: https://github.com/alipay/alipay-sdk-nodejs-all/blob/10d78e0adc7f310d5b07567ce7e4c13a3f6c768f/lib/util.ts
|
||||
export const signingParameters: SigningParameters = (
|
||||
parameters: AlipayNativeConfig & Record<string, string | undefined>
|
||||
): Record<string, string> => {
|
||||
const { biz_content, privateKey, ...rest } = parameters;
|
||||
const signParameters = snakeCaseKeys(
|
||||
biz_content
|
||||
? {
|
||||
...rest,
|
||||
bizContent: JSON.stringify(snakeCaseKeys(JSON.parse(biz_content))),
|
||||
}
|
||||
: rest
|
||||
);
|
||||
|
||||
const decamelizeParameters = snakeCaseKeys(signParameters);
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
const sortedParametersAsString = Object.entries(decamelizeParameters)
|
||||
.map(([key, value]) => {
|
||||
// Supported Encodings can be found at https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings
|
||||
|
||||
if (value) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
return `${key}=${iconv.encode(value, rest.charset ?? 'utf8')}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const sign = crypto
|
||||
.createSign(alipaySigningAlgorithmMapping[rest.signType])
|
||||
.update(sortedParametersAsString, 'utf8')
|
||||
.sign(privateKey, 'base64');
|
||||
|
||||
return { ...decamelizeParameters, sign };
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest", "jest-matcher-specific-error"]
|
||||
},
|
||||
"include": ["src", "jest.config.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
* **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector-alipay:** parse code from json ([c248759](https://github.com/logto-io/logto/commit/c248759b53faa39a22906139f3a4049633c8b2a9))
|
||||
* **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
* **connectors:** handle authorization callback parameters in each connector respectively ([#1166](https://github.com/logto-io/logto/issues/1166)) ([097aade](https://github.com/logto-io/logto/commit/097aade2e2e1b1ea1531bcb4c1cca8d24961a9b9))
|
||||
* **console:** connector logo and platform icon ([#892](https://github.com/logto-io/logto/issues/892)) ([97e6bdd](https://github.com/logto-io/logto/commit/97e6bdd8aacdf12dcf99a984d7b5bcd2f61f1530))
|
||||
* **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
* **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
* **core:** update connector db schema ([#732](https://github.com/logto-io/logto/issues/732)) ([8e1533a](https://github.com/logto-io/logto/commit/8e1533a70267d459feea4e5174296b17bef84d48))
|
||||
* **core:** wrap aliyun direct mail connector ([#660](https://github.com/logto-io/logto/issues/660)) ([54b6209](https://github.com/logto-io/logto/commit/54b62094c8d8af0611cf64e39306c4f1a216e8f6))
|
||||
* **core:** wrap aliyun short message service connector ([#670](https://github.com/logto-io/logto/issues/670)) ([a06d3ee](https://github.com/logto-io/logto/commit/a06d3ee73ccc59f6aaef1dab4f45d6c118aab40d))
|
||||
* **native-connectors:** pass random state to native connector sdk ([#922](https://github.com/logto-io/logto/issues/922)) ([9679620](https://github.com/logto-io/logto/commit/96796203dd4247d7ecdee044f13f3d57f04ca461))
|
||||
* remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `lint:report` script ([#730](https://github.com/logto-io/logto/issues/730)) ([3b17324](https://github.com/logto-io/logto/commit/3b17324d189b2fe47985d0bee8b37b4ef1dbdd2b))
|
||||
* **connector-alipay-web:** rename input param name code to auth_code ([#1015](https://github.com/logto-io/logto/issues/1015)) ([1e27ee7](https://github.com/logto-io/logto/commit/1e27ee7630e08fb96cb08dfe9b5654b92bac6924))
|
|
@ -1,150 +0,0 @@
|
|||
# Alipay Web
|
||||
|
||||
The official Logto connector for Alipay social sign-in in web apps.
|
||||
|
||||
支付宝 web 应用社交登录官方 Logto 连接器 [中文文档](#支付宝网页连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Alipay Web](#alipay-web)
|
||||
- [Get started](#get-started)
|
||||
- [Register Alipay developer account](#register-alipay-developer-account)
|
||||
- [Create and configure Alipay app](#create-and-configure-alipay-app)
|
||||
- [Set up the Logto Alipay Web connector settings](#set-up-the-logto-alipay-web-connector-settings)
|
||||
- [Config types](#config-types)
|
||||
- [Test Alipay web connector](#test-alipay-web-connector)
|
||||
- [References](#references)
|
||||
- [支付宝网页连接器](#支付宝网页连接器)
|
||||
- [开始上手](#开始上手)
|
||||
- [注册支付宝开发者账号](#注册支付宝开发者账号)
|
||||
- [在支付宝开放平台上创建并且配置应用](#在支付宝开放平台上创建并且配置应用)
|
||||
- [设置支付宝网页连接器](#设置支付宝网页连接器)
|
||||
- [配置类型](#配置类型)
|
||||
- [测试支付宝网页连接器](#测试支付宝网页连接器)
|
||||
- [参考](#参考)
|
||||
|
||||
## Get started
|
||||
|
||||
Alipay Web connector is designed for desktop Web applications. It takes advantage of Alipay's OAuth 2.0 authentication workflow and enables Alipay users to sign in to other Apps using public Alipay user profiles without going through a troublesome register process.
|
||||
|
||||
## Register Alipay developer account
|
||||
|
||||
[Register an Alipay developer account](https://certifyweb.alipay.com/certify/reg/guide#/) if you don't have one.
|
||||
|
||||
## Create and configure Alipay app
|
||||
|
||||
1. Sign in to the [Alipay console](https://open.alipay.com/) with the account you have just registered.
|
||||
2. Go to "Web & Mobile Apps" (网页&移动应用) tab in "My Application" (我的应用) panel.
|
||||
3. Click "Create an App" (立即创建) button to start configuring your application.
|
||||
4. Name your application in "Application Name" (应用名称) following the naming conventions and upload your "Application Icon" (应用图标), make sure you choose "web application" (网页应用) as "App type" (应用类型).
|
||||
5. After finishing creating the application, we come to the Overview page, where we should click "add ability" (添加能力) to add "Third-party application authorization" (第三方应用授权), "Get member information" (获取会员信息) and "App Alipay login" (App 支付宝登录) before enabling Alipay sign-in.
|
||||
6. Go to [Alipay Customer Center](https://b.alipay.com/index2.htm), and sign in with the Alipay developer account. Click "Account Center" (账号中心) on the topbar and go to "APPID binding" (APPID 绑定), whose entrance can be found at the bottom of the sidebar. "Add binding" (添加绑定) by type in the APPID of the web application you just created in step 4.
|
||||
7. Click on "Sign" button of "App Alipay login", and finish signing process following the guide. After finishing this step, you are expected to find abilities you have just added in step 5 kicks in.
|
||||
8. Come back to Alipay open platform console page, and you can find "Interface signing method" (接口加签方式(密钥/证书)) in "development information" (开发信息) section. Click "set up" (设置) button, and you can find yourself on a page setting signing method. "Public Key" (公钥) is the preferred signing mode, and fill in contents from the public key file you have generated in the text input box.
|
||||
9. Set up "Authorization Redirect URI" (授权回调地址) by clicking "set up" (设置) button on the bottom of the Alipay console page. `${your_logto_origin}/callback/alipay-web` is the default redirect URI used in Logto core.
|
||||
10. After finishing all these steps, go back to the top right corner of Alipay console page, and click "Submit for review" (提交审核). Once the review is approved, you are good to go with a smooth Alipay sign-in flow.
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> You can use _openssl_ to generate key pairs on your local machine by executing following code snippet in terminal.
|
||||
>
|
||||
> ```bash
|
||||
> openssl genrsa -out private.pem 2048
|
||||
> openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||
> ```
|
||||
>
|
||||
> When filling in the public key on the Alipay app setup website, you need to remove the header and footer of `public.pem`, delete all newline characters, and paste the rest of the contents into the text input box for "public key".
|
||||
|
||||
## Set up the Logto Alipay Web connector settings
|
||||
|
||||
1. In [the Alipay console workspace](https://open.alipay.com/dev/workspace) go to "My application" (我的应用) panel and click "Web & Mobile Apps" (网页&移动应用) tab, you can find APPID of all applications.
|
||||
2. In step 7 of the previous part, you have already generated a key pair including a private key and a public key.
|
||||
3. Fill out the Logto connector settings:
|
||||
- Fill out the `appId` field with APPID you've got from step 1.
|
||||
- Fill out the `privateKey` field with contents from the private key file mentioned in step 2. Please MAKE SURE to use '\n' to replace all newline characters and do not remove header and footer in private key file.
|
||||
- Fill out the `signType` field with 'RSA2' due to the `Public key` signing mode we chose in step 7 of "Create And Configure Alipay Apps".
|
||||
- Fill out the `charset` field with either 'GBK' or 'UTF8'. You can leave this field blank as it is OPTIONAL. The default value is set to be 'UTF8'.
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type | Enum values |
|
||||
|------------|------------------------|------------------------------|
|
||||
| appId | string | N/A |
|
||||
| privateKey | string | N/A |
|
||||
| signType | enum string | 'RSA' \| 'RSA2' |
|
||||
| charset | enum string (OPTIONAL) | 'GBK' \| 'UTF8' \| undefined |
|
||||
|
||||
## Test Alipay web connector
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in#enable-connector-in-sign-in-experience).
|
||||
|
||||
Once Alipay web connector is enabled, you can build and run your web app to see if it works.
|
||||
|
||||
## References
|
||||
|
||||
- [Alipay Docs - Access Preparation - How to create an app](https://opendocs.alipay.com/support/01rau6)
|
||||
- [Alipay Docs - Web & Mobile Apps - Create an app](https://opendocs.alipay.com/open/200/105310)
|
||||
|
||||
# 支付宝网页连接器
|
||||
|
||||
## 开始上手
|
||||
|
||||
支付宝网页连接器是为桌面网页应用设计的。它采用了支付宝的 OAuth 2.0 认证流程,并使支付宝用户可以使用公开的支付宝用户信息登录其他应用,而不需要进行令人困惑的注册过程。
|
||||
|
||||
## 注册支付宝开发者账号
|
||||
|
||||
如果你还没有支付宝开发者账号,参考链接:[注册一个支付宝开发者账号](https://certifyweb.alipay.com/certify/reg/guide#/)
|
||||
|
||||
## 在支付宝开放平台上创建并且配置应用
|
||||
|
||||
1. 使用你所创建的支付宝开发者账号登录 [支付宝开放平台控制台](https://open.alipay.com/)。
|
||||
2. 在「我的应用」中选择「网页&移动应用」标签页。
|
||||
3. 点击「立即创建」开始创建并且配置你的应用
|
||||
4. 根据平台的命名规则通过「应用名称」字段给你的应用命名;在「应用图标」中上传应用图标;将「应用类型」设定为「网页应用」。
|
||||
5. 当应用创建成功后,我们进入到了「概览」页面,接下来我们在「能力列表」中点击「+ 添加能力」,将「App 支付宝登录」、「获取会员信息」、「第三方应用授权」添加到能力列表中。
|
||||
6. 使用开发者账号登录 [支付宝商家中心](https://b.alipay.com/index2.htm) 后,从顶栏菜单的进入「账号中心」,然后选择从左侧的菜单栏底部进入「APPID 绑定」页面。点击「+ 添加绑定」,之后输入你在步骤 4 中所创建的应用的 APPID。
|
||||
7. 点按「App 支付宝登录」旁边「签约」按钮,并按照提示完成签约。当此步骤完成后,步骤 5 中所添加的各种「能力」即可生效。
|
||||
8. 回到「支付宝开放平台控制台」中第 5 步所创建的应用的「概览」页面, 在该页面的「开发信息」中点击「接口加签方式(密钥/证书)」的「设置」链接,将「选择加签模式」设定为「公钥」,然后将你生成的公钥填入下方「填写公钥字符」的文本编辑框中。
|
||||
9. 点击「授权回调地址」的「设置链接」,选择你所需要的「回调地址类型」,将 Logto Core 默认使用的 `${your_logto_origin}/callback/alipay-web` 设置为「回调地址」。
|
||||
10. 当设置完以上的所有步骤,点击「概览」页面上方的「提交审核」,当审核通过后,你将可以顺利地使用支付宝登录自己的网页应用。
|
||||
|
||||
> ℹ️ **注意**
|
||||
>
|
||||
> 你可以用 _openssl_ 来在本地机器上,用下面这一段代码在终端里生成一个密钥对。
|
||||
>
|
||||
> ```bash
|
||||
> openssl genrsa -out private.pem 2048
|
||||
> openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||
> ```
|
||||
>
|
||||
> 在支付宝应用设置网页上填写公钥时,需要把生成的 `public.pem` 文件中内容的文件头和文件尾去掉,同时删除所有的换行符,再把剩下的内容粘贴到填写公钥的文本框中。
|
||||
|
||||
## 设置支付宝网页连接器
|
||||
|
||||
1. 在 [支付宝开放平台控制台](https://open.alipay.com/dev/workspace) 中,点击「我的应用」面板中的「网页&移动应用」,获取应用的 APPID。
|
||||
2. 获取你在上一部分的第 7 个步骤中生成的密钥对。
|
||||
3. 配置你的应用的支付宝网页连接器:
|
||||
- 将你在第 1 步中获取的 APPID 填入 `appId` 字段。
|
||||
- 将你在第 2 步中获得的密钥对的私钥填入 `privateKey` 字段。请保留私钥文件内容中的文件头和文件尾,并 **确保** 使用 '\n' 替换了所有换行符。
|
||||
- 将你在第 2 步中所获得的密钥的签名模式 'RSA2' 填入 `signType` 字段。
|
||||
- 在 `charset` 字段中填入 'GBK' 或 'UTF8' 字符串。这个字段也可以选择不填,此时我们会使用 'UTF8' 的默认值。
|
||||
|
||||
### 配置类型
|
||||
|
||||
| 名称 | 类型 | 枚举值 |
|
||||
|------------|------------------------|------------------------------|
|
||||
| appId | string | N/A |
|
||||
| privateKey | string | N/A |
|
||||
| signType | enum string | 'RSA' \| 'RSA2' |
|
||||
| charset | enum string (OPTIONAL) | 'GBK' \| 'UTF8' \| undefined |
|
||||
|
||||
## 测试支付宝网页连接器
|
||||
|
||||
大功告成。别忘了 [在登录体验中启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8)。
|
||||
|
||||
在支付宝网页连接器启用后,你可以构建并运行你的网页应用看看是否生效。
|
||||
|
||||
## 参考
|
||||
|
||||
- [支付宝文档中心 - 接入准备 - 如何创建应用](https://opendocs.alipay.com/support/01rau6)
|
||||
- [支付宝文档中心 - 网页&移动应用 - 创建应用](https://opendocs.alipay.com/open/200/105310)
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"appId": "<app-id>",
|
||||
"signType": "<signing-algorithm-either-RSA-or-RSA2>",
|
||||
"privateKey": "<private-key>",
|
||||
"charset": "<OPTIONAL-charset-either-GBK-or-UTF8>"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { Config, merge } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = merge({
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error'],
|
||||
});
|
||||
|
||||
export default config;
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.797 2H5.20356C3.43362 2 2 3.44594 2 5.2314V18.9389C2 20.7228 3.43362 22.1698 5.20356 22.1698H18.797C20.5669 22.1698 21.9995 20.7228 21.9995 18.9389V5.2314C21.9995 3.44594 20.5669 2 18.797 2Z" fill="#1677FF"/>
|
||||
<path d="M7.40468 17.5112C4.29324 17.5112 3.37319 15.0391 4.91124 13.6875C5.42428 13.2301 6.362 13.0073 6.86165 12.957C8.70977 12.7728 10.4208 13.484 12.4398 14.4779C11.0206 16.3448 9.21318 17.5112 7.40468 17.5112ZM18.4672 14.6653C17.6666 14.3949 16.5928 13.9815 15.397 13.545C16.1146 12.2854 16.6887 10.8513 17.0657 9.29287H13.1236V7.86086H17.9531V7.06077H13.1236V4.67444H11.1529C10.8069 4.67444 10.8069 5.01825 10.8069 5.01825V7.06131H5.92286V7.86086H10.8069V9.29287H6.77436V10.0919H14.5953C14.3194 11.0548 13.9418 11.9856 13.4691 12.8686C10.9306 12.0241 8.22298 11.3397 6.52159 11.7606C5.43392 12.0311 4.73291 12.5136 4.32162 13.0191C2.43173 15.338 3.78716 18.8591 7.77741 18.8591C10.1364 18.8591 12.4098 17.5326 14.1711 15.346C16.7985 16.6206 22.0006 18.8071 22.0006 18.8071V15.6903C22.0006 15.6903 21.3473 15.6379 18.4672 14.6653Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,64 +0,0 @@
|
|||
{
|
||||
"name": "@logto/connector-alipay-web",
|
||||
"version": "1.0.0-beta.8",
|
||||
"description": "Alipay implementation.",
|
||||
"main": "./lib/index.js",
|
||||
"exports": "./lib/index.js",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"license": "MPL-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"docs",
|
||||
"logo.svg",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "rm -rf lib/ && ncc build src/index.ts -o lib",
|
||||
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/connector-core": "^1.0.0-beta.8",
|
||||
"@silverhand/essentials": "^1.2.0",
|
||||
"@silverhand/jest-config": "1.0.0",
|
||||
"dayjs": "^1.10.5",
|
||||
"got": "^11.8.2",
|
||||
"iconv-lite": "0.6.3",
|
||||
"snakecase-keys": "^5.1.0",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^28.1.3",
|
||||
"@shopify/jest-koa-mocks": "^5.0.0",
|
||||
"@silverhand/eslint-config": "1.0.0",
|
||||
"@silverhand/ts-config": "1.0.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/lodash.pick": "^4.4.6",
|
||||
"@types/node": "^16.3.1",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"eslint": "^8.21.0",
|
||||
"jest": "^28.1.3",
|
||||
"jest-matcher-specific-error": "^1.0.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"nock": "^13.2.2",
|
||||
"prettier": "^2.7.1",
|
||||
"supertest": "^6.2.2",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { ConnectorMetadata, ConnectorPlatform } from '@logto/connector-core';
|
||||
|
||||
export const authorizationEndpoint = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm';
|
||||
export const alipayEndpoint = 'https://openapi.alipay.com/gateway.do';
|
||||
export const scope = 'auth_user';
|
||||
export const methodForAccessToken = 'alipay.system.oauth.token';
|
||||
export const methodForUserInfo = 'alipay.user.info.share';
|
||||
|
||||
export const alipaySigningAlgorithmMapping = {
|
||||
RSA: 'RSA-SHA1',
|
||||
RSA2: 'RSA-SHA256',
|
||||
} as const;
|
||||
export const alipaySigningAlgorithms = ['RSA', 'RSA2'] as const;
|
||||
export const charsetEnum = ['GBK', 'utf8'] as const;
|
||||
export const fallbackCharset = 'utf8';
|
||||
|
||||
export const invalidAccessTokenCode = ['20001'];
|
||||
|
||||
export const invalidAccessTokenSubCode = ['isv.code-invalid'];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'alipay-web',
|
||||
target: 'alipay',
|
||||
platform: ConnectorPlatform.Web,
|
||||
name: {
|
||||
en: 'Alipay',
|
||||
'zh-CN': '支付宝',
|
||||
'tr-TR': 'Alipay',
|
||||
'ko-KR': 'Alipay',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Alipay is a third-party mobile and online payment platform.',
|
||||
'zh-CN': '支付宝是一个第三方支付平台。',
|
||||
'tr-TR': 'Alipay, üçüncü şahıslara ait bir mobil ve çevrimiçi ödeme platformudur.',
|
||||
'ko-KR': 'Alipay는 서드파티 모바일 및 온라인 결제 플랫폼 입니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
configTemplate: './docs/config-template.json',
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
||||
|
||||
export const timestampFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
@ -1,235 +0,0 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-core';
|
||||
import nock from 'nock';
|
||||
|
||||
import createConnector, { getAccessToken } from '.';
|
||||
import { alipayEndpoint, authorizationEndpoint } from './constant';
|
||||
import { mockedAlipayConfigWithValidPrivateKey } from './mock';
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedAlipayConfigWithValidPrivateKey);
|
||||
|
||||
describe('getAuthorizationUri', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get a valid uri by redirectUri and state', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
const authorizationUri = await connector.getAuthorizationUri({
|
||||
state: 'some_state',
|
||||
redirectUri: 'http://localhost:3001/callback',
|
||||
});
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?app_id=2021000000000000&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&scope=auth_user&state=some_state`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
|
||||
it('should get an accessToken by exchanging with code', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const response = await getAccessToken('code', mockedAlipayConfigWithValidPrivateKey);
|
||||
const { accessToken } = response;
|
||||
expect(accessToken).toEqual('access_token');
|
||||
});
|
||||
|
||||
it('should throw when accessToken is empty', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: '',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
getAccessToken('code', mockedAlipayConfigWithValidPrivateKey)
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
});
|
||||
|
||||
it('should fail with wrong code', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
error_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid code',
|
||||
sub_code: 'isv.code-invalid ',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
getAccessToken('wrong_code', mockedAlipayConfigWithValidPrivateKey)
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid code')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.once()
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
});
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
|
||||
it('should get userInfo with accessToken', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '10000',
|
||||
msg: 'Success',
|
||||
user_id: '2088000000000000',
|
||||
nick_name: 'PlayboyEric',
|
||||
avatar: 'https://www.alipay.com/xxx.jpg',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
const { id, name, avatar } = await connector.getUserInfo({ auth_code: 'code' });
|
||||
expect(id).toEqual('2088000000000000');
|
||||
expect(name).toEqual('PlayboyEric');
|
||||
expect(avatar).toEqual('https://www.alipay.com/xxx.jpg');
|
||||
});
|
||||
|
||||
it('throw General error if auth_code not provided in input', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({})).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse, '{}')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw SocialAccessTokenInvalid with code 20001', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid auth token',
|
||||
sub_code: 'aop.invalid-auth-token',
|
||||
sub_msg: '无效的访问令牌',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'Invalid auth token')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw SocialAuthCodeInvalid with sub_code `isv.code-invalid`', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '40002',
|
||||
msg: 'Invalid auth code',
|
||||
sub_code: 'isv.code-invalid',
|
||||
sub_msg: '授权码 (auth_code) 错误、状态不对或过期',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid auth code')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw General error with other response error codes', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '40002',
|
||||
msg: 'Invalid parameter',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'Invalid parameter',
|
||||
code: '40002',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with right accessToken but empty userInfo', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '10000',
|
||||
msg: 'Success',
|
||||
user_id: undefined,
|
||||
nick_name: 'PlayboyEric',
|
||||
avatar: 'https://www.alipay.com/xxx.jpg',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with other request errors', async () => {
|
||||
nock(alipayEndpointUrl.origin).post(alipayEndpointUrl.pathname).query(true).reply(500);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'code' })).rejects.toThrow();
|
||||
});
|
||||
});
|
|
@ -1,203 +0,0 @@
|
|||
/**
|
||||
* The Implementation of OpenID Connect of Alipay Web Open Platform.
|
||||
* https://opendocs.alipay.com/support/01rg6h
|
||||
* https://opendocs.alipay.com/open/263/105808
|
||||
* https://opendocs.alipay.com/open/01emu5
|
||||
*/
|
||||
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
GetConnectorConfig,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
} from '@logto/connector-core';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import got from 'got';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
alipayEndpoint,
|
||||
authorizationEndpoint,
|
||||
methodForAccessToken,
|
||||
methodForUserInfo,
|
||||
scope,
|
||||
defaultMetadata,
|
||||
defaultTimeout,
|
||||
timestampFormat,
|
||||
fallbackCharset,
|
||||
invalidAccessTokenCode,
|
||||
invalidAccessTokenSubCode,
|
||||
} from './constant';
|
||||
import {
|
||||
alipayConfigGuard,
|
||||
AlipayConfig,
|
||||
accessTokenResponseGuard,
|
||||
userInfoResponseGuard,
|
||||
ErrorHandler,
|
||||
} from './types';
|
||||
import { signingParameters } from './utils';
|
||||
|
||||
export type { AlipayConfig } from './types';
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state, redirectUri }) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AlipayConfig>(config, alipayConfigGuard);
|
||||
|
||||
const { appId: app_id } = config;
|
||||
|
||||
const redirect_uri = encodeURI(redirectUri);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
app_id,
|
||||
redirect_uri, // The variable `redirectUri` should match {appId, appSecret}
|
||||
scope,
|
||||
state,
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (code: string, config: AlipayConfig) => {
|
||||
const { charset, ...rest } = config;
|
||||
const initSearchParameters = {
|
||||
method: methodForAccessToken,
|
||||
format: 'JSON',
|
||||
timestamp: dayjs().format(timestampFormat),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
...rest,
|
||||
charset: charset ?? fallbackCharset,
|
||||
};
|
||||
const signedSearchParameters = signingParameters(initSearchParameters);
|
||||
|
||||
const httpResponse = await got.post(alipayEndpoint, {
|
||||
searchParams: signedSearchParameters,
|
||||
timeout: defaultTimeout,
|
||||
});
|
||||
|
||||
const result = accessTokenResponseGuard.safeParse(JSON.parse(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
const { error_response, alipay_system_oauth_token_response } = result.data;
|
||||
|
||||
const { msg, sub_msg } = error_response ?? {};
|
||||
|
||||
assert(
|
||||
alipay_system_oauth_token_response,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg ?? sub_msg)
|
||||
);
|
||||
const { access_token: accessToken } = alipay_system_oauth_token_response;
|
||||
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
|
||||
return { accessToken };
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data) => {
|
||||
const { auth_code } = await authorizationCallbackHandler(data);
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AlipayConfig>(config, alipayConfigGuard);
|
||||
|
||||
const { accessToken } = await getAccessToken(auth_code, config);
|
||||
|
||||
assert(
|
||||
accessToken && config,
|
||||
new ConnectorError(ConnectorErrorCodes.InsufficientRequestParameters)
|
||||
);
|
||||
|
||||
const { charset, ...rest } = config;
|
||||
const initSearchParameters = {
|
||||
method: methodForUserInfo,
|
||||
format: 'JSON',
|
||||
timestamp: dayjs().format(timestampFormat),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
auth_token: accessToken,
|
||||
biz_content: JSON.stringify({}),
|
||||
...rest,
|
||||
charset: charset ?? fallbackCharset,
|
||||
};
|
||||
const signedSearchParameters = signingParameters(initSearchParameters);
|
||||
|
||||
const httpResponse = await got.post(alipayEndpoint, {
|
||||
searchParams: signedSearchParameters,
|
||||
timeout: defaultTimeout,
|
||||
});
|
||||
|
||||
const { body: rawBody } = httpResponse;
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(JSON.parse(rawBody));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
const { alipay_user_info_share_response } = result.data;
|
||||
|
||||
errorHandler(alipay_user_info_share_response);
|
||||
|
||||
const { user_id: id, avatar, nick_name: name } = alipay_user_info_share_response;
|
||||
|
||||
if (!id) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse);
|
||||
}
|
||||
|
||||
return { id, avatar, name };
|
||||
};
|
||||
|
||||
const errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => {
|
||||
if (invalidAccessTokenCode.includes(code)) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
|
||||
}
|
||||
|
||||
if (sub_code) {
|
||||
assert(
|
||||
!invalidAccessTokenSubCode.includes(sub_code),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: msg,
|
||||
code,
|
||||
sub_code,
|
||||
sub_msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const dataGuard = z.object({ auth_code: z.string() });
|
||||
|
||||
const result = dataGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAlipayConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: alipayConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAlipayConnector;
|
|
@ -1,26 +0,0 @@
|
|||
import { AlipayConfig } from './types';
|
||||
|
||||
export const mockedTimestamp = '2022-02-22 22:22:22';
|
||||
|
||||
export const mockedAlipayConfig: AlipayConfig = {
|
||||
appId: '2021000000000000',
|
||||
signType: 'RSA2',
|
||||
privateKey: '<private-key>',
|
||||
charset: 'utf8',
|
||||
};
|
||||
|
||||
export const mockedAlipayConfigWithValidPrivateKey: AlipayConfig = {
|
||||
appId: '2021000000000000',
|
||||
signType: 'RSA2',
|
||||
privateKey:
|
||||
'-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC52SvnlRfzJJDR\nA1h4MX2JWV7Yt1j+1gvtQuLh0RYbE0AgRyz8CXFcJegO8gNyUQ05vrc1RMVzvNh8\njfjLpIX8an88KE4FyoG5P8NWrwPw5ZXOnzdvNxAV8QWOU+rT4WAdCsx4++mLlb5v\nGL18R77f3WLgY23bFtcGr9q7/qOaLzNxEe4idX1eLf7Ba/gQRY0awA55/Epd1Mi7\nLqTfxTd11PoBZQPe0vnuChp3P2l1MNpIJ5G1eQ4RXgI4UMClEbGRlBN7GUlXy5p7\ng6RtvOcwmBNoE4i0/HbvaanY3u7oenST3iSzEXa2hXMjnZPvg0G4Y5mq/V6XJPTh\nJrFc9XzFAgMBAAECggEAXfmNtN10LdN4kugBLU3BL9mMF0Om8b1kbIXc2djzN5+l\nVm0HNy7DLphQXnZL/ds0N9XTKFFtEpgUU+8qNjcsNTXYvp+WzGDY9cZjTQrUkFRX\nSxLBYjBSpvWoHI8ceCVHh4f1Wtvu/VEr6Vt2PUi+IM7+d35vh1BmTJBRp6wcKBMH\nXdfjWIi5z37pTXD3OTfUjBCtzA2DX0vY6UTsmD9UI0Mb6IJdT6qugiGODFdlsduA\nWJoZlXV1VbHcvGt7DoeQgzA45sr5siUnm+ntTVBHOR/hoZQrr0DY/O/MLKYUj/+r\nZMKKpx/7VHnWfMia2EOHfjW8vUlnraUzI+5E2/FzIQKBgQDgi7S7pfRux8YONGP2\nRtHPkF8d0YllsfKedhqF3cQlJ1dhxzVqHOi1IFn6ttuuYy5UsP5apYa2kj2UUPCa\nZGGi19Vnc+RHThpR4K6/OGFrpbINAgiVJLj7F8GXzqeA7W2ZHMp1R+oB+oTxih6t\nU0dbeTP01kbBV1/7+ZUKPhLE6QKBgQDT4cMgq01F/WIGGd1GUHZQjH5bqtNiJpIf\n2Q2OTw/gn1DVnwDXpHuXPxtC3NRoaRW/dTqsF6AAkMja3voPM3sYJurGBdU8pZPC\nquc9mqqu6TR5gX3KL1lSESvMBEgfLUy/f0gI3JNw1mG17pIhnXmOB2be3HfZPcj3\nwKWlluY/fQKBgDLll97c3A3sPGll2K6vGMmqmNTCdRlW/36JmLN1NAuT4kuoguP9\nj4XWwm6A2kSp+It73vue/20MsuaWfiMQ08y8jYO4kirTekXK3vE7D2H+GeC28EkW\nHNPVa61ES1V++9Oz4fQ5i8JNDatOOmvhL5B9ZZh+pWUXsAsGZJEAxvJZAoGAMPHO\n5GYN1KQil6wz3EFMA3Fg4wYEDIFCcg7uvbfvwACtaJtxU18QmbCfOIPQoUndFzwa\nUJSohljrvPuTIh3PSpX618GTL45EIszd2/I1iXAfig3qo+DqLjX/OwKmMmWBfB8H\n4dwqRv+O1LsGkLNS2AdHsSWWnd1S5kBfQ3AnQfUCgYACM8ldXZv7uGt9uZBmxile\nB0Hg5w7F1v9VD/m9ko+INAISz8OVkD83pCEoyHwlr20JjiF+yzAakOuq6rBi+l/V\n1veSiTDUcZhciuq1G178dFYepJqisFBu7bAM+WBS4agTTtxdSLZkHeS4VX+H3DOc\ntri43NXw6QS7uQ5/+2TsEw==\n-----END PRIVATE KEY-----',
|
||||
charset: 'utf8',
|
||||
};
|
||||
|
||||
export const mockedAlipayPublicParameters = {
|
||||
format: 'JSON',
|
||||
grantType: 'authorization_code',
|
||||
timestamp: mockedTimestamp,
|
||||
version: '1.0',
|
||||
method: '<method-placeholder>',
|
||||
};
|
|
@ -1,60 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { alipaySigningAlgorithms, charsetEnum } from './constant';
|
||||
|
||||
export const alipayConfigGuard = z.object({
|
||||
appId: z.string(),
|
||||
privateKey: z.string(),
|
||||
signType: z.enum(alipaySigningAlgorithms),
|
||||
charset: z.enum(charsetEnum).optional(),
|
||||
});
|
||||
|
||||
export type AlipayConfig = z.infer<typeof alipayConfigGuard>;
|
||||
|
||||
// `error_response` and `alipay_system_oauth_token_response` are mutually exclusive.
|
||||
export const errorResponseGuard = z.object({
|
||||
code: z.string(),
|
||||
msg: z.string(), // To know `code` and `msg` details, see: https://opendocs.alipay.com/common/02km9f
|
||||
sub_code: z.string().optional(),
|
||||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
export const alipaySystemOauthTokenResponseGuard = z.object({
|
||||
user_id: z.string(), // Unique Alipay ID, 16 digits starts with '2088'
|
||||
access_token: z.string(),
|
||||
expires_in: z.number(), // In seconds (is string type in docs which is not true)
|
||||
refresh_token: z.string(),
|
||||
re_expires_in: z.number(), // Expiration timeout of refresh token, in seconds (is string type in docs which is not true)
|
||||
});
|
||||
|
||||
export const accessTokenResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
error_response: z.optional(errorResponseGuard),
|
||||
alipay_system_oauth_token_response: z.optional(alipaySystemOauthTokenResponseGuard),
|
||||
});
|
||||
|
||||
export type AccessTokenResponse = z.infer<typeof accessTokenResponseGuard>;
|
||||
|
||||
export const alipayUserInfoShareResponseGuard = z.object({
|
||||
user_id: z.string().optional(), // String of digits with max length of 16
|
||||
avatar: z.string().optional(), // URL of avatar
|
||||
province: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
nick_name: z.string().optional(),
|
||||
gender: z.string().optional(), // Enum type: 'F' for female, 'M' for male
|
||||
code: z.string(),
|
||||
msg: z.string(), // To know `code` and `msg` details, see: https://opendocs.alipay.com/common/02km9f
|
||||
sub_code: z.string().optional(),
|
||||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
type AlipayUserInfoShareResponse = z.infer<typeof alipayUserInfoShareResponseGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
alipay_user_info_share_response: alipayUserInfoShareResponseGuard,
|
||||
});
|
||||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
export type ErrorHandler = (response: AlipayUserInfoShareResponse) => void;
|
|
@ -1,57 +0,0 @@
|
|||
import { methodForAccessToken } from './constant';
|
||||
import { mockedAlipayConfigWithValidPrivateKey, mockedAlipayPublicParameters } from './mock';
|
||||
import { signingParameters } from './utils';
|
||||
|
||||
const listenJSONParse = jest.spyOn(JSON, 'parse');
|
||||
const listenJSONStringify = jest.spyOn(JSON, 'stringify');
|
||||
|
||||
describe('signingParameters', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const testingParameters = {
|
||||
...mockedAlipayPublicParameters,
|
||||
...mockedAlipayConfigWithValidPrivateKey,
|
||||
method: methodForAccessToken,
|
||||
code: '7ffeb112fbb6495c9e7dfb720380DD39',
|
||||
};
|
||||
|
||||
it('should return exact signature with the given parameters (functionality check)', () => {
|
||||
const decamelizedParameters = signingParameters(testingParameters);
|
||||
expect(decamelizedParameters.sign).toBe(
|
||||
'jqVzRnwdvBEIocvKGZlZ4X3CK0pEsm8HpRWL9FtGS+P8ZRehh+Wvb3lmXWf0fhTIHmcZahQMAnLFO3OmqcwlUrs4PuRgPVmLG6mK087tkw/GP18hlstnD1hN3DS98eZZQsn8psxdHQ1qtzuik1fM0hiZvR7d/Pr72yNhIzgzWa66wBXJGYc6cmSQzB7g5hFg7L/SC55Xk205tkXkenPO9ti2TY8+bWOEZ4hAteWGftwCROz+1ne3EVrt2e/LpQQvRmDPhMIRVEShmcGTNj0ovnjN2K4Uo/YB7+hPLJkrGpYBV4hDEV91KQ9RybmE927xgIzXl7xbiHvK+BayFGNzFA=='
|
||||
);
|
||||
});
|
||||
|
||||
it('should return exact signature with the given parameters (with empty property in testingParameters)', () => {
|
||||
const decamelizedParameters = signingParameters({
|
||||
...testingParameters,
|
||||
emptyProperty: '',
|
||||
});
|
||||
expect(decamelizedParameters.sign).toBe(
|
||||
'jqVzRnwdvBEIocvKGZlZ4X3CK0pEsm8HpRWL9FtGS+P8ZRehh+Wvb3lmXWf0fhTIHmcZahQMAnLFO3OmqcwlUrs4PuRgPVmLG6mK087tkw/GP18hlstnD1hN3DS98eZZQsn8psxdHQ1qtzuik1fM0hiZvR7d/Pr72yNhIzgzWa66wBXJGYc6cmSQzB7g5hFg7L/SC55Xk205tkXkenPO9ti2TY8+bWOEZ4hAteWGftwCROz+1ne3EVrt2e/LpQQvRmDPhMIRVEShmcGTNj0ovnjN2K4Uo/YB7+hPLJkrGpYBV4hDEV91KQ9RybmE927xgIzXl7xbiHvK+BayFGNzFA=='
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call JSON.parse() when biz_content is empty', () => {
|
||||
signingParameters(testingParameters);
|
||||
expect(listenJSONParse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call JSON.parse() when biz_content is not empty', () => {
|
||||
signingParameters({
|
||||
...testingParameters,
|
||||
biz_content: JSON.stringify({ AB: 'AB' }),
|
||||
});
|
||||
expect(listenJSONParse).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call JSON.stringify() when some value is object string', () => {
|
||||
signingParameters({
|
||||
...testingParameters,
|
||||
testObject: JSON.stringify({ AB: 'AB' }),
|
||||
});
|
||||
expect(listenJSONStringify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
import * as crypto from 'crypto';
|
||||
|
||||
import * as iconv from 'iconv-lite';
|
||||
import snakeCaseKeys from 'snakecase-keys';
|
||||
|
||||
import { alipaySigningAlgorithmMapping, fallbackCharset } from './constant';
|
||||
import { AlipayConfig } from './types';
|
||||
|
||||
export type SigningParameters = (
|
||||
parameters: AlipayConfig & Record<string, string | undefined>
|
||||
) => Record<string, string>;
|
||||
|
||||
// Reference: https://github.com/alipay/alipay-sdk-nodejs-all/blob/10d78e0adc7f310d5b07567ce7e4c13a3f6c768f/lib/util.ts
|
||||
export const signingParameters: SigningParameters = (
|
||||
parameters: AlipayConfig & Record<string, string | undefined>
|
||||
): Record<string, string> => {
|
||||
const { biz_content, privateKey, ...rest } = parameters;
|
||||
const signParameters = snakeCaseKeys(
|
||||
biz_content
|
||||
? {
|
||||
...rest,
|
||||
bizContent: JSON.stringify(snakeCaseKeys(JSON.parse(biz_content))),
|
||||
}
|
||||
: rest
|
||||
);
|
||||
|
||||
const decamelizeParameters = snakeCaseKeys(signParameters);
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
const sortedParametersAsString = Object.entries(decamelizeParameters)
|
||||
.map(([key, value]) => {
|
||||
// Supported Encodings can be found at https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings
|
||||
|
||||
if (value) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
return `${key}=${iconv.encode(value, rest.charset ?? fallbackCharset)}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const sign = crypto
|
||||
.createSign(alipaySigningAlgorithmMapping[rest.signType])
|
||||
.update(sortedParametersAsString, 'utf8')
|
||||
.sign(privateKey, 'base64');
|
||||
|
||||
return { ...decamelizeParameters, sign };
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest", "jest-matcher-specific-error"]
|
||||
},
|
||||
"include": ["src", "jest.config.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
* **connector:** passwordless connector send test msg with unsaved config ([#1539](https://github.com/logto-io/logto/issues/1539)) ([0297f6c](https://github.com/logto-io/logto/commit/0297f6c52f7b5d730de44fbb08f88c2e9b951874))
|
||||
* **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector-sendgrid-email:** add sendgrid email connector ([#850](https://github.com/logto-io/logto/issues/850)) ([b887655](https://github.com/logto-io/logto/commit/b8876558275e28ca921d4eeea6c38f8559810a11))
|
||||
* **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
* **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
* **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
* **core:** update connector db schema ([#732](https://github.com/logto-io/logto/issues/732)) ([8e1533a](https://github.com/logto-io/logto/commit/8e1533a70267d459feea4e5174296b17bef84d48))
|
||||
* **core:** wrap aliyun direct mail connector ([#660](https://github.com/logto-io/logto/issues/660)) ([54b6209](https://github.com/logto-io/logto/commit/54b62094c8d8af0611cf64e39306c4f1a216e8f6))
|
||||
* **core:** wrap aliyun short message service connector ([#670](https://github.com/logto-io/logto/issues/670)) ([a06d3ee](https://github.com/logto-io/logto/commit/a06d3ee73ccc59f6aaef1dab4f45d6c118aab40d))
|
||||
* remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
* **sms/email-connectors:** expose third-party API request error message ([#1059](https://github.com/logto-io/logto/issues/1059)) ([4cfd578](https://github.com/logto-io/logto/commit/4cfd5788d24d55017a8ace53fed99082f87691cb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `lint:report` script ([#730](https://github.com/logto-io/logto/issues/730)) ([3b17324](https://github.com/logto-io/logto/commit/3b17324d189b2fe47985d0bee8b37b4ef1dbdd2b))
|
|
@ -1,204 +0,0 @@
|
|||
# Aliyun direct mail connector
|
||||
|
||||
The official Logto connector for Aliyun connector for direct mail service.
|
||||
|
||||
阿里云邮件推送服务 Logto 官方连接器 [中文文档](#阿里云邮件连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Aliyun direct mail connector](#aliyun-direct-mail-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Set up an email service in Aliyun DirectMail Console](#set-up-an-email-service-in-aliyun-directmail-console)
|
||||
- [Create an Aliyun account](#create-an-aliyun-account)
|
||||
- [Enable and configure Aliyun Direct Mail](#enable-and-configure-aliyun-direct-mail)
|
||||
- [Compose the connector JSON](#compose-the-connector-json)
|
||||
- [Test Aliyun DM connector](#test-aliyun-dm-connector)
|
||||
- [Config types](#config-types)
|
||||
- [阿里云邮件连接器](#阿里云邮件连接器)
|
||||
- [开始上手](#开始上手)
|
||||
- [在阿里云邮件服务控制台中配置一个邮件服务](#在阿里云邮件服务控制台中配置一个邮件服务)
|
||||
- [注册阿里云帐号](#注册阿里云帐号)
|
||||
- [启用并配置阿里云邮件服务](#启用并配置阿里云邮件服务)
|
||||
- [编写连接器的 JSON](#编写连接器的-json)
|
||||
- [测试阿里云邮件连接器](#测试阿里云邮件连接器)
|
||||
- [配置类型](#配置类型)
|
||||
|
||||
## Get started
|
||||
|
||||
Aliyun is a primary cloud service provider in Asia, offering many cloud services, including DM (direct mail). Aliyun DM Connector is a plugin provided by the Logto team to call the Aliyun DM service APIs, with the help of which Logto end-users can register and sign in to their Logto account via mail verification code (or in other words, passcode).
|
||||
|
||||
## Set up an email service in Aliyun DirectMail Console
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> You can skip some sections if you have already finished.
|
||||
|
||||
### Create an Aliyun account
|
||||
|
||||
Head to [Aliyun](https://aliyun.com/) and create your Aliyun account if you don't have one.
|
||||
|
||||
### Enable and configure Aliyun Direct Mail
|
||||
|
||||
Go to the [DM service console page](https://www.aliyun.com/product/directmail) and sign in. Enable the Direct Mail service by clicking the "Apply to enable" (申请开通) button on the top left of the page and begin the configuration process.
|
||||
|
||||
Starting from the [DM admin console page](https://dm.console.aliyun.com/), you should:
|
||||
1. Go to "Email Domains" (发信域名) from the sidebar and add "New Domain" (新建域名) following the instructions.
|
||||
2. Customize "Sender Addresses" (发信地址) and "Email Tags" (邮件标签) respectively.
|
||||
|
||||
After finishing setup, there are two different ways to test:
|
||||
- Go to the [DirectMail Overview page](https://dm.console.aliyun.com/), find "Operation Guide" (操作引导) at the bottom of the page, and click on "Send Emails" (发送邮件). You will find all the different kinds of testing methods.
|
||||
- Follow the path "Send Emails" (发送邮件) -> "Email Tasks" (发送邮件) in the sidebar to create a testing task.
|
||||
|
||||
## Compose the connector JSON
|
||||
|
||||
1. From the [DM admin console page](https://dm.console.aliyun.com/), hover on your avatar in the top right corner and go to "AccessKey Management" (AccessKey 管理), and click "Create AccessKey" (创建 AccessKey). You will get an "AccessKey ID" and "AccessKey Secret" pair after finishing security verification. Please keep them properly.
|
||||
2. Go to the "Sender Addresses" (发信地址) or "Email Tags" (邮件标签) tab you just visited from the [DM admin console page](https://dm.console.aliyun.com/), you can find _Sender Address_ or _Email Tag_ easily.
|
||||
3. Fill out the Aliyun DM Connector settings:
|
||||
- Fill out the `accessKeyId` and `accessKeySecret` fields with access key pairs you've got from step 1.
|
||||
- Fill out the `accountName` and `fromAlias` field with "Sender Address" and "Email Tag" which were found in step 2. All templates will share this signature name. (You can leave `fromAlias` blank as it is OPTIONAL.)
|
||||
- You can add multiple DM connector templates for different cases. Here is an example of adding a single template:
|
||||
- Fill out the `subject` field, which will work as title of the sending email.
|
||||
- Fill out the `content` field with arbitrary string-type contents. Do not forget to leave `{{code}}` placeholder for random passcode.
|
||||
- Fill out `usageType` field with either `Register`, `SignIn` or `Test` for different use cases. (`usageType` is a Logto property to identify the proper use case.)
|
||||
|
||||
Here is an example of Aliyun DM connector config JSON.
|
||||
|
||||
```json
|
||||
{
|
||||
"accessKeyId": "<your-access-key-id>",
|
||||
"accessKeySecret": "<your-access-key-secret>",
|
||||
"accountName": "<noreply@logto.io>",
|
||||
"fromAlias": "<OPTIONAL-logto>",
|
||||
"templates": [
|
||||
{
|
||||
"subject": "<register-template-subject>",
|
||||
"content": "<Logto: Your passcode is {{code}}. (regitser template)>",
|
||||
"usageType": "Register"
|
||||
},
|
||||
{
|
||||
"subject": "<sign-in-template-subject>",
|
||||
"content": "<Logto: Your passcode is {{code}}. (sign-in template)>",
|
||||
"usageType": "SignIn"
|
||||
},
|
||||
{
|
||||
"subject": "<test-template-subject>",
|
||||
"content": "<Logto: Your passcode is {{code}}. (test template)>",
|
||||
"usageType": "Test"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Test Aliyun DM connector
|
||||
|
||||
You can type in an email address and click on "Send" to see whether the settings can work before "Save and Done".
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-passcode-sign-in/#enable-connector-in-sign-in-experience).
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
|-----------------|-------------------|
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| accountName | string |
|
||||
| fromAlias | string (OPTIONAL) |
|
||||
| templates | Template[] |
|
||||
|
||||
| Template Properties | Type | Enum values |
|
||||
|---------------------|-------------|----------------------------------|
|
||||
| subject | string | N/A |
|
||||
| content | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'Test' |
|
||||
|
||||
# 阿里云邮件连接器
|
||||
|
||||
## 开始上手
|
||||
|
||||
阿里云是亚洲地区一个重要的云服务厂商,提供了包括邮件服务在内的诸多云服务。
|
||||
|
||||
本连接器是 Logto 官方提供的阿里云邮件连接器,帮助终端用户通过邮件验证码进行登录注册。
|
||||
|
||||
## 在阿里云邮件服务控制台中配置一个邮件服务
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> 你可以跳过已经完成的部分。
|
||||
|
||||
### 注册阿里云帐号
|
||||
|
||||
前往 [阿里云](https://aliyun.com/) 并完成帐号的注册。
|
||||
|
||||
### 启用并配置阿里云邮件服务
|
||||
|
||||
来到 [阿里云邮件服务](https://www.aliyun.com/product/directmail) 然后登录。点按页面左上的「申请开通」按钮以开通邮件服务并开始配置流程。
|
||||
|
||||
从 [邮件服务管理控制台](https://dm.console.aliyun.com/) 开始:
|
||||
1. 从侧边栏进入到「发信域名」,点按「新建域名」并完成指引。
|
||||
2. 依次配置好「发信地址」和「邮件标签」。
|
||||
|
||||
在完成了设置之后,这里提供了两种测试的方法:
|
||||
- 前往 [邮件服务管理控制台概览](https://dm.console.aliyun.com/),在该页面底部找到「操作引导」框并点按「发送邮件」。你可以找到很多不同的测试方法。
|
||||
- 在侧边栏中选择「发送邮件」->「发送邮件」,在这里你可以「新建发送任务」来测试。
|
||||
|
||||
## 编写连接器的 JSON
|
||||
|
||||
1. 在 [邮件服务管理控制台](https://dm.console.aliyun.com/),鼠标停在右上角你的头像上,进入「AccessKey 管理」,点按「创建 AccessKey」。完成了安全验证之后,你会得到一对「AccessKey ID」和「AccessKey Secret」,请妥善保管他们。
|
||||
2. 从 [邮件服务管理控制台](https://dm.console.aliyun.com/) 的侧边栏,分别进入「发信地址」和「邮件标签」。这里你可以找到之前创建的 _发信地址_ 和 _邮件标签_。
|
||||
3. 完成阿里云邮件服务连接器的设置:
|
||||
- 用你在步骤 1 中拿到的一对「AccessKey ID」和「AccessKey Secret」来分别填入 `accessKeyId` 和 `accessKeySecret`。
|
||||
- 用步骤 2 中的 _发信地址_ 和 _邮件标签_ 填写 `accountName` 和 `fromAlias`。(`fromAlias` 可以不填写,它是 **可选的**。)
|
||||
- 你可以添加多个邮件服务模板以应对不同的用户场景。这里展示填写单个模板的例子:
|
||||
- 在 `subject` 栏填写发送邮件的 _标题_。
|
||||
- 在 `content` 栏中填写字符形式的内容。不要忘了在内容中插入 `{{code}}` 占位符,在真实发送时他会被替换成随机生成的验证码。
|
||||
- `usageType` 栏填写 `Register`,`SignIn` 或者 `Test` 其中之一以分别对应 _注册_,_登录_ 和 _测试_ 的不同场景。(`usageType` 是 Logto 的属性,用来确定使用场景。)
|
||||
|
||||
这是一个阿里云邮件服务连接器 JSON 配置的样例。
|
||||
|
||||
```json
|
||||
{
|
||||
"accessKeyId": "<your-access-key-id>",
|
||||
"accessKeySecret": "<your-access-key-secret>",
|
||||
"accountName": "<noreply@logto.io>",
|
||||
"fromAlias": "<OPTIONAL-logto>",
|
||||
"templates": [
|
||||
{
|
||||
"subject": "<register-template-subject>",
|
||||
"content": "<Logto: Your passcode is {{code}}. (regitser template)>",
|
||||
"usageType": "Register"
|
||||
},
|
||||
{
|
||||
"subject": "<sign-in-template-subject>",
|
||||
"content": "<Logto: Your passcode is {{code}}. (sign-in template)>",
|
||||
"usageType": "SignIn"
|
||||
},
|
||||
{
|
||||
"subject": "<test-template-subject>",
|
||||
"content": "<Logto: Your passcode is {{code}}. (test template)>",
|
||||
"usageType": "Test"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 测试阿里云邮件连接器
|
||||
|
||||
你可以在「保存并完成」之前输入一个邮件地址并点按「发送」来测试配置是否可以正常工作。
|
||||
|
||||
大功告成!快去 [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8) 吧。
|
||||
|
||||
### 配置类型
|
||||
|
||||
| 名称 | 类型 |
|
||||
|-----------------|-------------------|
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| accountName | string |
|
||||
| fromAlias | string (OPTIONAL) |
|
||||
| templates | Template[] |
|
||||
|
||||
| 模板属性 | 类型 | 枚举值 |
|
||||
|-----------|-------------|----------------------------------|
|
||||
| subject | string | N/A |
|
||||
| content | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'Test' |
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"accessKeyId": "<access-key-id>",
|
||||
"accessKeySecret": "<access-key-secret>",
|
||||
"accountName": "<verified-account-name>",
|
||||
"fromAlias": "<OPTIONAL-connector-alias>",
|
||||
"templates": [
|
||||
{
|
||||
"usageType": "SignIn",
|
||||
"subject": "<sign-in-template-subject>",
|
||||
"content": "<sign-in-template-content>"
|
||||
},
|
||||
{
|
||||
"usageType": "Register",
|
||||
"subject": "<register-template-subject>",
|
||||
"content": "<register-template-content>"
|
||||
},
|
||||
{
|
||||
"usageType": "Test",
|
||||
"subject": "<test-template-subject>",
|
||||
"content": "<test-template-content>"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@silverhand/jest-config';
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.71942 6.071C3.80784 6.2585 2.82876 6.98664 2.42188 7.78477C2.015 8.58291 2 8.77916 2 12.1389C2 13.9317 2.03 15.4083 2.075 15.639C2.34251 17.0065 3.4119 18.0759 4.77942 18.3434C5.1938 18.4184 10.5873 18.4565 10.5873 18.3734C10.5873 18.3131 10.1279 16.5052 10.1051 16.4827C10.0976 16.4677 9.14074 16.2568 7.98072 16.0077C5.2763 15.4202 5.35568 15.4427 5.13317 15.1715L4.93723 14.938V9.46918L5.13317 9.23167C5.35911 8.96042 5.2763 8.98323 7.98072 8.39541L10.1051 7.9204C10.1283 7.90478 10.5876 6.09662 10.5876 6.03975C10.5876 5.96787 5.0813 6.00225 4.71942 6.07725V6.071ZM13.4502 6.08225C13.4577 6.21787 13.8796 7.87509 13.9252 7.9204C13.9402 7.9429 14.9721 8.17665 16.2077 8.4401C17.4434 8.70354 18.5431 8.97135 18.6409 9.02761C18.7734 9.10448 18.8846 9.21323 18.965 9.34386C19.0856 9.5548 19.1006 9.78074 19.1006 12.2064C19.1006 14.6321 19.0856 14.858 18.965 15.0689C18.8828 15.198 18.7719 15.3062 18.6409 15.3852C18.5431 15.4383 17.4534 15.7015 16.2077 15.9727L13.9252 16.4962C13.8799 16.5415 13.458 18.1987 13.4502 18.3343C13.4502 18.4171 13.9927 18.4321 16.373 18.4093C19.6543 18.3793 19.695 18.3718 20.5312 17.8068C21.1319 17.3934 21.5856 16.7999 21.8269 16.1118C22 15.6071 22 15.5618 22 12.2095C22 8.85729 22 8.81198 21.8269 8.30728C21.5855 7.61926 21.1318 7.02571 20.5312 6.61226C19.695 6.04787 19.6575 6.03975 16.373 6.01006C13.9927 5.98756 13.4502 5.99881 13.4502 6.08506V6.08225ZM10.0876 12.0517C10.0635 12.1323 10.0584 12.2173 10.0726 12.3002C10.0951 12.4252 10.2308 12.4358 12.0008 12.4583L13.8992 12.4733V11.9461H12.0189C10.4445 11.9461 10.1204 11.9611 10.0908 12.0514L10.0876 12.0517Z" fill="#FF6A00"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -1,55 +0,0 @@
|
|||
{
|
||||
"name": "@logto/connector-aliyun-dm",
|
||||
"version": "1.0.0-beta.8",
|
||||
"description": "Aliyun DM connector implementation.",
|
||||
"main": "./lib/index.js",
|
||||
"exports": "./lib/index.js",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"license": "MPL-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"docs",
|
||||
"logo.svg",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "rm -rf lib/ && ncc build src/index.ts -o lib",
|
||||
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/connector-core": "^1.0.0-beta.8",
|
||||
"@silverhand/essentials": "^1.2.0",
|
||||
"@silverhand/jest-config": "1.0.0",
|
||||
"got": "^11.8.2",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^28.1.3",
|
||||
"@silverhand/eslint-config": "1.0.0",
|
||||
"@silverhand/ts-config": "1.0.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/node": "^16.3.1",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"eslint": "^8.21.0",
|
||||
"jest": "^28.1.3",
|
||||
"lint-staged": "^13.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { ConnectorMetadata } from '@logto/connector-core';
|
||||
|
||||
export const endpoint = 'https://dm.aliyuncs.com/';
|
||||
|
||||
export const staticConfigs = {
|
||||
Format: 'json',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Version: '2015-11-23',
|
||||
};
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'aliyun-direct-mail',
|
||||
target: 'aliyun-dm',
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Aliyun Direct Mail',
|
||||
'zh-CN': '阿里云邮件推送',
|
||||
'tr-TR': 'Aliyun Direct Mail',
|
||||
'ko-KR': 'Aliyun 다이렉트 메일',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Aliyun provides cloud computing services to online businesses.',
|
||||
'zh-CN': '阿里云是全球性的云服务提供商。',
|
||||
'tr-TR': 'Aliyun, çevrimiçi işletmelere bulut bilişim hizmetleri sunmaktadır.',
|
||||
'ko-KR': 'Aliyun는 온라인 비지니스를 위해 클라우딩 컴퓨팅 서비스를 제공합니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
configTemplate: './docs/config-template.json',
|
||||
};
|
|
@ -1,50 +0,0 @@
|
|||
import { MessageTypes } from '@logto/connector-core';
|
||||
|
||||
import createConnector from '.';
|
||||
import { mockedConfig } from './mock';
|
||||
import { singleSendMail } from './single-send-mail';
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConfig);
|
||||
|
||||
jest.mock('./single-send-mail', () => {
|
||||
return {
|
||||
singleSendMail: jest.fn(() => {
|
||||
return {
|
||||
body: JSON.stringify({ EnvId: 'env-id', RequestId: 'request-id' }),
|
||||
statusCode: 200,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('sendMessage()', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call singleSendMail() and replace code in content', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await connector.sendMessage({
|
||||
to: 'to@email.com',
|
||||
type: MessageTypes.SignIn,
|
||||
payload: { code: '1234' },
|
||||
});
|
||||
expect(singleSendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
HtmlBody: 'Your code is 1234, 1234 is your code',
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if template is missing', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.sendMessage({
|
||||
to: 'to@email.com',
|
||||
type: MessageTypes.Register,
|
||||
payload: { code: '1234' },
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
|
@ -1,105 +0,0 @@
|
|||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorType,
|
||||
CreateConnector,
|
||||
EmailConnector,
|
||||
GetConnectorConfig,
|
||||
SendMessageFunction,
|
||||
validateConfig,
|
||||
} from '@logto/connector-core';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { defaultMetadata } from './constant';
|
||||
import { singleSendMail } from './single-send-mail';
|
||||
import {
|
||||
AliyunDmConfig,
|
||||
aliyunDmConfigGuard,
|
||||
sendEmailResponseGuard,
|
||||
sendMailErrorResponseGuard,
|
||||
} from './types';
|
||||
|
||||
const sendMessage =
|
||||
(getConfig: GetConnectorConfig): SendMessageFunction =>
|
||||
// eslint-disable-next-line complexity
|
||||
async (data, inputConfig) => {
|
||||
const { to, type, payload } = data;
|
||||
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
|
||||
validateConfig<AliyunDmConfig>(config, aliyunDmConfigGuard);
|
||||
const { accessKeyId, accessKeySecret, accountName, fromAlias, templates } = config;
|
||||
const template = templates.find((template) => template.usageType === type);
|
||||
|
||||
assert(
|
||||
template,
|
||||
new ConnectorError(
|
||||
ConnectorErrorCodes.TemplateNotFound,
|
||||
`Cannot find template for type: ${type}`
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const httpResponse = await singleSendMail(
|
||||
{
|
||||
AccessKeyId: accessKeyId,
|
||||
AccountName: accountName,
|
||||
ReplyToAddress: 'false',
|
||||
AddressType: '1',
|
||||
ToAddress: to,
|
||||
FromAlias: fromAlias,
|
||||
Subject: template.subject,
|
||||
HtmlBody:
|
||||
typeof payload.code === 'string'
|
||||
? template.content.replace(/{{code}}/g, payload.code)
|
||||
: template.content,
|
||||
},
|
||||
accessKeySecret
|
||||
);
|
||||
|
||||
const result = sendEmailResponseGuard.safeParse(JSON.parse(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof HTTPError) {
|
||||
const {
|
||||
response: { body: rawBody },
|
||||
} = error;
|
||||
|
||||
assert(
|
||||
typeof rawBody === 'string',
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
|
||||
);
|
||||
|
||||
errorHandler(rawBody);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const errorHandler = (errorResponseBody: string) => {
|
||||
const result = sendMailErrorResponseGuard.safeParse(JSON.parse(errorResponseBody));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
const { Message: errorDescription, ...rest } = result.data;
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription, ...rest });
|
||||
};
|
||||
|
||||
const createAliyunDmConnector: CreateConnector<EmailConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Email,
|
||||
configGuard: aliyunDmConfigGuard,
|
||||
sendMessage: sendMessage(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAliyunDmConnector;
|
|
@ -1,29 +0,0 @@
|
|||
export const mockedParameters = {
|
||||
AccessKeyId: 'testid',
|
||||
AccountName: "<a%b'>",
|
||||
Action: 'SingleSendMail',
|
||||
AddressType: '1',
|
||||
Format: 'XML',
|
||||
HtmlBody: '4',
|
||||
RegionId: 'cn-hangzhou',
|
||||
ReplyToAddress: 'true',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Subject: '3',
|
||||
TagName: '2',
|
||||
ToAddress: '1@test.com',
|
||||
Version: '2015-11-23',
|
||||
};
|
||||
|
||||
export const mockedConfig = {
|
||||
accessKeyId: 'accessKeyId',
|
||||
accessKeySecret: 'accessKeySecret',
|
||||
accountName: 'accountName',
|
||||
templates: [
|
||||
{
|
||||
usageType: 'SignIn',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,26 +0,0 @@
|
|||
import { singleSendMail } from './single-send-mail';
|
||||
import { request } from './utils';
|
||||
|
||||
jest.mock('./utils');
|
||||
|
||||
describe('singleSendMail', () => {
|
||||
it('should call request with action SingleSendMail', async () => {
|
||||
await singleSendMail(
|
||||
{
|
||||
AccessKeyId: '<access-key-id>',
|
||||
AccountName: 'noreply@example.com',
|
||||
AddressType: '1',
|
||||
FromAlias: 'CompanyName',
|
||||
HtmlBody: 'test from logto',
|
||||
ReplyToAddress: 'false',
|
||||
Subject: 'test',
|
||||
ToAddress: 'user@example.com',
|
||||
},
|
||||
'<access-key-secret>'
|
||||
);
|
||||
const calledData = (request as jest.MockedFunction<typeof request>).mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
const payload = calledData?.[1];
|
||||
expect(payload).toHaveProperty('Action', 'SingleSendMail');
|
||||
});
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
import { endpoint, staticConfigs } from './constant';
|
||||
import { PublicParameters, SingleSendMail } from './types';
|
||||
import { request } from './utils';
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/29444.html
|
||||
*/
|
||||
export const singleSendMail = async (
|
||||
parameters: PublicParameters & SingleSendMail,
|
||||
accessKeySecret: string
|
||||
) => {
|
||||
return request(
|
||||
endpoint,
|
||||
{ Action: 'SingleSendMail', ...staticConfigs, ...parameters },
|
||||
accessKeySecret
|
||||
);
|
||||
};
|
|
@ -1,69 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const sendEmailResponseGuard = z.object({
|
||||
EnvId: z.string(),
|
||||
RequestId: z.string(),
|
||||
});
|
||||
|
||||
export type SendEmailResponse = z.infer<typeof sendEmailResponseGuard>;
|
||||
|
||||
/**
|
||||
* UsageType here is used to specify the use case of the template, can be either
|
||||
* 'Register', 'SignIn', 'ForgotPassword' or 'Test'.
|
||||
*/
|
||||
const templateGuard = z.object({
|
||||
usageType: z.string(),
|
||||
subject: z.string(),
|
||||
content: z.string(), // With variable {{code}}, support HTML
|
||||
});
|
||||
|
||||
export const aliyunDmConfigGuard = z.object({
|
||||
accessKeyId: z.string(),
|
||||
accessKeySecret: z.string(),
|
||||
accountName: z.string(),
|
||||
fromAlias: z.string().optional(),
|
||||
templates: z.array(templateGuard),
|
||||
});
|
||||
|
||||
export type AliyunDmConfig = z.infer<typeof aliyunDmConfigGuard>;
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/29444.html
|
||||
*/
|
||||
export type SingleSendMail = {
|
||||
AccountName: string;
|
||||
AddressType: '0' | '1';
|
||||
ClickTrace?: '0' | '1';
|
||||
FromAlias?: string;
|
||||
HtmlBody?: string;
|
||||
ReplyToAddress: 'true' | 'false';
|
||||
Subject: string;
|
||||
TagName?: string;
|
||||
TextBody?: string;
|
||||
ToAddress: string;
|
||||
};
|
||||
|
||||
export type PublicParameters = {
|
||||
AccessKeyId: string;
|
||||
Format?: string; // 'json' or 'xml', default: 'json'
|
||||
RegionId?: string; // 'cn-hangzhou' | 'ap-southeast-1' | 'ap-southeast-2'
|
||||
Signature?: string;
|
||||
SignatureMethod?: string;
|
||||
SignatureNonce?: string;
|
||||
SignatureVersion?: string;
|
||||
Timestamp?: string;
|
||||
Version?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @doc https://next.api.aliyun.com/troubleshoot
|
||||
*/
|
||||
export const sendMailErrorResponseGuard = z.object({
|
||||
Code: z.string(),
|
||||
Message: z.string(),
|
||||
RequestId: z.string().optional(),
|
||||
HostId: z.string().optional(),
|
||||
Recommend: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SendMailErrorResponse = z.infer<typeof sendMailErrorResponseGuard>;
|
|
@ -1,32 +0,0 @@
|
|||
import got from 'got';
|
||||
|
||||
import { mockedParameters } from './mock';
|
||||
import { getSignature, request } from './utils';
|
||||
|
||||
jest.mock('got');
|
||||
|
||||
describe('getSignature', () => {
|
||||
it('should get valid signature', () => {
|
||||
const parameters = {
|
||||
...mockedParameters,
|
||||
SignatureNonce: 'c1b2c332-4cfb-4a0f-b8cc-ebe622aa0a5c',
|
||||
Timestamp: '2016-10-20T06:27:56Z',
|
||||
};
|
||||
const signature = getSignature(parameters, 'testsecret', 'POST');
|
||||
expect(signature).toEqual('llJfXJjBW3OacrVgxxsITgYaYm0=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
it('should call axios.post with extended params', async () => {
|
||||
const parameters = mockedParameters;
|
||||
await request('http://test.endpoint.com', parameters, 'testsecret');
|
||||
const calledData = (got.post as jest.MockedFunction<typeof got.post>).mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
const payload = calledData?.[0].form as URLSearchParams;
|
||||
expect(payload.get('AccessKeyId')).toEqual('testid');
|
||||
expect(payload.get('Timestamp')).not.toBeNull();
|
||||
expect(payload.get('SignatureNonce')).not.toBeNull();
|
||||
expect(payload.get('Signature')).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
import { createHmac } from 'crypto';
|
||||
|
||||
import got from 'got';
|
||||
|
||||
import { PublicParameters } from './types';
|
||||
|
||||
// Aliyun has special escape rules.
|
||||
// https://help.aliyun.com/document_detail/29442.html
|
||||
const escaper = (string_: string) =>
|
||||
encodeURIComponent(string_)
|
||||
.replace(/\*/g, '%2A')
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/!/g, '%21')
|
||||
.replace(/"/g, '%22')
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/\+/g, '%2B');
|
||||
|
||||
export const getSignature = (
|
||||
parameters: Record<string, string>,
|
||||
secret: string,
|
||||
method: string
|
||||
) => {
|
||||
const canonicalizedQuery = Object.keys(parameters)
|
||||
.map((key) => {
|
||||
const value = parameters[key];
|
||||
|
||||
return value === undefined ? '' : `${escaper(key)}=${escaper(value)}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice()
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const stringToSign = `${method.toUpperCase()}&${escaper('/')}&${escaper(canonicalizedQuery)}`;
|
||||
|
||||
return createHmac('sha1', `${secret}&`).update(stringToSign).digest('base64');
|
||||
};
|
||||
|
||||
export const request = async (
|
||||
url: string,
|
||||
parameters: PublicParameters & Record<string, string>,
|
||||
accessKeySecret: string
|
||||
) => {
|
||||
const finalParameters: Record<string, string> = {
|
||||
...parameters,
|
||||
SignatureNonce: String(Math.random()),
|
||||
Timestamp: new Date().toISOString(),
|
||||
};
|
||||
const signature = getSignature(finalParameters, accessKeySecret, 'POST');
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(finalParameters)) {
|
||||
payload.append(key, value);
|
||||
}
|
||||
payload.append('Signature', signature);
|
||||
|
||||
return got.post({
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
form: payload,
|
||||
});
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": ["src", "jest.config.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
}
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
* **connector:** passwordless connector send test msg with unsaved config ([#1539](https://github.com/logto-io/logto/issues/1539)) ([0297f6c](https://github.com/logto-io/logto/commit/0297f6c52f7b5d730de44fbb08f88c2e9b951874))
|
||||
* **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connector:** fix Aliyun SMS connector error handling ([#1227](https://github.com/logto-io/logto/issues/1227)) ([d9ba729](https://github.com/logto-io/logto/commit/d9ba72985d016a762b3946dcbb6917db562e9b0b))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.0](https://github.com/logto-io/logto/compare/v0.1.1-alpha.0...v0.1.2-alpha.0) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector-sendgrid-email:** add sendgrid email connector ([#850](https://github.com/logto-io/logto/issues/850)) ([b887655](https://github.com/logto-io/logto/commit/b8876558275e28ca921d4eeea6c38f8559810a11))
|
||||
* **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
* **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
* **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
* **core:** update connector db schema ([#732](https://github.com/logto-io/logto/issues/732)) ([8e1533a](https://github.com/logto-io/logto/commit/8e1533a70267d459feea4e5174296b17bef84d48))
|
||||
* **core:** wrap aliyun short message service connector ([#670](https://github.com/logto-io/logto/issues/670)) ([a06d3ee](https://github.com/logto-io/logto/commit/a06d3ee73ccc59f6aaef1dab4f45d6c118aab40d))
|
||||
* remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
* **sms/email-connectors:** expose third-party API request error message ([#1059](https://github.com/logto-io/logto/issues/1059)) ([4cfd578](https://github.com/logto-io/logto/commit/4cfd5788d24d55017a8ace53fed99082f87691cb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `lint:report` script ([#730](https://github.com/logto-io/logto/issues/730)) ([3b17324](https://github.com/logto-io/logto/commit/3b17324d189b2fe47985d0bee8b37b4ef1dbdd2b))
|
||||
* **connector-aliyun-sms:** fix config guard, remove unnecessary fields ([#1229](https://github.com/logto-io/logto/issues/1229)) ([4ee7752](https://github.com/logto-io/logto/commit/4ee775273ac6c97b6580a40ec20cb3f5df8285f4))
|
|
@ -1,204 +0,0 @@
|
|||
# Aliyun short message service connector
|
||||
|
||||
The official Logto connector for Aliyun short message service.
|
||||
|
||||
阿里云短信服务 Logto 官方连接器 [中文文档](#阿里云短信连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Aliyun short message service connector](#aliyun-short-message-service-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Set up a short message service in Aliyun SMS Console](#set-up-a-short-message-service-in-aliyun-sms-console)
|
||||
- [Create an Aliyun account](#create-an-aliyun-account)
|
||||
- [Enable and Configure Aliyun Short Message Service](#enable-and-configure-aliyun-short-message-service)
|
||||
- [Compose the connector JSON](#compose-the-connector-json)
|
||||
- [Test Aliyun SMS connector](#test-aliyun-sms-connector)
|
||||
- [Config types](#config-types)
|
||||
- [References](#references)
|
||||
- [阿里云短信连接器](#阿里云短信连接器)
|
||||
- [在阿里云短信服务控制台中配置一个短信服务](#在阿里云短信服务控制台中配置一个短信服务)
|
||||
- [创建阿里云账号](#创建阿里云账号)
|
||||
- [启用并配置阿里云短信服务](#启用并配置阿里云短信服务)
|
||||
- [编写连接器的 JSON](#编写连接器的-json)
|
||||
- [测试阿里云短信连接器](#测试阿里云短信连接器)
|
||||
- [配置类型](#配置类型)
|
||||
- [参考](#参考)
|
||||
|
||||
## Get started
|
||||
|
||||
Aliyun is a primary cloud service provider in Asia, offering many cloud services, including SMS (short message service). Aliyun SMS Connector is a plugin provided by the Logto team to call the Aliyun SMS service, with the help of which Logto end-users can register and sign in to their Logto account via SMS verification code (or in other words, passcode).
|
||||
|
||||
## Set up a short message service in Aliyun SMS Console
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> You can skip some sections if you have already finished.
|
||||
|
||||
### Create an Aliyun account
|
||||
|
||||
Go to the [Aliyun website](https://cn.aliyun.com/) and register your Aliyun account if you don't have one.
|
||||
|
||||
### Enable and Configure Aliyun Short Message Service
|
||||
|
||||
1. Sign-in with your Aliyun account at the [Aliyun website](https://cn.aliyun.com/) and go to the [SMS service console page](https://www.aliyun.com/product/sms).
|
||||
2. Click the "Open for free" (免费开通) button on the top left of the SMS service page and begin the configuration process.
|
||||
3. Read and agree to the "SMS service activation Agreement" (短信服务开通条款) and click "Subscribe to a service" (开通服务) to move on.
|
||||
4. You are now on the [SMS service console page](https://dysms.console.aliyun.com/overview), go to either "Mainland China" (国内消息) or "Outside Mainland China" (国际/港澳台消息) button on the sidebar per your use case.
|
||||
5. Add signature and template following the guidelines, and provide the materials or information required for review.
|
||||
- Remember to select "Verification Code Message" (验证码) as "Scenario" (适用场景) when filling out the signature application and also "Verification Code Message" (验证码) for "Type" (模板类型) when applying for a template review because we are using these signatures and templates to send passcode. Currently, we do not support sending SMS messages other than verification-code-related text messages.
|
||||
- Also, use `{{code}}` as a placeholder where you want to place your digital passcode in template contents.
|
||||
6. After submitting your SMS signature and template application, you need to wait for it to take effect. At this point, we can go back to the [SMS service console page](https://dysms.console.aliyun.com/overview) and send a test SMS. If your signatures and templates are ready for use, you can try them directly; if they are not taking effect yet, Aliyun also provides test templates.
|
||||
- You may need to recharge a small amount of money before sending test messages.
|
||||
- You may also be asked to bind a test phone number before sending test messages. For more details, go to "Quick Start" (快速学习) tab from the sidebar of the [SMS service console page](https://dysms.console.aliyun.com/overview).
|
||||
|
||||
## Compose the connector JSON
|
||||
|
||||
1. From the [SMS service console page](https://dysms.console.aliyun.com/overview), hover on your avatar in the top right corner and go to "AccessKey Management" (AccessKey 管理), and click "Create AccessKey" (创建 AccessKey). You will get an "AccessKey ID" and "AccessKey Secret" pair after finishing security verification. Please keep them properly.
|
||||
2. Go to the "Mainland China" (国内消息) or "Outside Mainland China" (国际/港澳台消息) tab you just visited, you can find "Signature" (签名名称) and "Template Code" (模板 CODE) easily.
|
||||
- If you want to use the test-only signature and template, go to the "Quick Start" (快速学习) tab instead, and you will find them below "Signature & Templates (For Test Only)".
|
||||
3. Fill out the Aliyun SMS Connector settings:
|
||||
- Fill out the `accessKeyId` and `accessKeySecret` fields with access key pairs you've got from step 1.
|
||||
- Fill out the `signName` field with "Signature" (签名名称) which is mentioned in step 2. All templates will share this signature name.
|
||||
- You can add multiple SMS connector templates for different cases. Here is an example of adding a single template:
|
||||
- Fill the `templateCode` field, which is how you can control SMS context, with "Template Code" (模板 CODE) from step 2.
|
||||
- Fill out `usageType` field with either `Register`, `SignIn` or `Test` for different use cases. (`usageType` is a Logto property to identify the proper use case.)
|
||||
|
||||
Here is an example of Aliyun SMS connector config JSON.
|
||||
|
||||
```json
|
||||
{
|
||||
"accessKeyId": "<your-access-key-id>",
|
||||
"accessKeySecret": "<your-access-key-secret>",
|
||||
"signName": "<Aliyun>",
|
||||
"templates": [
|
||||
{
|
||||
"templateCode": "<SMS_123456>",
|
||||
"usageType": "Register"
|
||||
},
|
||||
{
|
||||
"templateCode": "<SMS_234567>",
|
||||
"usageType": "SignIn"
|
||||
},
|
||||
{
|
||||
"templateCode": "<SMS_345678>",
|
||||
"usageType": "Test"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Test Aliyun SMS connector
|
||||
|
||||
You can type in a phone number and click on "Send" to see whether the settings can work before "Save and Done".
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-passcode-sign-in/#enable-connector-in-sign-in-experience).
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
|-----------------|------------|
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| signName | string |
|
||||
| templates | Template[] |
|
||||
|
||||
| Template Properties | Type | Enum values |
|
||||
|---------------------|-------------|----------------------------------|
|
||||
| templateCode | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'Test' |
|
||||
|
||||
|
||||
## References
|
||||
|
||||
- [Aliyun SMS - Quick Start](https://dysms.console.aliyun.com/quickstart)
|
||||
|
||||
# 阿里云短信连接器
|
||||
|
||||
阿里云是亚洲地区一个重要的云服务厂商,提供了包括短信服务在内的诸多云服务。
|
||||
|
||||
本连接器是 Logto 官方提供的阿里云短信连接器,帮助终端用户通过短信验证码进行登录注册。
|
||||
|
||||
## 在阿里云短信服务控制台中配置一个短信服务
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> 你可以跳过已经完成的部分。
|
||||
|
||||
### 创建阿里云账号
|
||||
|
||||
前往 [阿里云](https://cn.aliyun.com/) 并完成账号注册。
|
||||
|
||||
### 启用并配置阿里云短信服务
|
||||
|
||||
1. 用刚刚在 [阿里云](https://cn.aliyun.com/) 注册额账号登录并前往 [短信服务控制台](https://www.aliyun.com/product/sms)。
|
||||
2. 点按短信服务页面左上角的「免费开通」按钮并开始配置的流程。
|
||||
3. 阅读并同意「短信服务开通条款」和「开通服务」以继续。
|
||||
4. 你现在处于「[短信服务控制台概览](https://dysms.console.aliyun.com/overview)」,根据你的用户场景,点击侧边栏中的「国内消息」或者「国际/港澳台消息」。
|
||||
5. 跟随指引添加签名和模板,并提供相应的材料和信息以便审核:
|
||||
- 注意:添加 **签名** 时要在「适用场景」栏选择「验证码」,添加 **模板** 时「模板类型」也要选择「验证码」,因为我们的使用这些签名和模板就是用来发送验证码的。目前我们暂不支持除了发送验证码之外别的类型的文字短信。
|
||||
- 请同时注意要在模板的内容中加上 `{{code}}` 的占位符,在发送短信是会被随机生成的验证码所替代。
|
||||
6. 提交了短信签名和模板的申请之后,需要等待它们生效。这时候我们可以回到 [短信服务控制台概览](https://dysms.console.aliyun.com/overview) 发送测试短信。如果你的签名和模板都已经通过审核,你可以直接使用它们测试;如果它们还没有通过审核,阿里云也提供了测试模板供使用。
|
||||
- 在发送测试短信之前,可能你需要对账户进行小额的充值。
|
||||
- 测试时也需要提前绑定测试使用的手机号码以成功收到测试短信。点击 [短信服务控制台概览](https://dysms.console.aliyun.com/overview) 侧边栏上的「快速学习」标签页以了解更多。
|
||||
|
||||
## 编写连接器的 JSON
|
||||
|
||||
1. 前往 [短信服务控制台概览](https://dysms.console.aliyun.com/overview),将鼠标悬停在页面右上角的头像处,进入「AccessKey 管理」并点按「创建 AccessKey」。完成了安全验证之后,你会得到一对「AccessKey ID」和「AccessKey Secret」,请妥善保管他们。
|
||||
2. 前往你之前访问过的「国内消息」或「国际/港澳台消息」标签页,可以很快找到「签名名称」和「模板 CODE」。
|
||||
- 如果你想使用测试专用的签名模板, 则前往「快速开始」标签页,你就能在「测试专用签名模版」下方找到它们。
|
||||
3. 完成阿里云短信服务连接器的设置:
|
||||
- 用你在步骤 1 中拿到的一对「AccessKey ID」和「AccessKey Secret」来分别填入 `accessKeyId` 和 `accessKeySecret`。
|
||||
- 用你在步骤 2 中拿到的「签名名称」填入 `signName` 栏。所有的模板都会共用这个签名。
|
||||
- 你可以添加多个短信服务模板以应对不同的用户场景。这里展示填写单个模板的例子:
|
||||
- `templateCode` 栏是你可以用来控制所发送短信内容的属性。它们的值从步骤 2 中的「模板 CODE」获取。
|
||||
- `usageType` 栏填写 `Register`,`SignIn` 或者 `Test` 其中之一以分别对应 _注册_,_登录_ 和 _测试_ 的不同场景。(`usageType` 是 Logto 的属性,用来确定使用场景。)
|
||||
|
||||
这是一个阿里云短信服务连接器 JSON 配置的样例。
|
||||
|
||||
```json
|
||||
{
|
||||
"accessKeyId": "<your-access-key-id>",
|
||||
"accessKeySecret": "<your-access-key-secret>",
|
||||
"signName": "<Aliyun>",
|
||||
"templates": [
|
||||
{
|
||||
"templateCode": "<SMS_123456>",
|
||||
"usageType": "Register"
|
||||
},
|
||||
{
|
||||
"templateCode": "<SMS_234567>",
|
||||
"usageType": "SignIn"
|
||||
},
|
||||
{
|
||||
"templateCode": "<SMS_345678>",
|
||||
"usageType": "Test"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 测试阿里云短信连接器
|
||||
|
||||
你可以在「保存并完成」之前输入一个手机号码并点按「发送」来测试配置是否可以正常工作。
|
||||
|
||||
大功告成!快去 [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8) 吧。
|
||||
|
||||
### 配置类型
|
||||
|
||||
| 名称 | 类型 |
|
||||
|-----------------|------------|
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| signName | string |
|
||||
| templates | Template[] |
|
||||
|
||||
| 模板属性 | 类型 | 枚举值 |
|
||||
|--------------|-------------|----------------------------------|
|
||||
| templateCode | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'Test' |
|
||||
|
||||
|
||||
## 参考
|
||||
|
||||
- [阿里云短信服务 - 快速学习](https://dysms.console.aliyun.com/quickstart)
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"accessKeyId": "<access-key-id>",
|
||||
"accessKeySecret": "<access-key-secret>",
|
||||
"signName": "<sign-name>",
|
||||
"templates": [
|
||||
{
|
||||
"usageType": "SignIn",
|
||||
"templateCode": "<template-code>"
|
||||
},
|
||||
{
|
||||
"usageType": "Register",
|
||||
"templateCode": "<template-code>"
|
||||
},
|
||||
{
|
||||
"usageType": "Test",
|
||||
"templateCode": "<template-code>"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@silverhand/jest-config';
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.71942 6.071C3.80784 6.2585 2.82876 6.98664 2.42188 7.78477C2.015 8.58291 2 8.77916 2 12.1389C2 13.9317 2.03 15.4083 2.075 15.639C2.34251 17.0065 3.4119 18.0759 4.77942 18.3434C5.1938 18.4184 10.5873 18.4565 10.5873 18.3734C10.5873 18.3131 10.1279 16.5052 10.1051 16.4827C10.0976 16.4677 9.14074 16.2568 7.98072 16.0077C5.2763 15.4202 5.35568 15.4427 5.13317 15.1715L4.93723 14.938V9.46918L5.13317 9.23167C5.35911 8.96042 5.2763 8.98323 7.98072 8.39541L10.1051 7.9204C10.1283 7.90478 10.5876 6.09662 10.5876 6.03975C10.5876 5.96787 5.0813 6.00225 4.71942 6.07725V6.071ZM13.4502 6.08225C13.4577 6.21787 13.8796 7.87509 13.9252 7.9204C13.9402 7.9429 14.9721 8.17665 16.2077 8.4401C17.4434 8.70354 18.5431 8.97135 18.6409 9.02761C18.7734 9.10448 18.8846 9.21323 18.965 9.34386C19.0856 9.5548 19.1006 9.78074 19.1006 12.2064C19.1006 14.6321 19.0856 14.858 18.965 15.0689C18.8828 15.198 18.7719 15.3062 18.6409 15.3852C18.5431 15.4383 17.4534 15.7015 16.2077 15.9727L13.9252 16.4962C13.8799 16.5415 13.458 18.1987 13.4502 18.3343C13.4502 18.4171 13.9927 18.4321 16.373 18.4093C19.6543 18.3793 19.695 18.3718 20.5312 17.8068C21.1319 17.3934 21.5856 16.7999 21.8269 16.1118C22 15.6071 22 15.5618 22 12.2095C22 8.85729 22 8.81198 21.8269 8.30728C21.5855 7.61926 21.1318 7.02571 20.5312 6.61226C19.695 6.04787 19.6575 6.03975 16.373 6.01006C13.9927 5.98756 13.4502 5.99881 13.4502 6.08506V6.08225ZM10.0876 12.0517C10.0635 12.1323 10.0584 12.2173 10.0726 12.3002C10.0951 12.4252 10.2308 12.4358 12.0008 12.4583L13.8992 12.4733V11.9461H12.0189C10.4445 11.9461 10.1204 11.9611 10.0908 12.0514L10.0876 12.0517Z" fill="#FF6A00"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -1,55 +0,0 @@
|
|||
{
|
||||
"name": "@logto/connector-aliyun-sms",
|
||||
"version": "1.0.0-beta.8",
|
||||
"description": "Aliyun SMS connector implementation.",
|
||||
"main": "./lib/index.js",
|
||||
"exports": "./lib/index.js",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"license": "MPL-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"docs",
|
||||
"logo.svg",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "rm -rf lib/ && ncc build src/index.ts -o lib",
|
||||
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/connector-core": "^1.0.0-beta.8",
|
||||
"@silverhand/essentials": "^1.2.0",
|
||||
"@silverhand/jest-config": "1.0.0",
|
||||
"got": "^11.8.2",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^28.1.3",
|
||||
"@silverhand/eslint-config": "1.0.0",
|
||||
"@silverhand/ts-config": "1.0.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/node": "^16.3.1",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"eslint": "^8.21.0",
|
||||
"jest": "^28.1.3",
|
||||
"lint-staged": "^13.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import { ConnectorMetadata } from '@logto/connector-core';
|
||||
|
||||
export const endpoint = 'https://dysmsapi.aliyuncs.com/';
|
||||
|
||||
export const staticConfigs = {
|
||||
Format: 'json',
|
||||
RegionId: 'cn-hangzhou',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Version: '2017-05-25',
|
||||
};
|
||||
|
||||
/**
|
||||
* Details of SmsTemplateType can be found at:
|
||||
* https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/QuerySmsTemplateList.
|
||||
*
|
||||
* For our use case is to send passcode sms for passwordless sign-in/up as well as
|
||||
* reset password, the default value of type code is set to be 2.
|
||||
*/
|
||||
export enum SmsTemplateType {
|
||||
Notification = 0,
|
||||
Promotion = 1,
|
||||
Passcode = 2,
|
||||
InternationalMessage = 6,
|
||||
PureNumber = 7,
|
||||
}
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'aliyun-short-message-service',
|
||||
target: 'aliyun-sms',
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Aliyun Short Message Service',
|
||||
'zh-CN': '阿里云短信服务',
|
||||
'tr-TR': 'Aliyun SMS Servisi',
|
||||
'ko-KR': 'Aliyun Short 메세지 서비스',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Aliyun provides cloud computing services to online businesses.',
|
||||
'zh-CN': '阿里云是全球性的云服务提供商。',
|
||||
'tr-TR': 'Aliyun, çevrimiçi işletmelere bulut bilişim hizmetleri sunmaktadır.',
|
||||
'ko-KR': 'Aliyun는 온라인 비지니스를 위해 클라우딩 컴퓨팅 서비스를 제공합니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
configTemplate: './docs/config-template.json',
|
||||
};
|
|
@ -1,54 +0,0 @@
|
|||
import { MessageTypes } from '@logto/connector-core';
|
||||
|
||||
import createConnector from '.';
|
||||
import { mockedConnectorConfig, phoneTest, codeTest } from './mock';
|
||||
import { sendSms } from './single-send-text';
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConnectorConfig);
|
||||
|
||||
jest.mock('./single-send-text', () => {
|
||||
return {
|
||||
sendSms: jest.fn(() => {
|
||||
return {
|
||||
body: JSON.stringify({ Code: 'OK', RequestId: 'request-id', Message: 'OK' }),
|
||||
statusCode: 200,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('sendMessage()', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call singleSendMail() and replace code in content', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await connector.sendMessage({
|
||||
to: phoneTest,
|
||||
type: MessageTypes.SignIn,
|
||||
payload: { code: codeTest },
|
||||
});
|
||||
expect(sendSms).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
AccessKeyId: mockedConnectorConfig.accessKeyId,
|
||||
PhoneNumbers: phoneTest,
|
||||
SignName: mockedConnectorConfig.signName,
|
||||
TemplateCode: 'TemplateCode',
|
||||
TemplateParam: `{"code":"${codeTest}"}`,
|
||||
}),
|
||||
mockedConnectorConfig.accessKeySecret
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if template is missing', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.sendMessage({
|
||||
to: phoneTest,
|
||||
type: MessageTypes.Register,
|
||||
payload: { code: codeTest },
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
|
@ -1,97 +0,0 @@
|
|||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
GetConnectorConfig,
|
||||
SendMessageFunction,
|
||||
SmsConnector,
|
||||
CreateConnector,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
} from '@logto/connector-core';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { defaultMetadata } from './constant';
|
||||
import { sendSms } from './single-send-text';
|
||||
import { aliyunSmsConfigGuard, AliyunSmsConfig, sendSmsResponseGuard } from './types';
|
||||
|
||||
const sendMessage =
|
||||
(getConfig: GetConnectorConfig): SendMessageFunction =>
|
||||
async (data, inputConfig) => {
|
||||
const { to, type, payload } = data;
|
||||
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
|
||||
validateConfig<AliyunSmsConfig>(config, aliyunSmsConfigGuard);
|
||||
const { accessKeyId, accessKeySecret, signName, templates } = config;
|
||||
const template = templates.find(({ usageType }) => usageType === type);
|
||||
|
||||
assert(
|
||||
template,
|
||||
new ConnectorError(ConnectorErrorCodes.TemplateNotFound, `Cannot find template!`)
|
||||
);
|
||||
|
||||
try {
|
||||
const httpResponse = await sendSms(
|
||||
{
|
||||
AccessKeyId: accessKeyId,
|
||||
PhoneNumbers: to,
|
||||
SignName: signName,
|
||||
TemplateCode: template.templateCode,
|
||||
TemplateParam: JSON.stringify(payload),
|
||||
},
|
||||
accessKeySecret
|
||||
);
|
||||
|
||||
const { body: rawBody } = httpResponse;
|
||||
|
||||
const { Code, Message, ...rest } = parseResponseString(rawBody);
|
||||
|
||||
if (Code !== 'OK') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: Message,
|
||||
Code,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
|
||||
return httpResponse;
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof HTTPError)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const {
|
||||
response: { body: rawBody },
|
||||
} = error;
|
||||
|
||||
assert(typeof rawBody === 'string', new ConnectorError(ConnectorErrorCodes.InvalidResponse));
|
||||
|
||||
const { Code, Message, ...rest } = parseResponseString(rawBody);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: Message,
|
||||
Code,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const parseResponseString = (response: string) => {
|
||||
const result = sendSmsResponseGuard.safeParse(JSON.parse(response));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAliyunSmsConnector: CreateConnector<SmsConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Sms,
|
||||
configGuard: aliyunSmsConfigGuard,
|
||||
sendMessage: sendMessage(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAliyunSmsConnector;
|
|
@ -1,33 +0,0 @@
|
|||
export const mockedConnectorConfig = {
|
||||
accessKeyId: 'accessKeyId',
|
||||
accessKeySecret: 'accessKeySecret',
|
||||
signName: 'signName',
|
||||
templates: [
|
||||
{
|
||||
type: 2,
|
||||
usageType: 'SignIn',
|
||||
templateCode: 'TemplateCode',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const phoneTest = '13012345678';
|
||||
export const codeTest = '1234';
|
||||
|
||||
export const mockedParameters = {
|
||||
AccessKeyId: 'testid',
|
||||
AccountName: "<a%b'>",
|
||||
Action: 'SingleSendMail',
|
||||
AddressType: '1',
|
||||
Format: 'XML',
|
||||
HtmlBody: '4',
|
||||
RegionId: 'cn-hangzhou',
|
||||
ReplyToAddress: 'true',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Subject: '3',
|
||||
TagName: '2',
|
||||
ToAddress: '1@test.com',
|
||||
Version: '2015-11-23',
|
||||
};
|
||||
export const mockedRandomCode = 1235;
|
|
@ -1,26 +0,0 @@
|
|||
import { mockedRandomCode } from './mock';
|
||||
import { sendSms } from './single-send-text';
|
||||
import { request } from './utils';
|
||||
|
||||
jest.mock('./utils');
|
||||
|
||||
describe('sendSms', () => {
|
||||
it('should call request with action sendSms', async () => {
|
||||
const code = mockedRandomCode;
|
||||
|
||||
await sendSms(
|
||||
{
|
||||
AccessKeyId: '<access-key-id>',
|
||||
PhoneNumbers: '13912345678',
|
||||
SignName: '阿里云短信测试',
|
||||
TemplateCode: ' SMS_154950909',
|
||||
TemplateParam: JSON.stringify({ code }),
|
||||
},
|
||||
'<access-key-secret>'
|
||||
);
|
||||
const calledData = (request as jest.MockedFunction<typeof request>).mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
const payload = calledData?.[1];
|
||||
expect(payload).toHaveProperty('Action', 'SendSms');
|
||||
});
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
import { endpoint, staticConfigs } from './constant';
|
||||
import { PublicParameters, SendSms } from './types';
|
||||
import { request } from './utils';
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/101414.html
|
||||
*/
|
||||
export const sendSms = async (parameters: PublicParameters & SendSms, accessKeySecret: string) => {
|
||||
return request(endpoint, { Action: 'SendSms', ...staticConfigs, ...parameters }, accessKeySecret);
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { SmsTemplateType } from './constant';
|
||||
|
||||
export const sendSmsResponseGuard = z.object({
|
||||
BizId: z.string().optional(),
|
||||
Code: z.string(),
|
||||
Message: z.string(),
|
||||
RequestId: z.string(),
|
||||
});
|
||||
|
||||
export type SendSmsResponse = z.infer<typeof sendSmsResponseGuard>;
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/101414.html
|
||||
*/
|
||||
export type SendSms = {
|
||||
OutId?: string;
|
||||
PhoneNumbers: string; // 11 digits w/o prefix (can be multiple phone numbers with separator `,`)
|
||||
SignName: string; // Name of SMS signature
|
||||
SmsUpExtendCode?: string;
|
||||
TemplateCode: string; // Text message template ID
|
||||
TemplateParam?: string; // Stringified JSON (used to fill in text template)
|
||||
};
|
||||
|
||||
export type PublicParameters = {
|
||||
AccessKeyId: string;
|
||||
Format?: string; // 'json' or 'xml', default: 'json'
|
||||
RegionId?: string; // 'cn-hangzhou' | 'ap-southeast-1' | 'ap-southeast-2'
|
||||
Signature?: string;
|
||||
SignatureMethod?: string;
|
||||
SignatureNonce?: string;
|
||||
SignatureVersion?: string;
|
||||
Timestamp?: string;
|
||||
Version?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* UsageType here is used to specify the use case of the template, can be either
|
||||
* 'Register', 'SignIn', 'ForgotPassword' or 'Test'.
|
||||
*
|
||||
* Type here in the template is used to specify the purpose of sending the sms,
|
||||
* can be either item in SmsTemplateType.
|
||||
* As the SMS is applied for sending passcode, the value should always be 2 in our case.
|
||||
*/
|
||||
const templateGuard = z.object({
|
||||
type: z.nativeEnum(SmsTemplateType).default(2),
|
||||
usageType: z.string(),
|
||||
templateCode: z.string(),
|
||||
});
|
||||
|
||||
export const aliyunSmsConfigGuard = z.object({
|
||||
accessKeyId: z.string(),
|
||||
accessKeySecret: z.string(),
|
||||
signName: z.string(),
|
||||
templates: z.array(templateGuard),
|
||||
});
|
||||
|
||||
export type AliyunSmsConfig = z.infer<typeof aliyunSmsConfigGuard>;
|
|
@ -1,32 +0,0 @@
|
|||
import got from 'got';
|
||||
|
||||
import { mockedParameters } from './mock';
|
||||
import { getSignature, request } from './utils';
|
||||
|
||||
jest.mock('got');
|
||||
|
||||
describe('getSignature', () => {
|
||||
it('should get valid signature', () => {
|
||||
const parameters = {
|
||||
...mockedParameters,
|
||||
SignatureNonce: 'c1b2c332-4cfb-4a0f-b8cc-ebe622aa0a5c',
|
||||
Timestamp: '2016-10-20T06:27:56Z',
|
||||
};
|
||||
const signature = getSignature(parameters, 'testsecret', 'POST');
|
||||
expect(signature).toEqual('llJfXJjBW3OacrVgxxsITgYaYm0=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
it('should call axios.post with extended params', async () => {
|
||||
const parameters = mockedParameters;
|
||||
await request('http://test.endpoint.com', parameters, 'testsecret');
|
||||
const calledData = (got.post as jest.MockedFunction<typeof got.post>).mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
const payload = calledData?.[0].form as URLSearchParams;
|
||||
expect(payload.get('AccessKeyId')).toEqual('testid');
|
||||
expect(payload.get('Timestamp')).not.toBeNull();
|
||||
expect(payload.get('SignatureNonce')).not.toBeNull();
|
||||
expect(payload.get('Signature')).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
import { createHmac } from 'crypto';
|
||||
|
||||
import got from 'got';
|
||||
|
||||
import { PublicParameters } from './types';
|
||||
|
||||
// Aliyun has special escape rules.
|
||||
// https://help.aliyun.com/document_detail/29442.html
|
||||
const escaper = (string_: string) =>
|
||||
encodeURIComponent(string_)
|
||||
.replace(/\*/g, '%2A')
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/!/g, '%21')
|
||||
.replace(/"/g, '%22')
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/\+/g, '%2B');
|
||||
|
||||
export const getSignature = (
|
||||
parameters: Record<string, string>,
|
||||
secret: string,
|
||||
method: string
|
||||
) => {
|
||||
const canonicalizedQuery = Object.keys(parameters)
|
||||
.map((key) => {
|
||||
const value = parameters[key];
|
||||
|
||||
return value === undefined ? '' : `${escaper(key)}=${escaper(value)}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice()
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const stringToSign = `${method.toUpperCase()}&${escaper('/')}&${escaper(canonicalizedQuery)}`;
|
||||
|
||||
return createHmac('sha1', `${secret}&`).update(stringToSign).digest('base64');
|
||||
};
|
||||
|
||||
export const request = async (
|
||||
url: string,
|
||||
parameters: PublicParameters & Record<string, string>,
|
||||
accessKeySecret: string
|
||||
) => {
|
||||
const finalParameters: Record<string, string> = {
|
||||
...parameters,
|
||||
SignatureNonce: String(Math.random()),
|
||||
Timestamp: new Date().toISOString(),
|
||||
};
|
||||
const signature = getSignature(finalParameters, accessKeySecret, 'POST');
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(finalParameters)) {
|
||||
payload.append(key, value);
|
||||
}
|
||||
payload.append('Signature', signature);
|
||||
|
||||
return got.post({
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
form: payload,
|
||||
});
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": ["src", "jest.config.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
* **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expose zod error ([#1474](https://github.com/logto-io/logto/issues/1474)) ([81b63f0](https://github.com/logto-io/logto/commit/81b63f07bb412abf1f2b42059bac2ffcfc86272c))
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.2-alpha.0](https://github.com/logto-io/logto/compare/v0.1.1-alpha.0...v0.1.2-alpha.0) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector:** apple ([#966](https://github.com/logto-io/logto/issues/966)) ([7400ed8](https://github.com/logto-io/logto/commit/7400ed8896fdceda6165a0540413efb4e3a47438))
|
||||
* **connectors:** handle authorization callback parameters in each connector respectively ([#1166](https://github.com/logto-io/logto/issues/1166)) ([097aade](https://github.com/logto-io/logto/commit/097aade2e2e1b1ea1531bcb4c1cca8d24961a9b9))
|
||||
* **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connector:** test ([d327c6f](https://github.com/logto-io/logto/commit/d327c6fdf5f4a3fbc68618f46df7ac213d77aed5))
|
|
@ -1,164 +0,0 @@
|
|||
# Apple connector
|
||||
|
||||
The official Logto connector for Apple social sign-in.
|
||||
|
||||
Apple 社交登录 Logto 官方连接器 [中文文档](#apple-连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Apple connector](#apple-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Enable Sign in with Apple for your app](#enable-sign-in-with-apple-for-your-app)
|
||||
- [Create an identifier](#create-an-identifier)
|
||||
- [Enable Sign in with Apple for your identifier](#enable-sign-in-with-apple-for-your-identifier)
|
||||
- [Compose the connector JSON](#compose-the-connector-json)
|
||||
- [Test Apple connector](#test-apple-connector)
|
||||
- [Apple 连接器](#apple-连接器)
|
||||
- [开始上手](#开始上手)
|
||||
- [为你的应用启用「通过 Apple 登录」](#为你的应用启用通过-apple-登录)
|
||||
- [创建一个 identifier](#创建一个-identifier)
|
||||
- [为你的 identifier 启用「通过 Apple 登录」](#为你的-identifier-启用通过-apple-登录)
|
||||
- [编写连接器的 JSON](#编写连接器的-json)
|
||||
- [测试 Apple 连接器](#测试-apple-连接器)
|
||||
|
||||
## Get started
|
||||
|
||||
If you don't know the concept of the connector or don't know how to add this connector to your Sign-in experience, please see [Logto tutorial](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in).
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> Apple sign-in is required for AppStore if you have other social sign-in methods in your app.
|
||||
> Having Apple sign-in on Android devices is great if you also provide an Android app.
|
||||
|
||||
You need to enroll [Apple Developer Program](https://developer.apple.com/programs/) before continuing.
|
||||
|
||||
### Enable Sign in with Apple for your app
|
||||
|
||||
> ⚠️ **Caution**
|
||||
>
|
||||
> Even if you want to implement Sign in with Apple on a web app only, you still need to have an existing app that embraces the AppStore ecosystem (i.e., have a valid App ID).
|
||||
|
||||
You can do it via Xcode -> Project settings -> Signing & Capabilities, or visit [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/bundleId).
|
||||
|
||||

|
||||
|
||||
See the "Enable an App ID" section in [Apple official docs](https://developer.apple.com/documentation/sign_in_with_apple/configuring_your_environment_for_sign_in_with_apple) for more info.
|
||||
|
||||
### Create an identifier
|
||||
|
||||
1. Visit [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/serviceId), then click the "+" button next to "Identifier".
|
||||
2. In the "Register a new identifier" page, choose "Services IDs" and click "Continue".
|
||||
3. Fill out "Description" and "Identifier" (E.g., `Logto Test` and `io.logto.test`), then click "Continue".
|
||||
4. Double-check the info and click "Register".
|
||||
|
||||
### Enable Sign in with Apple for your identifier
|
||||
|
||||
Click the identifier you just created. Check "Sign in with Apple" on the details page and click "Configure".
|
||||
|
||||

|
||||
|
||||
In the opening modal, select the App ID you just enabled Sign in with Apple.
|
||||
|
||||
Enter the domain of your Logto instance without protocol and port, e.g., `your.logto.domain`; then enter the "Return URL" (i.e., Redirect URI), which is the Logto URL with `/callback/apple-universal`, e.g., `https://your.logto.domain/callback/apple-universal`.
|
||||
|
||||

|
||||
|
||||
Click "Next" then "Done" to close the modal. Click "Continue" on the top-right corner, then click "Save" to save your configuration.
|
||||
|
||||
> ⚠️ **Caution**
|
||||
>
|
||||
> Apple does NOT allow Return URLs with HTTP protocol and `localhost` domain.
|
||||
>
|
||||
> If you want to test locally, you need to edit `/etc/hosts` file to map localhost to a custom domain and set up a local HTTPS environment. [mkcert](https://github.com/FiloSottile/mkcert) can help you for setting up local HTTPS.
|
||||
|
||||
## Compose the connector JSON
|
||||
|
||||
You need to use the identifier that fills in the [Create an identifier](#create-an-identifier) section to compose the JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"clientId": "io.logto.test"
|
||||
}
|
||||
```
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> This connector doesn't support customizing `scope` (e.g., name, email) yet since Apple requires `form_post` response mode when `scope` is not empty, which is incompatible with the current connector design.
|
||||
>
|
||||
> We'll figure out this later.
|
||||
|
||||
## Test Apple connector
|
||||
|
||||
That's it. The Apple connector should be available in both web and native apps. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in#enable-connector-in-sign-in-experience).
|
||||
|
||||
# Apple 连接器
|
||||
|
||||
## 开始上手
|
||||
|
||||
如果你还不知道连接器的概念,或者还不知道如何将本连接器添加至你的「登录体验」,请先参见 [Logto 教程](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)。
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> 如果你的应用有其他的社交登录方式,AppStore 要求必须同时有 Apple 登录。
|
||||
> 如果同时提供 Android 应用,在 Android 设备上同时提供 Apple 登录会让用户体验很棒。
|
||||
|
||||
在继续前,你需要加入 [Apple Developer Program](https://developer.apple.com/programs/)。
|
||||
|
||||
### 为你的应用启用「通过 Apple 登录」
|
||||
|
||||
> ⚠️ **Caution**
|
||||
>
|
||||
> 即使你只想在 web 应用中实现「通过 Apple 登录」,你仍需要拥有一个拥抱 AppStore 生态的应用(即:有一个有效的 App ID)。
|
||||
|
||||
你可以通过 Xcode -> Project settings -> Signing & Capabilities 来启用,或者访问 [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/bundleId)。
|
||||
|
||||

|
||||
|
||||
参见 [Apple 官方文档](https://developer.apple.com/documentation/sign_in_with_apple/configuring_your_environment_for_sign_in_with_apple) 里的「Enable an App ID」章节以了解更多。
|
||||
|
||||
### 创建一个 identifier
|
||||
|
||||
1. 访问 [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/serviceId),并点按在「Identifier」旁边的「+」按钮。
|
||||
2. 在「Register a new identifier」页面,选择「Services IDs」并点按「Continue」。
|
||||
3. 填写「Description」与「Identifier」(例如 `Logto Test` 和 `io.logto.test`),并点按「Continue」。
|
||||
4. 再次检查相关信息并点按「Register」。
|
||||
|
||||
### 为你的 identifier 启用「通过 Apple 登录」
|
||||
|
||||
点按你刚刚创建的 identifier。在详情页勾选「Sign in with Apple」并点按「Configure」。
|
||||
|
||||

|
||||
|
||||
在打开的对话框中,选择刚刚启用了「通过 Apple 登录」的 App ID。
|
||||
|
||||
输入你的 Logto 实例域名(不含协议和端口),例如 `your.logto.domain`;并输入「Return URL」(即 Redirect URI)。Return URL 的值是 Logto URL 加上 `/callback/apple-universal`,例如 `https://your.logto.domain/callback/apple-universal`。
|
||||
|
||||

|
||||
|
||||
点按「Next」以及「Done」以关闭对话框。点按右上角的「Continue」,接着点按「Save」以保存你的配置。
|
||||
|
||||
> ⚠️ **Caution**
|
||||
>
|
||||
> Apple _不允许_ HTTP 协议或 `localhost` 域名作为 Return URL。
|
||||
>
|
||||
> 如果你想在本地进行测试,你需要编辑 `/etc/hosts` 文件以映射 localhost 到一个自定义域名,并设置一个本地的 HTTPS 环境。[mkcert](https://github.com/FiloSottile/mkcert) 可以帮助你设置本地 HTTPS。
|
||||
|
||||
## 编写连接器的 JSON
|
||||
|
||||
你需要使用在 [创建一个 identifier](#创建一个-identifier) 章节中填写的 identifier 来编写此 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"clientId": "io.logto.test"
|
||||
}
|
||||
```
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> 本连接器暂时不支持自定义 `scope`(例如 name,email)。因为在 `scope` 非空时,Apple 要求 `response_mode` 为 `form_post`,与现在连接器的设计不兼容。
|
||||
>
|
||||
> 我们将稍后解决这个问题。
|
||||
|
||||
## 测试 Apple 连接器
|
||||
|
||||
大功告成。Apple 连接器应该在 web 和原生应用中都可用了。别忘了 [在登录体验中启用本连接器](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8)。
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"clientId": "<client-id>"
|
||||
}
|
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -1,7 +0,0 @@
|
|||
import { Config, merge } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = merge({
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error'],
|
||||
});
|
||||
|
||||
export default config;
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.9107 12.5628C16.9186 11.874 17.1013 11.1984 17.4415 10.5994C17.7817 10.0004 18.2684 9.49755 18.8559 9.13791C18.4824 8.60614 17.9908 8.16818 17.4196 7.85839C16.8484 7.54861 16.2131 7.37543 15.5637 7.35246C14.1618 7.21035 12.8283 8.1777 12.118 8.1777C11.4072 8.1777 10.3099 7.37333 9.14719 7.39552C8.38414 7.41674 7.6397 7.63582 6.98679 8.03129C6.33389 8.42677 5.79492 8.98507 5.4227 9.65151C3.83654 12.4057 5.01783 16.4858 6.56357 18.7206C7.31998 19.8131 8.22163 21.0436 9.40557 20.9988C10.5465 20.954 10.9771 20.2619 12.3546 20.2619C13.7325 20.2619 14.1201 20.9988 15.3253 20.9766C16.552 20.9539 17.3305 19.8615 18.0808 18.7649C18.6178 17.9764 19.0374 17.114 19.3265 16.2049C18.6123 15.8998 18.003 15.3925 17.5737 14.7453C17.1444 14.0981 16.9139 13.3394 16.9107 12.5628ZM14.6455 5.87397C15.3249 5.0772 15.6614 4.04404 15.5818 3C14.5561 3.09949 13.6074 3.5876 12.9302 4.36423C12.5923 4.74047 12.3336 5.18089 12.1696 5.65924C12.0055 6.13759 11.9394 6.64407 11.9753 7.1485C12.4884 7.15558 12.9962 7.04428 13.4593 6.82323C13.9224 6.60219 14.3283 6.27736 14.6455 5.87397Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.9107 12.5628C16.9186 11.874 17.1013 11.1984 17.4415 10.5994C17.7817 10.0004 18.2684 9.49755 18.8559 9.13791C18.4824 8.60614 17.9908 8.16818 17.4196 7.85839C16.8484 7.54861 16.2131 7.37543 15.5637 7.35246C14.1618 7.21035 12.8283 8.1777 12.118 8.1777C11.4072 8.1777 10.3099 7.37333 9.14719 7.39552C8.38414 7.41674 7.6397 7.63582 6.98679 8.03129C6.33389 8.42677 5.79492 8.98507 5.4227 9.65151C3.83654 12.4057 5.01783 16.4858 6.56357 18.7206C7.31998 19.8131 8.22163 21.0436 9.40557 20.9988C10.5465 20.954 10.9771 20.2619 12.3546 20.2619C13.7325 20.2619 14.1201 20.9988 15.3253 20.9766C16.552 20.9539 17.3305 19.8615 18.0808 18.7649C18.6178 17.9764 19.0374 17.114 19.3265 16.2049C18.6123 15.8998 18.003 15.3925 17.5737 14.7453C17.1444 14.0981 16.9139 13.3394 16.9107 12.5628ZM14.6455 5.87397C15.3249 5.0772 15.6614 4.04404 15.5818 3C14.5561 3.09949 13.6074 3.5876 12.9302 4.36423C12.5923 4.74047 12.3336 5.18089 12.1696 5.65924C12.0055 6.13759 11.9394 6.64407 11.9753 7.1485C12.4884 7.15558 12.9962 7.04428 13.4593 6.82323C13.9224 6.60219 14.3283 6.27736 14.6455 5.87397Z" fill="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1,59 +0,0 @@
|
|||
{
|
||||
"name": "@logto/connector-apple",
|
||||
"version": "1.0.0-beta.8",
|
||||
"description": "Apple web connector implementation.",
|
||||
"main": "./lib/index.js",
|
||||
"exports": "./lib/index.js",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"license": "MPL-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"docs",
|
||||
"logo.svg",
|
||||
"README.md",
|
||||
"logo-dark.svg"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "rm -rf lib/ && ncc build src/index.ts -o lib",
|
||||
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/connector-core": "^1.0.0-beta.8",
|
||||
"@silverhand/essentials": "^1.2.0",
|
||||
"@silverhand/jest-config": "1.0.0",
|
||||
"got": "^11.8.2",
|
||||
"jose": "^4.3.8",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^28.1.3",
|
||||
"@silverhand/eslint-config": "1.0.0",
|
||||
"@silverhand/ts-config": "1.0.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/node": "^16.3.1",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"eslint": "^8.21.0",
|
||||
"jest": "^28.1.3",
|
||||
"jest-matcher-specific-error": "^1.0.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"nock": "^13.2.2",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import { ConnectorMetadata, ConnectorPlatform } from '@logto/connector-core';
|
||||
|
||||
// https://appleid.apple.com/.well-known/openid-configuration
|
||||
export const issuer = 'https://appleid.apple.com';
|
||||
export const authorizationEndpoint = `${issuer}/auth/authorize`;
|
||||
export const accessTokenEndpoint = `${issuer}/auth/token`;
|
||||
export const jwksUri = `${issuer}/auth/keys`;
|
||||
|
||||
// Note: only support fixed scope for v1.
|
||||
export const scope = ''; // Note: `openid` is required when adding more scope(s)
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'apple-universal',
|
||||
target: 'apple',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
name: {
|
||||
en: 'Apple',
|
||||
'zh-CN': 'Apple',
|
||||
'tr-TR': 'Apple',
|
||||
'ko-KR': 'Apple',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: './logo-dark.svg',
|
||||
description: {
|
||||
en: 'Apple is a multinational high-end provider of hardware and software.',
|
||||
'zh-CN': 'Apple 是全球领先的高端消费者软硬件提供商。',
|
||||
'tr-TR': 'Apple, çok uluslu bir üst düzey donanım ve yazılım sağlayıcısıdır.',
|
||||
'ko-KR': 'Apple은 하드웨어와 소프트웨어의 다국적 공급자 입니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
configTemplate: './docs/config-template.json',
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
|
@ -1,74 +0,0 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-core';
|
||||
import { jwtVerify } from 'jose';
|
||||
|
||||
import createConnector from '.';
|
||||
import { authorizationEndpoint } from './constant';
|
||||
import { mockedConfig } from './mock';
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConfig);
|
||||
|
||||
jest.mock('jose', () => ({
|
||||
jwtVerify: jest.fn(),
|
||||
createRemoteJWKSet: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getAuthorizationUri', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get a valid uri by redirectUri and state', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
const authorizationUri = await connector.getAuthorizationUri({
|
||||
state: 'some_state',
|
||||
redirectUri: 'http://localhost:3000/callback',
|
||||
});
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=&state=some_state&response_type=code+id_token&response_mode=fragment`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get user info from id token payload', async () => {
|
||||
const userId = 'userId';
|
||||
const mockJwtVerify = jwtVerify as jest.Mock;
|
||||
mockJwtVerify.mockImplementationOnce(() => ({ payload: { sub: userId } }));
|
||||
const connector = await createConnector({ getConfig });
|
||||
const userInfo = await connector.getUserInfo({ id_token: 'idToken' });
|
||||
expect(userInfo).toEqual({ id: userId });
|
||||
});
|
||||
|
||||
it('should throw if id token is missing', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({})).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, '{}')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if verify id token failed', async () => {
|
||||
const mockJwtVerify = jwtVerify as jest.Mock;
|
||||
mockJwtVerify.mockImplementationOnce(() => {
|
||||
throw new Error('jwtVerify failed');
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ id_token: 'id_token' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the id token payload does not contains sub', async () => {
|
||||
const mockJwtVerify = jwtVerify as jest.Mock;
|
||||
mockJwtVerify.mockImplementationOnce(() => ({
|
||||
payload: { iat: 123_456 },
|
||||
}));
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ id_token: 'id_token' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,91 +0,0 @@
|
|||
import {
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
GetConnectorConfig,
|
||||
validateConfig,
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
ConnectorType,
|
||||
} from '@logto/connector-core';
|
||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
|
||||
import { scope, defaultMetadata, jwksUri, issuer, authorizationEndpoint } from './constant';
|
||||
import { appleConfigGuard, AppleConfig, dataGuard } from './types';
|
||||
|
||||
// TO-DO: support nonce validation
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state, redirectUri }) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
|
||||
validateConfig<AppleConfig>(config, appleConfigGuard);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope,
|
||||
state,
|
||||
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113
|
||||
response_type: 'code id_token',
|
||||
response_mode: 'fragment',
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data) => {
|
||||
const { id_token: idToken } = await authorizationCallbackHandler(data);
|
||||
|
||||
if (!idToken) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||
}
|
||||
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AppleConfig>(config, appleConfigGuard);
|
||||
|
||||
const { clientId } = config;
|
||||
|
||||
try {
|
||||
const { payload } = await jwtVerify(idToken, createRemoteJWKSet(new URL(jwksUri)), {
|
||||
issuer,
|
||||
audience: clientId,
|
||||
});
|
||||
|
||||
if (!payload.sub) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||
}
|
||||
|
||||
return {
|
||||
id: payload.sub,
|
||||
};
|
||||
} catch {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const result = dataGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAppleConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: appleConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAppleConnector;
|
|
@ -1,4 +0,0 @@
|
|||
export const mockedConfig = {
|
||||
clientId: '<client-id>',
|
||||
clientSecret: '<client-secret>',
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const appleConfigGuard = z.object({
|
||||
clientId: z.string(),
|
||||
});
|
||||
|
||||
export type AppleConfig = z.infer<typeof appleConfigGuard>;
|
||||
|
||||
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
|
||||
export const dataGuard = z.object({
|
||||
id_token: z.string(),
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest", "jest-matcher-specific-error"]
|
||||
},
|
||||
"include": ["src", "jest.config.ts"]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **connector:** azure active directory connector added ([#1662](https://github.com/logto-io/logto/issues/1662)) ([875a828](https://github.com/logto-io/logto/commit/875a82883161b79b11873bcfce2856e7b84502b4))
|
||||
* **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
|
@ -1,52 +0,0 @@
|
|||
# Azure AD connector
|
||||
|
||||
The Azure AD connector provides a succinct way for your application to use Azure’s OAuth 2.0 authentication system.
|
||||
|
||||
**Table of contents**
|
||||
- [Azure AD connector](#azure-ad-connector)
|
||||
- [Set up Azure AD in the Azure Portal](#set-up-azure-ad-in-the-azure-portal)
|
||||
- [Configure your client secret](#configure-your-client-secret)
|
||||
- [Compose the connector JSON](#compose-the-connector-json)
|
||||
- [Config types](#config-types)
|
||||
- [References](#references)
|
||||
|
||||
## Set up Azure AD in the Azure Portal
|
||||
|
||||
- Visit the [Azure Portal](https://portal.azure.com/#home) and sign in with your Azure account. You need to have an active subscription to access Azure AD.
|
||||
- Click the **Azure Active Directory** from the services they offer, and click the **App Registrations** from the left menu.
|
||||
- Click **New Registration** at the top and enter a description, select your **access type** and add your **Redirect URI**, which redirect the user to the application after logging in. In our case, this will be `${your_logto_origin}/callback/azuread-universal`. e.g. `https://logto.dev/callback/azuread-universal`. You need to select Web as Platform.
|
||||
- If you select **Single Tenant** for access type then you need to enter **TenantID**, else you need to enter `common` as Tenant ID.
|
||||
|
||||
## Configure your client secret
|
||||
- In your newly created project, click the **Certificates & Secrets** to get a client secret, and click the **New client secret** from the top.
|
||||
- Enter a description and an expiration.
|
||||
- This will only show your client secret once. Save the **value** to a secure location.
|
||||
|
||||
## Compose the connector JSON
|
||||
- Add your App Registration's **Client ID** into logto json.
|
||||
- Add your **Client Secret** into logto json.
|
||||
- Add your App Registration's **Tenant ID** into logto json.
|
||||
- Add your Microsoft **Login Url** into logto json. This defaults to "https://login.microsoftonline.com/" for many applications, but you can set your custom domain if you have one. (Don't forget the trailing slash)
|
||||
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"clientId": "<client-id>",
|
||||
"clientSecret": "<client-secret>",
|
||||
"tenantId": "<tenant-id>", // use "common" if you did't select **Single Tenant**
|
||||
"cloudInstance": "https://login.microsoftonline.com/"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
| ------------- | ------ |
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
| tenantId | string |
|
||||
| cloudInstance | string |
|
||||
|
||||
## References
|
||||
* [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=nodejs)
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"clientId": "<client-id>",
|
||||
"clientSecret": "<client-secret>",
|
||||
"tenantId": "<tenant-id>",
|
||||
"cloudInstance": "https://login.microsoftonline.com/"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { Config, merge } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = merge({
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error'],
|
||||
});
|
||||
|
||||
export default config;
|
|
@ -1,12 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
|
||||
<defs>
|
||||
<linearGradient id="a" x1="359" y1="184" x2="747" y2="786" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#54aef0"/>
|
||||
<stop offset="1" stop-color="#3499e4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="#53b1e0" d="M482,422L482,602,512,793.3,907.7,538.9,752,452,482,422Z"/>
|
||||
<path fill="url(#a)" d="M992,637.6,512,948.9,482,902l30-52.5L938.7,575.2ZM512,75.2,482,272l30,174.4,395.7,92.5Z"/>
|
||||
<path fill="#9cebff" d="M512,446.4L272,452,116.3,538.9,512,793.3,512,446.4Z"/>
|
||||
<path fill="#50e6ff" d="M85.3,575.2,512,849.5v99.4L32,637.6Zm31-36.3L512,446.4V75.2Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 676 B |
|
@ -1,59 +0,0 @@
|
|||
{
|
||||
"name": "@logto/connector-azuread",
|
||||
"version": "1.0.0-beta.8",
|
||||
"description": "Azure AD connector implementation.",
|
||||
"main": "./lib/index.js",
|
||||
"exports": "./lib/index.js",
|
||||
"author": "Mobilist Inc. <info@mobilist.com.tr>",
|
||||
"license": "MPL-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"docs",
|
||||
"logo.svg",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "rm -rf lib/ && ncc build src/index.ts -o lib",
|
||||
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^1.12.0",
|
||||
"@logto/connector-core": "^1.0.0-beta.8",
|
||||
"@silverhand/essentials": "^1.2.0",
|
||||
"@silverhand/jest-config": "1.0.0",
|
||||
"axios": "^0.27.2",
|
||||
"got": "^11.8.2",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^28.1.3",
|
||||
"@silverhand/eslint-config": "1.0.0",
|
||||
"@silverhand/ts-config": "1.0.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/node": "^16.3.1",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"eslint": "^8.21.0",
|
||||
"jest": "^28.1.3",
|
||||
"jest-matcher-specific-error": "^1.0.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"nock": "^13.2.2",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { ConnectorMetadata, ConnectorPlatform } from '@logto/connector-core';
|
||||
|
||||
export const graphAPIEndpoint = 'https://graph.microsoft.com/v1.0/me';
|
||||
export const scopes = ['User.Read'];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'azuread-universal',
|
||||
target: 'azuread',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
name: {
|
||||
en: 'Azure Active Directory',
|
||||
'zh-CN': 'Azure Active Directory',
|
||||
'tr-TR': 'Azure Active Directory',
|
||||
'ko-KR': 'Azure Active Directory',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Azure Active Directory is the biggest AD provider.',
|
||||
'zh-CN': 'Azure Active Directory is the biggest AD provider.',
|
||||
'tr-TR': 'Azure Active Directory en büyük AD servisidir.',
|
||||
'ko-KR': 'Azure Active Directory is the biggest AD provider.',
|
||||
},
|
||||
readme: './README.md',
|
||||
configTemplate: './docs/config-template.json',
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|