1- import { Output , Parameter } from '@aws-sdk/client-cloudformation' ;
1+ import { Output , Parameter , ResourceToImport } from '@aws-sdk/client-cloudformation' ;
2+ import {
3+ DescribeUserPoolCommand ,
4+ DescribeIdentityProviderCommand ,
5+ ListIdentityProvidersCommand ,
6+ } from '@aws-sdk/client-cognito-identity-provider' ;
27import { AmplifyError } from '@aws-amplify/amplify-cli-core' ;
38import { retrieveOAuthValues } from '../oauth-values-retriever' ;
49import { ForwardCategoryRefactorer } from '../workflow/forward-category-refactorer' ;
10+ import { RefactorBlueprint } from '../workflow/category-refactorer' ;
511import { CFNResource } from '../../_infra/cfn-template' ;
12+ import { AmplifyMigrationOperation } from '../../_infra/operation' ;
13+ import { extractStackNameFromId } from '../utils' ;
14+ import CLITable from 'cli-table3' ;
615
716const HOSTED_PROVIDER_META_PARAMETER_NAME = 'hostedUIProviderMeta' ;
817const HOSTED_PROVIDER_CREDENTIALS_PARAMETER_NAME = 'hostedUIProviderCreds' ;
@@ -19,27 +28,51 @@ export const USER_POOL_TYPE = 'AWS::Cognito::UserPool';
1928export const IDENTITY_POOL_TYPE = 'AWS::Cognito::IdentityPool' ;
2029export const IDENTITY_POOL_ROLE_ATTACHMENT_TYPE = 'AWS::Cognito::IdentityPoolRoleAttachment' ;
2130export const USER_POOL_DOMAIN_TYPE = 'AWS::Cognito::UserPoolDomain' ;
31+ export const USER_POOL_IDENTITY_PROVIDER_TYPE = 'AWS::Cognito::UserPoolIdentityProvider' ;
2232
2333export const RESOURCE_TYPES = [
2434 USER_POOL_TYPE ,
2535 USER_POOL_CLIENT_TYPE ,
2636 IDENTITY_POOL_TYPE ,
2737 IDENTITY_POOL_ROLE_ATTACHMENT_TYPE ,
2838 USER_POOL_DOMAIN_TYPE ,
39+ USER_POOL_IDENTITY_PROVIDER_TYPE ,
2940] ;
3041
42+ interface IdpConfig {
43+ readonly providerName : string ;
44+ readonly providerType : string ;
45+ readonly clientId : string ;
46+ readonly clientSecret : string ;
47+ readonly authorizeScopes : string ;
48+ readonly attributeMapping : Record < string , string > ;
49+ }
50+
51+ interface SocialAuthConfig {
52+ readonly userPoolId : string ;
53+ readonly domain : string ;
54+ readonly providers : IdpConfig [ ] ;
55+ }
56+
3157/**
3258 * Forward refactorer for the auth:Cognito resource.
3359 *
3460 * Moves main auth resources from Gen1 to Gen2.
61+ * For social auth apps, imports Gen1's LambdaCallout-created IDPs and domain
62+ * into the Gen2 stack as native CFN resources during the move phase.
3563 */
3664export class AuthCognitoForwardRefactorer extends ForwardCategoryRefactorer {
65+ /**
66+ * Returns the full set including domain and IDP types. These types don't exist in the
67+ * Gen1 CFN template (they're created by a Lambda trigger), so they won't appear in the
68+ * refactor mappings. They are imported into Gen2 as a separate step in move().
69+ */
3770 protected resourceTypes ( ) : string [ ] {
3871 return RESOURCE_TYPES ;
3972 }
4073
4174 /**
42- * OAuth hook: retrieves credentials and updates hostedUIProviderCreds parameter.
75+ * OAuth hook: retrieves credentials and updates the hostedUIProviderCreds parameter.
4376 */
4477 protected override async resolveOAuthParameters ( parameters : Parameter [ ] , outputs : Output [ ] ) : Promise < Parameter [ ] > {
4578 const oAuthParam = parameters . find ( ( p ) => p . ParameterKey === HOSTED_PROVIDER_META_PARAMETER_NAME ) ;
@@ -68,9 +101,104 @@ export class AuthCognitoForwardRefactorer extends ForwardCategoryRefactorer {
68101 } ) ;
69102 }
70103 credsParam . ParameterValue = JSON . stringify ( oAuthValues ) ;
104+
71105 return parameters ;
72106 }
73107
108+ /**
109+ * Executes the standard resource refactor, then imports Gen1's
110+ * physical domain and IDPs into the Gen2 stack as native CFN resources.
111+ */
112+ protected override async move ( blueprint : RefactorBlueprint ) : Promise < AmplifyMigrationOperation [ ] > {
113+ const baseOps = await super . move ( blueprint ) ;
114+
115+ const importOp = await this . buildImportSocialAuthOperation ( blueprint ) ;
116+ if ( importOp ) {
117+ return [ ...baseOps , importOp ] ;
118+ }
119+
120+ return baseOps ;
121+ }
122+
123+ /**
124+ * Builds an operation that imports Gen1's physical domain and IDPs into the
125+ * Gen2 stack. Returns undefined if the app doesn't use social auth.
126+ */
127+ private async buildImportSocialAuthOperation ( blueprint : RefactorBlueprint ) : Promise < AmplifyMigrationOperation | undefined > {
128+ const socialAuthConfig = await this . fetchSocialAuthConfig ( blueprint . sourceStackId ) ;
129+ if ( ! socialAuthConfig ) {
130+ return undefined ;
131+ }
132+
133+ const gen2StackId = blueprint . targetStackId ;
134+ const gen2Template = await this . cfn . fetchTemplate ( gen2StackId ) ;
135+ const gen2IdpLogicalIds = new Map < string , string > ( ) ;
136+ let gen2DomainLogicalId : string | undefined ;
137+
138+ // Find the Gen2 logical IDs we'll import the physical Gen1 resources into
139+ // We require providerName + logicalId to disambiguate between multiple providers
140+ for ( const [ logicalId , resource ] of Object . entries ( gen2Template . Resources ) ) {
141+ if ( resource . Type === USER_POOL_DOMAIN_TYPE ) {
142+ gen2DomainLogicalId = logicalId ;
143+ } else if ( resource . Type === USER_POOL_IDENTITY_PROVIDER_TYPE ) {
144+ const providerName = resource . Properties . ProviderName as string ;
145+ if ( providerName ) {
146+ gen2IdpLogicalIds . set ( providerName , logicalId ) ;
147+ }
148+ }
149+ }
150+
151+ if ( ! gen2DomainLogicalId ) {
152+ this . debug ( 'No Gen2 UserPoolDomain resource found — skipping import' ) ;
153+ return undefined ;
154+ }
155+
156+ if ( gen2IdpLogicalIds . size === 0 ) {
157+ this . debug ( 'No Gen2 UserPoolIdentityProvider resources found — skipping import' ) ;
158+ return undefined ;
159+ }
160+
161+ return {
162+ resource : this . resource ,
163+ validate : ( ) => undefined ,
164+ describe : async ( ) => {
165+ const gen2StackName = extractStackNameFromId ( gen2StackId ) ;
166+ const table = new CLITable ( {
167+ head : [ 'Source Physical ID' , 'Target Logical ID' ] ,
168+ style : { head : [ ] } ,
169+ } ) ;
170+ table . push ( [ socialAuthConfig . domain , gen2DomainLogicalId ! ] ) ;
171+ for ( const provider of socialAuthConfig . providers ) {
172+ const logicalId = gen2IdpLogicalIds . get ( provider . providerName ) ;
173+ if ( logicalId ) {
174+ const label =
175+ provider . providerType !== provider . providerName
176+ ? `${ provider . providerName } (${ provider . providerType } )`
177+ : provider . providerName ;
178+ table . push ( [ label , logicalId ] ) ;
179+ }
180+ }
181+ return [ `Import social auth resources into '${ gen2StackName } '\n\n${ table . toString ( ) } ` ] ;
182+ } ,
183+ execute : async ( ) => {
184+ const templateForImport = await this . cfn . fetchTemplate ( gen2StackId ) ;
185+
186+ const { resourcesToImport, templateAdditions } = this . buildImportSpec ( socialAuthConfig , gen2DomainLogicalId ! , gen2IdpLogicalIds ) ;
187+
188+ for ( const [ logicalId , resource ] of Object . entries ( templateAdditions ) ) {
189+ templateForImport . Resources [ logicalId ] = resource ;
190+ }
191+
192+ await this . cfn . importResources ( {
193+ stackName : gen2StackId ,
194+ templateBody : templateForImport ,
195+ resourcesToImport,
196+ resource : this . resource ,
197+ } ) ;
198+ } ,
199+ } ;
200+ }
201+
74202 protected override match ( sourceId : string , sourceResource : CFNResource , targetId : string , targetResource : CFNResource ) : boolean {
75203 if ( sourceResource . Type !== targetResource . Type ) {
76204 return false ;
@@ -101,4 +229,122 @@ export class AuthCognitoForwardRefactorer extends ForwardCategoryRefactorer {
101229 // in gen2 all auth resources are in a single auth nested stack
102230 return this . findNestedStack ( this . gen2Branch , 'auth' ) ;
103231 }
232+
233+ /**
234+ * Fetches domain and IDP config directly from Cognito. These resources are
235+ * Lambda-created (not in the Gen1 CFN template) so the live API is the only source.
236+ */
237+ private async fetchSocialAuthConfig ( sourceStackId : string ) : Promise < SocialAuthConfig | undefined > {
238+ const sourceStack = await this . gen1Env . fetchStack ( sourceStackId ) ;
239+ const userPoolId = ( sourceStack . Outputs ?? [ ] ) . find ( ( o ) => o . OutputKey === USER_POOL_ID_OUTPUT_KEY_NAME ) ?. OutputValue ;
240+ if ( ! userPoolId ) {
241+ return undefined ;
242+ }
243+
244+ const cognitoClient = this . gen1App . clients . cognitoIdentityProvider ;
245+
246+ const poolResponse = await cognitoClient . send ( new DescribeUserPoolCommand ( { UserPoolId : userPoolId } ) ) ;
247+ const domain = poolResponse ?. UserPool ?. Domain ;
248+ if ( ! domain ) {
249+ this . debug ( 'Gen1 UserPool has no domain — skipping social auth import' ) ;
250+ return undefined ;
251+ }
252+
253+ const listResponse = await cognitoClient . send ( new ListIdentityProvidersCommand ( { UserPoolId : userPoolId } ) ) ;
254+ const providerSummaries = listResponse ?. Providers ?? [ ] ;
255+ if ( providerSummaries . length === 0 ) {
256+ this . debug ( 'Gen1 UserPool has no identity providers — skipping social auth import' ) ;
257+ return undefined ;
258+ }
259+
260+ const providers : IdpConfig [ ] = [ ] ;
261+ for ( const summary of providerSummaries ) {
262+ const providerName = summary . ProviderName ;
263+ if ( ! providerName ) continue ;
264+
265+ const describeResponse = await cognitoClient . send (
266+ new DescribeIdentityProviderCommand ( { UserPoolId : userPoolId , ProviderName : providerName } ) ,
267+ ) ;
268+ const idp = describeResponse . IdentityProvider ;
269+ if ( ! idp ?. ProviderDetails ) continue ;
270+
271+ providers . push ( {
272+ providerName,
273+ providerType : idp . ProviderType ?? providerName ,
274+ clientId : idp . ProviderDetails . client_id ?? '' ,
275+ clientSecret : idp . ProviderDetails . client_secret ?? '' ,
276+ authorizeScopes : idp . ProviderDetails . authorize_scopes ?? '' ,
277+ attributeMapping : ( idp . AttributeMapping as Record < string , string > ) ?? { } ,
278+ } ) ;
279+ }
280+
281+ this . debug ( `Fetched social auth config: domain=${ domain } , providers=${ providers . map ( ( p ) => p . providerName ) . join ( ',' ) } ` ) ;
282+ return { userPoolId, domain, providers } ;
283+ }
284+
285+ /**
286+ * Builds the CFN import spec: template additions with DeletionPolicy: Retain
287+ * (so rollback can orphan them without deleting the physical resources) and
288+ * resource identifiers for the import change set.
289+ */
290+ private buildImportSpec (
291+ config : SocialAuthConfig ,
292+ domainLogicalId : string ,
293+ idpLogicalIds : Map < string , string > ,
294+ ) : { resourcesToImport : ResourceToImport [ ] ; templateAdditions : Record < string , CFNResource > } {
295+ const resourcesToImport : ResourceToImport [ ] = [ ] ;
296+ const templateAdditions : Record < string , CFNResource > = { } ;
297+
298+ templateAdditions [ domainLogicalId ] = {
299+ Type : USER_POOL_DOMAIN_TYPE ,
300+ DeletionPolicy : 'Retain' ,
301+ Properties : {
302+ Domain : config . domain ,
303+ UserPoolId : config . userPoolId ,
304+ } ,
305+ } ;
306+ resourcesToImport . push ( {
307+ ResourceType : USER_POOL_DOMAIN_TYPE ,
308+ LogicalResourceId : domainLogicalId ,
309+ ResourceIdentifier : {
310+ UserPoolId : config . userPoolId ,
311+ Domain : config . domain ,
312+ } ,
313+ } ) ;
314+
315+ for ( const provider of config . providers ) {
316+ const logicalId = idpLogicalIds . get ( provider . providerName ) ;
317+ if ( ! logicalId ) {
318+ this . debug ( `No Gen2 logical ID for provider ${ provider . providerName } — skipping import` ) ;
319+ continue ;
320+ }
321+
322+ templateAdditions [ logicalId ] = {
323+ Type : USER_POOL_IDENTITY_PROVIDER_TYPE ,
324+ DeletionPolicy : 'Retain' ,
325+ Properties : {
326+ UserPoolId : config . userPoolId ,
327+ ProviderName : provider . providerName ,
328+ ProviderType : provider . providerType ,
329+ ProviderDetails : {
330+ client_id : provider . clientId ,
331+ client_secret : provider . clientSecret ,
332+ authorize_scopes : provider . authorizeScopes ,
333+ } ,
334+ AttributeMapping : provider . attributeMapping ,
335+ } ,
336+ } ;
337+
338+ resourcesToImport . push ( {
339+ ResourceType : USER_POOL_IDENTITY_PROVIDER_TYPE ,
340+ LogicalResourceId : logicalId ,
341+ ResourceIdentifier : {
342+ UserPoolId : config . userPoolId ,
343+ ProviderName : provider . providerName ,
344+ } ,
345+ } ) ;
346+ }
347+
348+ return { resourcesToImport, templateAdditions } ;
349+ }
104350}
0 commit comments