Skip to content

feat(connector): add Aliyun SMS authentication service#8385

Open
CertStone wants to merge 6 commits intologto-io:masterfrom
CertStone:feat/aliyun-sms-compatibility
Open

feat(connector): add Aliyun SMS authentication service#8385
CertStone wants to merge 6 commits intologto-io:masterfrom
CertStone:feat/aliyun-sms-compatibility

Conversation

@CertStone
Copy link
Copy Markdown

Summary

This PR introduces a brand new SMS connector (connector-aliyun-sms-mas) for Aliyun's SMS Authentication Service (号码认证服务 - 短信认证), specifically utilizing the SendSmsVerifyCode (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

  • Unit Tests: Added unit tests for the new connector, including testing parameter validation, payload construction, and mocking the SendSmsVerifyCode API responses to ensure the logic behaves as expected under different scenarios.
  • Manual Testing: Configured the newly created connector locally and successfully sent test SMS messages using valid Aliyun credentials and template configurations.

Checklist

  • .changeset
  • unit tests
  • integration tests
  • necessary TSDoc comments

- 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
Copilot AI review requested due to automatic review settings February 25, 2026 06:28
@github-actions github-actions Bot added the feature Cool stuff label Feb 25, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 25, 2026

COMPARE TO master

Total Size Diff ⚠️ 📈 +44.78 KB

Diff by File
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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-mas package 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.

Comment on lines +1 to +102
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 },
});
};
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +50
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' },
];
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
"@silverhand/essentials",
"got",
"nock",
"snakecase-keys",
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"snakecase-keys",

Copilot uses AI. Check for mistakes.
@github-actions github-actions Bot added size/xl and removed size/xl labels Mar 1, 2026
@darcyYe
Copy link
Copy Markdown
Contributor

darcyYe commented Apr 2, 2026

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 SendSmsVerifyCode docs, TemplateParam supports two modes: passing a placeholder so Aliyun generates and verifies the code, or passing a concrete code value directly. The docs also make it clear that once a concrete code is passed, Aliyun can no longer verify it for you. This implementation always sends a Logto-generated code and never integrates the MAS-managed verification path or its related capabilities such as verification lifecycle management.

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.

@darcyYe
Copy link
Copy Markdown
Contributor

darcyYe commented Apr 2, 2026

I’m concerned about exposing strictPhoneRegionNumberCheck as an optional switch for this connector.

According to the Aliyun MAS docs, this API is for mainland China numbers only, and the request is sent with CountryCode: 86. In the current implementation, disabling this switch skips local validation but still sends the request as a mainland-China SMS request, which means unsupported or malformed numbers can still reach the provider.

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 +86. Also, instead of maintaining a connector-specific normalization rule here, it would be better to reuse the existing phone-number handling pattern we already use elsewhere in the codebase based on libphonenumber-js, so parsing and validation stay consistent across connectors and sign-in flows.

@darcyYe
Copy link
Copy Markdown
Contributor

darcyYe commented Apr 2, 2026

The error mapping looks incomplete compared with the official API docs.

In the Aliyun SendSmsVerifyCode documentation, both BUSINESS_LIMIT_CONTROL and FREQUENCY_FAIL are frequency-control style failures. Right now the connector maps only BUSINESS_LIMIT_CONTROL to RateLimitExceeded, while FREQUENCY_FAIL falls back to a generic error.

I think FREQUENCY_FAIL should also be surfaced as RateLimitExceeded (or another retryable / rate-limited classification), so downstream behavior stays aligned with the provider semantics and callers get a more actionable error.

@CertStone
Copy link
Copy Markdown
Author

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.

@darcyYe
Copy link
Copy Markdown
Contributor

darcyYe commented Apr 9, 2026

No problem, take your time. Once you've made the changes, please let us know, and we'll proceed with the review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

feature request: Add a New Request Method for Aliyun SMS

3 participants