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..135c577 100644 --- a/src/main/java/com/illumina/basespace/ApiClientManager.java +++ b/src/main/java/com/illumina/basespace/ApiClientManager.java @@ -43,11 +43,15 @@ 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 static final String AUTHORIZATION_PENDING_ERROR_TYPE = "authorization_pending"; + private ApiClientManager() { - } public static synchronized ApiClientManager instance() @@ -82,38 +86,51 @@ protected String requestAccessToken(ApiConfiguration configuration) { return configuration.getAccessToken(); } - + + AuthVerificationCode authCode = getAuthVerificationCode(configuration); + + String uri = authCode.getVerificationWithCodeUri(); + BrowserLaunch.openURL(uri); + + return startAccessTokenPolling(authCode, configuration); + } + catch(BaseSpaceException bs) + { + throw bs; + } + catch(Throwable t) + { + t.printStackTrace(); + throw new RuntimeException("Error requesting access token from BaseSpace: " + t.getMessage()); + } + } + + /** + * 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(); 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 +138,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()); + } + } + + protected String startAccessTokenPolling(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 = getAccessToken(resource, form); + } return accessToken; - - } - catch(BaseSpaceException bs) + } catch(BaseSpaceException bs) { throw bs; } @@ -184,5 +193,97 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio throw new RuntimeException("Error requesting access token from BaseSpace: " + t.getMessage()); } } + + /** + * 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 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(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",deviceCode); + form.add("grant_type","device"); + + Client client = getHttpClient(); + WebResource resource = client.resource(UriBuilder.fromUri(configuration.getApiRootUri()) + .path(configuration.getVersion()) + .path(configuration.getAccessTokenUriFragment()) + .build()); + return getAccessToken(resource, form); + } + + private String getAccessToken(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); + 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) { + + 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; + } } 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() {