/* * Copyright 2017 Red Hat, Inc. * * 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.iiop.openjdk.csiv2; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.AccessController; import java.security.Principal; 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 com.sun.corba.se.impl.interceptors.ClientRequestInfoImpl; import com.sun.corba.se.impl.transport.SocketOrChannelContactInfoImpl; import com.sun.corba.se.pept.transport.ContactInfo; import com.sun.corba.se.spi.transport.CorbaConnection; import org.jboss.as.controller.capability.RuntimeCapability; import org.jboss.as.server.CurrentServiceContainer; import org.jboss.msc.service.ServiceContainer; import org.jboss.msc.service.ServiceName; import org.omg.CORBA.Any; import org.omg.CORBA.BAD_PARAM; import org.omg.CORBA.CompletionStatus; import org.omg.CORBA.LocalObject; import org.omg.CORBA.ORB; import org.omg.CSI.AuthorizationElement; import org.omg.CSI.EstablishContext; import org.omg.CSI.GSS_NT_ExportedNameHelper; import org.omg.CSI.ITTAnonymous; import org.omg.CSI.IdentityToken; import org.omg.CSI.MTContextError; import org.omg.CSI.SASContextBody; import org.omg.CSI.SASContextBodyHelper; import org.omg.CSIIOP.CompoundSecMech; import org.omg.CSIIOP.EstablishTrustInClient; import org.omg.CSIIOP.IdentityAssertion; import org.omg.GSSUP.InitialContextToken; import org.omg.IOP.Codec; import org.omg.IOP.CodecPackage.FormatMismatch; import org.omg.IOP.CodecPackage.InvalidTypeForEncoding; import org.omg.IOP.CodecPackage.TypeMismatch; import org.omg.IOP.ServiceContext; import org.omg.PortableInterceptor.ClientRequestInfo; import org.omg.PortableInterceptor.ClientRequestInterceptor; import org.omg.PortableInterceptor.ForwardRequest; import org.wildfly.iiop.openjdk.logging.IIOPLogger; 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.auth.client.MatchRule; import org.wildfly.security.auth.principal.AnonymousPrincipal; import org.wildfly.security.auth.server.SecurityDomain; import org.wildfly.security.auth.server.SecurityIdentity; import org.wildfly.security.manager.WildFlySecurityManager; /** * This implementation of {@code org.omg.PortableInterceptor.ClientRequestInterceptor} inserts the security attribute * service (SAS) context into outgoing IIOP requests and handles the SAS messages received from the target security * service in the SAS context of incoming IIOP replies. * <p/> * When creating the SAS context, this implementation looks for an Elytron {@link AuthenticationConfiguration} that matches * the target URI (in the form iiop://hostname:port) and then uses the configuration to obtain the security info (like * username and password) that is inserted into the security tokens that are set in the SAS context. * <p/> * The type of security tokens that are constructed depends on the target security requirements: * <ul> * <li> * If the target supports identity propagation, the identity obtained from the Elytron configuration that matches * the target URI and the purpose {@code client-auth} is used to build the {@link IdentityToken} that is inserted * into the SAS context. This usually means using a configuration backed by a security domain so that the current * authenticated identity in that domain is used to build the identity token. * </li> * <li> * If in addition to the identity token the target requires username/password authentication, it means the target * expects this runtime (server) to identify itself using its own username and credentials. Once this runtime * has been authenticated, the identity contained in the identity token is used as a run-as identity. * <p/> * In terms of configuration, it must match the target URI and the purpose {@code server-auth} and it is usually * a config that defines this server's auth-name and associated credential via credential-reference. * </li> * <li> * If the target doesn't support identity propagation but supports username/password authentication, the identity * and credentials obtained from the Elytron configuration that matches the target URI and the purpose {@code client-auth} * are used to build the {@link InitialContextToken}. Again, this usually means using a configuration backed by a * security domain so that the current authenticated identity in that domain and its associated credentials are * used to build the initial context token. * </li> * </ul> */ public class ElytronSASClientInterceptor extends LocalObject implements ClientRequestInterceptor { private static final int SAS_CONTEXT_ID = org.omg.IOP.SecurityAttributeService.value; private static final String AUTHENTICATION_CONTEXT_CAPABILITY = "org.wildfly.security.authentication-context"; private static final RuntimeCapability<Void> AUTHENTICATION_CONTEXT_RUNTIME_CAPABILITY = RuntimeCapability .Builder.of(AUTHENTICATION_CONTEXT_CAPABILITY, true, AuthenticationContext.class) .build(); private static final AuthenticationContextConfigurationClient AUTH_CONFIG_CLIENT = AccessController.doPrivileged(AuthenticationContextConfigurationClient.ACTION); private static final IdentityToken ABSENT_IDENTITY_TOKEN; static { ABSENT_IDENTITY_TOKEN = new IdentityToken(); ABSENT_IDENTITY_TOKEN.absent(true); } private static final byte[] NO_AUTHENTICATION_TOKEN = {}; private static final AuthorizationElement[] NO_AUTHORIZATION_TOKEN = {}; private static String authenticationContextName; public static void setAuthenticationContextName(final String authenticationContextName) { ElytronSASClientInterceptor.authenticationContextName = authenticationContextName; } private Codec codec; private AuthenticationContext authContext; public ElytronSASClientInterceptor(final Codec codec) { this.codec = codec; // initialize the authentication context. final ServiceContainer container = this.currentServiceContainer(); if(authenticationContextName != null) { final ServiceName authContextServiceName = AUTHENTICATION_CONTEXT_RUNTIME_CAPABILITY.getCapabilityServiceName(authenticationContextName); this.authContext = (AuthenticationContext) container.getRequiredService(authContextServiceName).getValue(); } else { this.authContext = null; } } @Override public void send_request(ClientRequestInfo ri) throws ForwardRequest { try { CompoundSecMech secMech = CSIv2Util.getMatchingSecurityMech(ri, codec, EstablishTrustInClient.value, /* client supports */ (short) 0 /* client requires */); if (secMech == null) { return; } // these "null tokens" will be changed if needed. IdentityToken identityToken = ABSENT_IDENTITY_TOKEN; byte[] encodedAuthenticationToken = NO_AUTHENTICATION_TOKEN; final URI uri = this.getURI(ri); if(uri == null) { return; } SecurityDomain domain = SecurityDomain.getCurrent(); SecurityIdentity currentIdentity = null; if(domain != null) { currentIdentity = domain.getCurrentSecurityIdentity(); } final AuthenticationContext authContext; if(this.authContext != null) { authContext = this.authContext; } else if(currentIdentity == null || currentIdentity.isAnonymous()) { authContext = AuthenticationContext.captureCurrent(); } else { authContext = AuthenticationContext.empty().with(MatchRule.ALL, AuthenticationConfiguration.EMPTY.useForwardedIdentity(domain)); } if ((secMech.sas_context_mech.target_supports & IdentityAssertion.value) != 0) { final AuthenticationConfiguration configuration = AUTH_CONFIG_CLIENT. getAuthenticationConfiguration(uri, authContext, -1, null, null, "client-auth"); final Principal principal = AUTH_CONFIG_CLIENT.getPrincipal(configuration); if (principal != null && principal != AnonymousPrincipal.getInstance()) { // The name scope needs to be externalized. String name = principal.getName(); if (name.indexOf('@') < 0) { name += "@default"; // hardcoded (REVISIT!) } byte[] principalName = name.getBytes(StandardCharsets.UTF_8); // encode the principal name as mandated by RFC2743. byte[] encodedName = CSIv2Util.encodeGssExportedName(principalName); // encapsulate the encoded name. Any any = ORB.init().create_any(); byte[] encapsulatedEncodedName; GSS_NT_ExportedNameHelper.insert(any, encodedName); try { encapsulatedEncodedName = codec.encode_value(any); } catch (InvalidTypeForEncoding e) { throw IIOPLogger.ROOT_LOGGER.unexpectedException(e); } // create identity token. identityToken = new IdentityToken(); identityToken.principal_name(encapsulatedEncodedName); } else if ((secMech.sas_context_mech.supported_identity_types & ITTAnonymous.value) != 0) { // no run-as or caller identity and the target supports ITTAnonymous: use the anonymous identity. identityToken = new IdentityToken(); identityToken.anonymous(true); } // target might require an additional initial context token with a username/password pair for authentication. if ((secMech.as_context_mech.target_requires & EstablishTrustInClient.value) != 0) { encodedAuthenticationToken = this.createInitialContextToken(uri, "server-auth", secMech); } } else if ((secMech.as_context_mech.target_supports & EstablishTrustInClient.value) != 0) { // target doesn't require an identity token but supports username/password authentication - try to build // an initial context token using the configuration. encodedAuthenticationToken = this.createInitialContextToken(uri, "client-auth", secMech); } if (identityToken != ABSENT_IDENTITY_TOKEN || encodedAuthenticationToken != NO_AUTHENTICATION_TOKEN) { // at least one non-null token was created, create EstablishContext message with it. EstablishContext message = new EstablishContext(0, // stateless ctx id NO_AUTHORIZATION_TOKEN, identityToken, encodedAuthenticationToken); // create SAS context with the EstablishContext message. SASContextBody contextBody = new SASContextBody(); contextBody.establish_msg(message); // stuff the SAS context into the outgoing request. final Any any = ORB.init().create_any(); SASContextBodyHelper.insert(any, contextBody); ServiceContext sc = new ServiceContext(SAS_CONTEXT_ID, codec.encode_value(any)); ri.add_request_service_context(sc, true /*replace existing context*/); } } catch (Exception e) { throw IIOPLogger.ROOT_LOGGER.unexpectedException(e); } } @Override public void send_poll(ClientRequestInfo ri) { } @Override public void receive_reply(ClientRequestInfo ri) { try { ServiceContext sc = ri.get_reply_service_context(SAS_CONTEXT_ID); Any msg = codec.decode_value(sc.context_data, SASContextBodyHelper.type()); SASContextBody contextBody = SASContextBodyHelper.extract(msg); // At this point contextBody should contain a CompleteEstablishContext message, which does not require any // treatment. ContextError messages should arrive via receive_exception(). IIOPLogger.ROOT_LOGGER.tracef("receive_reply: got SAS reply, type %d", contextBody.discriminator()); if (contextBody.discriminator() == MTContextError.value) { // should not happen. throw IIOPLogger.ROOT_LOGGER.unexpectedContextErrorInSASReply(0, CompletionStatus.COMPLETED_YES); } } catch (BAD_PARAM e) { // no service context with sasContextId: do nothing } catch (FormatMismatch | TypeMismatch e) { throw IIOPLogger.ROOT_LOGGER.errorParsingSASReply(e, 0, CompletionStatus.COMPLETED_YES); } } @Override public void receive_exception(ClientRequestInfo ri) throws ForwardRequest { try { ServiceContext sc = ri.get_reply_service_context(SAS_CONTEXT_ID); Any msg = codec.decode_value(sc.context_data, SASContextBodyHelper.type()); SASContextBody contextBody = SASContextBodyHelper.extract(msg); // At this point contextBody may contain either a CompleteEstablishContext message or a ContextError message. // Neither message requires any treatment. We decoded the context body just to check that it contains // a well-formed message. IIOPLogger.ROOT_LOGGER.tracef("receive_exception: got SAS reply, type %d", contextBody.discriminator()); } catch (BAD_PARAM e) { // no service context with sasContextId: do nothing. } catch (FormatMismatch | TypeMismatch e) { throw IIOPLogger.ROOT_LOGGER.errorParsingSASReply(e, 0, CompletionStatus.COMPLETED_MAYBE); } } @Override public void receive_other(ClientRequestInfo ri) throws ForwardRequest { } @Override public String name() { return "ElytronSASClientInterceptor"; } @Override public void destroy() { } /** * Get a reference to the current {@link ServiceContainer}. * * @return a reference to the current {@link ServiceContainer}. */ private ServiceContainer currentServiceContainer() { if(WildFlySecurityManager.isChecking()) { return AccessController.doPrivileged(CurrentServiceContainer.GET_ACTION); } return CurrentServiceContainer.getServiceContainer(); } /** * Build an {@link URI} using the information extracted from the specified {@link ClientRequestInfo}. The format of * the URI built by this method is "iiop://hostname:port". * * @param clientRequestInfo the {@link ClientRequestInfo} used to obtain the target information necessary to build * the {@link URI}. * @return the constructed {@link URI} instance. * @throws URISyntaxException if a syntax error is found when building the {@link URI}. */ private URI getURI(final ClientRequestInfo clientRequestInfo) throws URISyntaxException { final StringBuilder builder = new StringBuilder("iiop:"); if (clientRequestInfo instanceof ClientRequestInfoImpl) { ClientRequestInfoImpl infoImpl = (ClientRequestInfoImpl) clientRequestInfo; CorbaConnection connection = (CorbaConnection) infoImpl.connection(); if(connection == null) { return null; } ContactInfo info = connection.getContactInfo(); if (info instanceof SocketOrChannelContactInfoImpl) { String hostname = ((SocketOrChannelContactInfoImpl) info).getHost(); if (hostname != null) builder.append("//").append(hostname); int port = ((SocketOrChannelContactInfoImpl) info).getPort(); if (port > 0) builder.append(":").append(port); } } else { return null; } return new URI(builder.toString()); } /** * Create an encoded {@link InitialContextToken} with an username/password pair obtained from an Elytron client configuration * matched by the specified {@link URI} and purpose. * * @param uri the target {@link URI}. * @param purpose a {@link String} representing the purpose of the configuration that will be used. * @param secMech a reference to the {@link CompoundSecMech} that was found in the {@link ClientRequestInfo}. * @return the encoded {@link InitialContextToken}, if a valid username is obtained from the matched configuration; * an empty {@code byte[]} otherwise; * @throws Exception if an error occurs while building the encoded {@link InitialContextToken}. */ private byte[] createInitialContextToken(final URI uri, final String purpose, final CompoundSecMech secMech) throws Exception { AuthenticationContext authContext = this.authContext == null ? AuthenticationContext.captureCurrent() : this.authContext; // obtain the configuration that matches the URI and purpose. final AuthenticationConfiguration configuration = AUTH_CONFIG_CLIENT.getAuthenticationConfiguration(uri, authContext, -1, null, null, purpose); // get the callback handler from the configuration and use it to obtain a username/password pair. final CallbackHandler handler = AUTH_CONFIG_CLIENT.getCallbackHandler(configuration); final NameCallback nameCallback = new NameCallback("Username: "); final PasswordCallback passwordCallback = new PasswordCallback("Password: ", false); try { handler.handle(new Callback[]{nameCallback, passwordCallback}); } catch (UnsupportedCallbackException e) { return NO_AUTHENTICATION_TOKEN; } // if the name callback contains a valid username we create the initial context token. if (nameCallback.getName() != null && !nameCallback.getName().equals(AnonymousPrincipal.getInstance().getName())) { byte[] encodedTargetName = secMech.as_context_mech.target_name; String name = nameCallback.getName(); if (name.indexOf('@') < 0) { byte[] decodedTargetName = CSIv2Util.decodeGssExportedName(encodedTargetName); String targetName = new String(decodedTargetName, StandardCharsets.UTF_8); name += "@" + targetName; // "@default" } byte[] username = name.getBytes(StandardCharsets.UTF_8); byte[] password = {}; if (passwordCallback.getPassword() != null) password = new String(passwordCallback.getPassword()).getBytes(StandardCharsets.UTF_8); // create the initial context token and ASN.1-encode it, as defined in RFC 2743. InitialContextToken authenticationToken = new InitialContextToken(username, password, encodedTargetName); return CSIv2Util.encodeInitialContextToken(authenticationToken, codec); } return NO_AUTHENTICATION_TOKEN; } }