2022-04-28 15:16:05 +08:00
|
|
|
|
/**
|
|
|
|
|
* The Implementation of OpenID Connect of Google Identity Platform.
|
|
|
|
|
* https://developers.google.com/identity/protocols/oauth2/openid-connect
|
|
|
|
|
*/
|
|
|
|
|
import {
|
|
|
|
|
ConnectorError,
|
|
|
|
|
ConnectorErrorCodes,
|
|
|
|
|
GetAccessToken,
|
|
|
|
|
GetAuthorizationUri,
|
|
|
|
|
GetUserInfo,
|
|
|
|
|
ConnectorMetadata,
|
|
|
|
|
ValidateConfig,
|
|
|
|
|
SocialConnector,
|
|
|
|
|
GetConnectorConfig,
|
|
|
|
|
} from '@logto/connector-types';
|
|
|
|
|
import { conditional, assert } from '@silverhand/essentials';
|
|
|
|
|
import got, { RequestError as GotRequestError } from 'got';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
accessTokenEndpoint,
|
|
|
|
|
authorizationEndpoint,
|
|
|
|
|
scope,
|
|
|
|
|
userInfoEndpoint,
|
|
|
|
|
defaultMetadata,
|
|
|
|
|
defaultTimeout,
|
|
|
|
|
} from './constant';
|
2022-05-13 14:22:37 +08:00
|
|
|
|
import { googleConfigGuard, AccessTokenResponse, GoogleConfig, UserInfoResponse } from './types';
|
2022-04-28 15:16:05 +08:00
|
|
|
|
|
|
|
|
|
export class GoogleConnector implements SocialConnector {
|
|
|
|
|
public metadata: ConnectorMetadata = defaultMetadata;
|
|
|
|
|
|
|
|
|
|
public readonly getConfig: GetConnectorConfig<GoogleConfig>;
|
|
|
|
|
|
|
|
|
|
constructor(getConnectorConfig: GetConnectorConfig<GoogleConfig>) {
|
|
|
|
|
this.getConfig = getConnectorConfig;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public validateConfig: ValidateConfig = async (config: unknown) => {
|
|
|
|
|
const result = googleConfigGuard.safeParse(config);
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error.message);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
2022-05-12 12:17:17 +08:00
|
|
|
|
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
2022-04-28 15:16:05 +08:00
|
|
|
|
|
|
|
|
|
const queryParameters = new URLSearchParams({
|
|
|
|
|
client_id: config.clientId,
|
|
|
|
|
redirect_uri: redirectUri,
|
|
|
|
|
response_type: 'code',
|
|
|
|
|
state,
|
|
|
|
|
scope,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public getAccessToken: GetAccessToken = async (code, redirectUri) => {
|
2022-05-12 12:17:17 +08:00
|
|
|
|
const { clientId, clientSecret } = await this.getConfig(
|
|
|
|
|
this.metadata.target,
|
|
|
|
|
this.metadata.platform
|
|
|
|
|
);
|
2022-04-28 15:16:05 +08:00
|
|
|
|
|
|
|
|
|
// Note:Need to decodeURIComponent on code
|
|
|
|
|
// https://stackoverflow.com/questions/51058256/google-api-node-js-invalid-grant-malformed-auth-code
|
|
|
|
|
const { access_token: accessToken } = await got
|
|
|
|
|
.post(accessTokenEndpoint, {
|
|
|
|
|
form: {
|
|
|
|
|
code: decodeURIComponent(code),
|
|
|
|
|
client_id: clientId,
|
|
|
|
|
client_secret: clientSecret,
|
|
|
|
|
redirect_uri: redirectUri,
|
|
|
|
|
grant_type: 'authorization_code',
|
|
|
|
|
},
|
|
|
|
|
timeout: defaultTimeout,
|
|
|
|
|
followRedirect: true,
|
|
|
|
|
})
|
|
|
|
|
.json<AccessTokenResponse>();
|
|
|
|
|
|
|
|
|
|
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
|
|
|
|
|
|
|
|
|
return { accessToken };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
|
|
|
|
const { accessToken } = accessTokenObject;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const {
|
|
|
|
|
sub: id,
|
|
|
|
|
picture: avatar,
|
|
|
|
|
email,
|
|
|
|
|
email_verified,
|
|
|
|
|
name,
|
|
|
|
|
} = await got
|
|
|
|
|
.post(userInfoEndpoint, {
|
|
|
|
|
headers: {
|
|
|
|
|
authorization: `Bearer ${accessToken}`,
|
|
|
|
|
},
|
|
|
|
|
timeout: defaultTimeout,
|
|
|
|
|
})
|
|
|
|
|
.json<UserInfoResponse>();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
avatar,
|
|
|
|
|
email: conditional(email_verified && email),
|
|
|
|
|
name,
|
|
|
|
|
};
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
if (error instanceof GotRequestError && error.response?.statusCode === 401) {
|
|
|
|
|
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|