[DSC-196] Porting of OpenID authentication provider

This commit is contained in:
Luca Giamminonni
2021-10-13 16:31:56 +02:00
committed by Hardy Pottinger
parent 79c7ab77b1
commit 6521564d71
14 changed files with 1508 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.authenticate;
import static java.lang.String.format;
import static java.net.URLEncoder.encode;
import static org.apache.commons.lang.BooleanUtils.toBoolean;
import static org.apache.commons.lang3.StringUtils.isAnyBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.UnsupportedEncodingException;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.dspace.authenticate.oidc.OidcClient;
import org.dspace.authenticate.oidc.model.OidcTokenResponseDTO;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.EPersonService;
import org.dspace.services.ConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* OpenID Connect Authentication for DSpace.
*
* This implementation doesn't allow/needs to register user, which may be holder
* by the openID authentication server.
*
* @link https://openid.net/developers/specs/
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*/
public class OidcAuthenticationBean implements AuthenticationMethod {
public static final String OIDC_AUTH_ATTRIBUTE = "oidc";
private final static String LOGIN_PAGE_URL_FORMAT = "%s?client_id=%s&response_type=code&scope=%s&redirect_uri=%s";
private static final Logger LOGGER = LoggerFactory.getLogger(OidcAuthenticationBean.class);
@Autowired
private ConfigurationService configurationService;
@Autowired
private OidcClient oidcClient;
@Autowired
private EPersonService ePersonService;
@Override
public boolean allowSetPassword(Context context, HttpServletRequest request, String username) throws SQLException {
return false;
}
@Override
public boolean isImplicit() {
return false;
}
@Override
public boolean canSelfRegister(Context context, HttpServletRequest request, String username) throws SQLException {
return canSelfRegister();
}
@Override
public void initEPerson(Context context, HttpServletRequest request, EPerson eperson) throws SQLException {
}
@Override
public List<Group> getSpecialGroups(Context context, HttpServletRequest request) throws SQLException {
return List.of();
}
@Override
public String getName() {
return OIDC_AUTH_ATTRIBUTE;
}
@Override
public int authenticate(Context context, String username, String password, String realm, HttpServletRequest request)
throws SQLException {
if (request == null) {
LOGGER.warn("Unable to authenticate using OIDC because the request object is null.");
return BAD_ARGS;
}
if (request.getAttribute(OIDC_AUTH_ATTRIBUTE) == null) {
return NO_SUCH_USER;
}
String code = (String) request.getParameter("code");
if (StringUtils.isEmpty(code)) {
LOGGER.warn("The incoming request has not code parameter");
return NO_SUCH_USER;
}
return authenticateWithOidc(context, code, request);
}
private int authenticateWithOidc(Context context, String code, HttpServletRequest request) throws SQLException {
OidcTokenResponseDTO accessToken = getOidcAccessToken(code);
if (accessToken == null) {
LOGGER.warn("No access token retrieved by code");
return NO_SUCH_USER;
}
Map<String, Object> userInfo = getOidcUserInfo(accessToken.getAccessToken());
String email = getAttributeAsString(userInfo, getEmailAttribute());
if (StringUtils.isBlank(email)) {
LOGGER.warn("No email found in the user info attributes");
return NO_SUCH_USER;
}
EPerson ePerson = ePersonService.findByEmail(context, email);
if (ePerson != null) {
return ePerson.canLogIn() ? logInEPerson(context, ePerson) : BAD_ARGS;
}
return canSelfRegister() ? registerNewEPerson(context, userInfo, email) : NO_SUCH_USER;
}
@Override
public String loginPageURL(Context context, HttpServletRequest request, HttpServletResponse response) {
String authorizeUrl = configurationService.getProperty("authentication-oidc.authorize-endpoint");
String clientId = configurationService.getProperty("authentication-oidc.client-id");
String clientSecret = configurationService.getProperty("authentication-oidc.client-secret");
String redirectUri = configurationService.getProperty("authentication-oidc.redirect-url");
String tokenUrl = configurationService.getProperty("authentication-oidc.token-endpoint");
String userInfoUrl = configurationService.getProperty("authentication-oidc.user-info-endpoint");
String scopes = String.join(" ", configurationService.getArrayProperty("authentication-oidc.scopes"));
String email = getEmailAttribute();
if (isAnyBlank(authorizeUrl, clientId, redirectUri, scopes, clientSecret, tokenUrl, userInfoUrl, email)) {
LOGGER.error("Missing mandatory configuration properties for OidcAuthenticationBean");
return "";
}
try {
return format(LOGIN_PAGE_URL_FORMAT, authorizeUrl, clientId, scopes, encode(redirectUri, "UTF-8"));
} catch (UnsupportedEncodingException e) {
LOGGER.error(e.getMessage(), e);
return "";
}
}
private int logInEPerson(Context context, EPerson ePerson) {
context.setCurrentUser(ePerson);
return SUCCESS;
}
private int registerNewEPerson(Context context, Map<String, Object> userInfo, String email) throws SQLException {
try {
context.turnOffAuthorisationSystem();
EPerson eperson = ePersonService.create(context);
eperson.setNetid(email);
eperson.setEmail(email);
String firstName = getAttributeAsString(userInfo, getFirstNameAttribute());
if (firstName != null) {
eperson.setFirstName(context, firstName);
}
String lastName = getAttributeAsString(userInfo, getLastNameAttribute());
if (lastName != null) {
eperson.setLastName(context, lastName);
}
eperson.setCanLogIn(true);
eperson.setSelfRegistered(true);
ePersonService.update(context, eperson);
context.setCurrentUser(eperson);
context.dispatchEvents();
return SUCCESS;
} catch (Exception ex) {
LOGGER.error("An error occurs registering a new EPerson from OIDC", ex);
context.rollback();
return NO_SUCH_USER;
} finally {
context.restoreAuthSystemState();
}
}
private OidcTokenResponseDTO getOidcAccessToken(String code) {
try {
return oidcClient.getAccessToken(code);
} catch (Exception ex) {
LOGGER.error("An error occurs retriving the OIDC access_token", ex);
return null;
}
}
private Map<String, Object> getOidcUserInfo(String accessToken) {
try {
return oidcClient.getUserInfo(accessToken);
} catch (Exception ex) {
LOGGER.error("An error occurs retriving the OIDC user info", ex);
return Map.of();
}
}
private String getAttributeAsString(Map<String, Object> userInfo, String attribute) {
if (isBlank(attribute)) {
return null;
}
return userInfo.containsKey(attribute) ? String.valueOf(userInfo.get(attribute)) : null;
}
private String getEmailAttribute() {
return configurationService.getProperty("authentication-oidc.user-info.email");
}
private String getFirstNameAttribute() {
return configurationService.getProperty("authentication-oidc.user-info.first-name");
}
private String getLastNameAttribute() {
return configurationService.getProperty("authentication-oidc.user-info.last-name");
}
private boolean canSelfRegister() {
String canSelfRegister = configurationService.getProperty("authentication-oidc.can-self-register", "true");
if (isBlank(canSelfRegister)) {
return true;
}
return toBoolean(canSelfRegister);
}
public OidcClient getOidcClient() {
return this.oidcClient;
}
public void setOidcClient(OidcClient oidcClient) {
this.oidcClient = oidcClient;
}
}