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

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

The 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