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,
|
|
|
|
|
GetAuthorizationUri,
|
|
|
|
|
GetUserInfo,
|
|
|
|
|
ConnectorMetadata,
|
|
|
|
|
ValidateConfig,
|
|
|
|
|
SocialConnector,
|
|
|
|
|
GetConnectorConfig,
|
2022-05-29 13:59:00 +08:00
|
|
|
|
codeWithRedirectDataGuard,
|
2022-04-28 15:16:05 +08:00
|
|
|
|
} 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-06-10 10:41:45 +08:00
|
|
|
|
import {
|
|
|
|
|
googleConfigGuard,
|
|
|
|
|
GoogleConfig,
|
|
|
|
|
accessTokenResponseGuard,
|
|
|
|
|
userInfoResponseGuard,
|
|
|
|
|
} from './types';
|
2022-04-28 15:16:05 +08:00
|
|
|
|
|
2022-05-24 13:54:37 +08:00
|
|
|
|
export default class GoogleConnector implements SocialConnector {
|
2022-04-28 15:16:05 +08:00
|
|
|
|
public metadata: ConnectorMetadata = defaultMetadata;
|
|
|
|
|
|
2022-05-23 17:49:29 +08:00
|
|
|
|
constructor(public readonly getConfig: GetConnectorConfig<GoogleConfig>) {}
|
2022-04-28 15:16:05 +08:00
|
|
|
|
|
|
|
|
|
public validateConfig: ValidateConfig = async (config: unknown) => {
|
|
|
|
|
const result = googleConfigGuard.safeParse(config);
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
2022-07-08 19:43:43 +08:00
|
|
|
|
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
|
2022-04-28 15:16:05 +08:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2022-05-29 13:59:00 +08:00
|
|
|
|
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
|
2022-05-24 11:39:44 +08:00
|
|
|
|
const config = await this.getConfig(this.metadata.id);
|
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()}`;
|
|
|
|
|
};
|
|
|
|
|
|
2022-05-29 13:59:00 +08:00
|
|
|
|
public getAccessToken = async (code: string, redirectUri: string) => {
|
2022-05-24 11:39:44 +08:00
|
|
|
|
const { clientId, clientSecret } = await this.getConfig(this.metadata.id);
|
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
|
2022-06-10 10:41:45 +08:00
|
|
|
|
const httpResponse = await got.post(accessTokenEndpoint, {
|
|
|
|
|
form: {
|
|
|
|
|
code: decodeURIComponent(code),
|
|
|
|
|
client_id: clientId,
|
|
|
|
|
client_secret: clientSecret,
|
|
|
|
|
redirect_uri: redirectUri,
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { access_token: accessToken } = result.data;
|
2022-04-28 15:16:05 +08:00
|
|
|
|
|
|
|
|
|
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
|
|
|
|
|
|
|
|
|
return { accessToken };
|
|
|
|
|
};
|
|
|
|
|
|
2022-05-29 13:59:00 +08:00
|
|
|
|
public getUserInfo: GetUserInfo = async (data) => {
|
2022-06-26 18:03:53 +08:00
|
|
|
|
const { code, redirectUri } = await this.authorizationCallbackHandler(data);
|
2022-05-29 13:59:00 +08:00
|
|
|
|
const { accessToken } = await this.getAccessToken(code, redirectUri);
|
2022-04-28 15:16:05 +08:00
|
|
|
|
|
|
|
|
|
try {
|
2022-06-10 10:41:45 +08:00
|
|
|
|
const httpResponse = await got.post(userInfoEndpoint, {
|
|
|
|
|
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 { sub: id, picture: avatar, email, email_verified, name } = result.data;
|
2022-04-28 15:16:05 +08:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
avatar,
|
|
|
|
|
email: conditional(email_verified && email),
|
|
|
|
|
name,
|
|
|
|
|
};
|
|
|
|
|
} catch (error: unknown) {
|
2022-06-10 10:41:45 +08:00
|
|
|
|
assert(
|
|
|
|
|
!(error instanceof GotRequestError && error.response?.statusCode === 401),
|
|
|
|
|
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
|
|
|
|
);
|
|
|
|
|
|
2022-04-28 15:16:05 +08:00
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-06-26 18:03:53 +08:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
};
|
2022-04-28 15:16:05 +08:00
|
|
|
|
}
|