-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cognito UserPools - Hosted UI Customizations (including logo upload) #6953
Comments
I am suffering from the same problem, in that there is no property for "ImageFile", Will this issue every be fixed? |
@0xdevalias just tried your workaround, but it gives me an error for a JPG 😭 (didn't try PNG, but I suspect it doesn't matter).
Also tried passing the buffer without any encoding, but also didn't work. |
Running into this issue as well. I've tried about 6 variations of encoding ImageFile and it just won't take with:
|
Here's what I synth when I run into this error: "Create": {
"Fn::Join": [
"",
[
"{\"service\":\"CognitoIdentityServiceProvider\",\"action\":\"setUICustomization\",\"parameters\":{\"UserPoolId\":\"",
{
"Ref": "pool056F3F7E"
},
"\",\"ClientId\":\"ALL\",\"ImageFile\":{\"0\":137,\"1\":80,\"2\":78,\"3\":71,\"4\":13,\"5\":10,\"6\":26,\"7\":10,
....
\"4013\":96,\"4014\":130},\"CSS\":\"\"},\"physicalResourceId\":{\"id\":\"cognito-ui-logo\"}}"
]
]
}, To clean this up, imageFile looks like this:
I'm not sure, but I wonder if the culprit is the encodeJson function found here aws-cdk/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts Line 410 in ea56e69
Does anyone have any differently formatted imageFiles? |
Been looking at this issue for 2 days :( . I guess it's impossible as of right now. |
I've moved bits over to managing using SDK and JS scripts directly that I run after to get around this as I ran out of time. E.g.:
|
Hi all, this is my hacky solution, I hope you all find it helpful.
OK, so why is the onEvent lambda a no-op? Well,
IMO, the construct that creates the UserPoolDomain should do the awaiting – since you can't really login using that domain until it's Active anyway. But since it doesn't, this is the approach I took. Here are some files that may help: This is the construct, which assumes that the assets (logo and css) are in import {
Construct,
CustomResource,
Duration,
RemovalPolicy,
} from '@aws-cdk/core';
import * as s3_asset from '@aws-cdk/aws-s3-assets';
import * as cr from '@aws-cdk/custom-resources';
import * as lambda_node from '@aws-cdk/aws-lambda-nodejs';
import * as iam from '@aws-cdk/aws-iam';
import * as cognito from '@aws-cdk/aws-cognito';
import * as logs from '@aws-cdk/aws-logs';
import path from 'path';
import {UpdateCognitoUiProperties} from './cognitoUiStyle.setUiStyle';
export interface CognitoUiStyleProps {
userPool: cognito.IUserPool;
userPoolClient: cognito.IUserPoolClient;
}
export class CognitoUiStyle extends Construct {
constructor(scope: Construct, id: string, props: CognitoUiStyleProps) {
super(scope, id);
const eventFn = new lambda_node.NodejsFunction(this, 'noop', {
description: 'No op waiting for domain to come up',
});
const completeFn = new lambda_node.NodejsFunction(this, 'setUiStyle', {
description: 'Update Cognito Hosted UI Style',
});
const policy = new iam.PolicyStatement({
actions: [
'cognito-idp:SetUICustomization',
'cognito-idp:DescribeUserPool',
],
effect: iam.Effect.ALLOW,
resources: [props.userPool.userPoolArn],
});
completeFn.addToRolePolicy(policy);
// Note that the resource for DescribeUserPoolDomain needs to be "*" since we can't get an ARN for the cognitoDomain.
completeFn.addToRolePolicy(
new iam.PolicyStatement({
actions: ['cognito-idp:DescribeUserPoolDomain'],
effect: iam.Effect.ALLOW,
resources: ['*'],
})
);
const cssFileAsset = new s3_asset.Asset(this, 'Css', {
path: path.resolve(__dirname, '..', 'static', 'hosted-ui.css'),
readers: [completeFn],
});
const logoAsset = new s3_asset.Asset(this, 'Logo', {
path: path.resolve(__dirname, '..', 'static', 'logo.png'),
readers: [completeFn],
});
const provider = new cr.Provider(this, 'CrProvider', {
onEventHandler: eventFn,
isCompleteHandler: completeFn,
queryInterval: Duration.seconds(30),
totalTimeout: Duration.hours(1),
logRetention: logs.RetentionDays.ONE_WEEK,
});
const crProperties: UpdateCognitoUiProperties = {
cssLocator: {
bucketName: cssFileAsset.s3BucketName,
objectKey: cssFileAsset.s3ObjectKey,
},
logoLocator: {
bucketName: logoAsset.s3BucketName,
objectKey: logoAsset.s3ObjectKey,
},
userPoolClientId: props.userPoolClient.userPoolClientId,
userPoolId: props.userPool.userPoolId,
};
new CustomResource(this, 'CustomResource', {
serviceToken: provider.serviceToken,
removalPolicy: RemovalPolicy.DESTROY,
properties: crProperties,
resourceType: 'Custom::CognitoUiCustomization',
});
}
} This is the noop: import {CdkCustomResourceHandler} from 'aws-lambda';
export const handler: CdkCustomResourceHandler = async event => {
const {userPoolId} = event.ResourceProperties;
switch (event.RequestType) {
case 'Create':
case 'Update':
return {
PhysicalResourceId: userPoolId + '-' + new Date().toISOString(),
};
case 'Delete':
return {
PhysicalResourceId: event.PhysicalResourceId,
};
}
} This is the lambda that does the customization (I'm using runtypes to type-check the ResourceProperties and stream-buffers to read the S3 Readable stream into a Buffer). import {
CognitoIdentityProviderClient,
DescribeUserPoolCommand,
DescribeUserPoolDomainCommand,
DomainDescriptionType,
SetUICustomizationCommand,
UserPoolType,
} from '@aws-sdk/client-cognito-identity-provider';
import {GetObjectCommand, S3Client} from '@aws-sdk/client-s3';
import {
CdkCustomResourceIsCompleteHandler,
CdkCustomResourceIsCompleteResponse,
CloudFormationCustomResourceCreateEvent,
CloudFormationCustomResourceUpdateEvent,
} from 'aws-lambda';
import * as runtypes from 'runtypes';
import {Readable} from 'stream';
import streamBuffers from 'stream-buffers';
const s3ResourceLocatorRuntype = runtypes.Record({
bucketName: runtypes.String,
objectKey: runtypes.String,
});
export const updateCognitoUiPropertiesRuntype = runtypes.Record({
userPoolId: runtypes.String,
userPoolClientId: runtypes.String,
cssLocator: s3ResourceLocatorRuntype,
logoLocator: s3ResourceLocatorRuntype,
});
export type S3ResourceLocator = runtypes.Static<
typeof s3ResourceLocatorRuntype
>;
export type UpdateCognitoUiProperties = runtypes.Static<
typeof updateCognitoUiPropertiesRuntype
>;
const s3Client = new S3Client({region: process.env.AWS_REGION});
const cognitoClient = new CognitoIdentityProviderClient({
region: process.env.AWS_REGION,
});
export const handler: CdkCustomResourceIsCompleteHandler = async event => {
switch (event.RequestType) {
case 'Create':
case 'Update':
return createCognitoUiSettings(event);
case 'Delete':
return {
IsComplete: true,
};
}
}
);
async function createCognitoUiSettings(
event: CdkCustomResourceIsCompleteEvent
): Promise<CdkCustomResourceIsCompleteResponse> {
const {cssLocator, logoLocator, userPoolClientId, userPoolId} =
updateCognitoUiPropertiesRuntype.check(event.ResourceProperties);
console.log('Checking userpool domain');
const userPool = await getUserPool(userPoolId);
if (!userPool.Domain && !userPool.CustomDomain) {
return {
IsComplete: false,
};
}
const domain = await getUserPoolDomain(userPool);
if (domain.Status?.toLowerCase() !== 'active') {
console.log('Domain is not yet active');
return {
IsComplete: false,
};
}
console.log('Updating cognito settings');
// Load resources from S3
const [cssResource, logoResource] = await Promise.all([
s3Client
.send(
new GetObjectCommand({
Bucket: cssLocator.bucketName,
Key: cssLocator.objectKey,
})
)
.then(async ({Body}) => getFileContents(Body as Readable)),
s3Client
.send(
new GetObjectCommand({
Bucket: logoLocator.bucketName,
Key: logoLocator.objectKey,
})
)
.then(async ({Body}) => getFileContents(Body as Readable)),
]);
const cssContents = cssResource.toString('utf-8');
const res = await cognitoClient.send(
new SetUICustomizationCommand({
UserPoolId: userPoolId,
ClientId: userPoolClientId,
CSS: cssContents,
ImageFile: logoResource,
})
);
return {
IsComplete: true,
Data: res.UICustomization,
};
}
async function getFileContents(readable: Readable): Promise<Buffer> {
return new Promise((resolve, reject) => {
const writable = new streamBuffers.WritableStreamBuffer();
writable.on('error', err => reject(err));
writable.on('finish', () => {
const buf = writable.getContents();
if (!buf) {
reject(new Error('Failed to load data'));
return;
}
resolve(buf);
});
readable.pipe(writable);
});
}
async function getUserPool(userPoolId: string): Promise<UserPoolType> {
return cognitoClient
.send(
new DescribeUserPoolCommand({
UserPoolId: userPoolId,
})
)
.then(({UserPool}) => {
if (!UserPool) {
throw new Error('Failed to get userPool');
}
return UserPool;
});
}
async function getUserPoolDomain(
userPool: UserPoolType
): Promise<DomainDescriptionType> {
// use the CustomDomain if there is one, otherwise the 'prefix'
const domain = userPool.CustomDomain ?? userPool.Domain;
if (!domain) {
throw new Error('No domain!');
}
return cognitoClient
.send(
new DescribeUserPoolDomainCommand({
Domain: domain,
})
)
.then(({DomainDescription}) => {
if (!DomainDescription) {
throw new Error('No domain description');
}
console.log('Domain', {DomainDescription});
return DomainDescription;
});
} |
@skrud-dt
This is caused by that "streamBuffers" npm library, which wasn't updated since 2018. ;) edit: |
Looking at the README in the repo for that library it suggests:
|
Does someone find a solution with the custom resource ? This is what I got :
|
Hello All, In Javascript, the AwsCustomResouce works just fine. Add your policy And then for the ImageFile you need to use a Blob. Something like this should get this construct to synth and deploy.
This will get you a ArrayBuffer with the parts required: {uInt8ArrayContents} and {byteLength} |
I was not able to properly customize the logo via CDK AwsCustomResource.
However, when downloading the deployed logo, it's a JPG whose contents are still base64 ( I would think the proper fix for this would be in the aws-sdk serializer. |
You are not using a blob, so you're basically providing an array of chunks to ImageFile. Blobs are the transport mechanism for these chunks. Convert your file is I show above. |
@Xenoha are you passing this directly to aws-sdk client? My image file is being serialized into base64 and synthesized for Cfn properly. It's the AwsCustomResource that is not working properly for me. And it does not seem possible to control the aws-sdk calls that AwsCustomResource executions make. |
This issue has received a significant amount of attention so we are automatically upgrading its priority. A member of the community will see the re-prioritization and provide an update on the issue. |
It'd be nice to have built-in support for the logo upload. In the absence of that, I used a simple workaround to upload my logo to S3 and load it into the page using CSS. This is quite a bit simpler than the other solutions posted which use custom Cfn resources. const LOGO_URL_TOKEN = '$LOGO_URL';
const COGNITO_UI_CSS = `
.banner-customizable {
background: url('${LOGO_URL_TOKEN}') no-repeat center;
}
`;
const hostedUiAssetsBucket = new s3.Bucket(
this,
'dmc-hosted-ui-assets',
{
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
publicReadAccess: true,
}
);
new s3Deployment.BucketDeployment(this, 'hosted-ui-assets-deploy', {
destinationBucket: hostedUiAssetsBucket,
sources: [
s3Deployment.Source.asset(
path.join(__dirname, 'hosted-ui-assets')
),
],
});
const uiCustomization =
new cognito.CfnUserPoolUICustomizationAttachment(
this,
'cognito-ui',
{
clientId: 'ALL',
css: COGNITO_UI_CSS.replace(
LOGO_URL_TOKEN,
hostedUiAssetsBucket.urlForObject('logo.png')
),
userPoolId: myUserPool.userPoolId,
}
); |
a variant on @JakeStoeffler's workaround is to use a data URL for the image in CSS. then you don't need the public S3 bucket: assets/custom.css: .banner-customizable {
background: url(data:image/png;base64,$IMAGE_DATA) no-repeat center;
background-size: 256px;
height: 256px;
} cdk source: import * as cog from "aws-cdk-lib/aws-cognito";
customizeAuthUI(pool: cog.IUserPool) {
const imageData = fs.readFileSync(path.join(__dirname, 'assets/icon.png'))
.toString('base64');
const cssData = fs.readFileSync(path.join(__dirname, 'assets/custom.css'))
.toString('utf8')
.replace('$IMAGE_DATA', imageData);
new cog.CfnUserPoolUICustomizationAttachment(this, 'UserPoolHostedUILogo', {
clientId: 'ALL',
userPoolId: pool.userPoolId,
css: cssData
});
} |
As per the #6765 tracking issue, CDK doesn't yet have construct support for all Cognito things. We can use escape hatches, but it would be good to have native support to be able to apply Cognito UserPools Hosted UI Customisations (CSS, logo upload, etc)
Use Case
To apply Cognito UserPool Hosted UI customizations as part of my CDK stack, without having to resort to escape hatches/workarounds.
Proposed Solution
Implement some new constructs that support the UI customisations. As Cloudformation doesn't currently support uploading the logo, this would probably be achieved by a custom resource that calls the AWS SDK or similar.
The following code snippets are my initial attempts to workaround this with the escape hatch, but while they seemed to deploy, I don't know that they actually worked in the end. In particular, I'm not sure the latter AWS SDK call was working particularly well for the image upload. Originally I intended to use the CloudFormation part for the CSS, and just do the logo via the SDK, but I chopped and changed the code a little in trying to get things working.
You require a domain set on the UserPool before you are able to apply customisations.
The logo can apparently only be JPG/PNG.
Refs:
Warning, this code may not actually work as it is currently:
Other
This is a 🚀 Feature Request
The text was updated successfully, but these errors were encountered: