diff --git a/infra/cdk/lib/auth-stack.ts b/infra/cdk/lib/auth-stack.ts index f07b87a..1bdce6c 100644 --- a/infra/cdk/lib/auth-stack.ts +++ b/infra/cdk/lib/auth-stack.ts @@ -24,6 +24,9 @@ export class AuthStack extends cdk.Stack { const googleClientSecret = this.node.tryGetContext("googleClientSecret") as string | undefined; const cognitoDomainPrefix = (this.node.tryGetContext("cognitoDomainPrefix") as string | undefined) ?? "eventform-auth"; const webHost = (this.node.tryGetContext("webHost") as string | undefined) ?? "eventform.murugappan.dev"; + // Second SPA (chat-app) reuses THIS pool + hosted UI domain so a user signs + // in once across both apps. Override host via: -c chatWebHost=chat.murugappan.dev + const chatWebHost = (this.node.tryGetContext("chatWebHost") as string | undefined) ?? "chat.murugappan.dev"; if (!googleClientId) throw new Error("Context key 'googleClientId' is required (-c googleClientId=)"); if (!googleClientSecret) throw new Error("Context key 'googleClientSecret' is required (-c googleClientSecret=)"); @@ -118,6 +121,35 @@ export class AuthStack extends cdk.Stack { // The app client must be created after the Google IdP is registered appClient.node.addDependency(googleIdp); + // Second app client: chat-app. A separate SPA on its own origin, but it + // reuses this user pool and hosted UI domain. Because the Cognito SSO + // session cookie lives on the shared hosted-UI domain, a user who has + // signed in to eventform is silently signed in to chat-app (and vice + // versa) — only the callback URLs and client id differ. PKCE, no secret. + const chatAppClient = new cognito.UserPoolClient(this, "ChatAppClient", { + userPool, + userPoolClientName: "chat-app-web", + generateSecret: false, + oAuth: { + flows: { authorizationCodeGrant: true }, + scopes: [ + cognito.OAuthScope.OPENID, + cognito.OAuthScope.EMAIL, + cognito.OAuthScope.PROFILE, + ], + callbackUrls: [ + `https://${chatWebHost}/auth/callback`, + "http://localhost:5173/auth/callback", + ], + logoutUrls: [ + `https://${chatWebHost}`, + "http://localhost:5173", + ], + }, + supportedIdentityProviders: [cognito.UserPoolClientIdentityProvider.GOOGLE], + }); + chatAppClient.node.addDependency(googleIdp); + // ---- Outputs ---- this.issuerUrl = `https://cognito-idp.${this.region}.amazonaws.com/${userPool.userPoolId}`; this.clientId = appClient.userPoolClientId; @@ -133,6 +165,13 @@ export class AuthStack extends cdk.Stack { description: "Cognito app client ID (set as COGNITO_CLIENT_ID / VITE_COGNITO_CLIENT_ID)", }); + new cdk.CfnOutput(this, "ChatAppClientId", { + value: chatAppClient.userPoolClientId, + description: + "chat-app Cognito app client ID. Set as chat-app's COGNITO_CLIENT_ID / VITE_COGNITO_CLIENT_ID. " + + "chat-app's COGNITO_ISSUER = this stack's IssuerUrl (same pool); hosted UI domain is shared (SSO).", + }); + new cdk.CfnOutput(this, "HostedDomain", { value: this.hostedDomainUrl, description: "Cognito hosted UI base URL (set as VITE_COGNITO_DOMAIN)", diff --git a/infra/cdk/test/auth-stack.test.ts b/infra/cdk/test/auth-stack.test.ts index 0f93c91..8faf18f 100644 --- a/infra/cdk/test/auth-stack.test.ts +++ b/infra/cdk/test/auth-stack.test.ts @@ -71,6 +71,21 @@ describe("AuthStack", () => { }); }); + it("creates a second app client for chat-app with its own callback URLs", () => { + const template = Template.fromStack(makeStack()); + template.resourceCountIs("AWS::Cognito::UserPoolClient", 2); + template.hasResourceProperties("AWS::Cognito::UserPoolClient", { + CallbackURLs: [ + "https://chat.murugappan.dev/auth/callback", + "http://localhost:5173/auth/callback", + ], + LogoutURLs: ["https://chat.murugappan.dev", "http://localhost:5173"], + GenerateSecret: false, + AllowedOAuthFlows: ["code"], + SupportedIdentityProviders: Match.arrayWith(["Google"]), + }); + }); + it("creates a Cognito hosted domain with the configured prefix", () => { const template = Template.fromStack(makeStack()); template.hasResourceProperties("AWS::Cognito::UserPoolDomain", {