mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(connector): azure active directory connector added (#1662)
* feat(connector): azure active directory connector added * refactor(connector): apply code review suggestions * refactor: removed PKCE * chore: update package and lockfile * refactor(connector): fix typo * refactor(connector): polish code Co-authored-by: Gao Sun <gao@silverhand.io>
This commit is contained in:
parent
f7bc349e03
commit
875a828831
17 changed files with 655 additions and 261 deletions
52
packages/connector-azuread/README.md
Normal file
52
packages/connector-azuread/README.md
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# 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)
|
6
packages/connector-azuread/docs/config-template.json
Normal file
6
packages/connector-azuread/docs/config-template.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"clientId": "<client-id>",
|
||||||
|
"clientSecret": "<client-secret>",
|
||||||
|
"tenantId": "<tenant-id>",
|
||||||
|
"cloudInstance": "https://login.microsoftonline.com/"
|
||||||
|
}
|
8
packages/connector-azuread/jest.config.ts
Normal file
8
packages/connector-azuread/jest.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { Config, merge } from '@silverhand/jest-config';
|
||||||
|
|
||||||
|
const config: Config.InitialOptions = merge({
|
||||||
|
testEnvironment: 'node',
|
||||||
|
setupFilesAfterEnv: ['jest-matcher-specific-error'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default config;
|
12
packages/connector-azuread/logo.svg
Normal file
12
packages/connector-azuread/logo.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<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>
|
After (image error) Size: 676 B |
60
packages/connector-azuread/package.json
Normal file
60
packages/connector-azuread/package.json
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"name": "@logto/connector-azuread",
|
||||||
|
"version": "1.0.0-beta.2",
|
||||||
|
"description": "Azure AD connector implementation.",
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"exports": "./lib/index.js",
|
||||||
|
"author": "Mobilist Inc. <info@mobilist.com.tr>",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"private": true,
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"docs",
|
||||||
|
"logo.svg",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"precommit": "lint-staged",
|
||||||
|
"build": "rm -rf lib/ && tsc -p tsconfig.build.json",
|
||||||
|
"lint": "eslint --ext .ts src",
|
||||||
|
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||||
|
"dev": "rm -rf lib/ && tsc-watch -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ./lib/index.js\"",
|
||||||
|
"test": "jest",
|
||||||
|
"test:coverage": "jest --coverage --silent",
|
||||||
|
"prepack": "pnpm build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/msal-node": "^1.12.0",
|
||||||
|
"@logto/connector-types": "^1.0.0-beta.2",
|
||||||
|
"@logto/schemas": "^1.0.0-beta.2",
|
||||||
|
"@logto/shared": "^1.0.0-beta.1",
|
||||||
|
"@silverhand/essentials": "^1.1.0",
|
||||||
|
"@silverhand/jest-config": "^0.17.0",
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"got": "^11.8.2",
|
||||||
|
"zod": "^3.14.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@jest/types": "^27.5.1",
|
||||||
|
"@silverhand/eslint-config": "^0.17.0",
|
||||||
|
"@silverhand/ts-config": "^0.17.0",
|
||||||
|
"@types/jest": "^27.4.1",
|
||||||
|
"@types/node": "^16.3.1",
|
||||||
|
"eslint": "^8.19.0",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"jest-matcher-specific-error": "^1.0.0",
|
||||||
|
"lint-staged": "^13.0.0",
|
||||||
|
"nock": "^13.2.2",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"ts-jest": "^27.1.1",
|
||||||
|
"tsc-watch": "^5.0.0",
|
||||||
|
"typescript": "^4.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "@silverhand"
|
||||||
|
},
|
||||||
|
"prettier": "@silverhand/eslint-config/.prettierrc"
|
||||||
|
}
|
25
packages/connector-azuread/src/constant.ts
Normal file
25
packages/connector-azuread/src/constant.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { ConnectorMetadata, ConnectorType, ConnectorPlatform } from '@logto/connector-types';
|
||||||
|
|
||||||
|
export const graphAPIEndpoint = 'https://graph.microsoft.com/v1.0/me';
|
||||||
|
export const scopes = ['User.Read'];
|
||||||
|
|
||||||
|
export const defaultMetadata: ConnectorMetadata = {
|
||||||
|
id: 'azuread-universal',
|
||||||
|
target: 'azuread',
|
||||||
|
type: ConnectorType.Social,
|
||||||
|
platform: ConnectorPlatform.Universal,
|
||||||
|
name: {
|
||||||
|
en: 'Azure Active Directory',
|
||||||
|
'zh-CN': '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.',
|
||||||
|
},
|
||||||
|
readme: './README.md',
|
||||||
|
configTemplate: './docs/config-template.json',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTimeout = 5000;
|
11
packages/connector-azuread/src/index.test.ts
Normal file
11
packages/connector-azuread/src/index.test.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { GetConnectorConfig } from '@logto/connector-types';
|
||||||
|
|
||||||
|
import AzureADConnector from '.';
|
||||||
|
|
||||||
|
const getConnectorConfig = jest.fn() as GetConnectorConfig;
|
||||||
|
|
||||||
|
describe('Azure AD connector', () => {
|
||||||
|
it('init without exploding', () => {
|
||||||
|
expect(() => new AzureADConnector(getConnectorConfig)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
169
packages/connector-azuread/src/index.ts
Normal file
169
packages/connector-azuread/src/index.ts
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConfidentialClientApplication,
|
||||||
|
AuthorizationCodeRequest,
|
||||||
|
AuthorizationUrlRequest,
|
||||||
|
CryptoProvider,
|
||||||
|
} from '@azure/msal-node';
|
||||||
|
import {
|
||||||
|
ConnectorError,
|
||||||
|
ConnectorErrorCodes,
|
||||||
|
GetAuthorizationUri,
|
||||||
|
GetUserInfo,
|
||||||
|
ConnectorMetadata,
|
||||||
|
Connector,
|
||||||
|
SocialConnectorInstance,
|
||||||
|
GetConnectorConfig,
|
||||||
|
codeWithRedirectDataGuard,
|
||||||
|
} from '@logto/connector-types';
|
||||||
|
import { assert, conditional } from '@silverhand/essentials';
|
||||||
|
import got, { HTTPError } from 'got';
|
||||||
|
|
||||||
|
import { scopes, defaultMetadata, defaultTimeout, graphAPIEndpoint } from './constant';
|
||||||
|
import {
|
||||||
|
azureADConfigGuard,
|
||||||
|
AzureADConfig,
|
||||||
|
accessTokenResponseGuard,
|
||||||
|
userInfoResponseGuard,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export default class AzureADConnector implements SocialConnectorInstance<AzureADConfig> {
|
||||||
|
public metadata: ConnectorMetadata = defaultMetadata;
|
||||||
|
|
||||||
|
public clientApplication!: ConfidentialClientApplication;
|
||||||
|
public authCodeUrlParams!: AuthorizationUrlRequest;
|
||||||
|
|
||||||
|
cryptoProvider = new CryptoProvider();
|
||||||
|
private readonly authCodeRequest!: AuthorizationCodeRequest;
|
||||||
|
|
||||||
|
private _connector?: Connector;
|
||||||
|
|
||||||
|
public get connector() {
|
||||||
|
if (!this._connector) {
|
||||||
|
throw new ConnectorError(ConnectorErrorCodes.General);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._connector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set connector(input: Connector) {
|
||||||
|
this._connector = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(public readonly getConfig: GetConnectorConfig) {}
|
||||||
|
|
||||||
|
public validateConfig(config: unknown): asserts config is AzureADConfig {
|
||||||
|
const result = azureADConfigGuard.safeParse(config);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
|
||||||
|
const config = await this.getConfig(this.metadata.id);
|
||||||
|
|
||||||
|
this.validateConfig(config);
|
||||||
|
const { clientId, clientSecret, cloudInstance, tenantId } = config;
|
||||||
|
|
||||||
|
this.authCodeUrlParams = {
|
||||||
|
scopes,
|
||||||
|
state,
|
||||||
|
redirectUri,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clientApplication = new ConfidentialClientApplication({
|
||||||
|
auth: {
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
authority: new URL(path.join(cloudInstance, tenantId)).toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authCodeUrlParameters = {
|
||||||
|
...this.authCodeUrlParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
const authCodeUrl = await this.clientApplication.getAuthCodeUrl(authCodeUrlParameters);
|
||||||
|
|
||||||
|
return authCodeUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getAccessToken = async (code: string, redirectUri: string) => {
|
||||||
|
const codeRequest = {
|
||||||
|
...this.authCodeRequest,
|
||||||
|
redirectUri,
|
||||||
|
scopes: ['User.Read'],
|
||||||
|
code,
|
||||||
|
};
|
||||||
|
|
||||||
|
const authResult = await this.clientApplication.acquireTokenByCode(codeRequest);
|
||||||
|
|
||||||
|
const result = accessTokenResponseGuard.safeParse(authResult);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken } = result.data;
|
||||||
|
|
||||||
|
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||||
|
|
||||||
|
return { accessToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
public getUserInfo: GetUserInfo = async (data) => {
|
||||||
|
const { code, redirectUri } = await this.authorizationCallbackHandler(data);
|
||||||
|
const { accessToken } = await this.getAccessToken(code, redirectUri);
|
||||||
|
|
||||||
|
const config = await this.getConfig(this.metadata.id);
|
||||||
|
|
||||||
|
this.validateConfig(config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const httpResponse = await got.get(graphAPIEndpoint, {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = userInfoResponseGuard.safeParse(JSON.parse(httpResponse.body));
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, mail, displayName } = result.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
email: conditional(mail),
|
||||||
|
name: conditional(displayName),
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof HTTPError) {
|
||||||
|
const { statusCode, body: rawBody } = error.response;
|
||||||
|
|
||||||
|
if (statusCode === 401) {
|
||||||
|
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||||
|
const result = codeWithRedirectDataGuard.safeParse(parameterObject);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
};
|
||||||
|
}
|
34
packages/connector-azuread/src/types.ts
Normal file
34
packages/connector-azuread/src/types.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const azureADConfigGuard = z.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string(),
|
||||||
|
cloudInstance: z.string(),
|
||||||
|
tenantId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AzureADConfig = z.infer<typeof azureADConfigGuard>;
|
||||||
|
|
||||||
|
export const accessTokenResponseGuard = z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
scopes: z.array(z.string()),
|
||||||
|
tokenType: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AccessTokenResponse = z.infer<typeof accessTokenResponseGuard>;
|
||||||
|
|
||||||
|
export const userInfoResponseGuard = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
displayName: z.string().nullish(),
|
||||||
|
givenName: z.string().nullish(),
|
||||||
|
surname: z.string().nullish(),
|
||||||
|
userPrincipalName: z.string().nullish(),
|
||||||
|
jobTitle: z.string().nullish(),
|
||||||
|
mail: z.string().nullish(),
|
||||||
|
mobilePhone: z.string().nullish(),
|
||||||
|
officeLocation: z.boolean().nullish(),
|
||||||
|
preferredLanguage: z.string().nullish(),
|
||||||
|
businessPhones: z.array(z.string()).nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
10
packages/connector-azuread/tsconfig.base.json
Normal file
10
packages/connector-azuread/tsconfig.base.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "lib",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
packages/connector-azuread/tsconfig.build.json
Normal file
5
packages/connector-azuread/tsconfig.build.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.base",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.test.ts"]
|
||||||
|
}
|
7
packages/connector-azuread/tsconfig.json
Normal file
7
packages/connector-azuread/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node", "jest", "jest-matcher-specific-error"]
|
||||||
|
},
|
||||||
|
"include": ["src", "jest.config.ts"]
|
||||||
|
}
|
7
packages/connector-azuread/tsconfig.test.json
Normal file
7
packages/connector-azuread/tsconfig.test.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"isolatedModules": false,
|
||||||
|
"allowJs": true,
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@
|
||||||
"@logto/connector-aliyun-dm": "^1.0.0-beta.2",
|
"@logto/connector-aliyun-dm": "^1.0.0-beta.2",
|
||||||
"@logto/connector-aliyun-sms": "^1.0.0-beta.2",
|
"@logto/connector-aliyun-sms": "^1.0.0-beta.2",
|
||||||
"@logto/connector-apple": "^1.0.0-beta.2",
|
"@logto/connector-apple": "^1.0.0-beta.2",
|
||||||
|
"@logto/connector-azuread": "^1.0.0-beta.2",
|
||||||
"@logto/connector-facebook": "^1.0.0-beta.2",
|
"@logto/connector-facebook": "^1.0.0-beta.2",
|
||||||
"@logto/connector-github": "^1.0.0-beta.2",
|
"@logto/connector-github": "^1.0.0-beta.2",
|
||||||
"@logto/connector-google": "^1.0.0-beta.2",
|
"@logto/connector-google": "^1.0.0-beta.2",
|
||||||
|
|
|
@ -9,6 +9,7 @@ const defaultPackages = [
|
||||||
'@logto/connector-facebook',
|
'@logto/connector-facebook',
|
||||||
'@logto/connector-github',
|
'@logto/connector-github',
|
||||||
'@logto/connector-google',
|
'@logto/connector-google',
|
||||||
|
'@logto/connector-azuread',
|
||||||
'@logto/connector-sendgrid-email',
|
'@logto/connector-sendgrid-email',
|
||||||
'@logto/connector-smtp',
|
'@logto/connector-smtp',
|
||||||
'@logto/connector-twilio-sms',
|
'@logto/connector-twilio-sms',
|
||||||
|
|
|
@ -57,6 +57,12 @@ const googleConnector = {
|
||||||
config: {},
|
config: {},
|
||||||
createdAt: 1_646_382_233_000,
|
createdAt: 1_646_382_233_000,
|
||||||
};
|
};
|
||||||
|
const azureADConnector = {
|
||||||
|
id: 'azuread-universal',
|
||||||
|
enabled: false,
|
||||||
|
config: {},
|
||||||
|
createdAt: 1_646_382_233_000,
|
||||||
|
};
|
||||||
const sendGridMailConnector = {
|
const sendGridMailConnector = {
|
||||||
id: 'sendgrid-email-service',
|
id: 'sendgrid-email-service',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -97,6 +103,7 @@ const connectors = [
|
||||||
facebookConnector,
|
facebookConnector,
|
||||||
githubConnector,
|
githubConnector,
|
||||||
googleConnector,
|
googleConnector,
|
||||||
|
azureADConnector,
|
||||||
sendGridMailConnector,
|
sendGridMailConnector,
|
||||||
smtpConnector,
|
smtpConnector,
|
||||||
twilioSmsConnector,
|
twilioSmsConnector,
|
||||||
|
|
501
pnpm-lock.yaml
generated
501
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue