0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-24 22:05:56 -05:00
logto/packages/connector-google/src/index.ts

135 lines
3.9 KiB
TypeScript
Raw Normal View History

/**
* 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,
codeWithRedirectDataGuard,
} 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';
import {
googleConfigGuard,
GoogleConfig,
accessTokenResponseGuard,
userInfoResponseGuard,
} from './types';
export default class GoogleConnector implements SocialConnector {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<GoogleConfig>) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
const result = googleConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
const config = await this.getConfig(this.metadata.id);
const queryParameters = new URLSearchParams({
client_id: config.clientId,
redirect_uri: redirectUri,
response_type: 'code',
state,
scope,
});
return `${authorizationEndpoint}?${queryParameters.toString()}`;
};
public getAccessToken = async (code: string, redirectUri: string) => {
const { clientId, clientSecret } = await this.getConfig(this.metadata.id);
// NoteNeed to decodeURIComponent on code
// https://stackoverflow.com/questions/51058256/google-api-node-js-invalid-grant-malformed-auth-code
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;
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
return { accessToken };
};
public getUserInfo: GetUserInfo = async (data) => {
const { code, redirectUri } = await this.authorizationCallbackHandler(data);
const { accessToken } = await this.getAccessToken(code, redirectUri);
try {
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;
return {
id,
avatar,
email: conditional(email_verified && email),
name,
};
} catch (error: unknown) {
assert(
!(error instanceof GotRequestError && error.response?.statusCode === 401),
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
);
throw error;
}
};
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;
};
}