aws-sdk-java-v2
aws-sdk-java-v2 copied to clipboard
Cognito user pool (with device tracking ON): Session refresh fails with error - "Invalid Refresh Token"
Describe the bug
I am trying to retrieve a new access token using the Cognito refresh token through the InitiateAuth API. The user pool has device tracking enabled. As per the documentation. I added the DEVICE_KEY parameter for REFRESH_TOKEN_AUTH auth flow, but I am keep getting Invalid Refresh Token error. If I turn off the device tracking, it will work fine.
Initiating refresh request: { "AuthFlow" : "REFRESH_TOKEN_AUTH", "AuthParameters" : { "DEVICE_KEY" : "us-east-1_6e3cb5cf-6c22-4110-97cc-fa43e3b13280", "REFRESH_TOKEN" : "eyJjdHkiOiJKV1QiLC..." }, "ClientId" : "28bt9dfokuqqcn6g68k10gjr6h" }
Expected Behavior
Return access and id token
Current Behavior
NotAuthorizedException - Invalid Refresh Token
Reproduction Steps
It's exact same issue similar to https://github.com/aws/aws-sdk-cpp/discussions/1903 and https://github.com/aws/aws-sdk-cpp/issues/851
Possible Solution
No response
Additional Information/Context
No response
AWS Java SDK version used
cognitoIdentityprovider 2.17.258
JDK version used
openjdk version "17.0.3" 2022-04-19
Operating System and version
macOS Monterey 12.5.1
NotAuthorizedException
usually means the aws credential or role used to make the request does not have permission to perform the action. Can this be the case? How often do you see the error, sometimes or every time you make a REFRESH_TOKEN_AUTH call?
Can you provide the full stacktrace with the error?
@debora-ito I got the same error every time if I disable the device tracking. It will work with the same refresh token.
Can you show a full stacktrace?
software.amazon.awssdk.services.cognitoidentityprovider.model.NotAuthorizedException: Invalid Refresh Token. (Service: CognitoIdentityProvider, Status Code: 400, Request ID: 8b328158-a9a6-47dc-b53c-3d69e68109f4) at software.amazon.awssdk.core.internal.http.CombinedResponseHandler.handleErrorResponse(CombinedResponseHandler.java:125) at software.amazon.awssdk.core.internal.http.CombinedResponseHandler.handleResponse(CombinedResponseHandler.java:82) at software.amazon.awssdk.core.internal.http.CombinedResponseHandler.handle(CombinedResponseHandler.java:60) at software.amazon.awssdk.core.internal.http.CombinedResponseHandler.handle(CombinedResponseHandler.java:41) at software.amazon.awssdk.core.internal.http.pipeline.stages.HandleResponseStage.execute(HandleResponseStage.java:40) at software.amazon.awssdk.core.internal.http.pipeline.stages.HandleResponseStage.execute(HandleResponseStage.java:30) at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:73) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:42) at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:78) at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:40) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:50) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:36) at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:81) at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:36) at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:56) at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:36) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.executeWithTimer(ApiCallTimeoutTrackingStage.java:80) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:60) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:42) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:48) at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:31) at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:37) at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:26) at software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient$RequestExecutionBuilderImpl.execute(AmazonSyncHttpClient.java:193) at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.invoke(BaseSyncClientHandler.java:103) at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.doExecute(BaseSyncClientHandler.java:167) at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.lambda$execute$1(BaseSyncClientHandler.java:82) at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.measureApiCallSuccess(BaseSyncClientHandler.java:175) at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.execute(BaseSyncClientHandler.java:76) at software.amazon.awssdk.core.client.handler.SdkSyncClientHandler.execute(SdkSyncClientHandler.java:45) at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler.execute(AwsSyncClientHandler.java:56) at software.amazon.awssdk.services.cognitoidentityprovider.DefaultCognitoIdentityProviderClient.initiateAuth(DefaultCognitoIdentityProviderClient.java:5498) at software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient.initiateAuth(CognitoIdentityProviderClient.java:7691) at com.paywithextend.auth.service.CognitoService.renewAuth(CognitoService.kt:237) at com.paywithextend.auth.api.AuthController.renewAuth(AuthController.kt:256) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1070) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) at javax.servlet.http.HttpServlet.service(HttpServlet.java:681) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at com.paywithextend.common.logging.LogbackDiagnosticContext.doFilter(LogbackDiagnosticContext.kt:34) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.AbstractRequestLoggingFilter.doFilterInternal(AbstractRequestLoggingFilter.java:289) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:337) at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilterInternal(AuthorizationFilter.java:73) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:122) at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:116) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:126) at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:109) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:198) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:112) at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:82) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:221) at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186) at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354) at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:687) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.base/java.lang.Thread.run(Thread.java:833)
Same error if I make the call with oauth2 endpoint.
curl --location --request POST 'https://auth-staging-stage.auth.us-east-1.amazoncognito.com/oauth2/token'
Getting 400 invalid grant error if the user pool has device tracking enabled.
{ "eventVersion": "1.08", "userIdentity": { "accountId": "949075974776" }, "eventTime": "2022-09-14T22:47:23Z", "eventSource": "cognito-idp.amazonaws.com", "eventName": "Token_POST", "awsRegion": "us-east-1", "sourceIPAddress": "71.190.59.27", "userAgent": "curl/7.79.1", "errorCode": "400", "requestParameters": null, "responseElements": null, "additionalEventData": { "responseParameters": { "status": 400 }, "requestParameters": { "refresh_token": [ "HIDDEN_DUE_TO_SECURITY_REASONS" ], "device_key": [ "us-east-1_e5aa0983-759e-478c-bf4b-6fa73b2af7a5" ], "grant_type": [ "refresh_token" ], "client_id": [ "28bt9dfokuqqcn6g68k10gjr6h" ] }, "userPoolDomain": "auth-staging-stage.auth.us-east-1.amazoncognito.com", "userPoolId": "us-east-1_UQwd7UCJb" }, "requestID": "956f5629-1456-4c07-87c7-d7cda4c0b76b", "eventID": "2179c3cc-21ca-4638-90de-c6da567245c7", "readOnly": false, "eventType": "AwsServiceEvent", "managementEvent": true, "recipientAccountId": "949075974776", "serviceEventDetails": { "serviceAccountId": "745623467555" }, "eventCategory": "Management" }
Same request will go through and get 200 ok response if I turn off the device tracking. user same refresh token from previous steps.
{ "eventVersion": "1.08", "userIdentity": { "accountId": "949075974776" }, "eventTime": "2022-09-14T22:47:59Z", "eventSource": "cognito-idp.amazonaws.com", "eventName": "Token_POST", "awsRegion": "us-east-1", "sourceIPAddress": "71.190.59.27", "userAgent": "curl/7.79.1", "requestParameters": null, "responseElements": null, "additionalEventData": { "responseParameters": { "status": 200 }, "requestParameters": { "refresh_token": [ "HIDDEN_DUE_TO_SECURITY_REASONS" ], "device_key": [ "us-east-1_e5aa0983-759e-478c-bf4b-6fa73b2af7a5" ], "grant_type": [ "refresh_token" ], "client_id": [ "28bt9dfokuqqcn6g68k10gjr6h" ] }, "userPoolDomain": "auth-staging-stage.auth.us-east-1.amazoncognito.com", "userPoolId": "us-east-1_UQwd7UCJb" }, "requestID": "4045cb79-7ac9-4fa3-bb24-5be071a76e5e", "eventID": "7c5929b6-9c0e-41bf-a8db-14c1ba4344bd", "readOnly": false, "eventType": "AwsServiceEvent", "managementEvent": true, "recipientAccountId": "949075974776", "serviceEventDetails": { "serviceAccountId": "745623467555" }, "eventCategory": "Management" }
This looks like a validation on the service side related to that device tracking. I don't have expertise in Cognito so I'll relay this to the Cognito team and wait for their reply.
Here's the Cognito team response, and I quote:
Investigation Details: Please note, if “remember devices" feature is turned on (Either to "Always" or "User Opt In") for the user pool, then the device needs to be confirmed first with the help of ConfirmDevice API call before refreshing the tokens. Hence, after USER_PASSWORD_AUTH we have to confirm the device and then perform the REFRESH_TOKEN_AUTH, else will observe the error Invalid Refresh Token.
So, the detailed flow will be as follows:
Perform USER_PASSWORD_AUTH using the InitiatAuth API, which will return following output:
== OUTPUT ==
{
"ChallengeParameters": {},
"AuthenticationResult": {
"AccessToken": "----ACCESS_TOKEN----",
"ExpiresIn": 3600,
"TokenType": "Bearer",
"RefreshToken": "----REFRESH_TOKEN----",
"IdToken": "----ID_TOKEN----",
"NewDeviceMetadata": {
"DeviceKey": "----DEVICE_KEY----",
"DeviceGroupKey": "----DEVICE_GROUP----"
}
}
}
Next, perform ConfirmDevice API call with below parameters:
== INPUT ==
ACCESS_TOKEN - access token that you received in the initiate auth output
DEVICE_KEY - device key that you received in the initiate auth output
DeviceSecretVerifierConfigType - This parameter has 2 sub value as shown below
"DeviceSecretVerifierConfig": {
"PasswordVerifier": "string",
"Salt": "string"
}
To generate the “DeviceSecretVerifierConfig” parameters please refer the “Call ConfirmDevice” section in LINK
For more information on ConfirmDevice API call please refer LINK
== OUTPUT ==
{
"UserConfirmationNecessary": false
}
Finally, perform the REFRESH_TOKEN_AUTH with below parameters:
== INPUT ==
REFRESH_TOKEN - taken from the output of USER_PASSWORD_AUTH,
SECRET_HASH - [OPTINAL] If client secret is configured for the App cleint
DEVICE_KEY - taken from the output of USER_PASSWORD_AUTH.
For more information about initiateAuth please refer LINK
== OUTPUT ==
{
"ChallengeParameters": {},
"AuthenticationResult": {
"AccessToken": "----ACCESS_TOKEN----",
"ExpiresIn": 3600,
"TokenType": "Bearer",
"IdToken": "----ID_TOKEN----"
}
}
The customer is missing the ConfirmDevice step, which is causing the issue.
Let us know if this helps.
Thanks will try it out
@debora-ito
This suggested flow is a bit problematic if you have a lot of users logging in as the ConfirmDevice
call utilizes the UserResourceUpdate
quota which is low and not adjustable..
I've tried to defer the confirm device call to be done lazily (before token refresh) and I occasionally get an invalid device key
error. This makes me think the confirm device call has a window of validity for each device key.
How would you suggest working around these scalability limitations?