org.xwiki.contrib.oidc.auth.internal.OIDCUserManager.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.contrib.oidc.auth.internal.OIDCUserManager.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.contrib.oidc.auth.internal;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.security.Principal;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.securityfilter.realm.SimplePrincipal;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.context.concurrent.ExecutionContextRunnable;
import org.xwiki.contrib.oidc.OIDCUserInfo;
import org.xwiki.contrib.oidc.auth.internal.store.OIDCUserStore;
import org.xwiki.contrib.oidc.event.OIDCUserEventData;
import org.xwiki.contrib.oidc.event.OIDCUserUpdated;
import org.xwiki.contrib.oidc.event.OIDCUserUpdating;
import org.xwiki.contrib.oidc.provider.internal.OIDCException;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.SpaceReference;
import org.xwiki.observation.ObservationManager;
import org.xwiki.query.QueryException;

import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import com.nimbusds.openid.connect.sdk.claims.Address;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.classes.BaseClass;
import com.xpn.xwiki.user.api.XWikiRightService;
import com.xpn.xwiki.web.XWikiRequest;

/**
 * Various tools to manipulate users.
 * 
 * @version $Id$
 * @since 1.2
 */
@Component(roles = OIDCUserManager.class)
@Singleton
public class OIDCUserManager {
    @Inject
    private Provider<XWikiContext> xcontextProvider;

    @Inject
    private OIDCClientConfiguration configuration;

    @Inject
    private OIDCUserStore store;

    @Inject
    private ObservationManager observation;

    @Inject
    private ComponentManager componentManager;

    @Inject
    private Logger logger;

    private Executor executor = Executors.newFixedThreadPool(1);

    public void updateUserInfoAsync() throws MalformedURLException, URISyntaxException {
        final URI userInfoEndpoint = this.configuration.getUserInfoOIDCEndpoint();
        final IDTokenClaimsSet idToken = this.configuration.getIdToken();
        final BearerAccessToken accessToken = this.configuration.getAccessToken();

        this.executor.execute(new ExecutionContextRunnable(new Runnable() {
            @Override
            public void run() {
                try {
                    updateUserInfo(userInfoEndpoint, idToken, accessToken);
                } catch (Exception e) {
                    logger.error("Failed to update user informations", e);
                }
            }
        }, this.componentManager));
    }

    public void checkUpdateUserInfo() {
        Date date = this.configuration.removeUserInfoExpirationDate();
        if (date != null) {
            if (date.before(new Date())) {
                try {
                    updateUserInfoAsync();
                } catch (Exception e) {
                    this.logger.error("Failed to update user informations", e);
                }

                // Restart user information expiration counter
                this.configuration.resetUserInfoExpirationDate();
            } else {
                // Put back the date
                this.configuration.setUserInfoExpirationDate(date);
            }
        }
    }

    public Principal updateUserInfo(BearerAccessToken accessToken)
            throws URISyntaxException, IOException, ParseException, OIDCException, XWikiException, QueryException {
        Principal principal = updateUserInfo(this.configuration.getUserInfoOIDCEndpoint(),
                this.configuration.getIdToken(), accessToken);

        // Restart user information expiration counter
        this.configuration.resetUserInfoExpirationDate();

        return principal;
    }

    public Principal updateUserInfo(URI userInfoEndpoint, IDTokenClaimsSet idToken, BearerAccessToken accessToken)
            throws IOException, ParseException, OIDCException, XWikiException, QueryException {
        // Get OIDC user info
        UserInfoRequest userinfoRequest = new UserInfoRequest(userInfoEndpoint, accessToken);
        HTTPRequest userinfoHTTP = userinfoRequest.toHTTPRequest();
        userinfoHTTP.setHeader("User-Agent", this.getClass().getPackage().getImplementationTitle() + '/'
                + this.getClass().getPackage().getImplementationVersion());
        HTTPResponse httpResponse = userinfoHTTP.send();
        UserInfoResponse userinfoResponse = UserInfoResponse.parse(httpResponse);

        if (!userinfoResponse.indicatesSuccess()) {
            UserInfoErrorResponse error = (UserInfoErrorResponse) userinfoResponse;
            throw new OIDCException("Failed to get user info", error.getErrorObject());
        }

        UserInfoSuccessResponse userinfoSuccessResponse = (UserInfoSuccessResponse) userinfoResponse;
        UserInfo userInfo = userinfoSuccessResponse.getUserInfo();

        // Update/Create XWiki user
        return updateUser(idToken, userInfo);
    }

    private Principal updateUser(IDTokenClaimsSet idToken, UserInfo userInfo)
            throws XWikiException, QueryException {
        XWikiDocument userDocument = this.store.searchDocument(idToken.getIssuer().getValue(),
                userInfo.getSubject().toString());

        XWikiDocument modifiableDocument;
        boolean newUser;
        if (userDocument == null) {
            userDocument = getNewUserDocument(idToken, userInfo);

            newUser = true;
            modifiableDocument = userDocument;
        } else {
            // Don't change the document author to not change document execution right

            newUser = false;
            modifiableDocument = userDocument.clone();
        }

        XWikiContext xcontext = this.xcontextProvider.get();

        // Set user fields
        BaseObject userObject = modifiableDocument
                .getXObject(xcontext.getWiki().getUserClass(xcontext).getDocumentReference(), true, xcontext);

        // Address
        Address address = userInfo.getAddress();
        if (address != null) {
            userObject.set("address", address.getFormatted(), xcontext);
        }

        // Email
        if (userInfo.getEmail() != null) {
            userObject.set("email", userInfo.getEmail().toUnicodeString(), xcontext);
        }

        // Last name
        if (userInfo.getFamilyName() != null) {
            userObject.set("last_name", userInfo.getFamilyName(), xcontext);
        }

        // First name
        if (userInfo.getGivenName() != null) {
            userObject.set("first_name", userInfo.getGivenName(), xcontext);
        }

        // Phone
        if (userInfo.getPhoneNumber() != null) {
            userObject.set("phone", userInfo.getPhoneNumber(), xcontext);
        }

        // Default locale
        if (userInfo.getLocale() != null) {
            userObject.set("default_language", Locale.forLanguageTag(userInfo.getLocale()).toString(), xcontext);
        }

        // Time Zone
        if (userInfo.getZoneinfo() != null) {
            userObject.set("timezone", userInfo.getZoneinfo(), xcontext);
        }

        // Website
        if (userInfo.getWebsite() != null) {
            userObject.set("blog", userInfo.getWebsite().toString(), xcontext);
        }

        // Avatar
        if (userInfo.getPicture() != null) {
            try {
                String filename = FilenameUtils.getName(userInfo.getPicture().toString());
                URLConnection connection = userInfo.getPicture().toURL().openConnection();
                connection.setRequestProperty("User-Agent", this.getClass().getPackage().getImplementationTitle()
                        + '/' + this.getClass().getPackage().getImplementationVersion());
                try (InputStream content = connection.getInputStream()) {
                    modifiableDocument.addAttachment(filename, content, xcontext);
                }
                userObject.set("avatar", filename, xcontext);
            } catch (IOException e) {
                this.logger.warn("Failed to get user avatar from URL [{}]: {}", userInfo.getPicture(),
                        ExceptionUtils.getRootCauseMessage(e));
            }
        }

        // XWiki claims
        updateXWikiClaims(modifiableDocument, userObject.getXClass(xcontext), userObject, userInfo, xcontext);

        // Set OIDC fields
        this.store.updateOIDCUser(modifiableDocument, idToken.getIssuer().getValue(),
                userInfo.getSubject().getValue());

        // Prevent data to send with the event
        OIDCUserEventData eventData = new OIDCUserEventData(new NimbusOIDCIdToken(idToken),
                new NimbusOIDCUserInfo(userInfo));

        // Notify
        this.observation.notify(new OIDCUserUpdating(modifiableDocument.getDocumentReference()), modifiableDocument,
                eventData);

        // Apply the modifications
        if (newUser || userDocument.apply(modifiableDocument)) {
            String comment;
            if (newUser) {
                comment = "Create user from OpenID Connect";
            } else {
                comment = "Update user from OpenID Connect";
            }

            xcontext.getWiki().saveDocument(userDocument, comment, xcontext);

            // Now let's add new the user to XWiki.XWikiAllGroup
            if (newUser) {
                xcontext.getWiki().setUserDefaultGroup(userDocument.getFullName(), xcontext);
            }

            // Notify
            this.observation.notify(new OIDCUserUpdated(userDocument.getDocumentReference()), userDocument,
                    eventData);
        }

        return new SimplePrincipal(userDocument.getPrefixedFullName());
    }

    private void updateXWikiClaims(XWikiDocument userDocument, BaseClass userClass, BaseObject userObject,
            UserInfo userInfo, XWikiContext xcontext) {
        for (Map.Entry<String, Object> entry : userInfo.toJSONObject().entrySet()) {
            if (entry.getKey().startsWith(OIDCUserInfo.CLAIMPREFIX_XWIKI_USER)) {
                String xwikiKey = entry.getKey().substring(OIDCUserInfo.CLAIMPREFIX_XWIKI_USER.length());

                // Try in the user object
                if (userClass.getField(xwikiKey) != null) {
                    setValue(userObject, xwikiKey, entry.getValue(), xcontext);

                    continue;
                }

                // Try in the whole user document
                BaseObject xobject = userDocument.getFirstObject(xwikiKey);
                if (xobject != null) {
                    setValue(xobject, xwikiKey, entry.getValue(), xcontext);

                    continue;
                }
            }
        }
    }

    private void setValue(BaseObject xobject, String key, Object value, XWikiContext xcontext) {
        Object cleanValue;

        if (value instanceof List) {
            cleanValue = value;
        } else {
            // Go through String to be safe
            // TODO: find a more effective converter (the best would be to userObject#set to be stronger)
            cleanValue = Objects.toString(value);
        }

        xobject.set(key, cleanValue, xcontext);
    }

    private XWikiDocument getNewUserDocument(IDTokenClaimsSet idToken, UserInfo userInfo) throws XWikiException {
        XWikiContext xcontext = this.xcontextProvider.get();

        // TODO: add support for subwikis
        SpaceReference spaceReference = new SpaceReference(xcontext.getMainXWiki(), "XWiki");

        // Generate default document name
        String documentName = formatUserName(idToken, userInfo);

        // Find not already existing document
        DocumentReference reference = new DocumentReference(documentName, spaceReference);
        XWikiDocument document = xcontext.getWiki().getDocument(reference, xcontext);
        for (int index = 0; !document.isNew(); ++index) {
            reference = new DocumentReference(documentName + '-' + index, spaceReference);

            document = xcontext.getWiki().getDocument(reference, xcontext);
        }

        // Initialize document
        document.setCreator(XWikiRightService.SUPERADMIN_USER);
        document.setAuthorReference(document.getCreatorReference());
        document.setContentAuthorReference(document.getCreatorReference());
        xcontext.getWiki().protectUserPage(document.getFullName(), "edit", document, xcontext);

        return document;
    }

    private String clean(String str) {
        return StringUtils.removePattern(str, "[\\.\\:\\s,@\\^]");
    }

    private void putVariable(Map<String, String> map, String key, String value) {
        map.put(key, value);
        map.put(key + ".clean", clean(value));
    }

    private String formatUserName(IDTokenClaimsSet idToken, UserInfo userInfo) {
        Map<String, String> map = new HashMap<>();

        // User informations
        putVariable(map, "oidc.user.subject", userInfo.getSubject().getValue());
        putVariable(map, "oidc.user.mail", userInfo.getEmail().getAddress());
        putVariable(map, "oidc.user.familyName", userInfo.getFamilyName());
        putVariable(map, "oidc.user.givenName", userInfo.getGivenName());

        // Provider (only XWiki OIDC providers)
        URL providerURL = this.configuration.getXWikiProvider();
        if (providerURL != null) {
            putVariable(map, "oidc.provider", providerURL.toString());
            putVariable(map, "oidc.provider.host", providerURL.getHost());
            putVariable(map, "oidc.provider.path", providerURL.getPath());
            putVariable(map, "oidc.provider.protocol", providerURL.getProtocol());
            putVariable(map, "oidc.provider.port", String.valueOf(providerURL.getPort()));
        }

        // Issuer
        putVariable(map, "oidc.issuer", idToken.getIssuer().getValue());
        try {
            URI issuerURI = new URI(idToken.getIssuer().getValue());
            putVariable(map, "oidc.issuer.host", issuerURI.getHost());
            putVariable(map, "oidc.issuer.path", issuerURI.getPath());
            putVariable(map, "oidc.issuer.scheme", issuerURI.getScheme());
            putVariable(map, "oidc.issuer.port", String.valueOf(issuerURI.getPort()));
        } catch (URISyntaxException e) {
            // TODO: log something ?
        }

        StrSubstitutor substitutor = new StrSubstitutor(map);

        return substitutor.replace(this.configuration.getUserNameFormater());
    }

    public void logout() {
        XWikiRequest request = this.xcontextProvider.get().getRequest();

        // TODO: remove cookies

        // Make sure the session is free from anything related to a previously authenticated user (i.e. in case we are
        // just after a logout)
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_SESSION_ACCESSTOKEN);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_SESSION_IDTOKEN);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_SESSION_USERINFO_EXPORATIONDATE);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_ENDPOINT_AUTHORIZATION);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_ENDPOINT_TOKEN);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_ENDPOINT_USERINFO);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_IDTOKENCLAIMS);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_INITIAL_REQUEST);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_XWIKIPROVIDER);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_STATE);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_USER_NAMEFORMATER);
        request.getSession().removeAttribute(OIDCClientConfiguration.PROP_USERINFOCLAIMS);
    }
}