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

org.wildfly.httpclient.common.PoolAuthenticationContext Maven / Gradle / Ivy

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2017 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.httpclient.common;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.LinkedBlockingDeque;
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 javax.security.auth.callback.UnsupportedCallbackException;

import org.wildfly.security.auth.client.AuthenticationConfiguration;
import org.wildfly.security.auth.client.AuthenticationContext;
import org.wildfly.security.auth.client.AuthenticationContextConfigurationClient;
import io.undertow.client.ClientRequest;
import io.undertow.client.ClientResponse;
import io.undertow.client.ClientExchange;
import io.undertow.security.impl.AuthenticationInfoToken;
import io.undertow.security.impl.DigestWWWAuthenticateToken;
import io.undertow.server.session.SecureRandomSessionIdGenerator;
import io.undertow.util.FlexBase64;
import io.undertow.util.HeaderValues;
import io.undertow.util.Headers;
import io.undertow.util.HexConverter;
import io.undertow.util.StatusCodes;
import io.undertow.util.AttachmentKey;
import org.wildfly.security.auth.principal.NamePrincipal;

/**
 * Class that holds authentication information for a connection
 *
 * @author Stuart Douglas
 */
class PoolAuthenticationContext {

    private static final AttachmentKey DIGEST = AttachmentKey.create(DigestImpl.class);

    private static final AuthenticationContextConfigurationClient AUTH_CONTEXT_CLIENT;

    static {
        AUTH_CONTEXT_CLIENT = AccessController.doPrivileged((PrivilegedAction) AuthenticationContextConfigurationClient::new);
    }

    private volatile Type current;

    private static final LinkedBlockingDeque digestList = new LinkedBlockingDeque<>();

    private static final SecureRandomSessionIdGenerator cnonceGenerator = new SecureRandomSessionIdGenerator();

    boolean handleResponse(ClientResponse response) {
        if (response.getResponseCode() != StatusCodes.UNAUTHORIZED) {
            return false;
        }
        String authenticate = response.getResponseHeaders().getFirst(Headers.WWW_AUTHENTICATE);
        if (authenticate == null) {
            return false;
        }
        String auth = authenticate.toLowerCase(Locale.ENGLISH);
        if (auth.startsWith("basic ")) {
            current = Type.BASIC;
            return true;
        }
        if (auth.startsWith("digest ")) {
            current = Type.DIGEST;

            Map result = DigestWWWAuthenticateToken.parseHeader(authenticate.substring(7));
            DigestImpl current = new DigestImpl();
            current.domain = result.get(DigestWWWAuthenticateToken.DOMAIN);
            current.nonce = result.get(DigestWWWAuthenticateToken.NONCE);
            current.opaque = result.get(DigestWWWAuthenticateToken.OPAQUE);
            current.algorithm = result.get(DigestWWWAuthenticateToken.ALGORITHM);
            String s = result.get(DigestWWWAuthenticateToken.MESSAGE_QOP);
            current.qop = null;
            if (s != null) {
                for (String p : s.split(",")) {
                    if (p.equals("auth")) {
                        current.qop = p;
                    }
                }
                if (current.qop == null) {
                    throw HttpClientMessages.MESSAGES.unsupportedQopInDigest();
                }
            }
            current.realm = result.get(DigestWWWAuthenticateToken.REALM);
            current.nccount = 1;
            if (current.algorithm.startsWith("\"")) {
                current.algorithm = current.algorithm.substring(1, current.algorithm.length() - 1);
            }
            digestList.add(current);
            return true;

        }
        return false;
    }

    static String createTargetUri(URI uri, ClientRequest request) {
        String path;
        String query;
        int pos = request.getPath().indexOf("?");
        if (pos > 0) {
            path = request.getPath().substring(0, pos);
            query = request.getPath().substring(pos + 1);
        } else {
            path = request.getPath();
            query = null;
        }
        String scheme = uri.getScheme();
        String host = uri.getHost();
        int port = uri.getPort();
        StringBuilder uriBuilder = new StringBuilder();
        if (scheme != null) {
            uriBuilder.append(scheme);
            uriBuilder.append(':');
        }
        if (host != null) {
            uriBuilder.append("//");
            boolean needBrackets = ((host.indexOf(':') >= 0)
                    && !host.startsWith("[")
                    && !host.endsWith("]"));
            if (needBrackets) {
                uriBuilder.append('[');
            }
            uriBuilder.append(host);
            if (needBrackets) {
                uriBuilder.append(']');
            }
        }
        if (port != -1 && !(("http".equals(scheme) && port == 80) || ("https".equals(scheme) && port == 443))) {
            uriBuilder.append(':');
            uriBuilder.append(port);
        }
        uriBuilder.append(path);
        if (query != null && !query.isEmpty()) {
            uriBuilder.append("?");
            uriBuilder.append(query);
        }
        return uriBuilder.toString();
    }

    boolean prepareRequest(URI uri, ClientRequest request, AuthenticationConfiguration authenticationConfiguration) {
        if (current == Type.NONE) {
            return false;
        }
        AuthenticationConfiguration config = authenticationConfiguration;
        if (config == null) {
            config = AUTH_CONTEXT_CLIENT.getAuthenticationConfiguration(uri, AuthenticationContext.captureCurrent());
        }

        final CallbackHandler callbackHandler = AUTH_CONTEXT_CLIENT.getCallbackHandler(config);

        // TODO: also try credential callback, passing in DIGEST parameters (if any) when DIGEST is in use
        NameCallback nameCallback = new NameCallback("user name");
        PasswordCallback passwordCallback = new PasswordCallback("password", false);
        try {
            callbackHandler.handle(new Callback[]{nameCallback, passwordCallback});
        } catch (IOException | UnsupportedCallbackException e) {
            return false;
        }
        final String name = nameCallback.getName();
        if (name == null) {
            return false;
        }
        char[] password = passwordCallback.getPassword();
        if (password == null) {
            return false;
        }
        Principal principal = new NamePrincipal(name);
        if (current == Type.BASIC) {
            String challenge = principal.getName() + ":" + new String(password);
            request.getRequestHeaders().put(Headers.AUTHORIZATION, "Basic " + FlexBase64.encodeString(challenge.getBytes(StandardCharsets.UTF_8), false));
            return true;
        } else if (current == Type.DIGEST) {
            DigestImpl current = digestList.poll();
            if (current == null) {
                return false;
            }
            String cnonce = cnonceGenerator.createSessionId();
            String digestUri = createTargetUri(uri, request);
            request.putAttachment(DIGEST, current);
            StringBuilder sb = new StringBuilder("Digest username=\"");
            sb.append(principal.getName());
            sb.append("\", uri=\"");
            sb.append(digestUri);
            sb.append("\", realm=\"");
            sb.append(current.realm);
            sb.append("\"");
            StringBuilder ncBuilder = new StringBuilder();
            if (current.qop != null) {
                sb.append(", nc=");
                String nonceCountString = Integer.toHexString(current.nccount++);
                for (int i = nonceCountString.length(); i < 8; ++i) {
                    ncBuilder.append("0"); //must be 8 digits long
                }
                ncBuilder.append(nonceCountString);
                sb.append(ncBuilder.toString());

                sb.append(", cnonce=\"");
                sb.append(cnonce);
                sb.append("\"");
            }
            sb.append(", algorithm=");
            sb.append(current.algorithm);
            sb.append(", nonce=\"");
            sb.append(current.nonce);
            sb.append("\", opaque=\"");
            sb.append(current.opaque);
            sb.append("\", qop=auth"); //TODO: fix this? What do we want to do about auth-int

            //calculate the response
            String a1 = principal.getName() + ":" + current.realm + ":" + new String(password);
            String a2 = request.getMethod().toString() + ":" + digestUri;

            try {
                MessageDigest digest = MessageDigest.getInstance(current.algorithm);
                digest.update(a1.getBytes(StandardCharsets.UTF_8));
                byte[] hashedA1 = HexConverter.convertToHexBytes(digest.digest());
                digest.reset();
                digest.update(a2.getBytes(StandardCharsets.UTF_8));
                String hashedA2 = HexConverter.convertToHexString(digest.digest());
                digest.reset();
                digest.update(hashedA1);
                digest.update((byte) ':');
                digest.update(current.nonce.getBytes(StandardCharsets.UTF_8));
                digest.update((byte) ':');
                if (current.qop != null) {
                    digest.update(ncBuilder.toString().getBytes(StandardCharsets.UTF_8));
                    digest.update((byte) ':');
                    digest.update(cnonce.getBytes(StandardCharsets.UTF_8));
                    digest.update((byte) ':');
                    digest.update("auth".getBytes(StandardCharsets.UTF_8));
                    digest.update((byte) ':');
                }
                digest.update(hashedA2.getBytes(StandardCharsets.UTF_8));
                sb.append(", response=\"");
                sb.append(HexConverter.convertToHexString(digest.digest()));
                sb.append("\"");
                request.getRequestHeaders().put(Headers.AUTHORIZATION, sb.toString());
                return true;
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(e);
            }
        }
        return false;
    }

    boolean isStale(ClientExchange exchange) {
        if (current != Type.DIGEST) {
            return false;
        }
        ClientResponse response = exchange.getResponse();
        if (response.getResponseCode() != StatusCodes.UNAUTHORIZED) {
            DigestImpl digest = exchange.getRequest().getAttachment(DIGEST);
            if(digest != null) {
                String authInfo = response.getResponseHeaders().getFirst(Headers.AUTHENTICATION_INFO);
                if(authInfo != null) {
                    try {
                        Map result = AuthenticationInfoToken.parseHeader(authInfo);
                        String next = result.get(AuthenticationInfoToken.NEXT_NONCE);
                        if (next != null) {
                            digest.nonce = next;
                        }
                    } catch (Exception e) {
                        HttpClientMessages.MESSAGES.failedToParseAuthenticationInfo(e);
                    }
                }
                digestList.add(digest);
            }
            return false;
        }
        HeaderValues headers = response.getResponseHeaders().get(Headers.WWW_AUTHENTICATE);
        if (headers == null) {
            return false;
        }
        for (String authenticate : headers) {
            String auth = authenticate.toLowerCase(Locale.ENGLISH);
            if (!auth.startsWith("digest ")) {
                continue;
            }
            Map result = DigestWWWAuthenticateToken.parseHeader(authenticate.substring(7));
            if (result.containsKey(DigestWWWAuthenticateToken.STALE)) {
                return true;
            }
        }
        return false;
    }

    enum Type {
        NONE,
        BASIC,
        DIGEST
    }

    private static final class DigestImpl {

        private String realm;
        private String domain;
        private String nonce;
        private String opaque;
        private String algorithm;
        private String qop;
        private int nccount = 1;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy