feat(connector): add Aliyun SMS authentication service#8385
feat(connector): add Aliyun SMS authentication service#8385CertStone wants to merge 6 commits intologto-io:masterfrom
Conversation
- Implement SendSmsVerifyCode API for Aliyun DYPNS - Pass generated code via TemplateParam to support Logto's native validation - Add standard connector test cases Fixes logto-io#7952
COMPARE TO
|
| Name | Diff |
|---|---|
| .changeset/tough-apes-obey.md | 📈 +98 Bytes |
| packages/connectors/connector-aliyun-sms-mas/README.md | 📈 +9.91 KB |
| packages/connectors/connector-aliyun-sms-mas/logo.svg | 📈 +1.7 KB |
| packages/connectors/connector-aliyun-sms-mas/package.json | 📈 +1.59 KB |
| packages/connectors/connector-aliyun-sms-mas/src/constant.ts | 📈 +4.66 KB |
| packages/connectors/connector-aliyun-sms-mas/src/index.test.ts | 📈 +6.07 KB |
| packages/connectors/connector-aliyun-sms-mas/src/index.ts | 📈 +4.84 KB |
| packages/connectors/connector-aliyun-sms-mas/src/mock.ts | 📈 +1.19 KB |
| packages/connectors/connector-aliyun-sms-mas/src/send-verify-code.ts | 📈 +1.07 KB |
| packages/connectors/connector-aliyun-sms-mas/src/types.ts | 📈 +2.8 KB |
| packages/connectors/connector-aliyun-sms-mas/src/utils.test.ts | 📈 +4.65 KB |
| packages/connectors/connector-aliyun-sms-mas/src/utils.ts | 📈 +4.54 KB |
| pnpm-lock.yaml | 📈 +1.66 KB |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new SMS connector for Aliyun's Message Authentication Service (MAS), which is a specialized SMS service for verification codes. Unlike the existing connector-aliyun-sms, this service uses system-provided signatures and templates, requires no enterprise qualification, and only supports China mainland mobile numbers.
Changes:
- Added new
@logto/connector-aliyun-sms-maspackage with full implementation including types, utilities, API integration, and tests - Implemented HMAC-SHA1 signature generation following Aliyun's authentication requirements
- Added comprehensive unit tests for the connector with error handling scenarios
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Added dependency entries for the new connector package |
| packages/connectors/connector-aliyun-sms-mas/package.json | Package configuration with dependencies and build scripts |
| packages/connectors/connector-aliyun-sms-mas/src/types.ts | Type definitions for API requests/responses and configuration validation |
| packages/connectors/connector-aliyun-sms-mas/src/utils.ts | Utility functions for URL encoding, date formatting, signature generation, and HTTP requests |
| packages/connectors/connector-aliyun-sms-mas/src/send-verify-code.ts | Wrapper function for SendSmsVerifyCode API call |
| packages/connectors/connector-aliyun-sms-mas/src/constant.ts | Connector metadata, system signatures, and form configuration |
| packages/connectors/connector-aliyun-sms-mas/src/index.ts | Main connector implementation with message sending and error handling |
| packages/connectors/connector-aliyun-sms-mas/src/index.test.ts | Comprehensive unit tests for the connector functionality |
| packages/connectors/connector-aliyun-sms-mas/src/mock.ts | Mock data for testing |
| packages/connectors/connector-aliyun-sms-mas/README.md | Bilingual documentation (English and Chinese) with setup instructions |
| packages/connectors/connector-aliyun-sms-mas/logo.svg | Aliyun logo SVG |
| .changeset/tough-apes-obey.md | Changeset file marking this as a minor version addition |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { type Optional } from '@silverhand/essentials'; | ||
| import { got } from 'got'; | ||
| import { createHmac, randomUUID } from 'node:crypto'; | ||
|
|
||
| import type { PublicParameters } from './types.js'; | ||
|
|
||
| /** | ||
| * Aliyun has special escape rules for URL encoding | ||
| * @see https://help.aliyun.com/document_detail/29442.html | ||
| * Special characters that need to be encoded differently from standard encodeURIComponent | ||
| */ | ||
| const escaper = (string_: string) => | ||
| encodeURIComponent(string_) | ||
| .replaceAll('!', '%21') | ||
| .replaceAll('"', '%22') | ||
| .replaceAll("'", '%27') | ||
| .replaceAll('(', '%28') | ||
| .replaceAll(')', '%29') | ||
| .replaceAll('*', '%2A') | ||
| .replaceAll('+', '%2B'); | ||
|
|
||
| /** | ||
| * Format date string to 'YYYY-MM-DDThh:mm:ssZ' format | ||
| * Trims milliseconds from ISO string to match Aliyun's expected format | ||
| */ | ||
| const formatDateString = (date: Date) => { | ||
| const rawString = date.toISOString(); | ||
| return rawString.replace(/\.\d{3}Z$/, 'Z'); // Trim milliseconds | ||
| }; | ||
|
|
||
| /** | ||
| * Generate HMAC-SHA1 signature for Aliyun API requests | ||
| * @see https://help.aliyun.com/document_detail/29442.html | ||
| * | ||
| * The signature is calculated using: | ||
| * 1. HTTP method (uppercase) | ||
| * 2. URL-encoded path ("/") | ||
| * 3. Canonicalized query string (sorted, URL-encoded parameters) | ||
| * | ||
| * Format: BASE64(HMAC-SHA1(HTTP_METHOD + "&" + encode("/") + "&" + encode(CanonicalizedQueryString))) | ||
| */ | ||
| export const getSignature = ( | ||
| parameters: Record<string, string>, | ||
| secret: string, | ||
| method: string | ||
| ) => { | ||
| // Sort parameters by key and construct canonicalized query string | ||
| const canonicalizedQuery = Object.entries(parameters) | ||
| .map(([key, value]) => { | ||
| return `${escaper(key)}=${escaper(value)}`; | ||
| }) | ||
| .slice() | ||
| .sort() | ||
| .join('&'); | ||
|
|
||
| // Construct string to sign | ||
| const stringToSign = `${method.toUpperCase()}&${escaper('/')}&${escaper(canonicalizedQuery)}`; | ||
|
|
||
| // Calculate HMAC-SHA1 signature | ||
| return createHmac('sha1', `${secret}&`).update(stringToSign).digest('base64'); | ||
| }; | ||
|
|
||
| /** | ||
| * Make signed HTTP POST request to Aliyun API | ||
| * | ||
| * This function: | ||
| * 1. Adds required signature parameters (SignatureNonce, Timestamp) | ||
| * 2. Generates HMAC-SHA1 signature | ||
| * 3. Makes the HTTP POST request | ||
| * | ||
| * @param url - API endpoint URL | ||
| * @param parameters - Request parameters including AccessKeyId and API-specific params | ||
| * @param accessKeySecret - Aliyun Access Key Secret for signing | ||
| */ | ||
| export const request = async ( | ||
| url: string, | ||
| parameters: PublicParameters & Record<string, string | number>, | ||
| accessKeySecret: string | ||
| ) => { | ||
| // Prepare final parameters with signature-related fields | ||
| // Convert all values to strings for signature calculation | ||
| const finalParameters = Object.entries<Optional<string | number>>({ | ||
| ...parameters, | ||
| SignatureNonce: randomUUID(), | ||
| Timestamp: formatDateString(new Date()), | ||
| }).reduce<Record<string, string>>( | ||
| (result, [key, value]) => (value === undefined ? result : { ...result, [key]: String(value) }), | ||
| {} | ||
| ); | ||
|
|
||
| // Generate signature | ||
| const signature = getSignature(finalParameters, accessKeySecret, 'POST'); | ||
|
|
||
| // Make the HTTP POST request | ||
| return got.post({ | ||
| url, | ||
| headers: { | ||
| 'Content-Type': 'application/x-www-form-urlencoded', | ||
| }, | ||
| form: { ...finalParameters, Signature: signature }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
The utility functions getSignature and request in utils.ts lack unit tests. The existing connector-aliyun-sms has comprehensive tests for these critical functions in utils.test.ts. Consider adding similar test coverage for this connector to ensure signature generation and HTTP request construction work correctly, especially since signature generation is security-critical.
| export const systemProvidedTemplates = [ | ||
| { code: '100001', usageType: 'SignIn', description: 'Login/Register' }, | ||
| { code: '100001', usageType: 'Register', description: 'Login/Register' }, | ||
| { code: '100003', usageType: 'ForgotPassword', description: 'Reset Password' }, | ||
| { code: '100002', usageType: 'BindNewIdentifier', description: 'Change Phone Number' }, | ||
| { code: '100004', usageType: 'BindNewIdentifier', description: 'Bind Phone Number' }, | ||
| { code: '100005', usageType: 'UserPermissionValidation', description: 'Verify Phone Number' }, | ||
| { code: '100001', usageType: 'Generic', description: 'General Verification' }, | ||
| ]; |
There was a problem hiding this comment.
The systemProvidedTemplates constant is exported but never imported or used in the codebase. Consider either using it for validation/documentation purposes, or removing it if it's not needed. If it's intended for future use or documentation, consider adding a comment explaining its purpose.
| "@silverhand/essentials", | ||
| "got", | ||
| "nock", | ||
| "snakecase-keys", |
There was a problem hiding this comment.
The eslintConfig includes "snakecase-keys" in the import/core-modules setting, but this package is not imported or used anywhere in the connector code. This appears to be copied from the aliyun-sms connector configuration. Consider removing it from the core-modules list to keep the configuration clean and accurate.
| "snakecase-keys", |
…lization and validation
|
Thanks for putting this together. I think the current implementation should document much more explicitly that this connector is using MAS as a delivery channel only, not as the native MAS verification flow. According to Aliyun’s That approach can be a reasonable tradeoff if the goal is to preserve Logto’s existing verification model, but it is a meaningful behavioral difference from what users would normally expect from “Message Authentication Service”. I’d suggest calling this out prominently in the README and connector description, so users understand that this connector intentionally uses MAS as a transport layer only and does not provide MAS-managed verification semantics. |
|
I’m concerned about exposing According to the Aliyun MAS docs, this API is for mainland China numbers only, and the request is sent with I think this connector should fail fast before sending any request unless the input can be parsed as a mainland China phone number with country code |
|
The error mapping looks incomplete compared with the official API docs. In the Aliyun I think |
|
Thanks for the review and the detailed feedback! Just a quick heads-up: I'll be quite busy with some other commitments for the next few weeks, so I won't be able to update the code immediately. I will definitely address these changes as soon as I have time again. |
|
No problem, take your time. Once you've made the changes, please let us know, and we'll proceed with the review. |
Summary
This PR introduces a brand new SMS connector (
connector-aliyun-sms-mas) for Aliyun's SMS Authentication Service (号码认证服务 - 短信认证), specifically utilizing theSendSmsVerifyCode(DYPNS) API.Background & Implementation:
As discussed in issue #7952, the new Aliyun SMS authentication workflow expects Aliyun to generate and validate the codes. Modifying Logto's core verification workflow for this single provider would be intrusive.
To resolve this without altering Logto's core logic, this connector takes advantage of a flexibility in Aliyun's API. By passing the Logto-generated verification code directly into the
TemplateParam(e.g.,{"code":"123456"}), Aliyun acts purely as a delivery channel. This allows Logto to seamlessly maintain its native "generate and validate" architecture while fully supporting the new Aliyun service.Fixes #7952
Testing
SendSmsVerifyCodeAPI responses to ensure the logic behaves as expected under different scenarios.Checklist
.changeset