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 ,
GetUserInfo ,
ConnectorError ,
ConnectorErrorCodes ,
2022-07-14 15:56:57 +08:00
Connector ,
SocialConnectorInstance ,
2022-04-28 15:55:49 +08:00
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' ;
2022-07-08 21:11:31 +08:00
import got , { HTTPError } from 'got' ;
2022-04-28 15:55:49 +08:00
import {
authorizationEndpoint ,
accessTokenEndpoint ,
userInfoEndpoint ,
defaultMetadata ,
defaultTimeout ,
2022-07-08 21:11:31 +08:00
invalidAccessTokenErrcode ,
invalidAuthCodeErrcode ,
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 ,
2022-07-26 15:56:52 +08:00
UserInfoResponseMessageParser ,
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-07-14 15:56:57 +08:00
export default class WechatNativeConnector implements SocialConnectorInstance < WechatNativeConfig > {
2022-04-28 15:55:49 +08:00
public metadata : ConnectorMetadata = defaultMetadata ;
2022-07-14 15:56:57 +08:00
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 ;
}
2022-04-28 15:55:49 +08:00
2022-07-14 00:12:01 +08:00
constructor ( public readonly getConfig : GetConnectorConfig ) { }
2022-04-28 15:55:49 +08:00
2022-07-14 00:12:01 +08:00
public validateConfig ( config : unknown ) : asserts config is WechatNativeConfig {
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-07-14 00:12:01 +08:00
}
2022-04-28 15:55:49 +08:00
2022-05-29 13:59:00 +08:00
public getAuthorizationUri : GetAuthorizationUri = async ( { state } ) = > {
2022-07-14 00:12:01 +08:00
const config = await this . getConfig ( this . metadata . id ) ;
this . validateConfig ( config ) ;
const { appId , universalLinks } = config ;
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-07-14 00:12:01 +08:00
const config = await this . getConfig ( this . metadata . id ) ;
this . validateConfig ( config ) ;
const { appId : appid , appSecret : secret } = config ;
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-07-26 15:56:52 +08:00
this . userInfoResponseMessageParser ( result . data ) ;
2022-04-28 15:55:49 +08:00
return { id : unionid ? ? openid , avatar : headimgurl , name : nickname } ;
} catch ( error : unknown ) {
2022-07-26 15:56:52 +08:00
return this . getUserInfoErrorHandler ( error ) ;
2022-04-28 15:55:49 +08:00
}
} ;
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 ;
2022-07-08 21:11:31 +08:00
if ( errcode ) {
assert (
! invalidAuthCodeErrcode . includes ( errcode ) ,
new ConnectorError ( ConnectorErrorCodes . SocialAuthCodeInvalid , errmsg )
) ;
throw new ConnectorError ( ConnectorErrorCodes . General , { errorDescription : errmsg , errcode } ) ;
}
2022-06-10 10:41:45 +08:00
} ;
2022-07-26 15:56:52 +08:00
private readonly userInfoResponseMessageParser : UserInfoResponseMessageParser = ( userInfo ) = > {
2022-06-10 10:41:45 +08:00
const { errcode , errmsg } = userInfo ;
2022-07-08 21:11:31 +08:00
if ( errcode ) {
assert (
! invalidAccessTokenErrcode . includes ( errcode ) ,
new ConnectorError ( ConnectorErrorCodes . SocialAccessTokenInvalid , errmsg )
) ;
throw new ConnectorError ( ConnectorErrorCodes . General , { errorDescription : errmsg , errcode } ) ;
}
2022-06-10 10:41:45 +08:00
} ;
2022-06-26 18:03:53 +08:00
2022-07-26 15:56:52 +08:00
private readonly getUserInfoErrorHandler = ( 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 ;
} ;
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
}