2022-04-28 15:55:49 +08:00
/ * *
* The Implementation of OpenID Connect of WeChat Web Open Platform .
* https : //developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
* /
import {
ConnectorMetadata ,
GetAuthorizationUri ,
ValidateConfig ,
GetUserInfo ,
ConnectorError ,
ConnectorErrorCodes ,
SocialConnector ,
GetConnectorConfig ,
2022-05-29 13:59:00 +08:00
codeDataGuard ,
2022-04-28 15:55:49 +08:00
} from '@logto/connector-types' ;
import { assert } from '@silverhand/essentials' ;
import got , { RequestError as GotRequestError } from 'got' ;
import {
authorizationEndpoint ,
accessTokenEndpoint ,
userInfoEndpoint ,
defaultMetadata ,
defaultTimeout ,
2022-05-13 14:22:37 +08:00
} from './constant' ;
import {
2022-04-28 15:55:49 +08:00
weChatNativeConfigGuard ,
2022-05-13 14:22:37 +08:00
AccessTokenResponse ,
UserInfoResponse ,
2022-04-28 15:55:49 +08:00
WeChatNativeConfig ,
2022-05-13 14:22:37 +08:00
} from './types' ;
2022-04-28 15:55:49 +08:00
2022-05-24 13:54:37 +08:00
export default class WeChatNativeConnector implements SocialConnector {
2022-04-28 15:55:49 +08:00
public metadata : ConnectorMetadata = defaultMetadata ;
2022-05-23 17:49:29 +08:00
constructor ( public readonly getConfig : GetConnectorConfig < WeChatNativeConfig > ) { }
2022-04-28 15:55:49 +08:00
public validateConfig : ValidateConfig = async ( config : unknown ) = > {
const result = weChatNativeConfigGuard . safeParse ( config ) ;
if ( ! result . success ) {
throw new ConnectorError ( ConnectorErrorCodes . InvalidConfig , result . error . message ) ;
}
} ;
2022-05-29 13:59:00 +08:00
public getAuthorizationUri : GetAuthorizationUri = async ( { state } ) = > {
const { appId , universalLinks } = await this . getConfig ( this . metadata . id ) ;
2022-04-28 15:55:49 +08:00
const queryParameters = new URLSearchParams ( {
2022-05-20 10:22:12 +08:00
app_id : appId ,
2022-05-25 14:23:53 +08:00
state ,
2022-05-29 13:59:00 +08:00
// `universalLinks` is used by Wechat open platform website,
// while `universal_link` is their API requirement.
. . . ( universalLinks && { universal_link : universalLinks } ) ,
2022-04-28 15:55:49 +08:00
} ) ;
return ` ${ authorizationEndpoint } ? ${ queryParameters . toString ( ) } ` ;
} ;
2022-05-29 13:59:00 +08:00
public getAccessToken = async ( code : string ) = > {
2022-05-24 11:39:44 +08:00
const { appId : appid , appSecret : secret } = await this . getConfig ( this . metadata . id ) ;
2022-04-28 15:55:49 +08:00
const {
access_token : accessToken ,
openid ,
errcode ,
2022-06-07 12:06:37 +08:00
errmsg ,
2022-04-28 15:55:49 +08:00
} = await got
. get ( accessTokenEndpoint , {
searchParams : { appid , secret , code , grant_type : 'authorization_code' } ,
timeout : defaultTimeout ,
} )
. json < AccessTokenResponse > ( ) ;
2022-06-07 12:06:37 +08:00
assert ( errcode !== 40 _029 , new ConnectorError ( ConnectorErrorCodes . SocialAuthCodeInvalid ) ) ;
assert ( ! errcode && accessToken && openid , new Error ( errmsg ? ? '' ) ) ;
2022-04-28 15:55:49 +08:00
return { accessToken , openid } ;
} ;
2022-05-29 13:59:00 +08:00
public getUserInfo : GetUserInfo = async ( data ) = > {
const { code } = codeDataGuard . parse ( data ) ;
const { accessToken , openid } = await this . getAccessToken ( code ) ;
2022-04-28 15:55:49 +08:00
try {
const { unionid , headimgurl , nickname , errcode , errmsg } = await got
. get ( userInfoEndpoint , {
searchParams : { access_token : accessToken , openid } ,
timeout : defaultTimeout ,
} )
. json < UserInfoResponse > ( ) ;
2022-05-27 21:21:23 +08:00
// Response properties of user info can be separated into two groups: (1) {unionid, headimgurl, nickname}, (2) {errcode, errmsg}.
// These two groups are mutually exclusive: if group (1) is not empty, group (2) should be empty and vice versa.
// 'errmsg' and 'errcode' turn to non-empty values or empty values at the same time. Hence, if 'errmsg' is non-empty then 'errcode' should be non-empty.
2022-04-28 15:55:49 +08:00
2022-05-27 21:21:23 +08:00
assert ( errcode !== 40 _001 , new ConnectorError ( ConnectorErrorCodes . SocialAccessTokenInvalid ) ) ;
assert ( ! errcode , new Error ( errmsg ) ) ;
2022-04-28 15:55:49 +08:00
return { id : unionid ? ? openid , avatar : headimgurl , name : nickname } ;
} catch ( error : unknown ) {
2022-05-27 21:21:23 +08:00
assert (
! ( error instanceof GotRequestError && error . response ? . statusCode === 401 ) ,
new ConnectorError ( ConnectorErrorCodes . SocialAccessTokenInvalid )
) ;
2022-04-28 15:55:49 +08:00
throw error ;
}
} ;
}