From dcbe305ea252e564e571ef9f372ad1552fd0f1e0 Mon Sep 17 00:00:00 2001 From: Michael Bespalov Date: Tue, 14 Jul 2020 17:13:26 +0300 Subject: [PATCH 1/4] refactor client creation to allow running each of the involved steps asynchronously --- .gitignore | 2 + .../illumina/basespace/ApiClientManager.java | 191 ++++++++++++------ 2 files changed, 136 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index 61cbca6..ac7fe23 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,9 @@ *.ear *.project *.classpath +.idea /.settings /target bin/ src/test/* +*.iml \ No newline at end of file diff --git a/src/main/java/com/illumina/basespace/ApiClientManager.java b/src/main/java/com/illumina/basespace/ApiClientManager.java index 820ad43..16a3c1c 100644 --- a/src/main/java/com/illumina/basespace/ApiClientManager.java +++ b/src/main/java/com/illumina/basespace/ApiClientManager.java @@ -43,11 +43,14 @@ public final class ApiClientManager { private static Logger logger = Logger.getLogger(ApiClientManager.class.getPackage().getName()); + private static final ObjectMapper mapper = new ObjectMapper(); + private static final ApiClientManager singletonObject = new ApiClientManager(); + + private static Client httpClient = createHttpClient(); private ApiClientManager() { - } public static synchronized ApiClientManager instance() @@ -82,38 +85,41 @@ protected String requestAccessToken(ApiConfiguration configuration) { return configuration.getAccessToken(); } - + + AuthVerificationCode authCode = requestAuthVerificationCode(configuration); + + String uri = authCode.getVerificationWithCodeUri(); + BrowserLaunch.openURL(uri); + + return requestAccessTokenByAuthCode(authCode, configuration); + } + catch(BaseSpaceException bs) + { + throw bs; + } + catch(Throwable t) + { + t.printStackTrace(); + throw new RuntimeException("Error requesting access token from BaseSpace: " + t.getMessage()); + } + } + + public AuthVerificationCode requestAuthVerificationCode(ApiConfiguration configuration) throws AccessDeniedException { + try + { Form form = new Form(); form.add("client_id", configuration.getClientId()); form.add("scope", configuration.getAuthorizationScope()); form.add("response_type", "device_code"); - Client client = Client.create(new DefaultClientConfig()); - client.addFilter(new ClientFilter() - { - @Override - public ClientResponse handle(ClientRequest request) throws ClientHandlerException - { - logger.fine(request.getMethod() + " to " + request.getURI().toString()); - ClientResponse response = null; - try - { - response = getNext().handle(request); - } - catch(ClientHandlerException t) - { - throw new BaseSpaceException(t.getMessage(), t,request.getURI()); - } - return response; - } - }); - + Client client = getHttpClient(); + WebResource resource = client.resource(UriBuilder.fromUri(configuration.getApiRootUri()) .path(configuration.getVersion()) .path(configuration.getAuthorizationUriFragment()) .build()); logger.finer(resource.toString()); - + ClientResponse response = resource.accept( MediaType.APPLICATION_XHTML_XML, MediaType.APPLICATION_FORM_URLENCODED, @@ -121,60 +127,52 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio .post(ClientResponse.class,form); String responseAsJSONString = response.getEntity(String.class); logger.finer(responseAsJSONString); - - final ObjectMapper mapper = new ObjectMapper(); + AuthVerificationCode authCode = mapper.readValue(responseAsJSONString, AuthVerificationCode.class); if (authCode.getError() != null) { throw new BaseSpaceException(authCode.getErrorDescription(),null,resource.getURI()); } - + logger.finer(authCode.toString()); - - String uri = authCode.getVerificationWithCodeUri(); - BrowserLaunch.openURL(uri); - + return authCode; + } + catch(BaseSpaceException bs) + { + throw bs; + } + catch(Throwable t) + { + t.printStackTrace(); + throw new RuntimeException("Error requesting auth verification code from BaseSpace: " + t.getMessage()); + } + } + + public String requestAccessTokenByAuthCode(AuthVerificationCode authCode, ApiConfiguration configuration){ + try { //Poll for approval - form = new Form(); + Form form = new Form(); form.add("client_id", configuration.getClientId()); form.add("client_secret",configuration.getClientSecret()); form.add("code",authCode.getDeviceCode()); form.add("grant_type","device"); - - resource = client.resource(UriBuilder.fromUri(configuration.getApiRootUri()) + + Client client = getHttpClient(); + + WebResource resource = client.resource(UriBuilder.fromUri(configuration.getApiRootUri()) .path(configuration.getVersion()) .path(configuration.getAccessTokenUriFragment()) .build()); - + String accessToken = null; while(accessToken == null) { long interval = authCode.getInterval() * 1000; Thread.sleep(interval); - response = resource.accept( - MediaType.APPLICATION_XHTML_XML, - MediaType.APPLICATION_FORM_URLENCODED, - MediaType.APPLICATION_JSON) - .post(ClientResponse.class,form); - - responseAsJSONString = response.getEntity(String.class); - logger.finer(responseAsJSONString); - if(response.getClientResponseStatus().getStatusCode() > 400) - { - AccessToken error = mapper.readValue(responseAsJSONString, AccessToken.class); - throw new BaseSpaceException(resource.getURI(),error.getErrorDescription(),response.getClientResponseStatus().getStatusCode()); - } - if(response.getClientResponseStatus().getStatusCode() == 200) - { - - AccessToken token = mapper.readValue(responseAsJSONString, AccessToken.class); - accessToken = token.getAccessToken(); - } - } + accessToken = pollForAccessToken(resource, form); + } return accessToken; - - } - catch(BaseSpaceException bs) + } catch(BaseSpaceException bs) { throw bs; } @@ -184,5 +182,84 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio throw new RuntimeException("Error requesting access token from BaseSpace: " + t.getMessage()); } } + + public String pollForAccessToken(AuthVerificationCode authCode, ApiConfiguration configuration) throws BaseSpaceException { + Form form = new Form(); + form.add("client_id", configuration.getClientId()); + form.add("client_secret",configuration.getClientSecret()); + form.add("code",authCode.getDeviceCode()); + form.add("grant_type","device"); + + Client client = getHttpClient(); + WebResource resource = client.resource(UriBuilder.fromUri(configuration.getApiRootUri()) + .path(configuration.getVersion()) + .path(configuration.getAccessTokenUriFragment()) + .build()); + return pollForAccessToken(resource, form); + } + + private String pollForAccessToken(WebResource resource, Form form) throws BaseSpaceException { + try { + ClientResponse response = resource.accept( + MediaType.APPLICATION_XHTML_XML, + MediaType.APPLICATION_FORM_URLENCODED, + MediaType.APPLICATION_JSON) + .post(ClientResponse.class, form); + + String responseAsJSONString = response.getEntity(String.class); + logger.finer(responseAsJSONString); + String accessToken = null; + if (response.getClientResponseStatus().getStatusCode() > 400) { + AccessToken error = mapper.readValue(responseAsJSONString, AccessToken.class); + throw new BaseSpaceException(resource.getURI(), error.getErrorDescription(), response.getClientResponseStatus().getStatusCode()); + } + if (response.getClientResponseStatus().getStatusCode() == 200) { + + AccessToken token = mapper.readValue(responseAsJSONString, AccessToken.class); + accessToken = token.getAccessToken(); + } + return accessToken; + } catch(BaseSpaceException bs) + { + throw bs; + } + catch(Throwable t) + { + t.printStackTrace(); + throw new RuntimeException("Error polling for access token from BaseSpace: " + t.getMessage()); + } + } + + private static Client createHttpClient(){ + try { + Client client = Client.create(new DefaultClientConfig()); + client.addFilter(new ClientFilter() { + @Override + public ClientResponse handle(ClientRequest request) throws ClientHandlerException { + logger.fine(request.getMethod() + " to " + request.getURI().toString()); + ClientResponse response = null; + try { + response = getNext().handle(request); + } catch (ClientHandlerException t) { + throw new BaseSpaceException(t.getMessage(), t, request.getURI()); + } + return response; + } + }); + return client; + } catch(Throwable t){ + return null; + } + } + + private Client getHttpClient() { + if (httpClient == null) { + httpClient = createHttpClient(); + if (httpClient == null) { + throw new RuntimeException("Error creating Web Client"); + } + } + return httpClient; + } } From a7e42e39e890454564bfc07772963cc8c88d14dc Mon Sep 17 00:00:00 2001 From: Michael Bespalov Date: Tue, 14 Jul 2020 17:54:32 +0300 Subject: [PATCH 2/4] adding documentation to public API --- .../illumina/basespace/ApiClientManager.java | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/illumina/basespace/ApiClientManager.java b/src/main/java/com/illumina/basespace/ApiClientManager.java index 16a3c1c..cd63ee2 100644 --- a/src/main/java/com/illumina/basespace/ApiClientManager.java +++ b/src/main/java/com/illumina/basespace/ApiClientManager.java @@ -86,12 +86,12 @@ protected String requestAccessToken(ApiConfiguration configuration) return configuration.getAccessToken(); } - AuthVerificationCode authCode = requestAuthVerificationCode(configuration); + AuthVerificationCode authCode = getAuthVerificationCode(configuration); String uri = authCode.getVerificationWithCodeUri(); BrowserLaunch.openURL(uri); - return requestAccessTokenByAuthCode(authCode, configuration); + return startAccessTokenPolling(authCode, configuration); } catch(BaseSpaceException bs) { @@ -104,7 +104,17 @@ protected String requestAccessToken(ApiConfiguration configuration) } } - public AuthVerificationCode requestAuthVerificationCode(ApiConfiguration configuration) throws AccessDeniedException { + /** + * Get the verification code and device code + * + * This API corresponds to the following step in the authentication flow: + * https://developer.basespace.illumina.com/docs/content/documentation/authentication/obtaining-access-tokens#Gettingtheverificationcodeanddevicecode + * + * @param configuration configuration for the session + * @return an auth verification code + * @throws AccessDeniedException if an auth verification token could not be obtained from BaseSpace + */ + public AuthVerificationCode getAuthVerificationCode(ApiConfiguration configuration) throws AccessDeniedException { try { Form form = new Form(); @@ -148,7 +158,7 @@ public AuthVerificationCode requestAuthVerificationCode(ApiConfiguration configu } } - public String requestAccessTokenByAuthCode(AuthVerificationCode authCode, ApiConfiguration configuration){ + protected String startAccessTokenPolling(AuthVerificationCode authCode, ApiConfiguration configuration){ try { //Poll for approval Form form = new Form(); @@ -169,7 +179,7 @@ public String requestAccessTokenByAuthCode(AuthVerificationCode authCode, ApiCon { long interval = authCode.getInterval() * 1000; Thread.sleep(interval); - accessToken = pollForAccessToken(resource, form); + accessToken = getAccessToken(resource, form); } return accessToken; } catch(BaseSpaceException bs) @@ -183,7 +193,18 @@ public String requestAccessTokenByAuthCode(AuthVerificationCode authCode, ApiCon } } - public String pollForAccessToken(AuthVerificationCode authCode, ApiConfiguration configuration) throws BaseSpaceException { + /** + * Perform a single polling attempt to get access token. This function should be called periodically while awaiting user authorization. + * + * This API corresponds to the following step in the authentication flow: + * https://developer.basespace.illumina.com/docs/content/documentation/authentication/obtaining-access-tokens#Gettingtheaccesstokenfornonweb-basedapps + * + * @param authCode verification code received from the previous step {@link this#getAuthVerificationCode} + * @param configuration configuration for the session + * @return an access token or null if the user has not yet approved the access request + * @throws AccessDeniedException if an auth verification token could not be obtained from BaseSpace + */ + public String getAccessToken(AuthVerificationCode authCode, ApiConfiguration configuration) throws BaseSpaceException { Form form = new Form(); form.add("client_id", configuration.getClientId()); form.add("client_secret",configuration.getClientSecret()); @@ -195,10 +216,10 @@ public String pollForAccessToken(AuthVerificationCode authCode, ApiConfiguration .path(configuration.getVersion()) .path(configuration.getAccessTokenUriFragment()) .build()); - return pollForAccessToken(resource, form); + return getAccessToken(resource, form); } - private String pollForAccessToken(WebResource resource, Form form) throws BaseSpaceException { + private String getAccessToken(WebResource resource, Form form) throws BaseSpaceException { try { ClientResponse response = resource.accept( MediaType.APPLICATION_XHTML_XML, From d429ecc7e6a7beefb81897b9f69184c040e03e6f Mon Sep 17 00:00:00 2001 From: Michael Bespalov Date: Tue, 14 Jul 2020 18:47:41 +0300 Subject: [PATCH 3/4] single poll api - only device code is needed to check for access token --- src/main/java/com/illumina/basespace/ApiClientManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/illumina/basespace/ApiClientManager.java b/src/main/java/com/illumina/basespace/ApiClientManager.java index cd63ee2..eeab98e 100644 --- a/src/main/java/com/illumina/basespace/ApiClientManager.java +++ b/src/main/java/com/illumina/basespace/ApiClientManager.java @@ -199,16 +199,16 @@ protected String startAccessTokenPolling(AuthVerificationCode authCode, ApiConfi * This API corresponds to the following step in the authentication flow: * https://developer.basespace.illumina.com/docs/content/documentation/authentication/obtaining-access-tokens#Gettingtheaccesstokenfornonweb-basedapps * - * @param authCode verification code received from the previous step {@link this#getAuthVerificationCode} + * @param deviceCode device code received from the previous step {@link this#getAuthVerificationCode} * @param configuration configuration for the session * @return an access token or null if the user has not yet approved the access request * @throws AccessDeniedException if an auth verification token could not be obtained from BaseSpace */ - public String getAccessToken(AuthVerificationCode authCode, ApiConfiguration configuration) throws BaseSpaceException { + public String getAccessToken(String deviceCode, ApiConfiguration configuration) throws BaseSpaceException { Form form = new Form(); form.add("client_id", configuration.getClientId()); form.add("client_secret",configuration.getClientSecret()); - form.add("code",authCode.getDeviceCode()); + form.add("code",deviceCode); form.add("grant_type","device"); Client client = getHttpClient(); From 88f0230b47f6da610e3c47a839c0a4b3735d0b6d Mon Sep 17 00:00:00 2001 From: Michael Bespalov Date: Wed, 15 Jul 2020 16:36:20 +0300 Subject: [PATCH 4/4] when user denied access, throw error instead of keeping waiting --- .../java/com/illumina/basespace/ApiClientManager.java | 9 ++++++--- .../basespace/infrastructure/BaseSpaceException.java | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/illumina/basespace/ApiClientManager.java b/src/main/java/com/illumina/basespace/ApiClientManager.java index eeab98e..135c577 100644 --- a/src/main/java/com/illumina/basespace/ApiClientManager.java +++ b/src/main/java/com/illumina/basespace/ApiClientManager.java @@ -48,7 +48,8 @@ public final class ApiClientManager private static final ApiClientManager singletonObject = new ApiClientManager(); private static Client httpClient = createHttpClient(); - + private static final String AUTHORIZATION_PENDING_ERROR_TYPE = "authorization_pending"; + private ApiClientManager() { @@ -230,9 +231,11 @@ private String getAccessToken(WebResource resource, Form form) throws BaseSpaceE String responseAsJSONString = response.getEntity(String.class); logger.finer(responseAsJSONString); String accessToken = null; - if (response.getClientResponseStatus().getStatusCode() > 400) { + if (response.getClientResponseStatus().getStatusCode() >= 400) { AccessToken error = mapper.readValue(responseAsJSONString, AccessToken.class); - throw new BaseSpaceException(resource.getURI(), error.getErrorDescription(), response.getClientResponseStatus().getStatusCode()); + if(!error.getError().equals(AUTHORIZATION_PENDING_ERROR_TYPE)){ + throw new BaseSpaceException(resource.getURI(), error.getErrorDescription(), error.getError(), response.getClientResponseStatus().getStatusCode()); + } } if (response.getClientResponseStatus().getStatusCode() == 200) { diff --git a/src/main/java/com/illumina/basespace/infrastructure/BaseSpaceException.java b/src/main/java/com/illumina/basespace/infrastructure/BaseSpaceException.java index 27c8144..5fb0629 100644 --- a/src/main/java/com/illumina/basespace/infrastructure/BaseSpaceException.java +++ b/src/main/java/com/illumina/basespace/infrastructure/BaseSpaceException.java @@ -27,6 +27,7 @@ public class BaseSpaceException extends RuntimeException { private URI uri; private int errorCode; + private String errorType; public BaseSpaceException(String message) { @@ -52,6 +53,11 @@ public BaseSpaceException(URI uri,String message,int errorCode) { this(message,null,uri,errorCode); } + + public BaseSpaceException(URI uri, String message, String errorType, int errorCode){ + this(uri, message, errorCode); + this.errorType = errorType; + } public BaseSpaceException(String message,Throwable cause,URI uri,int errorCode) { @@ -81,6 +87,10 @@ public int getErrorCode() return errorCode; } + public String getErrorType(){ + return errorType; + } + @Override public String getMessage() {