Skip to content

Commit

Permalink
Merge pull request #1432 from phac-nml/oltu-to-nimbusds
Browse files Browse the repository at this point in the history
OAuth: Apache OLTU to Nimbusds
  • Loading branch information
apetkau authored Jan 27, 2023
2 parents d60c04f + c8cbbef commit 3af1512
Show file tree
Hide file tree
Showing 13 changed files with 257 additions and 195 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ca.corefacility.bioinformatics.irida.exceptions;

public class IridaOAuthProblemException extends RuntimeException {

public IridaOAuthProblemException(String message) {
super(message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,68 @@
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
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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();

Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -100,27 +101,42 @@ 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<String, String> stateMap = (Map<String, String>) session.getAttribute(state);

Long apiId = Long.parseLong(stateMap.get("apiId"));
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);
Expand All @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 3af1512

Please sign in to comment.