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-06-27 00:41:15 +08:00
wechatNativeConfigGuard ,
2022-06-10 10:41:45 +08:00
accessTokenResponseGuard ,
GetAccessTokenErrorHandler ,
userInfoResponseGuard ,
GetUserInfoErrorHandler ,
2022-06-27 00:41:15 +08:00
WechatNativeConfig ,
2022-05-13 14:22:37 +08:00
} from './types' ;
2022-04-28 15:55:49 +08:00
2022-06-27 00:41:15 +08:00
export default class WechatNativeConnector implements SocialConnector {
2022-04-28 15:55:49 +08:00
public metadata : ConnectorMetadata = defaultMetadata ;
2022-06-27 00:41:15 +08:00
constructor ( public readonly getConfig : GetConnectorConfig < WechatNativeConfig > ) { }
2022-04-28 15:55:49 +08:00
public validateConfig : ValidateConfig = async ( config : unknown ) = > {
2022-06-27 00:41:15 +08:00
const result = wechatNativeConfigGuard . safeParse ( config ) ;
2022-04-28 15:55:49 +08:00
if ( ! result . success ) {
2022-07-08 19:43:43 +08:00
throw new ConnectorError ( ConnectorErrorCodes . InvalidConfig , result . error ) ;
2022-04-28 15:55:49 +08:00
}
} ;
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-06-10 10:41:45 +08:00
public getAccessToken = async (
code : string
) : Promise < { accessToken : string ; openid : 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
2022-06-10 10:41:45 +08:00
const httpResponse = await got . get ( accessTokenEndpoint , {
searchParams : { appid , secret , code , grant_type : 'authorization_code' } ,
timeout : defaultTimeout ,
} ) ;
const result = accessTokenResponseGuard . safeParse ( JSON . parse ( httpResponse . body ) ) ;
if ( ! result . success ) {
throw new ConnectorError ( ConnectorErrorCodes . InvalidResponse , result . error . message ) ;
}
2022-04-28 15:55:49 +08:00
2022-06-10 10:41:45 +08:00
const { access_token : accessToken , openid } = result . data ;
this . getAccessTokenErrorHandler ( result . data ) ;
assert ( accessToken && openid , new ConnectorError ( ConnectorErrorCodes . InvalidResponse ) ) ;
2022-04-28 15:55:49 +08:00
return { accessToken , openid } ;
} ;
2022-05-29 13:59:00 +08:00
public getUserInfo : GetUserInfo = async ( data ) = > {
2022-06-26 18:03:53 +08:00
const { code } = await this . authorizationCallbackHandler ( data ) ;
2022-05-29 13:59:00 +08:00
const { accessToken , openid } = await this . getAccessToken ( code ) ;
2022-04-28 15:55:49 +08:00
try {
2022-06-10 10:41:45 +08:00
const httpResponse = await got . get ( userInfoEndpoint , {
searchParams : { access_token : accessToken , openid } ,
timeout : defaultTimeout ,
} ) ;
const result = userInfoResponseGuard . safeParse ( JSON . parse ( httpResponse . body ) ) ;
if ( ! result . success ) {
throw new ConnectorError ( ConnectorErrorCodes . InvalidResponse , result . error . message ) ;
}
const { unionid , headimgurl , nickname } = result . data ;
2022-04-28 15:55:49 +08:00
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-06-10 10:41:45 +08:00
this . getUserInfoErrorHandler ( result . data ) ;
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 ;
}
} ;
2022-06-10 10:41:45 +08:00
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html
private readonly getAccessTokenErrorHandler : GetAccessTokenErrorHandler = ( accessToken ) = > {
const { errcode , errmsg } = accessToken ;
assert (
errcode !== 40 _029 ,
new ConnectorError ( ConnectorErrorCodes . SocialAuthCodeInvalid , errmsg )
) ;
assert (
errcode !== 40 _163 ,
new ConnectorError ( ConnectorErrorCodes . SocialAuthCodeInvalid , errmsg )
) ;
assert (
errcode !== 42 _003 ,
new ConnectorError ( ConnectorErrorCodes . SocialAuthCodeInvalid , errmsg )
) ;
assert ( ! errcode , new ConnectorError ( ConnectorErrorCodes . General , errmsg ) ) ;
} ;
private readonly getUserInfoErrorHandler : GetUserInfoErrorHandler = ( userInfo ) = > {
const { errcode , errmsg } = userInfo ;
assert (
! ( errcode === 40 _001 || errcode === 40 _014 ) ,
new ConnectorError ( ConnectorErrorCodes . SocialAccessTokenInvalid , errmsg )
) ;
assert ( ! errcode , new Error ( errmsg ? ? '' ) ) ;
} ;
2022-06-26 18:03:53 +08:00
private readonly authorizationCallbackHandler = async ( parameterObject : unknown ) = > {
const result = codeDataGuard . safeParse ( parameterObject ) ;
if ( ! result . success ) {
throw new ConnectorError ( ConnectorErrorCodes . General , JSON . stringify ( parameterObject ) ) ;
}
return result . data ;
} ;
2022-04-28 15:55:49 +08:00
}