diff --git a/CHANGELOG.md b/CHANGELOG.md index dba9cc18638..aaf3f52ab7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [UI]: Refreshed global search page to use Ant Design. See [PR 1409](https://github.com/phac-nml/irida/pull/1409) * [UI/Developer]: Removed old notification system hack and updated to use only Ant Design notifications. See [PR 1447](https://github.com/phac-nml/irida/pull/1447) * [UI]: Fixed bug where the `User` column was named `User Group` on the admin User Groups page. [See PR 1450](https://github.com/phac-nml/irida/pull/1450) +* [Developer]: Replaced Apache OLTU with Nimbusds for performing OAuth2 authentication flow during syncing and Galaxy exporting. See [PR 1432](https://github.com/phac-nml/irida/pull/1432) ## [22.09.7] - 2022/01/24 * [UI]: Fixed bugs on NCBI Export page preventing the NCBI `submission.xml` file from being properly written. See [PR 1451](https://github.com/phac-nml/irida/pull/1451) diff --git a/build.gradle.kts b/build.gradle.kts index 0e8305951a3..e1b573f3bab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -123,9 +123,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.security:spring-security-oauth2-authorization-server:0.3.1") implementation("org.springframework.security:spring-security-oauth2-resource-server:5.7.3") - implementation("org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.0") { - exclude(group = "org.slf4j") - } + implementation("com.nimbusds:oauth2-oidc-sdk:10.1") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.data:spring-data-envers") { exclude(group = "org.slf4j") diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaApiSecurityConfig.java b/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaApiSecurityConfig.java index 79727411cf2..5a16cfa6202 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaApiSecurityConfig.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaApiSecurityConfig.java @@ -2,8 +2,6 @@ import java.util.List; -import org.apache.oltu.oauth2.client.OAuthClient; -import org.apache.oltu.oauth2.client.URLConnectionClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -131,11 +129,6 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Bean - public OAuthClient oAuthClient() { - return new OAuthClient(new URLConnectionClient()); - } - /** * Default {@link DefaultWebSecurityExpressionHandler}. This is used by Thymeleaf's Spring Security plugin, and * isn't actually used anywhere in the back-end, but it needs to be in the back-end configuration classes because diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/exceptions/IridaOAuthProblemException.java b/src/main/java/ca/corefacility/bioinformatics/irida/exceptions/IridaOAuthProblemException.java new file mode 100644 index 00000000000..b1086aa479e --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/exceptions/IridaOAuthProblemException.java @@ -0,0 +1,9 @@ +package ca.corefacility.bioinformatics.irida.exceptions; + +public class IridaOAuthProblemException extends RuntimeException { + + public IridaOAuthProblemException(String message) { + super(message); + } + +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/errors/IridaCustomExceptionHandler.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/errors/IridaCustomExceptionHandler.java index dd1ed43f888..f19548b2c75 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/errors/IridaCustomExceptionHandler.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/errors/IridaCustomExceptionHandler.java @@ -7,7 +7,6 @@ import javax.servlet.http.HttpServletResponse; -import org.apache.oltu.oauth2.common.exception.OAuthProblemException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -23,6 +22,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import ca.corefacility.bioinformatics.irida.exceptions.EntityNotFoundException; +import ca.corefacility.bioinformatics.irida.exceptions.IridaOAuthProblemException; import ca.corefacility.bioinformatics.irida.exceptions.StorageException; import ca.corefacility.bioinformatics.irida.service.EmailController; @@ -76,14 +76,14 @@ public void handleAccessDeniedException(AccessDeniedException ex, HttpServletRes } /** - * Catch an {@link OAuthProblemException} and return an http 500 error + * Catch an {@link IridaOAuthProblemException} and return an http 500 error * - * @param ex the caught {@link OAuthProblemException} + * @param ex the caught {@link IridaOAuthProblemException} * @return A {@link ModelAndView} containing the name of the oauth error view */ - @ExceptionHandler(OAuthProblemException.class) + @ExceptionHandler(IridaOAuthProblemException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public ModelAndView handleOAuthProblemException(OAuthProblemException ex) { + public ModelAndView handleOAuthProblemException(IridaOAuthProblemException ex) { logger.error("OAuth exception: " + ex.getMessage(), ex); ModelAndView modelAndView = new ModelAndView(OAUTH_ERROR_PAGE); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/GalaxyRedirectionEndpointController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/GalaxyRedirectionEndpointController.java index 70135e877db..dcf34533119 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/GalaxyRedirectionEndpointController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/GalaxyRedirectionEndpointController.java @@ -3,19 +3,20 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import org.apache.oltu.oauth2.client.response.OAuthAuthzResponse; -import org.apache.oltu.oauth2.common.exception.OAuthProblemException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser; + /** * Controller for handling OAuth2 authorization codes for the Galaxy exporter - * - * - * */ @Controller @@ -23,39 +24,47 @@ public class GalaxyRedirectionEndpointController { private static final Logger logger = LoggerFactory.getLogger(GalaxyRedirectionEndpointController.class); + public static final String GALAXY_OAUTH_REDIRECT = "/galaxy/auth_code"; + /** * Receive the OAuth2 authorization code from IRIDA and pass it on to the client-side code - * @param model - * the model to write to - * @param request - * the incoming request - * @param session - * the user's session + * + * @param model the model to write to + * @param request the incoming request + * @param session the user's session * @return a template that will pass on the authorization code - * @throws OAuthProblemException if a valid OAuth authorization response cannot be created * @throws IllegalStateException if the callback URL is removed from an invalid session + * @throws ParseException */ - @RequestMapping("galaxy/auth_code") - public String passAuthCode(Model model, HttpServletRequest request, HttpSession session - ) throws OAuthProblemException, IllegalStateException { + @RequestMapping(GALAXY_OAUTH_REDIRECT) + public String passAuthCode(Model model, HttpServletRequest request, HttpSession session) + throws IllegalStateException, ParseException { logger.debug("Parsing auth code from HttpServletRequest"); + // Get the OAuth2 authorization code - OAuthAuthzResponse oar = OAuthAuthzResponse.oauthCodeAuthzResponse(request); - String code = oar.getCode(); + AuthenticationResponse authResponse = AuthenticationResponseParser + .parse(new ServletServerHttpRequest(request).getURI()); + + if (authResponse instanceof AuthenticationErrorResponse) { + logger.trace("Unexpected authentication error response during Galaxy OAuth flow", + authResponse.toErrorResponse().getErrorObject().toString()); + } + + String code = authResponse.toSuccessResponse().getAuthorizationCode().getValue(); model.addAttribute("auth_code", code); - + session.removeAttribute("galaxyExportToolCallbackURL"); - + return "templates/galaxy_auth_code.tmpl"; } /** - * Get the URL for the galaxy redirection location. This will be needed for the oauth flow to get its token. + * Get the URL for the galaxy redirection location. This will be needed for the oauth flow to get its token. * * @param baseURL The server's base URL * @return the URL of the galaxy oauth redirect location. */ public static String getGalaxyRedirect(String baseURL) { - return baseURL + "/galaxy/auth_code"; + return baseURL + GALAXY_OAUTH_REDIRECT; } } \ No newline at end of file diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/OltuAuthorizationController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/OltuAuthorizationController.java index 485c245ac9c..78b474d8fdd 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/OltuAuthorizationController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/OltuAuthorizationController.java @@ -10,23 +10,27 @@ import javax.servlet.http.HttpSession; import javax.ws.rs.core.UriBuilder; -import org.apache.oltu.oauth2.client.request.OAuthClientRequest; -import org.apache.oltu.oauth2.client.response.OAuthAuthzResponse; -import org.apache.oltu.oauth2.common.exception.OAuthProblemException; -import org.apache.oltu.oauth2.common.exception.OAuthSystemException; -import org.apache.oltu.oauth2.common.message.types.ResponseType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import ca.corefacility.bioinformatics.irida.exceptions.IridaOAuthProblemException; import ca.corefacility.bioinformatics.irida.model.RemoteAPI; import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; +import com.nimbusds.oauth2.sdk.*; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser; + /** * Controller for handling OAuth2 authorizations */ @@ -55,9 +59,8 @@ public OltuAuthorizationController(RemoteAPITokenService tokenService, RemoteAPI * @param remoteAPI The API we need to authenticate with * @param redirect The location to redirect back to after authentication is complete * @return A ModelAndView beginning the authentication procedure - * @throws OAuthSystemException if we can't read from the authorization server. */ - public String authenticate(HttpSession session, RemoteAPI remoteAPI, String redirect) throws OAuthSystemException { + public String authenticate(HttpSession session, RemoteAPI remoteAPI, String redirect) { // get the URI for the remote service we'll be requesting from String serviceURI = remoteAPI.getServiceURI(); @@ -68,7 +71,7 @@ public String authenticate(HttpSession session, RemoteAPI remoteAPI, String redi logger.debug("Redirect after authentication: " + redirect); // build a redirect URI to redirect to after auth flow is completed - String tokenRedirect = buildRedirectURI(); + URI tokenRedirect = buildRedirectURI(); // build state object which is used to extract the authCode to the correct remoteAPI String stateUuid = UUID.randomUUID().toString(); @@ -77,17 +80,15 @@ public String authenticate(HttpSession session, RemoteAPI remoteAPI, String redi stateMap.put("redirect", redirect); session.setAttribute(stateUuid, stateMap); - // build the redirect query to request an authorization code from the - // remote API - OAuthClientRequest request = OAuthClientRequest.authorizationLocation(serviceAuthLocation.toString()) - .setClientId(remoteAPI.getClientId()) - .setRedirectURI(tokenRedirect) - .setResponseType(ResponseType.CODE.toString()) - .setScope("read") - .setState(stateUuid) - .buildQueryMessage(); - - String locURI = request.getLocationUri(); + // build the redirect query to request an authorization code from the remote API + AuthorizationRequest request = new AuthorizationRequest.Builder(new ResponseType(ResponseType.Value.CODE), + new ClientID(remoteAPI.getClientId())).scope(new Scope("read")) + .state(new State(stateUuid)) + .redirectionURI(tokenRedirect) + .endpointURI(serviceAuthLocation) + .build(); + + String locURI = request.toURI().toString(); logger.trace("Authorization request location: " + locURI); return "redirect:" + locURI; @@ -100,19 +101,34 @@ public String authenticate(HttpSession session, RemoteAPI remoteAPI, String redi * @param response The response to redirect * @param state The state param which contains a map including apiId and redirect * @return A ModelAndView redirecting back to the resource that was requested - * @throws OAuthSystemException if we can't get an access token for the current request. - * @throws OAuthProblemException if we can't get a response from the authorization server + * @throws IridaOAuthProblemException + * @throws ParseException */ @RequestMapping(TOKEN_ENDPOINT) public String getTokenFromAuthCode(HttpServletRequest request, HttpServletResponse response, - @RequestParam("state") String state) throws OAuthSystemException, OAuthProblemException { + @RequestParam("state") String state) throws IridaOAuthProblemException, ParseException { + HttpSession session = request.getSession(); // Get the OAuth2 auth code - OAuthAuthzResponse oar = OAuthAuthzResponse.oauthCodeAuthzResponse(request); - String code = oar.getCode(); - logger.trace("Received auth code: " + code); - - HttpSession session = request.getSession(); + AuthenticationResponse authResponse = AuthenticationResponseParser + .parse(new ServletServerHttpRequest(request).getURI()); + + if (authResponse instanceof AuthenticationErrorResponse) { + logger.trace("Unexpected authentication response"); + throw new IridaOAuthProblemException(authResponse.toErrorResponse().getErrorObject().toString()); + } + + // Verify existence of state + if (authResponse.getState() == null) { + logger.trace("Authentication response did not contain a state"); + throw new IridaOAuthProblemException("State missing from authentication response"); + } else if (session.getAttribute(authResponse.getState().toString()) == null) { + logger.trace("State not present in session"); + throw new IridaOAuthProblemException("State not present in session"); + } + + AuthorizationCode code = authResponse.toSuccessResponse().getAuthorizationCode(); + logger.trace("Received auth code: " + code.getValue()); Map stateMap = (Map) session.getAttribute(state); @@ -120,7 +136,7 @@ public String getTokenFromAuthCode(HttpServletRequest request, HttpServletRespon String redirect = stateMap.get("redirect"); // Build the redirect URI to request a token from - String tokenRedirect = buildRedirectURI(); + URI tokenRedirect = buildRedirectURI(); // Read the RemoteAPI from the RemoteAPIService and get the base URI RemoteAPI remoteAPI = remoteAPIService.read(apiId); @@ -136,11 +152,11 @@ public String getTokenFromAuthCode(HttpServletRequest request, HttpServletRespon * * @return the redirect uri */ - private String buildRedirectURI() { + private URI buildRedirectURI() { - URI build = UriBuilder.fromUri(serverBase).path(TOKEN_ENDPOINT).build(); + URI redirectURI = UriBuilder.fromUri(serverBase).path(TOKEN_ENDPOINT).build(); - return build.toString(); + return redirectURI; } /** diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteAPIController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteAPIController.java index 846aec7ca72..9c802df02aa 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteAPIController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteAPIController.java @@ -5,7 +5,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -87,10 +86,9 @@ public String connectToAPI(@PathVariable Long apiId, Model model) { * @param request The incoming request method * @param ex The thrown exception * @return A redirect to the {@link OltuAuthorizationController}'s authentication - * @throws OAuthSystemException if the request cannot be authenticated. */ @ExceptionHandler(IridaOAuthException.class) - public String handleOAuthException(HttpServletRequest request, IridaOAuthException ex) throws OAuthSystemException { + public String handleOAuthException(HttpServletRequest request, IridaOAuthException ex) { logger.debug("Caught IridaOAuthException. Beginning OAuth2 authentication token flow."); String requestURI = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); HttpSession session = request.getSession(); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/RemoteAPITokenService.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/RemoteAPITokenService.java index 6578e42dcbd..f01a8b96a1d 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/RemoteAPITokenService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/RemoteAPITokenService.java @@ -1,12 +1,15 @@ package ca.corefacility.bioinformatics.irida.service; -import org.apache.oltu.oauth2.common.exception.OAuthProblemException; -import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import java.net.URI; import ca.corefacility.bioinformatics.irida.exceptions.EntityNotFoundException; +import ca.corefacility.bioinformatics.irida.exceptions.IridaOAuthProblemException; import ca.corefacility.bioinformatics.irida.model.RemoteAPI; import ca.corefacility.bioinformatics.irida.model.RemoteAPIToken; +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.ParseException; + /** * Service for saving and reading tokens for Remote APIs */ @@ -14,8 +17,7 @@ public interface RemoteAPITokenService { /** * Add a token to the store for a given service * - * @param token - * The token to create + * @param token The token to create * @return the created token */ public RemoteAPIToken create(RemoteAPIToken token); @@ -23,21 +25,17 @@ public interface RemoteAPITokenService { /** * Get a token for a given service * - * @param remoteAPI - * The {@link RemoteAPI} of the service root + * @param remoteAPI The {@link RemoteAPI} of the service root * @return A String OAuth2 token - * @throws EntityNotFoundException - * if the token could not be found + * @throws EntityNotFoundException if the token could not be found */ public RemoteAPIToken getToken(RemoteAPI remoteAPI) throws EntityNotFoundException; /** * Delete a token for the logged in user and a given {@link RemoteAPI} * - * @param remoteAPI - * the {@link RemoteAPI} to delete a token for - * @throws EntityNotFoundException - * if the token could not be found + * @param remoteAPI the {@link RemoteAPI} to delete a token for + * @throws EntityNotFoundException if the token could not be found */ public void delete(RemoteAPI remoteAPI) throws EntityNotFoundException; @@ -48,18 +46,16 @@ public interface RemoteAPITokenService { * @param remoteAPI the remote api to get a token for * @param tokenRedirect a redirect url to get the token from * @return the newly created token - * @throws OAuthSystemException If ther's a problem building the token request - * @throws OAuthProblemException If there's a problem with the token request + * @throws IridaOAuthProblemException If we could not successfully get a token + * @throws ParseException If there's a problem with the token request */ - public RemoteAPIToken createTokenFromAuthCode(String authcode, RemoteAPI remoteAPI, String tokenRedirect) - throws OAuthSystemException, OAuthProblemException; + public RemoteAPIToken createTokenFromAuthCode(AuthorizationCode authcode, RemoteAPI remoteAPI, URI tokenRedirect) + throws IridaOAuthProblemException, ParseException; /** - * Update a given {@link RemoteAPI}'s OAuth token by refresh token if - * available + * Update a given {@link RemoteAPI}'s OAuth token by refresh token if available * - * @param api - * the API to update + * @param api the API to update * @return the most recent token if available */ public RemoteAPIToken updateTokenFromRefreshToken(RemoteAPI api); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/RemoteAPITokenServiceImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/RemoteAPITokenServiceImpl.java index d9d0dc7acd9..7352e7bcb64 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/RemoteAPITokenServiceImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/RemoteAPITokenServiceImpl.java @@ -1,16 +1,11 @@ package ca.corefacility.bioinformatics.irida.service.impl; +import java.io.IOException; import java.net.URI; import java.util.Date; import javax.ws.rs.core.UriBuilder; -import org.apache.oltu.oauth2.client.OAuthClient; -import org.apache.oltu.oauth2.client.request.OAuthClientRequest; -import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse; -import org.apache.oltu.oauth2.common.exception.OAuthProblemException; -import org.apache.oltu.oauth2.common.exception.OAuthSystemException; -import org.apache.oltu.oauth2.common.message.types.GrantType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional; import ca.corefacility.bioinformatics.irida.exceptions.EntityNotFoundException; +import ca.corefacility.bioinformatics.irida.exceptions.IridaOAuthProblemException; import ca.corefacility.bioinformatics.irida.model.RemoteAPI; import ca.corefacility.bioinformatics.irida.model.RemoteAPIToken; import ca.corefacility.bioinformatics.irida.model.user.User; @@ -29,6 +25,14 @@ import ca.corefacility.bioinformatics.irida.repositories.user.UserRepository; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; +import com.nimbusds.oauth2.sdk.*; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.nimbusds.oauth2.sdk.token.RefreshToken; + /** * Service implementation for storing and reading remote api tokens */ @@ -40,15 +44,12 @@ public class RemoteAPITokenServiceImpl implements RemoteAPITokenService { private RemoteApiTokenRepository tokenRepository; private UserRepository userRepository; - private final OAuthClient oauthClient; @Autowired - public RemoteAPITokenServiceImpl(RemoteApiTokenRepository tokenRepository, UserRepository userRepository, - OAuthClient oauthClient) { + public RemoteAPITokenServiceImpl(RemoteApiTokenRepository tokenRepository, UserRepository userRepository) { super(); this.tokenRepository = tokenRepository; this.userRepository = userRepository; - this.oauthClient = oauthClient; } /** @@ -98,84 +99,95 @@ public RemoteAPIToken updateTokenFromRefreshToken(RemoteAPI api) { try { token = getToken(api); + } catch (EntityNotFoundException ex) { + logger.debug("Token not found for api " + api + ". Cannot update access token."); + return token; + } - String refreshToken = token.getRefreshToken(); - - if (refreshToken != null) { - URI serviceTokenLocation = UriBuilder.fromUri(api.getServiceURI()).path("oauth").path("token").build(); + String refreshTokenValue = token.getRefreshToken(); + if (refreshTokenValue == null) { + logger.trace("No refresh token for api " + api + ". Cannot update access token."); + return token; + } else { + RefreshToken refreshToken = new RefreshToken(refreshTokenValue); + + URI serviceTokenLocation = UriBuilder.fromUri(api.getServiceURI()).path("oauth").path("token").build(); + + ClientAuthentication clientAuth = new ClientSecretBasic(new ClientID(api.getClientId()), + new Secret(api.getClientSecret())); + + TokenRequest tokenRequest = new TokenRequest(serviceTokenLocation, clientAuth, + new RefreshTokenGrant(refreshToken)); + + TokenResponse tokenResponse; + try { + tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send()); + } catch (ParseException e) { + logger.error("Updating token by refresh token failed", e); + return token; + } catch (IOException e) { + logger.error("Updating token by refresh token failed", e); + return token; + } - OAuthClientRequest tokenRequest = OAuthClientRequest.tokenLocation(serviceTokenLocation.toString()) - .setClientId(api.getClientId()) - .setClientSecret(api.getClientSecret()) - .setRefreshToken(refreshToken) - .setGrantType(GrantType.REFRESH_TOKEN) - .buildBodyMessage(); + if (!tokenResponse.indicatesSuccess()) { + // We got an error response... + TokenErrorResponse errorResponse = tokenResponse.toErrorResponse(); + logger.error("Updating token by refresh token failed", errorResponse.getErrorObject().toString()); + } else { - OAuthJSONAccessTokenResponse accessToken = oauthClient.accessToken(tokenRequest); + AccessTokenResponse accessTokenResponse = tokenResponse.toSuccessResponse(); - token = buildTokenFromResponse(accessToken, api); + token = buildTokenFromResponse(accessTokenResponse, api); delete(api); token = create(token); logger.trace("Token for api " + api + " updated by refresh token."); - } else { - logger.trace("No refresh token for api " + api + ". Cannot update access token."); + } - } catch (EntityNotFoundException ex) { - logger.debug("Token not found for api " + api + ". Cannot update access token."); - } catch (OAuthProblemException | OAuthSystemException ex) { - logger.error("Updating token by refresh token failed", ex.getMessage()); } return token; } /** - * Get a new token from the given auth code - * - * @param authcode the auth code to create a token for - * @param remoteAPI the remote api to get a token for - * @param tokenRedirect a redirect url to get the token from - * @return a new token - * @throws OAuthSystemException If building the token request fails - * @throws OAuthProblemException If the token request fails + * {@inheritDoc} */ @Transactional - public RemoteAPIToken createTokenFromAuthCode(String authcode, RemoteAPI remoteAPI, String tokenRedirect) - throws OAuthSystemException, OAuthProblemException { + public RemoteAPIToken createTokenFromAuthCode(AuthorizationCode authcode, RemoteAPI remoteAPI, URI tokenRedirect) + throws IridaOAuthProblemException, ParseException { String serviceURI = remoteAPI.getServiceURI(); // Build the token location for this service URI serviceTokenLocation = UriBuilder.fromUri(serviceURI).path("oauth").path("token").build(); logger.trace("Remote token location: " + serviceTokenLocation); + ClientAuthentication clientAuth = new ClientSecretBasic(new ClientID(remoteAPI.getClientId()), + new Secret(remoteAPI.getClientSecret())); + // Create the token request form the given auth code - OAuthClientRequest tokenRequest = OAuthClientRequest.tokenLocation(serviceTokenLocation.toString()) - .setClientId(remoteAPI.getClientId()) - .setClientSecret(remoteAPI.getClientSecret()) - .setRedirectURI(tokenRedirect) - .setCode(authcode) - .setGrantType(GrantType.AUTHORIZATION_CODE) - .buildBodyMessage(); + TokenRequest tokenRequest = new TokenRequest(serviceTokenLocation, clientAuth, + new AuthorizationCodeGrant(authcode, tokenRedirect)); // execute the request - OAuthJSONAccessTokenResponse accessTokenResponse = oauthClient.accessToken(tokenRequest); + TokenResponse tokenResponse; + try { + tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send()); + } catch (IOException e) { + throw new IridaOAuthProblemException(e.getMessage()); + } - // read the response for the access token - String accessToken = accessTokenResponse.getAccessToken(); + if (!tokenResponse.indicatesSuccess()) { + // We got an error response... + TokenErrorResponse errorResponse = tokenResponse.toErrorResponse(); + throw new IridaOAuthProblemException(errorResponse.getErrorObject().toString()); + } - // Handle Refresh Tokens - String refreshToken = accessTokenResponse.getRefreshToken(); - - // check the token expiry - Long expiresIn = accessTokenResponse.getExpiresIn(); - Long currentTime = System.currentTimeMillis(); - Date expiry = new Date(currentTime + (expiresIn * ONE_SECOND_IN_MS)); - logger.trace("Token expiry: " + expiry); + AccessTokenResponse accessTokenResponse = tokenResponse.toSuccessResponse(); // create the OAuth2 token and store it - RemoteAPIToken token = new RemoteAPIToken(accessToken, refreshToken, remoteAPI, expiry); + RemoteAPIToken token = buildTokenFromResponse(accessTokenResponse, remoteAPI); return create(token); } @@ -216,21 +228,27 @@ protected RemoteAPIToken getOldTokenId(RemoteAPIToken apiToken) { return apiToken; } - private RemoteAPIToken buildTokenFromResponse(OAuthJSONAccessTokenResponse accessTokenResponse, - RemoteAPI remoteAPI) { + private RemoteAPIToken buildTokenFromResponse(AccessTokenResponse accessTokenResponse, RemoteAPI remoteAPI) { // read the response for the access token - String accessToken = accessTokenResponse.getAccessToken(); + AccessToken accessToken = accessTokenResponse.getTokens().getAccessToken(); // Handle Refresh Tokens - String refreshToken = accessTokenResponse.getRefreshToken(); + RefreshToken refreshToken = accessTokenResponse.getTokens().getRefreshToken(); // check the token expiry - Long expiresIn = accessTokenResponse.getExpiresIn(); + Long expiresIn = accessToken.getLifetime(); Long currentTime = System.currentTimeMillis(); Date expiry = new Date(currentTime + (expiresIn * ONE_SECOND_IN_MS)); logger.trace("Token expiry: " + expiry); // create the OAuth2 token - return new RemoteAPIToken(accessToken, refreshToken, remoteAPI, expiry); + RemoteAPIToken token; + if (refreshToken != null) { + token = new RemoteAPIToken(accessToken.getValue(), refreshToken.getValue(), remoteAPI, expiry); + } else { + token = new RemoteAPIToken(accessToken.getValue(), remoteAPI, expiry); + } + + return token; } } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/OltuAuthorizationControllerTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/OltuAuthorizationControllerTest.java index ba1f02afb4c..d42951f3d10 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/OltuAuthorizationControllerTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/OltuAuthorizationControllerTest.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.util.HashMap; @@ -12,8 +13,6 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import org.apache.oltu.oauth2.common.exception.OAuthProblemException; -import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -23,8 +22,10 @@ import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.ParseException; + +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -47,7 +48,7 @@ public void setUp() { } @Test - public void testAuthenticate() throws IOException, OAuthSystemException, UnsupportedEncodingException { + public void testAuthenticate() throws IOException, UnsupportedEncodingException { RemoteAPI remoteAPI = new RemoteAPI("name", "http://uri", "a description", "id", "secret"); remoteAPI.setId(1L); String redirect = "http://base"; @@ -62,12 +63,11 @@ public void testAuthenticate() throws IOException, OAuthSystemException, Unsuppo } @Test - public void testGetTokenFromAuthCode() - throws IOException, OAuthSystemException, OAuthProblemException, URISyntaxException { + public void testGetTokenFromAuthCode() throws IOException, URISyntaxException, ParseException { Long apiId = 1L; RemoteAPI remoteAPI = new RemoteAPI("name", "http://remoteLocation", "a description", "id", "secret"); remoteAPI.setId(apiId); - String code = "code"; + AuthorizationCode code = new AuthorizationCode("code"); String redirect = "http://originalPage"; String stateUuid = UUID.randomUUID().toString(); @@ -81,30 +81,30 @@ public void testGetTokenFromAuthCode() HttpServletRequest request = mock(HttpServletRequest.class); HttpServletResponse response = mock(HttpServletResponse.class); - Map requestParams = new HashMap<>(); - requestParams.put("code", new String[] { code }); + StringBuffer url = new StringBuffer(serverBase + OltuAuthorizationController.TOKEN_ENDPOINT); + String query = "code=" + code.getValue() + "&state=" + stateUuid; - when(request.getParameterMap()).thenReturn(requestParams); + when(request.getRequestURL()).thenReturn(url); + when(request.getQueryString()).thenReturn(query); when(request.getSession()).thenReturn(session); controller.getTokenFromAuthCode(request, response, stateUuid); verify(apiService).read(apiId); - ArgumentCaptor redirectArg = ArgumentCaptor.forClass(String.class); + ArgumentCaptor redirectArg = ArgumentCaptor.forClass(URI.class); verify(tokenService).createTokenFromAuthCode(eq(code), eq(remoteAPI), redirectArg.capture()); - String capturedRedirect = redirectArg.getValue(); + String capturedRedirect = redirectArg.getValue().toString(); assertTrue(capturedRedirect.contains(serverBase)); } @Test - public void testGetTokenFromAuthCodeExtraSlash() - throws IOException, OAuthSystemException, OAuthProblemException, URISyntaxException { + public void testGetTokenFromAuthCodeExtraSlash() throws IOException, URISyntaxException, ParseException { Long apiId = 1L; RemoteAPI remoteAPI = new RemoteAPI("name", "http://remoteLocation", "a description", "id", "secret"); remoteAPI.setId(apiId); - String code = "code"; + AuthorizationCode code = new AuthorizationCode("code"); String redirect = "http://originalPage"; String stateUuid = UUID.randomUUID().toString(); @@ -121,21 +121,52 @@ public void testGetTokenFromAuthCodeExtraSlash() HttpServletRequest request = mock(HttpServletRequest.class); HttpServletResponse response = mock(HttpServletResponse.class); - Map requestParams = new HashMap<>(); - requestParams.put("code", new String[] { code }); + StringBuffer url = new StringBuffer(serverBase + "/" + OltuAuthorizationController.TOKEN_ENDPOINT); + String query = "code=" + code.getValue() + "&state=" + stateUuid; - when(request.getParameterMap()).thenReturn(requestParams); + when(request.getRequestURL()).thenReturn(url); + when(request.getQueryString()).thenReturn(query); when(request.getSession()).thenReturn(session); controller.getTokenFromAuthCode(request, response, stateUuid); verify(apiService).read(apiId); - ArgumentCaptor redirectArg = ArgumentCaptor.forClass(String.class); + ArgumentCaptor redirectArg = ArgumentCaptor.forClass(URI.class); verify(tokenService).createTokenFromAuthCode(eq(code), eq(remoteAPI), redirectArg.capture()); - String capturedRedirect = redirectArg.getValue(); + String capturedRedirect = redirectArg.getValue().toString(); assertTrue(capturedRedirect.contains(serverBase)); assertFalse(capturedRedirect.contains(serverBase + "//")); } + + @Test + public void testGetTokenFromAuthCodeError() throws IOException, URISyntaxException, ParseException { + Long apiId = 1L; + RemoteAPI remoteAPI = new RemoteAPI("name", "http://remoteLocation", "a description", "id", "secret"); + remoteAPI.setId(apiId); + AuthorizationCode code = new AuthorizationCode("code"); + String redirect = "http://originalPage"; + + String stateUuid = UUID.randomUUID().toString(); + Map stateMap = new HashMap(); + stateMap.put("apiId", apiId.toString()); + stateMap.put("redirect", redirect); + + when(session.getAttribute(stateUuid)).thenReturn(stateMap); + + when(apiService.read(apiId)).thenReturn(remoteAPI); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + StringBuffer url = new StringBuffer(serverBase + OltuAuthorizationController.TOKEN_ENDPOINT); + + when(request.getRequestURL()).thenReturn(url); + when(request.getQueryString()).thenReturn(""); + when(request.getSession()).thenReturn(session); + + assertThrows(ParseException.class, () -> { + controller.getTokenFromAuthCode(request, response, stateUuid); + }); + } } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIControllerTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIControllerTest.java index bf3cd2d286b..e946daf426f 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIControllerTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIControllerTest.java @@ -4,7 +4,6 @@ import javax.servlet.http.HttpServletRequest; -import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.ui.ExtendedModelMap; @@ -64,7 +63,7 @@ public void testConnectToAPIActiveToken() { } @Test - public void testHandleOAuthException() throws IOException, OAuthSystemException { + public void testHandleOAuthException() throws IOException { HttpServletRequest request = mock(HttpServletRequest.class); String redirect = "http://request"; when(request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).thenReturn(redirect); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/service/impl/unit/RemoteAPITokenServiceImplTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/service/impl/unit/RemoteAPITokenServiceImplTest.java index afd5f65aa73..cc02e1c7695 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/service/impl/unit/RemoteAPITokenServiceImplTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/service/impl/unit/RemoteAPITokenServiceImplTest.java @@ -1,15 +1,7 @@ package ca.corefacility.bioinformatics.irida.service.impl.unit; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.util.Date; -import org.apache.oltu.oauth2.client.OAuthClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -24,6 +16,10 @@ import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; import ca.corefacility.bioinformatics.irida.service.impl.RemoteAPITokenServiceImpl; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + /** * Unit tests class for {@link RemoteAPITokenServiceImpl} */ @@ -34,14 +30,12 @@ public class RemoteAPITokenServiceImplTest { private RemoteAPIToken remoteAPIToken; private RemoteAPI remoteAPI; private User user; - private OAuthClient oauthClient; @BeforeEach public void setUp() { tokenRepository = mock(RemoteApiTokenRepository.class); userRepo = mock(UserRepository.class); - oauthClient = mock(OAuthClient.class); - service = new RemoteAPITokenServiceImpl(tokenRepository, userRepo, oauthClient); + service = new RemoteAPITokenServiceImpl(tokenRepository, userRepo); user = new User("tom", "an@email.com", "password1", "tom", "matthews", "123456789"); remoteAPI = new RemoteAPI("apiname", "http://nowhere", "a test api", "clientId", "clientSecret"); @@ -52,9 +46,9 @@ public void setUp() { @Test public void testAddToken() { when(userRepo.loadUserByUsername(user.getUsername())).thenReturn(user); - + service.create(remoteAPIToken); - + verify(tokenRepository).save(remoteAPIToken); verify(userRepo, times(2)).loadUserByUsername(user.getUsername()); verify(tokenRepository, times(0)).delete(remoteAPIToken); @@ -64,9 +58,9 @@ public void testAddToken() { public void testAddTokenExisting() { when(userRepo.loadUserByUsername(user.getUsername())).thenReturn(user); when(tokenRepository.readTokenForApiAndUser(remoteAPI, user)).thenReturn(remoteAPIToken); - + service.create(remoteAPIToken); - + verify(tokenRepository).save(remoteAPIToken); verify(userRepo, times(2)).loadUserByUsername(user.getUsername()); verify(tokenRepository).readTokenForApiAndUser(remoteAPI, user); @@ -85,11 +79,11 @@ public void testAddTokenNotLoggedIn() { public void testGetToken() { when(userRepo.loadUserByUsername(user.getUsername())).thenReturn(user); when(tokenRepository.readTokenForApiAndUser(remoteAPI, user)).thenReturn(remoteAPIToken); - + RemoteAPIToken token = service.getToken(remoteAPI); - + assertEquals(remoteAPIToken, token); - + verify(userRepo).loadUserByUsername(user.getUsername()); verify(tokenRepository).readTokenForApiAndUser(remoteAPI, user); } @@ -98,7 +92,7 @@ public void testGetToken() { public void testGetNotExisting() { when(userRepo.loadUserByUsername(user.getUsername())).thenReturn(user); when(tokenRepository.readTokenForApiAndUser(remoteAPI, user)).thenReturn(null); - + assertThrows(EntityNotFoundException.class, () -> { service.getToken(remoteAPI); });