org.wso2.carbon.identity.authenticator.x509Certificate.X509CertificateAuthenticator.java Source code

Java tutorial

Introduction

Here is the source code for org.wso2.carbon.identity.authenticator.x509Certificate.X509CertificateAuthenticator.java

Source

/*
 *  Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. licenses this file to you under the Apache License,
 *  Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */

package org.wso2.carbon.identity.authenticator.x509Certificate;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.ssl.asn1.ASN1InputStream;
import org.apache.commons.ssl.asn1.DEREncodable;
import org.apache.commons.ssl.asn1.DERSequence;
import org.apache.commons.ssl.asn1.DERTaggedObject;
import org.apache.commons.ssl.asn1.DERUTF8String;
import org.wso2.carbon.identity.application.authentication.framework.AbstractApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.LocalApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.config.model.StepConfig;
import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext;
import org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils;
import org.wso2.carbon.identity.application.common.model.ClaimMapping;
import org.wso2.carbon.identity.core.util.IdentityUtil;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Authenticator of X509Certificate.
 */
public class X509CertificateAuthenticator extends AbstractApplicationAuthenticator
        implements LocalApplicationAuthenticator {

    private Pattern alternativeNamesPatternCompiled;
    private Pattern subjectPatternCompiled;
    private String subjectAttributePattern;
    private String alternativeNamePattern;

    private static Log log = LogFactory.getLog(X509CertificateAuthenticator.class);

    public X509CertificateAuthenticator() {

        subjectAttributePattern = getAuthenticatorConfig().getParameterMap()
                .get(X509CertificateConstants.USER_NAME_REGEX);
        alternativeNamePattern = getAuthenticatorConfig().getParameterMap()
                .get(X509CertificateConstants.AlTN_NAMES_REGEX);
        if (alternativeNamePattern != null) {
            alternativeNamesPatternCompiled = Pattern.compile(alternativeNamePattern);
        }
        if (subjectAttributePattern != null) {
            subjectPatternCompiled = Pattern.compile(subjectAttributePattern);
        }
    }

    /**
     * Initialize the process and call servlet .
     *
     * @param httpServletRequest    http request
     * @param httpServletResponse   http response
     * @param authenticationContext authentication context
     * @throws AuthenticationFailedException
     */
    @Override
    protected void initiateAuthenticationRequest(HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse, AuthenticationContext authenticationContext)
            throws AuthenticationFailedException {
        try {
            if (authenticationContext.isRetrying()) {
                String errorPageUrl = IdentityUtil.getServerURL(X509CertificateConstants.ERROR_PAGE, false, false);
                String redirectUrl = errorPageUrl
                        + ("?" + FrameworkConstants.SESSION_DATA_KEY + "="
                                + authenticationContext.getContextIdentifier())
                        + "&" + X509CertificateConstants.AUTHENTICATORS + "=" + getName()
                        + X509CertificateConstants.RETRY_PARAM_FOR_CHECKING_CERTIFICATE
                        + authenticationContext.getProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE);
                authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE, "");
                if (log.isDebugEnabled()) {
                    log.debug("Redirect to error page: " + redirectUrl);
                }
                httpServletResponse.sendRedirect(redirectUrl);
            } else {
                String authEndpoint = getAuthenticatorConfig().getParameterMap()
                        .get(X509CertificateConstants.AUTHENTICATION_ENDPOINT_PARAMETER);
                if (StringUtils.isEmpty(authEndpoint)) {
                    authEndpoint = X509CertificateConstants.AUTHENTICATION_ENDPOINT;
                }
                String queryParams = FrameworkUtils.getQueryStringWithFrameworkContextId(
                        authenticationContext.getQueryParams(), authenticationContext.getCallerSessionKey(),
                        authenticationContext.getContextIdentifier());
                if (log.isDebugEnabled()) {
                    log.debug("Request sent to " + authEndpoint);
                }
                httpServletResponse.sendRedirect(authEndpoint + ("?" + queryParams));
            }
        } catch (IOException e) {
            throw new AuthenticationFailedException("Exception while redirecting to the login page", e);
        }
    }

    /**
     * Validate the certificate.
     *
     * @param httpServletRequest    http request
     * @param httpServletResponse   http response
     * @param authenticationContext authentication context
     * @throws AuthenticationFailedException
     */
    @Override
    protected void processAuthenticationResponse(HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse, AuthenticationContext authenticationContext)
            throws AuthenticationFailedException {
        Object object = httpServletRequest.getAttribute(X509CertificateConstants.X_509_CERTIFICATE);
        if (object != null) {
            X509Certificate[] certificates;
            if (object instanceof X509Certificate[]) {
                certificates = (X509Certificate[]) object;
            } else {
                throw new AuthenticationFailedException("Exception while casting the X509Certificate");
            }
            if (certificates.length > 0) {
                if (log.isDebugEnabled()) {
                    log.debug("X509 Certificate Checking in servlet is done! ");
                }
                X509Certificate cert = certificates[0];
                String certAttributes = String.valueOf(cert.getSubjectX500Principal());
                Map<ClaimMapping, String> claims;
                claims = getSubjectAttributes(authenticationContext, certAttributes);
                String alternativeName;
                String subjectAttribute;
                if (alternativeNamePattern != null) {
                    alternativeName = getMatchedAlternativeName(cert, authenticationContext);
                    validateUsingSubject(alternativeName, authenticationContext, cert, claims);
                    if (log.isDebugEnabled()) {
                        log.debug("Certificate validated using the alternative name: " + alternativeName);
                    }
                    authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_USERNAME,
                            alternativeName);
                } else if (subjectAttributePattern != null) {
                    subjectAttribute = getMatchedSubjectAttribute(certAttributes, authenticationContext);
                    validateUsingSubject(subjectAttribute, authenticationContext, cert, claims);
                    if (log.isDebugEnabled()) {
                        log.debug("Certificate validated using the certificate subject attribute: "
                                + subjectAttribute);
                    }
                    authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_USERNAME,
                            subjectAttribute);
                } else {
                    String userName = (String) authenticationContext
                            .getProperty(X509CertificateConstants.X509_CERTIFICATE_USERNAME);
                    if (StringUtils.isEmpty(userName)) {
                        authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                                X509CertificateConstants.USERNAME_NOT_FOUND_FOR_X509_CERTIFICATE_ATTRIBUTE);
                        throw new AuthenticationFailedException(
                                "Couldn't find the username for X509Certificate's attribute");
                    } else {
                        validateUsingSubject(userName, authenticationContext, cert, claims);
                        if (log.isDebugEnabled()) {
                            log.debug(
                                    "Certificate validated using the certificate username attribute: " + userName);
                        }
                    }
                }
            } else {
                throw new AuthenticationFailedException("X509Certificate object is null");
            }
        } else {
            authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                    X509CertificateConstants.X509_CERTIFICATE_NOT_FOUND_ERROR_CODE);
            throw new AuthenticationFailedException("Unable to find X509 Certificate in browser");
        }
    }

    /**
     * get String that matches UsernameRegex from subjectDN.
     *
     * @param certAttributes        certificate x500 principal
     * @param authenticationContext authentication context
     * @throws AuthenticationFailedException
     */
    private String getMatchedSubjectAttribute(String certAttributes, AuthenticationContext authenticationContext)
            throws AuthenticationFailedException {

        LdapName ldapDN;
        try {
            ldapDN = new LdapName(certAttributes);
        } catch (InvalidNameException e) {
            throw new AuthenticationFailedException("error occurred while get the certificate claims", e);
        }
        String userNameAttribute = getAuthenticatorConfig().getParameterMap()
                .get(X509CertificateConstants.USERNAME);
        List<String> matchedStringList = new ArrayList<>();
        for (Rdn distinguishNames : ldapDN.getRdns()) {
            if (subjectPatternCompiled != null && userNameAttribute.equals(distinguishNames.getType())) {
                Matcher m = subjectPatternCompiled.matcher(String.valueOf(distinguishNames.getValue()));
                addMatchStringsToList(m, matchedStringList);
            }
        }
        if (matchedStringList.isEmpty()) {
            authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                    X509CertificateConstants.X509_CERTIFICATE_SUBJECTDN_REGEX_NO_MATCHES_ERROR_CODE);
            log.debug(X509CertificateConstants.X509_CERTIFICATE_SUBJECTDN_REGEX_NO_MATCHES_ERROR);
            throw new AuthenticationFailedException(
                    X509CertificateConstants.X509_CERTIFICATE_SUBJECTDN_REGEX_NO_MATCHES_ERROR);
        } else if (matchedStringList.size() > 1) {
            authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                    X509CertificateConstants.X509_CERTIFICATE_SUBJECTDN_REGEX_MULTIPLE_MATCHES_ERROR_CODE);
            log.debug("More than one value matched with the given regex, matches: "
                    + Arrays.toString(matchedStringList.toArray()));
            throw new AuthenticationFailedException("More than one value matched with the given regex");
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Setting X509Certificate username attribute: " + userNameAttribute + " ,and value is "
                        + matchedStringList.get(0));
            }
            authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_USERNAME,
                    matchedStringList.get(0));
            return matchedStringList.get(0);
        }
    }

    /**
     * To add or validate the certificate against to the user name.
     *
     * @param userName              certificate's username
     * @param authenticationContext the authentication context
     * @param data                  certificate's data
     * @param claims                claims of the user
     * @param cert                  X509 certificate
     * @throws AuthenticationFailedException
     */
    private void addOrValidateCertificate(String userName, AuthenticationContext authenticationContext, byte[] data,
            Map<ClaimMapping, String> claims, X509Certificate cert) throws AuthenticationFailedException {

        boolean isUserCertValid;
        boolean isSelfRegistrationEnable = Boolean.parseBoolean(
                getAuthenticatorConfig().getParameterMap().get(X509CertificateConstants.ENFORCE_SELF_REGISTRATION));
        try {
            isUserCertValid = X509CertificateUtil.validateCertificate(userName, authenticationContext, data,
                    isSelfRegistrationEnable);
        } catch (AuthenticationFailedException e) {
            authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                    X509CertificateConstants.X509_CERTIFICATE_NOT_VALIDATED_ERROR_CODE);
            throw new AuthenticationFailedException("Error in validating the user certificate", e);
        }

        if (isUserCertValid) {
            allowUser(userName, claims, cert, authenticationContext);
        } else {
            authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                    X509CertificateConstants.X509_CERTIFICATE_NOT_VALID_ERROR_CODE);
            throw new AuthenticationFailedException("X509Certificate is not valid");
        }
    }

    /**
     * Check canHandle.
     *
     * @param httpServletRequest http request
     * @return boolean status
     */
    public boolean canHandle(HttpServletRequest httpServletRequest) {
        return (httpServletRequest.getParameter(X509CertificateConstants.SUCCESS) != null);
    }

    /**
     * Get context identifier.
     *
     * @param httpServletRequest http request
     * @return authenticator contextIdentifier
     */
    public String getContextIdentifier(HttpServletRequest httpServletRequest) {
        return httpServletRequest.getParameter(X509CertificateConstants.SESSION_DATA_KEY);
    }

    /**
     * Get the authenticator name.
     *
     * @return authenticator name
     */
    public String getName() {
        return X509CertificateConstants.AUTHENTICATOR_NAME;
    }

    /**
     * Get authenticator friendly name.
     *
     * @return authenticator friendly name
     */
    public String getFriendlyName() {
        return X509CertificateConstants.AUTHENTICATOR_FRIENDLY_NAME;
    }

    /**
     * Get username.
     *
     * @param authenticationContext authentication context
     * @return username username
     */
    private AuthenticatedUser getUsername(AuthenticationContext authenticationContext) {
        AuthenticatedUser authenticatedUser = null;
        for (int i = 1; i <= authenticationContext.getSequenceConfig().getStepMap().size(); i++) {
            StepConfig stepConfig = authenticationContext.getSequenceConfig().getStepMap().get(i);
            if (stepConfig.getAuthenticatedUser() != null && stepConfig.getAuthenticatedAutenticator()
                    .getApplicationAuthenticator() instanceof LocalApplicationAuthenticator) {
                authenticatedUser = stepConfig.getAuthenticatedUser();
                break;
            }
        }
        return authenticatedUser;
    }

    /**
     * @param authenticationContext authentication context
     * @param certAttributes        principal attributes from certificate.
     * @return claim map
     * @throws AuthenticationFailedException
     */
    protected Map<ClaimMapping, String> getSubjectAttributes(AuthenticationContext authenticationContext,
            String certAttributes) throws AuthenticationFailedException {
        Map<ClaimMapping, String> claims = new HashMap<>();
        LdapName ldapDN;
        try {
            ldapDN = new LdapName(certAttributes);
        } catch (InvalidNameException e) {
            throw new AuthenticationFailedException("error occurred while get the certificate claims", e);
        }
        String userNameAttribute = getAuthenticatorConfig().getParameterMap()
                .get(X509CertificateConstants.USERNAME);
        if (log.isDebugEnabled()) {
            log.debug("Getting username attribute: " + userNameAttribute);
        }
        for (Rdn distinguishNames : ldapDN.getRdns()) {
            claims.put(ClaimMapping.build(distinguishNames.getType(), distinguishNames.getType(), null, false),
                    String.valueOf(distinguishNames.getValue()));
            if (StringUtils.isNotEmpty(userNameAttribute)) {
                if (userNameAttribute.equals(distinguishNames.getType())) {
                    if (log.isDebugEnabled()) {
                        log.debug("Setting X509Certificate username attribute: " + userNameAttribute
                                + "and value is " + distinguishNames.getValue());
                    }
                    authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_USERNAME,
                            String.valueOf(distinguishNames.getValue()));
                }
            }
        }
        return claims;
    }

    /**
     * Allow user login into system.
     *
     * @param userName              username of the user.
     * @param claims                claim map.
     * @param cert                  x509 certificate.
     * @param authenticationContext authentication context.
     */
    private void allowUser(String userName, Map claims, X509Certificate cert,
            AuthenticationContext authenticationContext) {
        AuthenticatedUser authenticatedUserObj;
        authenticatedUserObj = AuthenticatedUser.createLocalAuthenticatedUserFromSubjectIdentifier(userName);
        authenticatedUserObj.setAuthenticatedSubjectIdentifier(String.valueOf(cert.getSerialNumber()));
        authenticatedUserObj.setUserAttributes(claims);
        authenticationContext.setSubject(authenticatedUserObj);
    }

    /**
     * Check whether status of retrying authentication.
     *
     * @return true, if retry authentication is enabled
     */
    @Override
    protected boolean retryAuthenticationEnabled() {
        return true;
    }

    /**
     * Get alternative name that match with the given regex from the certificate.
     *
     * @param cert                  x509 certificate.
     * @param authenticationContext authenticationContext
     */
    private String getMatchedAlternativeName(X509Certificate cert, AuthenticationContext authenticationContext)
            throws AuthenticationFailedException {

        List<String> matchedAlternativeNamesList = new ArrayList<>();
        try {
            Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
            if (altNames != null) {
                for (List item : altNames) {
                    ASN1InputStream decoder = null;
                    if (item.toArray()[1] instanceof byte[])
                        decoder = new ASN1InputStream((byte[]) item.toArray()[1]);
                    else if (item.toArray()[1] instanceof String) {
                        Matcher m = alternativeNamesPatternCompiled.matcher((String) item.toArray()[1]);
                        addMatchStringsToList(m, matchedAlternativeNamesList);
                    }
                    if (decoder == null)
                        continue;
                    String identity = decodeAlternativeName(decoder);
                    Matcher m = alternativeNamesPatternCompiled.matcher(identity);
                    addMatchStringsToList(m, matchedAlternativeNamesList);
                }
            } else {
                authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                        X509CertificateConstants.X509_CERTIFICATE_ALTERNATIVE_NAMES_NOTFOUND_ERROR_CODE);
                throw new AuthenticationFailedException(
                        X509CertificateConstants.X509_CERTIFICATE_ALTERNATIVE_NAMES_NOTFOUND_ERROR);
            }
        } catch (CertificateParsingException | IOException e) {
            throw new AuthenticationFailedException("Failed to Parse the certificate");
        }
        if (matchedAlternativeNamesList.isEmpty()) {
            authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                    X509CertificateConstants.X509_CERTIFICATE_ALTERNATIVE_NAMES_REGEX_NO_MATCHES_ERROR_CODE);
            throw new AuthenticationFailedException("Regex Configured but no matches found for the given regex");
        } else if (matchedAlternativeNamesList.size() > 1) {
            authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                    X509CertificateConstants.X509_CERTIFICATE_ALTERNATIVE_NAMES_REGEX_MULTIPLE_MATCHES_ERROR_CODE);
            throw new AuthenticationFailedException("More than one match for the given regex");
        } else {
            return matchedAlternativeNamesList.get(0);
        }

    }

    /**
     * Get decoded alternative name
     *
     * @param decoder ASN1 Decoder
     */
    private String decodeAlternativeName(ASN1InputStream decoder) throws IOException {

        DEREncodable encoded = decoder.readObject();
        encoded = ((DERSequence) encoded).getObjectAt(1);
        encoded = ((DERTaggedObject) encoded).getObject();
        encoded = ((DERTaggedObject) encoded).getObject();
        return ((DERUTF8String) encoded).getString();
    }

    /**
     * validate the certificate using the selected subject.
     *
     * @param subject               matched string or the username that uses to authenticate.
     * @param authenticationContext authenticationContext.
     * @param cert                  x509 certificate.
     * @param claims                user claims.
     */
    private void validateUsingSubject(String subject, AuthenticationContext authenticationContext,
            X509Certificate cert, Map<ClaimMapping, String> claims) throws AuthenticationFailedException {

        byte[] data;
        try {
            data = cert.getEncoded();
        } catch (CertificateEncodingException e) {
            throw new AuthenticationFailedException(
                    "Encoded certificate is not found in the certificate with subjectDN: " + cert.getSubjectDN(),
                    e);
        }
        AuthenticatedUser authenticatedUser = getUsername(authenticationContext);

        if (log.isDebugEnabled()) {
            log.debug("Getting X509Certificate username");
        }

        if (authenticatedUser != null) {
            if (log.isDebugEnabled()) {
                log.debug("Authenticated username is: " + authenticatedUser);
            }
            String authenticatedUserName = authenticatedUser.getAuthenticatedSubjectIdentifier();
            if (authenticatedUserName.equals(subject)) {
                addOrValidateCertificate(subject, authenticationContext, data, claims, cert);
            } else {
                authenticationContext.setProperty(X509CertificateConstants.X509_CERTIFICATE_ERROR_CODE,
                        X509CertificateConstants.USERNAME_CONFLICT);
                throw new AuthenticationFailedException(
                        "Couldn't find X509 certificate to this authenticated user: " + authenticatedUserName);
            }
        } else {
            addOrValidateCertificate(subject, authenticationContext, data, claims, cert);
        }
    }

    private void addMatchStringsToList(Matcher matcher, List matches) {

        while (matcher.find()) {
            matches.add(matcher.group());
        }
    }

}