mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
097aade2e2
* feat(connectors): handle authorization callback parameters in each connector respectively
134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
/**
|
||
* 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.message);
|
||
}
|
||
};
|
||
|
||
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);
|
||
|
||
// Note:Need 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;
|
||
};
|
||
}
|