Closed ethancdaniel closed 1 week ago
Hey @ethancdaniel :wave: thanks for raising this! While we work to reproduce the issue on our end, you can modify the user pool to automatically verify email and phone_number attributes https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.CfnUserPool.html#autoverifiedattributes
Hey @ethancdaniel, I was able to reproduce the issue. On a bit of digging, this appears to be an expected behavior on AWS Cognito. Refer to this post providing information on this behavior: https://repost.aws/questions/QUMvXcvbNqSNymZgbN2GLoqQ/cognito-external-provider-user-email-cannot-be-automatically-verified Additionally, refer to this similar issue providing a workaround using post confirmation trigger to set the attributes: https://github.com/aws-amplify/amplify-js/issues/5117#issuecomment-647725294
Thanks for the response @josefaidt and @ykethan! I followed your suggestion to change it to a post confirmation trigger, but it didn't work. Is there any way to modify the user pool to only require email verification when signing up with email but not for OAuth sign up? It's logging all three console.logs I added in cloudwatch by the way.
import type { PostConfirmationTriggerHandler } from "aws-lambda";
import { CognitoIdentityProvider } from "@aws-sdk/client-cognito-identity-provider";
export const handler: PostConfirmationTriggerHandler = async (
event,
context,
callback
) => {
console.log("hi");
if (event.request.userAttributes.email) {
console.log("has email");
if (
event.request.userAttributes["cognito:user_status"] ===
"EXTERNAL_PROVIDER"
) {
console.log("is external provider");
const cognitoIdServiceProvider = new CognitoIdentityProvider({
region: "us-east-2",
});
var params = {
UserAttributes: [
{
Name: "email_verified",
Value: "true",
},
],
UserPoolId: event.userPoolId,
Username: event.userName,
};
cognitoIdServiceProvider.adminUpdateUserAttributes(
params,
function (err: any) {
if (err) {
callback(null, event);
} else {
callback(null, event);
}
}
);
} else {
callback(null, event);
}
} else {
callback(null, event);
}
};
Hey @ethancdaniel it looks like you can potentially map the email_verified
attribute with google's verified_email
attribute from the profile
scope
https://developers.google.com/identity/sign-in/web/backend-auth#node.js
Can you try mapping this in your backend?
import { defineAuth, secret } from "@aws-amplify/backend";
import { preSignUp } from "../functions/pre-signup/resource";
/**
* Define and configure your auth resource
* @see https://docs.amplify.aws/gen2/build-a-backend/auth
*/
export const auth = defineAuth({
loginWith: {
email: true,
externalProviders: {
google: {
clientId: secret("GOOGLE_ID"),
clientSecret: secret("GOOGLE_SECRET"),
scopes: ["email", "profile"],
attributeMapping: {
email: "email",
email_verified: "email_verified"
},
},
callbackUrls: [
"http://localhost:3000/home",
"https://<my-amplify-url>/home",
],
logoutUrls: [
"http://localhost:3000",
"https://<my-amplify-url>",
],
},
},
userAttributes: {
preferredUsername: {
mutable: true,
required: false,
},
},
});
Hey @ethancdaniel it looks like you can potentially map the
email_verified
attribute with google'sverified_email
attribute from theprofile
scope https://developers.google.com/identity/sign-in/web/backend-auth#node.jsCan you try mapping this in your backend?
I've recreated this exactly and email_verified is not a part of the attribute mapping.
From aws-cdk-lib/aws-cognito/lib/user-pool-idps/base.d.ts
:
export interface AttributeMapping {
/**
* The user's postal address is a required attribute.
* @default - not mapped
*/
readonly address?: ProviderAttribute;
/**
* The user's birthday.
* @default - not mapped
*/
readonly birthdate?: ProviderAttribute;
/**
* The user's e-mail address.
* @default - not mapped
*/
readonly email?: ProviderAttribute;
/**
* The surname or last name of user.
* @default - not mapped
*/
readonly familyName?: ProviderAttribute;
/**
* The user's gender.
* @default - not mapped
*/
readonly gender?: ProviderAttribute;
/**
* The user's first name or give name.
* @default - not mapped
*/
readonly givenName?: ProviderAttribute;
/**
* The user's locale.
* @default - not mapped
*/
readonly locale?: ProviderAttribute;
/**
* The user's middle name.
* @default - not mapped
*/
readonly middleName?: ProviderAttribute;
/**
* The user's full name in displayable form.
* @default - not mapped
*/
readonly fullname?: ProviderAttribute;
/**
* The user's nickname or casual name.
* @default - not mapped
*/
readonly nickname?: ProviderAttribute;
/**
* The user's telephone number.
* @default - not mapped
*/
readonly phoneNumber?: ProviderAttribute;
/**
* The URL to the user's profile picture.
* @default - not mapped
*/
readonly profilePicture?: ProviderAttribute;
/**
* The user's preferred username.
* @default - not mapped
*/
readonly preferredUsername?: ProviderAttribute;
/**
* The URL to the user's profile page.
* @default - not mapped
*/
readonly profilePage?: ProviderAttribute;
/**
* The user's time zone.
* @default - not mapped
*/
readonly timezone?: ProviderAttribute;
/**
* Time, the user's information was last updated.
* @default - not mapped
*/
readonly lastUpdateTime?: ProviderAttribute;
/**
* The URL to the user's web page or blog.
* @default - not mapped
*/
readonly website?: ProviderAttribute;
/**
* Specify custom attribute mapping here and mapping for any standard attributes not supported yet.
* @default - no custom attribute mapping
*/
readonly custom?: {
[key: string]: ProviderAttribute;
};
}
I've also tried setting
attributeMapping: {
email: "email",
custom: {
email_verified: "email_verified",
}
}
Hey @ethancdaniel it looks like you can potentially map the
email_verified
attribute with google'sverified_email
attribute from theprofile
scope developers.google.com/identity/sign-in/web/backend-auth#node.js Can you try mapping this in your backend?I've recreated this exactly and email_verified is not a part of the attribute mapping.
From
aws-cdk-lib/aws-cognito/lib/user-pool-idps/base.d.ts
:export interface AttributeMapping { /** * The user's postal address is a required attribute. * @default - not mapped */ readonly address?: ProviderAttribute; /** * The user's birthday. * @default - not mapped */ readonly birthdate?: ProviderAttribute; /** * The user's e-mail address. * @default - not mapped */ readonly email?: ProviderAttribute; /** * The surname or last name of user. * @default - not mapped */ readonly familyName?: ProviderAttribute; /** * The user's gender. * @default - not mapped */ readonly gender?: ProviderAttribute; /** * The user's first name or give name. * @default - not mapped */ readonly givenName?: ProviderAttribute; /** * The user's locale. * @default - not mapped */ readonly locale?: ProviderAttribute; /** * The user's middle name. * @default - not mapped */ readonly middleName?: ProviderAttribute; /** * The user's full name in displayable form. * @default - not mapped */ readonly fullname?: ProviderAttribute; /** * The user's nickname or casual name. * @default - not mapped */ readonly nickname?: ProviderAttribute; /** * The user's telephone number. * @default - not mapped */ readonly phoneNumber?: ProviderAttribute; /** * The URL to the user's profile picture. * @default - not mapped */ readonly profilePicture?: ProviderAttribute; /** * The user's preferred username. * @default - not mapped */ readonly preferredUsername?: ProviderAttribute; /** * The URL to the user's profile page. * @default - not mapped */ readonly profilePage?: ProviderAttribute; /** * The user's time zone. * @default - not mapped */ readonly timezone?: ProviderAttribute; /** * Time, the user's information was last updated. * @default - not mapped */ readonly lastUpdateTime?: ProviderAttribute; /** * The URL to the user's web page or blog. * @default - not mapped */ readonly website?: ProviderAttribute; /** * Specify custom attribute mapping here and mapping for any standard attributes not supported yet. * @default - no custom attribute mapping */ readonly custom?: { [key: string]: ProviderAttribute; }; }
I've also tried setting
attributeMapping: { email: "email", custom: { email_verified: "email_verified", } }
@josefaidt Can you take a look at this when you get the chance? Doesn't seem like the email_verified attribute exists.
Hey @ethancdaniel you're right, it does not seem to exist in the CDK type, despite how you can configure it manually in the console per provider
just to verify whether this is going to work as expected, can you try modifying this in the console and signing in? if this works we can look towards expanding this type to support the flow
@josefaidt What do you mean by this? I'm not sure what you want me to modify it to. And should I leave my code as is, or should I change something?
@ethancdaniel sorry, the value would be email_verified
. From the google docs it looks to be available on the same attribute name
@josefaidt email_verified
is already set to email_verified
by default
does it give you an error when you attempt to save the changes?
No, that's what it already was set to by default, my first screenshot was because I thought you wanted me to remove the mapping @josefaidt
Ah, thanks for clarifying @ethancdaniel let me do a bit more digging. There is an option of using AdminUpdateUserAttributesCommand
in a post-authentication trigger or something similar, however that function will be invoked on each authentication event
Hey @ethancdaniel I was doing a bit of a dive on this and found you can use the PostConfirmation trigger and the SDK to update this on first sign-in!
Note: I've added a type to narrow the generic record type we see in the PostConfirmationTriggerHandler
, but making a note of this to potentially introduce this as a type from the backend package to narrow based on the auth definition 🙂
// amplify/auth/post-confirmation/handler.ts
import type { PostConfirmationTriggerHandler } from "aws-lambda"
import {
CognitoIdentityProviderClient,
AdminUpdateUserAttributesCommand,
} from "@aws-sdk/client-cognito-identity-provider"
const client = new CognitoIdentityProviderClient()
// declared this to type the user attributes we see in the event
type UserAttributes = {
sub: string
email_verified: "false" | "true"
identities: string
"cognito:user_status": "EXTERNAL_PROVIDER" | string
email: string
}
export const handler: PostConfirmationTriggerHandler = async (event) => {
console.log("event:", JSON.stringify(event, null, 2))
const userAttributes = event.request.userAttributes as UserAttributes
if (userAttributes["cognito:user_status"] === "EXTERNAL_PROVIDER") {
const command = new AdminUpdateUserAttributesCommand({
UserPoolId: event.userPoolId,
Username: event.userName,
UserAttributes: [
{
Name: "email_verified",
Value: "true",
},
],
})
try {
const result = await client.send(command)
console.log("processed", result.$metadata.requestId)
} catch (error) {
console.error(
"Unable to automatically verify external provider email",
error,
)
}
}
return event
}
// amplify/auth/resource.ts
import { defineAuth } from "@aws-amplify/backend"
import { postConfirmation } from "./post-confirmation/resource"
/**
* Define and configure your auth resource
* @see https://docs.amplify.aws/gen2/build-a-backend/auth
*/
export const auth = defineAuth({
loginWith: {
email: true,
externalProviders: {
saml: {
metadata: {
metadataType: "URL",
metadataContent:
"https://some-url",
},
attributeMapping: {
email: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
},
name: "MicrosoftEntraIDSAML",
},
logoutUrls: ["http://localhost:4321/", "hostedui://"],
callbackUrls: ["http://localhost:4321/", "hostedui://callback/"],
},
},
access: (allow) => [
allow.resource(postConfirmation).to(["updateUserAttributes"]),
],
triggers: {
postConfirmation,
},
})
@josefaidt This successfully changed email_verified
to true, but there are two problems.
1.
access: (allow) => [
allow.resource(postConfirmation).to(["updateUserAttributes"]),
],
access
is an experimental feature that AWS says not to use in production
session.tokens !== undefined
. Skipping email verification when signing in with an external provider is a very standard practice I've seen in websites, so if this is not possible without experimental features, is it that everyone is just using Auth0, Clerk, etc. instead of AWS Cognito?
Hey @ethancdaniel thanks for calling that out! I filed a PR to update the jsdoc for access
. For 2, are you seeing undefined tokens in your frontend app? Do you see them set in localStorage
?
Skipping email verification when signing in with an external provider...
This is the currently the default behavior for Amazon Cognito. In the example above we're asserting the email will be verified if coming from the external provider (in my case, SAML with Entra ID). This should not impact the tokens beyond setting the appropriate attribute value in the ID token -- in the event you'd like to have separate experiences for unverified users.
I got similar problem updating custom attribute in the preSignUp lambda
event.request.userAttributes['custom:MyAttribute']
The event returned by the lambda with new values, but these are not saved on a created user record
Closing the issue due to inactivity. Do reach out to us if you are still experiencing this issue
Environment information
Description
I'm trying to auto-verify / confirm users that sign up with Google OAuth. After searching online, pre-signup lambdas seem to be the easiest option available. These are the lambda function files I added and then added preSignUp as a trigger in
auth/resource.ts
. The lambda function logs true for bothautoConfirmUser
andautoVerifyEmail
but when I check the cognito console, the user is unverified. Any advice on fixing this strategy or using a completely different strategy that I am unaware of would be greatly appreciated.This is the doc I used as a guideline for the pre-signup trigger:
pre-signup/handler.ts
pre-signup/resource.ts
auth/resource.ts