openmhealth/shimmer

Programatic Fitbit Authorization Fails

soto14 opened this issue · 11 comments

I am following the Shimmer API process for programmatic authorization to the Fitbit API: https://github.com/openmhealth/shimmer#authorize-access-programmatically.

An org.openmhealth.shim.ShimException: A request for Fitbit data has failed. exception is thrown when calling the http://<:8083/authorize/{shimKey}/callback URL, and authorization to Fitbit with the Shimmer API fails.

The Shimmer APIs for programmatic authorization do work for Google Fit. Does the Shimmer API currently work with Fitbit Authorization? Is there a different process to use for programmatic authorization to Fitbit with the Shimmer API?

Thank you for the help!

@soto14 what's the cause of the exception?

I have a Spring Boot application with an endpoint mapped to /authorize/{shim-key}/callback. After authentication the Fitbit API redirects to that endpoint, which then calls the /authorize/{shim-key}/callback?code={code}&state={state} of the Shimmer API. The Shimmer API returns an HTTP 500 for that request.

Please let me know if this helps. I'm happy to provide any additional information. What's interesting is everything works for authentication with Google Fit.

The logs for the resource-server container have the following:

2018-08-23 21:39:18.732 WARN 11 --- [tp1997287019-14] org.eclipse.jetty.server.HttpChannel : /authorize/fitbit/callback

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.openmhealth.shim.ShimException: A request for Fitbit data has failed.
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:687)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:841)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1650)
at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:206)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1637)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:114)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:170)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:214)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1637)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1637)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:105)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1637)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1637)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1637)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:533)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:190)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1595)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:188)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1253)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:168)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:473)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1564)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:166)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1155)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
at org.eclipse.jetty.server.Server.handle(Server.java:564)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:317)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:251)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:279)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:110)
at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:124)
at org.eclipse.jetty.util.thread.Invocable.invokePreferred(Invocable.java:128)
at org.eclipse.jetty.util.thread.Invocable$InvocableExecutor.invoke(Invocable.java:222)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:294)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce(EatWhatYouKill.java:126)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:673)
at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:591)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.openmhealth.shim.ShimException: A request for Fitbit data has failed.
at org.openmhealth.shim.fitbit.FitbitShim.executeRequest(FitbitShim.java:317)
at org.openmhealth.shim.fitbit.FitbitShim.getDataForDateRange(FitbitShim.java:405)
at org.openmhealth.shim.fitbit.FitbitShim.getData(FitbitShim.java:204)
at org.openmhealth.shim.OAuth2Shim.trigger(OAuth2Shim.java:244)
at org.openmhealth.shim.OAuth2Shim.processRedirect(OAuth2Shim.java:196)
at org.openmhealth.shimmer.common.controller.LegacyAuthorizationController.processRedirect(LegacyAuthorizationController.java:185)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
... 77 common frames omitted
Caused by: org.springframework.web.client.HttpClientErrorException: 403 Forbidden
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:63)
at org.springframework.security.oauth2.client.http.OAuth2ErrorHandler.handleError(OAuth2ErrorHandler.java:172)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:628)
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:328)
at org.openmhealth.shim.fitbit.FitbitShim.executeRequest(FitbitShim.java:313)
... 95 common frames omitted

Since it works for Google Fit and not for Fitbit, it's most likely an issue with Fitbit setup. Either Fitbit credentials aren't set up properly in Shimmer, or the URLs are misconfigured on the Fitbit side. I'd try to get it working with Postman first to confirm the Fitbit side is set up right.

I used Postman to trouble shoot the issue this morning. Using Postman to authorize FitBit access as described here: https://github.com/openmhealth/shimmer#authorize-access-using-postman yields an HTTP 200 response with the following JSON payload.
{ "type": "ERROR", "details": "Access token denied.", "accessParameters": null, "requestParameters": null }

When I use Postman to test the FitBit API as described in the FitBit OAuth 2.0 tutorial: https://dev.fitbit.com/apps/oauthinteractivetutorial. I am able to authenticate with the FitBit API and query data.

When using the Shimmer APIs for FitBit authorization the following errors are logged. I'll only post a small portion of the stack trace to help readability.

org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException: Access token denied. at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:142) at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken(AuthorizationCodeAccessTokenProvider.java:209) at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:148) at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:121) at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221) at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173) at org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:648) at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:628) at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:328) at org.openmhealth.shim.fitbit.FitbitShim.executeRequest(FitbitShim.java:313) at org.openmhealth.shim.fitbit.FitbitShim.getDataForDateRange(FitbitShim.java:405) at org.openmhealth.shim.fitbit.FitbitShim.getData(FitbitShim.java:204) at org.openmhealth.shim.OAuth2Shim.trigger(OAuth2Shim.java:244) at org.openmhealth.shim.OAuth2Shim.processRedirect(OAuth2Shim.java:196) at org.openmhealth.shimmer.common.controller.LegacyAuthorizationController.processRedirect(LegacyAuthorizationController.java:185)

Looking through the stack trace and the code is making me think that the bearer token may not be added to the request as the FitBit API is expecting? Does that sound plausible?

Can you triple check your client ID and secret are correct and in the right place in the configuration?

The Docker Compose file uses the openmhealth/shimmer-resource-server image and passes in an environment file for Shimmer. The environment file sets the OPENMHEALTH_SHIM_FITBIT_CLIENT_ID and OPENMHEALTH_SHIM_FITBIT_CLIENT_SECRET environment variables and sets their values to OAuth 2.0 Client ID and Client Secret listed by FitBit for the application. The values in the environment file match those for the registered FitBit application.

@emersonf do you have any example projects that use Shimmer FitBit authentication? I'd just like to compare it to what I'm doing to make sure I'd using it as expected.

@emersonf great news. I was able to get Fitbit authentication working within my application. I had to move the authentication into a pop-up window for the application and then the process started working.

@soto14 glad you got it sorted out!

@soto14 I am facing the same problem with fitbit authentication. Can you tell me how did you move the authentication into a pop-up window. I am actually new to it and trying all of it out as instructed on the shimmer github post. TIA!

@Prachimk12 I was able to get it working by moving the login to a pop-up window. You can see how I did it here: https://github.com/gt-health/OMH-on-FHIR/blob/master/omhclient/app/components/omh-on-fhir-service/omh-on-fhir-service.js.