Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth: Apache OLTU to Nimbusds #1432

Merged
merged 14 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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