All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.wildfly.security.credential.source.OAuth2CredentialSource Maven / Gradle / Ivy

Go to download

This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up with different versions on classes on the class path).

There is a newer version: 34.0.0.Final
Show newest version
/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2016 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.wildfly.security.credential.source;

import static org.wildfly.common.Assert.checkNotNullParam;
import static org.wildfly.security.credential.source.ElytronMessages2.saslOAuth2;

import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.wildfly.common.Assert;
import org.wildfly.common.bytes.ByteStringBuilder;
import org.wildfly.common.iteration.CodePointIterator;
import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.auth.client.AuthenticationConfiguration;
import org.wildfly.security.auth.client.AuthenticationContext;
import org.wildfly.security.auth.client.AuthenticationContextConfigurationClient;
import org.wildfly.security.credential.BearerTokenCredential;
import org.wildfly.security.credential.Credential;
import org.wildfly.security.password.PasswordFactory;
import org.wildfly.security.password.interfaces.MaskedPassword;
import org.wildfly.security.password.spec.ClearPasswordSpec;
import org.wildfly.security.password.spec.MaskedPasswordSpec;

/**
 * A {@link CredentialSource} capable of authenticating against a OAuth2 compliant authorization server and obtaining
 * access tokens in form of a {@link BearerTokenCredential}.
 *
 * @author Pedro Igor
 */
@Deprecated
public class OAuth2CredentialSource implements CredentialSource {

    /**
     * Creates a new {@link Builder} instance in order to configure and build a {@link OAuth2CredentialSource}.
     *
     * @param tokenEndpointUrl the token endpoint that will be used to obtain OAuth2 access tokens
     * @return a new builder instance
     */
    public static Builder builder(URL tokenEndpointUrl) {
        return new Builder(tokenEndpointUrl);
    }

    private final URL tokenEndpointUri;
    private final Consumer> authenticationHandler;
    private String scopes;
    private final Supplier sslContextSupplier;
    private final Supplier hostnameVerifierSupplier;
    private static final char [] defaultKeyMaterial = "somearbitrarycrazystringthatdoesnotmatter".toCharArray();

    /**
     * Creates a new instance.
     *
     * @param tokenEndpointUrl         the OAuth2 Token Endpoint {@link URL}
     * @param authenticationHandler    a callback that can be used to push addition parameters to requests sent to the authorization server
     * @param scopes                   a string with the scope of the access request
     * @param sslContextSupplier       a supplier from where the {@link SSLContext} is obtained in case the token endpoint is using TLS/HTTPS
     * @param hostnameVerifierSupplier a supplier from where the {@link HostnameVerifier} is obtained in case the token endpoint is using TLS/HTTPS
     */
    private OAuth2CredentialSource(URL tokenEndpointUrl, Consumer> authenticationHandler, String scopes, Supplier sslContextSupplier, Supplier hostnameVerifierSupplier) {
        this.tokenEndpointUri = checkNotNullParam("tokenEndpointUri", tokenEndpointUrl);

        if (isHttps(tokenEndpointUrl)) {
            checkNotNullParam("sslContextSupplier", sslContextSupplier);
        }

        this.authenticationHandler = checkNotNullParam("authenticationHandler", authenticationHandler);
        this.scopes = scopes;
        this.sslContextSupplier = sslContextSupplier;
        this.hostnameVerifierSupplier = hostnameVerifierSupplier;
    }

    @Override
    public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) throws IOException {
        return BearerTokenCredential.class.isAssignableFrom(credentialType) ? SupportLevel.POSSIBLY_SUPPORTED : SupportLevel.UNSUPPORTED;
    }

    @Override
    public  C getCredential(Class credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) throws IOException {
        if (BearerTokenCredential.class.isAssignableFrom(credentialType)) {
            try {
                HttpURLConnection connection = null;

                try {
                    connection = openConnection();
                    connection.setDoOutput(true);
                    connection.setRequestMethod("POST");
                    connection.setInstanceFollowRedirects(false);

                    connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

                    HashMap parameters = new HashMap<>();

                    authenticationHandler.accept(parameters);

                    if (scopes != null) {
                        parameters.put("scope", scopes);
                    }

                    byte[] paramBytes = buildParameters(parameters);

                    try (OutputStream outputStream = connection.getOutputStream()) {
                        outputStream.write(paramBytes);
                    }

                    try (final InputStream inputStream = new BufferedInputStream(connection.getInputStream());
                            final JsonReader jsonReader = Json.createReader(inputStream)) {
                        JsonObject jsonObject = jsonReader.readObject();
                        String accessToken = jsonObject.getString("access_token");
                        return credentialType.cast(new BearerTokenCredential(accessToken));
                    }
                } catch (IOException ioe) {
                    InputStream errorStream = null;

                    if (connection != null && connection.getErrorStream() != null) {
                        errorStream = connection.getErrorStream();

                        try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8))) {
                            StringBuffer response = reader.lines().reduce(new StringBuffer(), StringBuffer::append, (buffer1, buffer2) -> buffer1);
                            saslOAuth2.errorf(ioe, "Unexpected response from server [%s]. Response: [%s]", tokenEndpointUri, response);
                        } catch (IOException ignore) {
                        }
                    }

                    throw saslOAuth2.mechUnableToHandleResponseFromServer(ioe);
                }
            } catch (Exception cause) {
                throw saslOAuth2.mechCallbackHandlerFailedForUnknownReason(cause);
            }
        }

        return null;
    }

    private SSLContext resolveSSLContext() {
        if (!isHttps(tokenEndpointUri)) {
            return null;
        }
        return sslContextSupplier == null ? null : sslContextSupplier.get();
    }

    private HttpURLConnection openConnection() throws IOException {
        saslOAuth2.debugf("Opening connection to [%s]", tokenEndpointUri);
        HttpURLConnection connection = (HttpURLConnection) tokenEndpointUri.openConnection();
        SSLContext sslContext = resolveSSLContext();

        if (sslContext != null) {
            HttpsURLConnection https = (HttpsURLConnection) connection;

            https.setSSLSocketFactory(sslContext.getSocketFactory());

            if (hostnameVerifierSupplier != null) {
                https.setHostnameVerifier(checkNotNullParam("hostnameVerifier", hostnameVerifierSupplier.get()));
            }
        }

        return connection;
    }

    private byte[] buildParameters(Map parameters) {
        ByteStringBuilder params = new ByteStringBuilder();

        parameters.entrySet().stream().forEach(entry -> {
            if (params.length() > 0) {
                params.append('&');
            }
            params.append(entry.getKey()).append('=').append(entry.getValue());
        });

        return params.toArray();
    }

    private boolean isHttps(URL tokenEndpointUrl) {
        return "https".equals(tokenEndpointUrl.getProtocol());
    }

    public static class Builder {

        private final URL tokenEndpointUrl;
        private String scopes;
        private Supplier sslContextSupplier = new Supplier() {
            @Override
            public SSLContext get() {
                AuthenticationContextConfigurationClient contextConfigurationClient = AccessController.doPrivileged(AuthenticationContextConfigurationClient.ACTION);
                try {
                    return contextConfigurationClient.getSSLContext(tokenEndpointUrl.toURI(), AuthenticationContext.captureCurrent());
                } catch (Exception cause) {
                    throw saslOAuth2.failedToObtainSSLContext(cause);
                }
            }
        };
        private Supplier hostnameVerifierSupplier;
        private Consumer> authenticationHandler;

        private Builder(URL tokenEndpointUrl) {
            this.tokenEndpointUrl = checkNotNullParam("tokenEndpointUrl", tokenEndpointUrl);
        }

        /**
         * The scopes to grant access.
         *
         * @param scopes the scopes to grant access.
         * @return this instance
         */
        public Builder grantScopes(String scopes) {
            this.scopes = checkNotNullParam("scopes", scopes);
            return this;
        }

        /**
         * 

Configure OAuth2 Resource Owner Password Grant Type as defined by the OAuth2 specification. * *

When using this grant type, make sure to also configure one of the supported client authentication methods. For instance, * make sure to provide client credentials via {@link #clientCredentials(String, String)}. * * @param userName the resource owner's user name * @param password the resource owner's password * @return this instance. */ public Builder useResourceOwnerPassword(String userName, String password) { configureAuthenticationHandler(parameters -> configureResourceOwnerCredentialsParameters(parameters, userName, password)); return this; } /** *

Configure OAuth2 Client Credentials Grant Type as defined by the OAuth2 specification. * * @param id the client id * @param secret the client secret * @return this instance. */ public Builder clientCredentials(String id, String secret) { checkNotNullParam("id", id); checkNotNullParam("secret", secret); configureAuthenticationHandler(parameters -> { AuthenticationContext context = AuthenticationContext.captureCurrent(); AuthenticationContextConfigurationClient client = AccessController.doPrivileged(AuthenticationContextConfigurationClient.ACTION); AuthenticationConfiguration configuration = client.getAuthenticationConfiguration(URI.create(tokenEndpointUrl.toString()), context); CallbackHandler handler = client.getCallbackHandler(configuration); // if there is a handler associated with the configuration, we try to resolve username/password and change grant type to resource owner credentials if (handler != null) { NameCallback nameCallback = new NameCallback("Username"); PasswordCallback passwordCallback = new PasswordCallback("Password", false); try { handler.handle(new Callback[]{nameCallback, passwordCallback}); } catch (Exception ignore) { } String userName = nameCallback.getName(); char[] password = passwordCallback.getPassword(); if (userName != null && password != null) { configureResourceOwnerCredentialsParameters(parameters, userName, String.valueOf(password)); } } configureClientCredentialsParameters(parameters, id, secret.toCharArray()); }); return this; } /** *

Configure OAuth2 Resource Owner Masked Password Grant Type as defined by the OAuth2 specification. * *

When using this grant type, make sure to also configure one of the supported client authentication methods. For instance, * make sure to provide client credentials via {@link #clientCredentials(String, String)}. * * @param userName the resource owner's user name * @param maskedPassword the masked password, as a string (must not be {@code null}) * @param algorithm the algorithm (can be {@code null}, default:"masked-MD5-DES") * @param initialKeyMaterial the initial key material, as a string(can be {@code null}, default:"somearbitrarycrazystringthatdoesnotmatter") * @param iterationCount the iteration count, as an integer (must not be less than 1) * @param salt the salt, as a string (must not be {@code null}) * @param initializationVector the initialization vector, as a string (can be {@code null}) * @return this instance. * @throws NoSuchAlgorithmException if algorithm used to get PasswordFactory instance is invalid * @throws InvalidKeySpecException if invalid spec is used to generate password */ public Builder useResourceOwnerMaskedPassword(String userName, String maskedPassword, String algorithm, String initialKeyMaterial, int iterationCount, String salt, String initializationVector) throws NoSuchAlgorithmException, InvalidKeySpecException { String password = convertMaskedPasswordToClearText(maskedPassword, algorithm, initialKeyMaterial, iterationCount, salt, initializationVector); configureAuthenticationHandler(parameters -> configureResourceOwnerCredentialsParameters(parameters, userName, password)); return this; } /** *

Configure OAuth2 Masked Client Credentials Grant Type as defined by the OAuth2 specification. * * @param id the client id * @param maskedSecret the masked password, as a string (must not be {@code null}) * @param algorithm the algorithm (can be {@code null}, default:"masked-MD5-DES") * @param initialKeyMaterial the initial key material, as a string(can be {@code null}, default:"somearbitrarycrazystringthatdoesnotmatter") * @param iterationCount the iteration count, as an integer (must not be less than 1) * @param salt the salt, as a string (must not be {@code null}) * @param initializationVector the initialization vector, as a string (can be {@code null}) * @return this instance. * @throws NoSuchAlgorithmException if algorithm used to get PasswordFactory instance is invalid * @throws InvalidKeySpecException if invalid spec is used to generate password */ public Builder maskedClientCredentials(String id, String maskedSecret, String algorithm, String initialKeyMaterial, int iterationCount, String salt, String initializationVector) throws NoSuchAlgorithmException, InvalidKeySpecException { String secret = convertMaskedPasswordToClearText(maskedSecret, algorithm, initialKeyMaterial, iterationCount, salt, initializationVector); checkNotNullParam("id", id); checkNotNullParam("secret", secret); configureAuthenticationHandler(parameters -> { AuthenticationContext context = AuthenticationContext.captureCurrent(); AuthenticationContextConfigurationClient client = AccessController.doPrivileged(AuthenticationContextConfigurationClient.ACTION); AuthenticationConfiguration configuration = client.getAuthenticationConfiguration(URI.create(tokenEndpointUrl.toString()), context); CallbackHandler handler = client.getCallbackHandler(configuration); // if there is a handler associated with the configuration, we try to resolve username/password and change grant type to resource owner credentials if (handler != null) { NameCallback nameCallback = new NameCallback("Username"); PasswordCallback passwordCallback = new PasswordCallback("Password", false); try { handler.handle(new Callback[]{nameCallback, passwordCallback}); } catch (Exception ignore) { } String userName = nameCallback.getName(); char[] password = passwordCallback.getPassword(); if (userName != null && password != null) { configureResourceOwnerCredentialsParameters(parameters, userName, String.valueOf(password)); } } configureClientCredentialsParameters(parameters, id, secret.toCharArray()); }); return this; } /** * TThe {@link SSLContext} to be used in case connections to remote server require TLS/HTTPS. * * @param sslContext the SSLContext * @return this instance */ public Builder useSslContext(SSLContext sslContext) { checkNotNullParam("sslContext", sslContext); sslContextSupplier = () -> sslContext; return this; } /** * TThe {@link HostnameVerifier} to be used in case connections to remote server require TLS/HTTPS. * * @param hostnameVerifier the HostnameVerifier * @return this instance */ public Builder useSslHostnameVerifier(HostnameVerifier hostnameVerifier) { checkNotNullParam("hostnameVerifier", hostnameVerifier); this.hostnameVerifierSupplier = () -> hostnameVerifier; return this; } /** * Creates a new {@link OAuth2CredentialSource} instance. * * @return a OAuth2 credential source */ public OAuth2CredentialSource build() { if (authenticationHandler == null) { authenticationHandler = parameters -> { AuthenticationContext context = AuthenticationContext.captureCurrent(); AuthenticationContextConfigurationClient client = AccessController.doPrivileged(AuthenticationContextConfigurationClient.ACTION); AuthenticationConfiguration configuration = client.getAuthenticationConfiguration(URI.create(tokenEndpointUrl.toString()), context); CallbackHandler handler = client.getCallbackHandler(configuration); NameCallback nameCallback = new NameCallback("Client ID"); PasswordCallback password1 = new PasswordCallback("Client Secret", false); try { handler.handle(new Callback[]{nameCallback, password1}); } catch (Exception ignore) { } String userName = nameCallback.getName(); char[] password = password1.getPassword(); configureClientCredentialsParameters(parameters, userName, password); }; } return new OAuth2CredentialSource(tokenEndpointUrl, authenticationHandler.andThen(parameters -> { if (!parameters.containsKey("client_id") || !parameters.containsKey("client_secret")) { throw saslOAuth2.oauth2ClientCredentialsNotProvided(); } }), scopes, sslContextSupplier, hostnameVerifierSupplier); } private void configureClientCredentialsParameters(Map parameters, String id, char[] secret) { parameters.putIfAbsent("grant_type", "client_credentials"); parameters.put("client_id", checkNotNullParam("client_id", id)); parameters.put("client_secret", checkNotNullParam("client_secret", secret == null ? null : String.valueOf(secret))); } private void configureResourceOwnerCredentialsParameters(Map parameters, String userName, String password) { parameters.put("grant_type", "password"); parameters.put("username", checkNotNullParam("userName", userName)); parameters.put("password", checkNotNullParam("password", password)); } private void configureAuthenticationHandler(Consumer> handler) { if (authenticationHandler == null) { authenticationHandler = handler; } else { authenticationHandler = authenticationHandler.andThen(handler); } } private String convertMaskedPasswordToClearText(String maskedPassword, String algorithm, String initialKeyMaterial, int iterationCount, String salt, String initializationVector) throws NoSuchAlgorithmException, InvalidKeySpecException { Assert.assertNotNull(maskedPassword); Assert.checkMinimumParameter("iterationCount", 1, iterationCount); Assert.assertNotNull(salt); byte[] finalMaskedSecretBytes = CodePointIterator.ofString(maskedPassword).base64Decode().drain(); if (algorithm == null) algorithm = MaskedPassword.ALGORITHM_MASKED_MD5_DES; char[] finalInitialKeyMaterial = initialKeyMaterial == null ? defaultKeyMaterial : initialKeyMaterial.toCharArray(); byte[] finalSalt = CodePointIterator.ofString(salt).asUtf8().drain(); byte[] finalInitializationVector = initializationVector == null ? null : CodePointIterator.ofString(initializationVector).base64Decode().drain(); MaskedPasswordSpec spec = new MaskedPasswordSpec(finalInitialKeyMaterial, iterationCount, finalSalt, finalMaskedSecretBytes, finalInitializationVector); PasswordFactory factory = PasswordFactory.getInstance(algorithm); MaskedPassword password = factory.generatePassword(spec).castAs(MaskedPassword.class); ClearPasswordSpec clearSpec = factory.getKeySpec(password, ClearPasswordSpec.class); return String.valueOf(clearSpec.getEncodedPassword()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy