Also, is there a way to import an existing user po...
# sst
r
Also, is there a way to import an existing user pool and identity pool to form an Auth construct?
f
Hey @Ross Coundon, lemme go thru each point:
sst.ApiAuthorizationType.JWT
 and define a 
HttpUserPoolAuthorizer
 as the 
defaultAuthorizer
If you are using identity pool, it sounds very much like you are using IAM auth instead JWT? I’d double check on this, b/c with JWT, u shouldn’t need an identity pool. (Identity pool is responsible of assigning IAM permissions to auth and unauth users)
there were a tonne of policies dictating what an authorised user and unauthorised user can do
If u want the authed users to interact directly with ur resources (ie. upload directly to an S3 bucket without proxied through API, which is the recommended way) they’d need S3 policies. But most of the interactions are probably going to go through the API (ie. talk ing DB), so there shouldn’t be a tonne of policies for the identity pool.
is there a way to import an existing user pool and identity pool to form an Auth construct?
Importing existing User Pool is not currently supported by high level CDK construct. Is this a blocker for u? I can dig into the low level Cfn constructs and put something together. Let me know!
r
Thanks @Frank. To the first point, this is where I'm confused. The app uses Amplify on the frontend to sign up users and allow them to login. The backend currently has both an identity pool and a user pool and does assign IAM permissions ( a role ) based on whether they're authenticated or not. However, none of the users are interacting directly with AWS resources, as you say, it's all via the API endpoints. So maybe this isn't needed? If I switch to IAM auth for the Api, how do I connect this up to the Auth construct? In the docs it seems to just set
Copy code
defaultAuthorizationType: ApiAuthorizationType.AWS_IAM,
is the fact that the Auth construct exists in the stack enough or am I missing some plumbing? Regarding the importing, if we can't import an existing user pool, I think we'll need to write a little user migration tool to grab signed up users out of the existing one and add them to the new one, then ask them to reset their passwords which isn't the end of the world as there's only one customer org that it applies to right now.
c
If I switch to IAM auth for the Api, how do I connect this up to the Auth construct?
If your users are not using AWS services it sounds like you should be using userpool auth and not IAM auth? If that is the case then yeah you can import an existing user pool when creating the authorizer for the API.
Copy code
const authorizer = new apigw.CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: "AN OBJECT POINTING TO EXISTING USER POOL"
    });

    const v1 = apiGateway.api.root.addResource('v1')

    const configs = v1.addResource('configs')
    configs.addMethod('ANY', new apigw.LambdaIntegration(ConfigsFunction.lambda, { proxy: true }),
      {
        authorizer: authorizer,
        authorizationType: apigw.AuthorizationType.COGNITO
      }
    )
Sorry read through the posts again, no you won't be able to import the Amplify userpool into cdk. Best bet would be to create the userpool in cdk and then point amplify at it. We just we through this exercise, and migrated userpool creation from amplify to cdk.
r
Thanks Chad. In the legacy app that I'm migrating, we don't actually create the userpool using Amplify, that's done via CFN and Serverless Framework. This is the definition of the authorizer from sls
Copy code
ApiGatewayAuthorizer: {
        Type: 'AWS::ApiGateway::Authorizer',
        DependsOn: ['ApiGatewayRestApi'],
        Properties: {
          AuthorizerResultTtlInSeconds: 300,
          IdentitySource: 'method.request.header.Authorization',
          Name: 'cognito_authorizer',
          RestApiId: {
            Ref: 'ApiGatewayRestApi',
          },
          Type: 'COGNITO_USER_POOLS',
          ProviderARNs: [
            {
              'Fn::GetAtt': ['CognitoUserPoolOmwUserPool', 'Arn'],
            },
          ],
        },
      },
and this is references on an HTTP event like this:
Copy code
SomeFunc: {
      handler: '.build/main/handler/some.handler',
      events: [
        {
          http: {
            method: 'get',
            path: '/somepath',
            cors,
            authorizer: {
              type: 'COGNITO_USER_POOLS',
              authorizerId: {
                Ref: 'ApiGatewayAuthorizer',
              },
            },
          },
        },
      ],
    },
So what you describe in terms of using a CognitoUserPoolsAuthorizer might actually be what I need. I'm trying to do this with the v2 HTTP API, whereas the legacy app was using the V1 ReST API - is your example above for v1?
I've checked and we actually pass details of the identify pool to the Amplify configuration on the front end. Although I'm not sure if this is necessary. If it is, I'm back to the question about how to set up the backend in SST/CDK
Copy code
Amplify.configure({
  Auth: {
    mandatorySignIn: true,
    region: amplifyConfig.cognito.REGION,
    userPoolId: amplifyConfig.cognito.USER_POOL_ID,
    identityPoolId: amplifyConfig.cognito.IDENTITY_POOL_ID,
    userPoolWebClientId: amplifyConfig.cognito.APP_CLIENT_ID,
  },
  API: {
    endpoints: [
      {
        name: 'someEndpoint',
        endpoint: amplifyConfig.apiGateway.URL,
        region: amplifyConfig.apiGateway.REGION,
        custom_header: async () => {
          const token = (await Auth.currentSession())
            .getIdToken()
            .getJwtToken();
          return {
            Authorization: `Bearer ${token}`,
          };
        },
      },
    ],
  },
});
c
Yeah so my example is with api gateway v1, but it would be the same with v2. https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-authorizers-readme.html
Ok makes sense; So looks like you're creating a user pool and then passing that to amplify and api. Can you create a new user pool in cdk?
If so then create user pool in CDK > pass to APIv2 pass to amplify and it should work
You can remove the identity pool if you just want basic user auth
r
I could create a new one and import the users, wouldn't be a huge deal. So you're saying I should ditch the Auth construct altogether? Do you know how I'd register this with the APIv2?
c
Which Auth construct are you talking about [can't see it in the code samples above]? But yeah, we don't have an Auth construct only a userpools and then an API Authorizer construct?
The above authorizer for the backend and then your existing amplify code should work. The only other thing you will need to handle for Amplify to work is CORS
r
Sorry, I meant the Auth construct from SST
c
We don't use SST (only CDK) but looking at the docs, the Auth construct acts a wrapper for all CDK auth constructs so it should work as well
If you want to use the Auth prop then you can point this to the Authorizer: https://docs.serverless-stack.com/constructs/Auth#cognitouserpool
r
Ok, great, thank you for all your help. Looks like I have some tinkering to do
c
No worries, happy to help!
r
So, I think, I'm back to setting the defaultAuthorizationType to ApiAuthorizationType.JWT with an HttpUserPoolAuthorizer as the defaultAuthorizer
c
Yeah that makes sense
Haha sorry, I see now we just went full circle. But yeah that should work
One thing you might have issues with is CORS when using the defaultAuthorizer. It depends how you are handling CORS but we couldn't use it because we need
OPTIONS
to be unauthenticated
r
Hmm, need to think about that
f
@Chad (cysense) thanks for the insights!
@Ross Coundon sorry I got pulled into something else last night. Reading from above, here’s what I gathered: • in current SLS setup: you are creating an API authorizer • in current SLS setup: you are creating both user pool and identity pool and passing the details to Amplify, BUT you are not sure if the identity pool was being used If I were to guess, the identity pool is not being used. So if you want to replicate your EXACT setup in SST, you should NOT use the
Auth
construct, your code would look something like this:
Copy code
// IMPORT EXISTING USER POOL & USER POOL CLIENT
const userPool = cognito.UserPool.fromUserPoolArn(...);
const userPoolClient = cognito.UserPoolClient.fromUserPoolClientId(...);

// CREATE API AUTHORIZER
const authorizer = new apigv2Authorizers.HttpUserPoolAuthorizer({
  userPool,
  userPoolClient,
});

// CREATE API
new sst.Api(this, "Api", {
  defaultAuthorizationType: ApiAuthorizationType.JWT,
  defaultAuthorizer: authorizer,
  defaultAuthorizationScopes: [..],
  routes: {
    "GET /notes": "src/list.main",
  },
});
Alternatively, you can also switch to IAM auth based off the same User Pool. No need to create a new pool (We will need to implement something b/c importing Identity Pool is not currently supported). The code would look something like:
Copy code
const userPool = // import
const userPoolClient = // import
const identityPool = // import
new sst.Auth(..) // pass in the 3 things above

// CREATE API
new sst.Api(this, "Api", {
  defaultAuthorizationType: ApiAuthorizationType.IAM,
  routes: {
    "GET /notes": "src/list.main",
  },
});
In the identity pool, you are granting IAM permissions to authed (and unauthed) users to be able to invoke the api. A benefit of using identity pool is that: 1. you can grant permissions to other AWS resources (ie. uploading to S3) 2. you can easily grant permissions to not-logged in users easily
Let me know if you want to chat further. Happy to jump on zoom or something.
r
Thanks Frank, yes you've got it. You're right, I don't think I need the identity pool but is there any harm in using the Auth construct in case it's useful for future development? I'm in two minds about importing the existing user pool, there are 91 users in there that I could just import and have them reset their password, wouldn't be the end of the world. In terms of what you've suggested, what I've currently written is quite similar but using the auth construct I.e.
Copy code
const api = new sst.Api(this, 'OmwPsoBeApi', {
      defaultAuthorizationType: sst.ApiAuthorizationType.JWT,
      defaultAuthorizer: new apigAuthorizers.HttpUserPoolAuthorizer({
        userPool: cognitoAuth.cognitoUserPool,
        userPoolClient: cognitoAuth.cognitoUserPoolClient,
      }),
    });
Does that look sensible? Thanks for your detailed help!
f
Yup that looks right. Thought, we are thinking a lot about JWT, and evaluating if making a dedicated construct for JWT makes sense.
Maybe hold off the using the
Auth
construct at the moment. I will keep you posted once we finalize on a solution for JWT.
r
ok, cool
thanks again
c
I'm in two minds about importing the existing user pool, there are 91 users in there that I could just import and have them reset their password, wouldn't be the end of the world.
Just last 2c from me. If I was in your position, I would move away from letting Amplify manage your cognito pool. I am not sure how much you have used Amplify but its been a huge pain for us working on multi account team. Literally every second day something breaks in Amplify (tbh we might also just be doing it wrong). I've spent the last month slowly moving constructs from Amplify to CDK and everything has been much smoother. I am now only using Amplify for CD (considering moving to code pipelines) and using the amplify auth UI components. So just my 2c, I wouldn't suggest anyone building anything larger than a hobby project in Amplify yet
r
Thanks Chad, that's useful feedback. All we use Amplify for is in our frontend VueJS application and for that we don't use the VueJS components, just the aws-amplify library. It wraps our API calls and a few auth (signup/signin/password reset) processes. The backend side of things is all generated by serverless framework and this little challenge is a part of the migration of that setup to SST
I don't suppose you could share some CDK code you've used for creating the Cognito components? I'm just a bit baffled here, when I try to signup I'm getting an error back from Cognito saying
Copy code
PreSignUp failed with error Missing "name" property.
But I can see via the console that name isn't a mandatory property so I'm thinking this is a side effect of some other issue. I don't currently have an identity pool set up (although the legacy app did) so this could be an issue but the docs aren't clear on whether this is required
c
Would be happy to share. QQ are you a using PreSignUp trigger?
That looks like an error from a PreSignUp trigger, so I think its your function erroring out and not cognito.
r
I am but that's not an error it could throw and I actually see no evidence from cloudwatch that it's even being called
c
Copy code
this.userPool = new cognito.UserPool(this, 'UserPool', {
      selfSignUpEnabled: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,  // !! remove for production
      signInAliases: {
        email: true,
        username: false
      },
      autoVerify: {
        email: true
      },
      lambdaTriggers: {
        preSignUp: this.PreSignupTrigger
      }
    });

    this.userPool.addClient("Frontend")
Thats what we're using for our userpool.
r
Thank you. That's pretty much what I have except for the addClient part - is that for the user pool client? This is what I have:
Copy code
const cognitoUserPool = new cognito.UserPool(this, 'cognitoUserPool', {
      selfSignUpEnabled: true,
      autoVerify: { email: true },
      signInAliases: { email: true, username: false },
      passwordPolicy: {
        minLength: 8,
        requireLowercase: true,
        requireDigits: true,
        requireSymbols: false,
        requireUppercase: true,
      },
      accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
      removalPolicy,
      userVerification: {
        emailBody: 'Thanks for signing up to the Appointment Assistant Portal! Your verification code is {####}',
        emailSubject: 'Your Appointment Assistant Portal verification code',
        emailStyle: cognito.VerificationEmailStyle.CODE,
      },
      lambdaTriggers: {
        preSignUp: preSignUpFunction,
        postConfirmation: postConfirmationFunction,
      },
    });

    const standardCognitoAttributes: cognito.StandardAttributesMask = {
      email: true,
      emailVerified: true,
      phoneNumberVerified: true,
    };

    const clientReadAttributes = new cognito.ClientAttributes().withStandardAttributes(standardCognitoAttributes);

    const clientWriteAttributes = new cognito.ClientAttributes().withStandardAttributes({
      ...standardCognitoAttributes,
      emailVerified: false,
      phoneNumberVerified: false,
    });

    const cognitoUserPoolClient = new cognito.UserPoolClient(this, 'cognitoUserPoolClient', {
      userPool: cognitoUserPool,
      authFlows: {
        userPassword: true,
      },
      supportedIdentityProviders: [cognito.UserPoolClientIdentityProvider.COGNITO],
      readAttributes: clientReadAttributes,
      writeAttributes: clientWriteAttributes,
    });
If I run the app using
sst start
for live debugging I just get
Copy code
PreSignUp failed with error RequestId: 18989d45-4a90-4edd-99da-8b673c1da1d9 Error: Runtime exited with error: exit status 1
and no logs anywhere 😞
Firebase Auth is so much more intuitive
c
So 2 things: 1. I am not sure your UserPoolClient is correct. With Amplify we're using USER_SRP_AUTH not USER_PASSWORD_AUTH. Not 100% sure if this would be an issue, but we're just using a default client and thats what works for us. 2. I am like 99% sure that your error is with the presignupfunction. Maybe try create the userpool without that and see what happens?
r
Great, on the same page with 2. Just deploying that very change. Re 1. so Cognito automatically provides a default client if you don't specify one?
c
No, I think you need to provide one, but the defaults it creates the client with works with a default amplify. Thats what our
this.userPool.addClient("Frontend")
does. But we have no need to read and write cognito attributes which its seems from your code you might need?
Ok but just looked at the docs and amplify works with user_password auth so that shouldn't be an issue
r
So, you were right, it's a problem with calling the hook functions. PresignUp works if I remove it but I get exactly the same error on the PostConfirmation
Copy code
PostConfirmation failed with error Missing \"name\" property.
The peculiar thing is, these work when hooked in via the sls framework but with CDK.
I assume it's something to do with the plumbing of those functions because I just don't think they're getting called
c
If you try test them in the console do they atleast run?
Copy code
lambdaTriggers: {
        preSignUp: preSignUpFunction,
        postConfirmation: postConfirmationFunction,
      },
This code should automatically setup the permissions for cognito to be able to invoke them so I don't think its a permissions issue
Are you lambda functions in a VPC? [Not sure if this would be an issue]
Actually the fact that you are getting those errors I think shows your lambda functions are running
If it was a networking or permissions issue I would expect it to timeout
r
Ha, think I've cracked it (although I've no idea why this works!). I removed
Copy code
supportedIdentityProviders: [cognito.UserPoolClientIdentityProvider.COGNITO],
from the User Pool Client definition...
Although now it's complaining that 'USER_SRP_AUTH is not enabled for the client' but at least I've got something I can work with now. Thank you so much for all your time and help, Chad
c
Haha great, also no idea why that worked! You can enable SRP with:
Copy code
authFlows: {
        userPassword: true,
        userSrp: true
      },
r
Yep, that did it. Now to battle with why the SST debug session isn't connecting! Thanks again