/** * 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.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; 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); private static final String OIDC_AUTHENTICATED = "oidc.authenticated"; @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 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 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) { request.setAttribute(OIDC_AUTHENTICATED, true); return ePerson.canLogIn() ? logInEPerson(context, ePerson) : BAD_ARGS; } // if self registration is disabled, warn about this failure to find a matching eperson if (! canSelfRegister()) { LOGGER.warn("Self registration is currently disabled for OIDC, and no ePerson could be found for email: {}", email); } 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[] defaultScopes = new String[] { "openid", "email", "profile" }; String scopes = String.join(" ", configurationService.getArrayProperty("authentication-oidc.scopes", defaultScopes)); if (isAnyBlank(authorizeUrl, clientId, redirectUri, clientSecret, tokenUrl, userInfoUrl)) { LOGGER.error("Missing mandatory configuration properties for OidcAuthenticationBean"); // prepare a Map of the properties which can not have sane defaults, but are still required final Map map = Map.of("authorizeUrl", authorizeUrl, "clientId", clientId, "redirectUri", redirectUri, "clientSecret", clientSecret, "tokenUrl", tokenUrl, "userInfoUrl", userInfoUrl); final Iterator> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { final Entry entry = iterator.next(); if (isBlank(entry.getValue())) { LOGGER.error(" * {} is missing", entry.getKey()); } } 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 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); 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 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 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", "email"); } private String getFirstNameAttribute() { return configurationService.getProperty("authentication-oidc.user-info.first-name", "given_name"); } private String getLastNameAttribute() { return configurationService.getProperty("authentication-oidc.user-info.last-name", "family_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; } @Override public boolean isUsed(final Context context, final HttpServletRequest request) { if (request != null && context.getCurrentUser() != null && request.getAttribute(OIDC_AUTHENTICATED) != null) { return true; } return false; } @Override public boolean canChangePassword(Context context, EPerson ePerson, String challenge) { return false; } }