Cómo añadir Google Sign-In a Amazon Cognito con Account Linking (CDK + PreSignUp)
Añadir Continuar con Google a una aplicación con Amazon Cognito es relativamente sencillo. La parte interesante aparece cuando un usuario ya tiene una cuenta con email y contraseña y, más adelante, entra con Google usando ese mismo email.
Por defecto, Cognito puede crear un segundo usuario federado. Acabas con dos identidades, dos valores de sub y, potencialmente, datos repartidos entre dos cuentas.
En este tutorial vamos a resolver las dos partes:
- Añadir Google como Identity Provider en Cognito con AWS CDK.
- Enlazar la identidad de Google con un usuario nativo de Cognito que ya exista con el mismo email verificado, usando un trigger
PreSignUp.
El ejemplo real es AWS Announcements Hub, una aplicación serverless construida con React, Amazon Cognito, Amazon API Gateway, AWS Lambda, Amazon DynamoDB y AWS CDK.
El tutorial parte de que ya tienes un User Pool de Cognito y el login nativo con email y contraseña funcionando.
Cómo funciona el flujo
Tu aplicación no autentica directamente contra Google. Cognito actúa como intermediario:
1
Usuario → App → endpoint OAuth de Cognito → Google → Cognito → App
Google autentica al usuario y redirige de vuelta a Cognito. Cognito devuelve después tokens de Cognito a tu aplicación, así que tu backend sigue validando los mismos JWT independientemente de si el usuario ha entrado con contraseña o con Google.
Paso 1: Configurar Google
- Crea o selecciona un proyecto.
- Configura Branding y la pantalla de consentimiento.
- Añade los scopes
openid,emailyprofile. - Crea un cliente OAuth de tipo Web application.
Configuración de Branding para el flujo OAuth
El cliente OAuth 2.0 utilizado por Cognito
El valor importante es la Authorized redirect URI. Google redirige a Cognito, no directamente a tu aplicación.
Por ejemplo, si tu dominio de Cognito es:
1
news-playingaws-prod.auth.eu-south-2.amazoncognito.com
registra:
1
https://news-playingaws-prod.auth.eu-south-2.amazoncognito.com/oauth2/idpresponse
El cliente OAuth con el callback /oauth2/idpresponse de Cognito
Guarda el Client ID y el Client Secret.
Paso 2: Guardar las credenciales de Google
No guardes el Google Client Secret en el repositorio.
En este ejemplo dejamos el Client ID en Parameter Store y el secret en Secrets Manager:
1
2
3
4
5
6
7
8
9
10
11
12
REGION=eu-south-2
aws ssm put-parameter \
--name "/news/prod/google-client-id" \
--value "TU_CLIENT_ID.apps.googleusercontent.com" \
--type String \
--region $REGION
aws secretsmanager create-secret \
--name "/news/prod/google-client-secret" \
--secret-string 'TU_CLIENT_SECRET' \
--region $REGION
ssm-secureparece la opción obvia para el secret, peroAWS::Cognito::UserPoolIdentityProviderno soporta de forma fiable esa referencia dinámica en estas propiedades. Secrets Manager evita ese problema.
Paso 3: Configurar Cognito con CDK
Necesitamos tres piezas: un dominio de Cognito, el Identity Provider de Google y la configuración OAuth del User Pool Client.
Dominio de Cognito
1
2
3
4
5
userPool.addDomain("ManagedLoginDomain", {
cognitoDomain: {
domainPrefix: `news-playingaws-${environment}`,
},
});
Identity Provider de Google
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const googleClientId = ssm.StringParameter.valueForStringParameter(
this,
`/news/${environment}/google-client-id`,
);
const googleProvider = new cognito.UserPoolIdentityProviderGoogle(this, "GoogleIdP", {
userPool,
clientId: googleClientId,
clientSecretValue: cdk.SecretValue.secretsManager(
`/news/${environment}/google-client-secret`,
),
scopes: ["openid", "email", "profile"],
attributeMapping: {
email: cognito.ProviderAttribute.GOOGLE_EMAIL,
emailVerified: cognito.ProviderAttribute.other("email_verified"),
givenName: cognito.ProviderAttribute.GOOGLE_GIVEN_NAME,
familyName: cognito.ProviderAttribute.GOOGLE_FAMILY_NAME,
},
});
Mapear email_verified es importante porque la lógica de linking solo confiará en identidades verificadas.
OAuth en el User Pool Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const userPoolClient = userPool.addClient("WebClient", {
authFlows: {
userPassword: true,
userSrp: true,
},
preventUserExistenceErrors: true,
oAuth: {
flows: {
authorizationCodeGrant: true,
},
scopes: [
cognito.OAuthScope.OPENID,
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.PROFILE,
],
callbackUrls: [
"https://news.playingaws.com",
"http://localhost:3000",
],
logoutUrls: [
"https://news.playingaws.com",
"http://localhost:3000",
],
},
supportedIdentityProviders: [
cognito.UserPoolClientIdentityProvider.COGNITO,
cognito.UserPoolClientIdentityProvider.GOOGLE,
],
});
userPoolClient.node.addDependency(googleProvider);
Usamos Authorization Code Grant. La librería del frontend se encargará de PKCE.
Paso 4: Añadir Google Sign-In con Amplify
Con Amplify v6, configura el dominio OAuth de Cognito y utiliza signInWithRedirect():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Amplify.configure({
Auth: {
Cognito: {
userPoolId,
userPoolClientId,
loginWith: {
oauth: {
domain: "news-playingaws-prod.auth.eu-south-2.amazoncognito.com",
scopes: ["openid", "email", "profile"],
redirectSignIn: [window.location.origin],
redirectSignOut: [window.location.origin],
responseType: "code",
},
},
},
},
});
export const signInWithGoogle = () =>
signInWithRedirect({ provider: "Google" });
El botón solo necesita llamar a signInWithGoogle().
El login nativo y Google Sign-In pueden convivir en la misma interfaz
Google realiza la autenticación y devuelve al usuario a Cognito
Paso 5: Enlazar Google con un usuario existente de Cognito
Esta es la parte que evita las cuentas duplicadas.
Imagina que un usuario ya está registrado con:
1
usuario@example.com + contraseña
Más adelante utiliza Continuar con Google con ese mismo email verificado.
Sin linking, Cognito puede crear un usuario federado separado, por ejemplo Google_....
Sin linking, la identidad nativa y la de Google pueden existir como usuarios separados
Lo solucionamos con un trigger PreSignUp. Cognito invoca la Lambda antes de crear el usuario externo, lo que nos permite asociar esa identidad de Google al usuario nativo que ya existe.
Lambda PreSignUp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import {
AdminLinkProviderForUserCommand,
CognitoIdentityProviderClient,
ListUsersCommand,
} from "@aws-sdk/client-cognito-identity-provider";
const client = new CognitoIdentityProviderClient({});
export async function handler(event: any) {
if (event.triggerSource !== "PreSignUp_ExternalProvider") {
return event;
}
const email = event.request.userAttributes.email;
const externalEmailVerified =
event.request.userAttributes.email_verified === "true";
if (!email || !externalEmailVerified) {
return event;
}
const separator = event.userName.indexOf("_");
if (separator < 1) {
return event;
}
const providerName = event.userName.slice(0, separator);
const providerUserId = event.userName.slice(separator + 1);
const { Users = [] } = await client.send(
new ListUsersCommand({
UserPoolId: event.userPoolId,
Filter: `email = "${email}"`,
Limit: 10,
}),
);
const nativeUser = Users.find((user) => {
const emailVerified = user.Attributes?.find(
(attribute) => attribute.Name === "email_verified",
)?.Value;
return (
user.Username &&
!user.Username.startsWith(`${providerName}_`) &&
emailVerified === "true"
);
});
if (!nativeUser?.Username) {
return event;
}
await client.send(
new AdminLinkProviderForUserCommand({
UserPoolId: event.userPoolId,
DestinationUser: {
ProviderName: "Cognito",
ProviderAttributeValue: nativeUser.Username,
},
SourceUser: {
ProviderName: providerName,
ProviderAttributeName: "Cognito_Subject",
ProviderAttributeValue: providerUserId,
},
}),
);
return event;
}
El detalle importante es que el usuario nativo existente es el destino. Su identidad de Cognito se conserva y la identidad de Google se añade encima.
Eso significa que cualquier dato de la aplicación que ya esté asociado al sub del usuario nativo sigue apuntando a la misma cuenta.
Enlaza identidades solo cuando confíes en el email de ambos lados. En este ejemplo, Google debe devolver
email_verified=truey el usuario existente de Cognito también debe tener el email verificado.
Conectar el trigger con CDK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const preSignUpFn = new lambda.Function(this, "PreSignUpTrigger", {
runtime: lambda.Runtime.NODEJS_24_X,
architecture: lambda.Architecture.ARM_64,
handler: "services/auth/pre-signup.handler",
code: lambda.Code.fromAsset("../../backend/dist"),
});
const userPool = new cognito.UserPool(this, "UserPool", {
// ...resto de la configuración...
lambdaTriggers: {
preSignUp: preSignUpFn,
},
});
preSignUpFn.role?.attachInlinePolicy(
new iam.Policy(this, "PreSignUpLinkPolicy", {
statements: [
new iam.PolicyStatement({
actions: [
"cognito-idp:ListUsers",
"cognito-idp:AdminLinkProviderForUser",
],
resources: [userPool.userPoolArn],
}),
],
}),
);
Para comprobarlo, crea una cuenta nativa, cierra sesión y entra después con Google usando el mismo email verificado. Cognito debería conservar la cuenta nativa y asociarle la identidad de Google, en lugar de crear un segundo usuario independiente.
Troubleshooting
redirect_uri_mismatchde Google: comprueba que la URL exacta de Cognito.../oauth2/idpresponseestá registrada en el cliente OAuth de Google.redirect_mismatchde Cognito: la URL enviada por la app debe coincidir exactamente con una de lascallbackUrlsdel User Pool Client.- Se siguen creando usuarios duplicados: comprueba que el trigger está configurado como
PreSignUp, recibePreSignUp_ExternalProvidery puede ejecutarAdminLinkProviderForUser.
Conclusión
Añadir Google Sign-In a Cognito es principalmente configuración. La parte que merece especial atención es la consolidación de identidades.
Sin account linking, un usuario que ya tenía una cuenta con contraseña puede acabar con un segundo usuario de Cognito respaldado por Google y con un sub distinto. Con una pequeña Lambda PreSignUp, puedes mantener el usuario nativo existente como identidad canónica y enlazar Google encima.
El resultado es sencillo para el usuario: una sola cuenta, dos formas de iniciar sesión.