From a94a45753a19a9993dbe41909b914918ce3a8fcb Mon Sep 17 00:00:00 2001 From: Lin Gao Date: Mon, 6 Mar 2023 11:22:43 +0800 Subject: [PATCH 1/2] [WFLY-17649] Adds a detailed quickstart for bearer authentication use cases --- ejb-security-jwt/README.adoc | 344 ++++++++++++++++++ ejb-security-jwt/app-one/ear/pom.xml | 80 ++++ .../application/META-INF/jboss-ejb-client.xml | 24 ++ ejb-security-jwt/app-one/ejb/pom.xml | 76 ++++ .../security/jwt/appone/JWTSecurityEJBA.java | 131 +++++++ .../jwt/appone/JWTSecurityEJBRemoteA.java | 58 +++ ejb-security-jwt/app-one/pom.xml | 44 +++ ejb-security-jwt/app-two/pom.xml | 89 +++++ .../security/jwt/apptwo/JWTSecurityEJBB.java | 60 +++ .../jwt/apptwo/JWTSecurityEJBRemoteB.java | 43 +++ ejb-security-jwt/client/pom.xml | 77 ++++ .../security/jwt/client/RemoteEJBClient.java | 96 +++++ .../resources/META-INF/wildfly-config.xml | 40 ++ .../configure-ejb-outbound-connection.cli | 17 + ejb-security-jwt/configure-elytron.cli | 23 ++ ejb-security-jwt/configure_server.adoc | 41 +++ .../keycloak/realm/realm-import.json | 197 ++++++++++ ejb-security-jwt/pom.xml | 169 +++++++++ ejb-security-jwt/restore-configuration.cli | 49 +++ pom.xml | 1 + 20 files changed, 1659 insertions(+) create mode 100644 ejb-security-jwt/README.adoc create mode 100644 ejb-security-jwt/app-one/ear/pom.xml create mode 100644 ejb-security-jwt/app-one/ear/src/main/application/META-INF/jboss-ejb-client.xml create mode 100644 ejb-security-jwt/app-one/ejb/pom.xml create mode 100644 ejb-security-jwt/app-one/ejb/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/appone/JWTSecurityEJBA.java create mode 100644 ejb-security-jwt/app-one/ejb/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/appone/JWTSecurityEJBRemoteA.java create mode 100644 ejb-security-jwt/app-one/pom.xml create mode 100644 ejb-security-jwt/app-two/pom.xml create mode 100644 ejb-security-jwt/app-two/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBB.java create mode 100644 ejb-security-jwt/app-two/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBRemoteB.java create mode 100644 ejb-security-jwt/client/pom.xml create mode 100644 ejb-security-jwt/client/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/client/RemoteEJBClient.java create mode 100644 ejb-security-jwt/client/src/main/resources/META-INF/wildfly-config.xml create mode 100644 ejb-security-jwt/configure-ejb-outbound-connection.cli create mode 100644 ejb-security-jwt/configure-elytron.cli create mode 100644 ejb-security-jwt/configure_server.adoc create mode 100644 ejb-security-jwt/keycloak/realm/realm-import.json create mode 100644 ejb-security-jwt/pom.xml create mode 100644 ejb-security-jwt/restore-configuration.cli diff --git a/ejb-security-jwt/README.adoc b/ejb-security-jwt/README.adoc new file mode 100644 index 0000000000..e29fcad9eb --- /dev/null +++ b/ejb-security-jwt/README.adoc @@ -0,0 +1,344 @@ +include::../shared-doc/attributes.adoc[] + += ejb-security-jwt: EJB Security with bearer token authentication and authorization +:author: Lin Gao +:level: Advanced +:technologies: EJB, Security, Bearer, OIDC, JWT +:requires-multiple-servers: true + +[abstract] +The `ejb-security-jwt` quickstart uses EJB and `OAUTHBEARER` SASL mechanism to demonstrate how to access a secured EJB deployed to {productNameFull} from a remote Java client application, and how an EJB deployed in one {productNameFull} calls a secured EJB deployed in another {productNameFull} using the same authentication context. + +:standalone-server-type: default +:archiveType: ear +:oidcIdp: KeyCloak +:oidcIdpLink: https://www.keycloak.org/ +:serverNameBase: {jbossHomeName} + +== What is it? + +The `ejb-security-jwt` quickstart shows how to access a remote secured EJB from a remote Java client application. It demonstrates the use of EJB and `OAUTHBEARER` SASL mechanism in {productNameFull}. It uses {oidcIdpLink}[{oidcIdp}] as the OIDC Identity Provider(IDP) with a predefined realm setup for this quickstart. + +This example consists of the following Maven projects, each with a shared parent: + +[cols="40%,60%",options="headers"] +|=== +|Project |Description + +a|`app-one` +a|An `EAR` application that can be called by the `client`. It shows current caller principal and if it has role of `user` and `admin`, it can also call the EJB deployed in a separate server from `app-two` using the same authentication context. + +[[ejba]] We can call the EJB in `app-one` as `EJBA` in this document, it will be deployed into {productName}_1 server. + +a|`app-two` +a|An `EJB` application that shows current caller principal and if it has role of `user` and `admin`. + +[[ejbb]] We can call the EJB in `app-two` as `EJBB` in this document, it will be deployed into {productName}_2 server. + +a|`client` +|This project builds the standalone client and executes it. +|=== + +The root `pom.xml` builds each of the subprojects in an appropriate order. + +The server configuration is done using CLI batch scripts located in the root of this quickstart folder. + +// System Requirements +include::../shared-doc/system-requirements.adoc[leveloffset=+1] +// Use of {jbossHomeName} +include::../shared-doc/use-of-jboss-home-name.adoc[leveloffset=+1] + +== Start A {oidcIdp} Server With Predefined Realm + +This quickstart needs an OIDC IDP from which to get the bearer token from. We use {oidcIdp} for this quickstart. + +{oidcIdp} supports starting a Docker container with a predefined realm setup + +.Start {oidcIdp} Server with realm setup using `keycloak/realm/realm-import.json`: +[source, subs="+quotes,attributes+"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt +$ docker run --rm -p 8180:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -v $(pwd)/keycloak/realm:/opt/keycloak/data/import quay.io/keycloak/keycloak:21.0.0 start-dev --import-realm +---- + +This predefined realm has the following configuration: + +A realm called `jwt-realm` is created, there is a client called `app` created under this realm, the secret for this client is `secret`, and there are 2 users created: + +[cols="30%,30%,40%",options="headers"] +|=== +|UserName | Password | Realm Roles +|quickstartUser |quickstartPwd1! | user +|admin |admin | user, admin +|=== + +You can also set up the realm manually, please refer to {oidcIdpLink} for detail. + +== Build the Project +. Navigate to the quickstart directory to build the project ++ +[source, subs="+quotes,attributes+", options="nowrap"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt +---- + +. Build the project ++ +[source,options="nowrap"] +---- +$ mvn clean install +---- + +== Demonstrations + +There are 2 parts in this quickstart, the first part is showing how the remote EJB client calls EJB deployed in {productName}_1 server using `OAUTHBEARER` SASL mechanism, the second one is showing how the remote EJB client calls EJB deployed in {productName}_1 server which in turn invokes EJB deployed in {productName}_2 server with the authentication context propagated. + +Let's start the first part. + +=== Remote EJB Client calls EJB using `OAUTHBEARER` mechanism + +:jbossHomeName: {serverNameBase}_1 +[#start-server-1] +include::../shared-doc/start-the-standalone-server.adoc[leveloffset=+3] + +:hostController: --controller=localhost:9990 +include::./configure_server.adoc[] + +==== Deploy EJB in `app-one` to {productName}_1 Server +The EJB `JWTSecurityEJBRemoteA` in `app-one` has one method declared: + +[source, java] +---- +String securityInfo(boolean recursive); +---- +which will return a String including the caller principal and the authorization check result if it has role of `user` and `admin`. + +Open a terminal, and use the following command to deploy EJB in `app-one` module to {productName}_1 Server: + +[source, bash, subs="+quotes,attributes+"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt/app-one/ejb/ +$ mvn wildfly:deploy +---- + +==== Configure and run EJB client application + +The remote EJB client application will invoke link:#ejba[EJBA] twice. The first invocation reads the authentication configuration from `META-INF/wildfly-config.xml` where the `quickstartUser` and it's password is specified to get the bearer token from a specified token endpoint url. The second invocation uses programmatic authentication switching to use `admin` user. The only difference between 2 invocations is that they are using different user, thus differnt roles. + +It has a system property called `recursive` in the client to decide if link:#ejba[EJBA] should call link:#ejbb[EJBB], we won't call EJBB in the first part, so we leave it as the default value: `false`. + +Open a terminal, run the following command to run the remote EJB client application: + +[source, bash] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt/client +$ mvn exec:exec +---- + +==== Investigate the Console Output + +When the client application runs, it performs the following steps: + +. Obtains a stateless session bean instance. +. Sends method invocations to the stateless bean to get current security information from server side. + +The following output is displayed in the terminal window: + +[source, bash] +---- +* * * * * * * * * * * recursive: false * * * * * * * * * * * * * * * * * * * + +Security Info in JWTSecurityEJBA: + Caller: [quickstartuser] + quickstartuser has user role: (true) + quickstartuser has admin role: (false) + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +* * * * * * * Below are invoked using admin account * * * * * * + +* * * * * * * * * * * recursive: false * * * * * * * * * * * * * * * * * * * + +Security Info in JWTSecurityEJBA: + Caller: [admin] + admin has user role: (true) + admin has admin role: (true) + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +---- + +We can see that the user `quickstartUser` has the `user` role, but does not have `admin` role, the user `admin` has both roles. It can be confirmed in the {oidcIdp} server setup. + + +Now let's jump to the second part. + +=== Propagate the authentication context using `OAUTHBEARER` mechanism for EJB calls EJB. + +In this part, we will demonstrate how the link:#ejba[EJBA] calls link:#ejbb[EJBB], and how to configure the {productName}_1 to propagate the authentication context from remote client using `OAUTHBEARER` SASL mechanism. + +==== Configure remote outbound connection in {productName}_1 Server +We need to create a `remote-outbound-connection` in remoting subsystem of {productName}_1 server with the authentication context specified in elytron subsystem to propagate. + +Open a terminal, and run the following command: + +[source,subs="+quotes,attributes+",options="nowrap"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt +$ __${jbossHomeName}__/bin/jboss-cli.sh -c --file=configure-ejb-outbound-connection.cli +---- + +You will see the following configuration in standalone.xml of {productName}_1 server: + +[source, xml] +---- + + + + + + + + + + + + + + + + + +---- + +==== Redeploy EJB in `app-one` to {productName}_1 Server +In the first part, we deployed EJB `jar` to {productName}_1 server for the demonstration, to be able to make link:#ejba[EJBA] calls link:#ejbb[EJBB], we need to package the EJBs in `EAR` to have ejb client dependency of EJBB inside. + +Open a terminal, and run the following command to undeploy the link:#ejba[EJBA] in jar archive, and deploy it again using ear archive: + +[source, bash, subs="+quotes,attributes+"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt/app-one/ejb +$ mvn wildfly:undeploy +$ cd ../ear +$ mvn wildfly:deploy +---- + +There is a file `META-INF/jbos-ejb-client.xml` in the `EAR` deployment, which specifies the outbound-connection reference to `ejb-outbound-connection`, it matches what was defined above, and it points to the HTTP port: `8280` which is used by the {productName}_2 server. + +==== Starts {productName}_2 server + +:jbossHomeName: {serverNameBase}_2 +Now it's time to start {productName}_2 server. Open a terminal, and like what you did to start link:#start-server-1[{productName}_1 server], you need to specify a port offset to avoid port conflicts: `-Djboss.socket.binding.port-offset=200`, this makes the HTTP port opened by {productName}_2 server becomes `8280`: + +.Example of starting {productName}_2 server in Linux system: +[source,subs="+quotes,attributes+"] +---- +$ __{jbossHomeName}__/bin/standalone.sh -Djboss.socket.binding.port-offset=200 +---- + +:hostController: --controller=localhost:10190 +include::./configure_server.adoc[] + +==== Deploy EJB in `app-two` to {productName}_2 Server +The EJB `JWTSecurityEJBRemoteB` in `app-two` has one method declared: + +[source, java] +---- +String securityInfo(); +---- +which will return a String including the caller principal and the authorization check result if it has role of `user` and `admin`. + +Open a terminal, and use the following command to deploy EJB in `app-two` module to {productName}_2 Server: + +[source, bash, subs="+quotes,attributes+"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt/app-two +$ mvn wildfly:deploy -Dwildfly.port=10190 +---- + +==== Configure and run EJB client application +Now let's run the remote client again, we will specify `-Drecursive=true` to let EJBA calls EJBB this time. + +Open a terminal, and run the following command: + +[source, bash] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt/client +$ mvn exec:exec -Drecursive=true +---- + + +==== Investigate the Console Output +The following output is displayed in the terminal window: + +[source, bash] +---- +* * * * * * * * * * * recursive: true * * * * * * * * * * * * * * * * * * * + +Security Info in JWTSecurityEJBA: + Caller: [quickstartuser] + quickstartuser has user role: (true) + quickstartuser has admin role: (false) + +=========== Below are invocation from remote EJB in app-two =========== +Security Info in JWTSecurityEJBB: + Caller: [quickstartuser] + quickstartuser has user role: (true) + quickstartuser has admin role: (false) + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +* * * * * * * Below are invoked using admin account * * * * * * + +* * * * * * * * * * * recursive: true * * * * * * * * * * * * * * * * * * * + +Security Info in JWTSecurityEJBA: + Caller: [admin] + admin has user role: (true) + admin has admin role: (true) + +=========== Below are invocation from remote EJB in app-two =========== +Security Info in JWTSecurityEJBB: + Caller: [admin] + admin has user role: (true) + admin has admin role: (true) + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +---- + +We can see that the user `quickstartUser` has the `user` role, but does not have `admin` role, the user `admin` has both roles. It can be confirmed in the {oidcIdp} server setup. + +We can also see that the invocation from EJBA to EJBB uses the same authentication context as what is used in remote client calls EJBA. + +== Undeploy the Archives + +To undeploy the components from the {productName} servers: + +. Navigate to the `app-one/ear` subdirectory: ++ +[source,options="nowrap"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt/app-one/ear/ +$ mvn wildfly:undeploy +---- + +. Navigate to the `app-two` subdirectory: ++ +[source,options="nowrap"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt/app-two +$ mvn wildfly:undeploy -Dwildfly.port=10190 +---- + +== Restore the servers +After un-deployed the archives from both servers, you can restore the server configurations: + +[source,subs="+quotes,attributes+",options="nowrap"] +---- +$ cd ${QUICKSTART_HOME}/ejb-security-jwt +$ __${serverNameBase}_1__/bin/jboss-cli.sh -c --file=restore-configuration.cli +$ __${serverNameBase}_2__/bin/jboss-cli.sh -c {hostController} --file=restore-configuration.cli +---- + diff --git a/ejb-security-jwt/app-one/ear/pom.xml b/ejb-security-jwt/app-one/ear/pom.xml new file mode 100644 index 0000000000..cf01f1bb7e --- /dev/null +++ b/ejb-security-jwt/app-one/ear/pom.xml @@ -0,0 +1,80 @@ + + + + 4.0.0 + + + org.wildfly.quickstarts + ejb-security-jwt-app-one + 31.0.0.Beta1-SNAPSHOT + + ejb-security-jwt-app-one-ear + ear + Quickstart: ejb-security-jwt - app-one-ear + This project has the EJBA for the demonstration, this is the ear artifact. + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + 8080 + + + + + + ${project.groupId} + ejb-security-jwt-app-one-ejb + ${project.version} + ejb + + + ${project.groupId} + ejb-security-jwt-app-two + ${project.version} + ejb + + + + + ${project.parent.artifactId} + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + maven-ear-plugin + + 8 + lib + @{artifactId}@.@{extension}@ + + + + + + diff --git a/ejb-security-jwt/app-one/ear/src/main/application/META-INF/jboss-ejb-client.xml b/ejb-security-jwt/app-one/ear/src/main/application/META-INF/jboss-ejb-client.xml new file mode 100644 index 0000000000..8b9a0ea658 --- /dev/null +++ b/ejb-security-jwt/app-one/ear/src/main/application/META-INF/jboss-ejb-client.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/ejb-security-jwt/app-one/ejb/pom.xml b/ejb-security-jwt/app-one/ejb/pom.xml new file mode 100644 index 0000000000..5f14dd76ba --- /dev/null +++ b/ejb-security-jwt/app-one/ejb/pom.xml @@ -0,0 +1,76 @@ + + + + 4.0.0 + + org.wildfly.quickstarts + ejb-security-jwt-app-one + 31.0.0.Beta1-SNAPSHOT + ../pom.xml + + ejb-security-jwt-app-one-ejb + ejb + Quickstart: ejb-security-jwt - app-one-ejb + This project has the EJBA for the demonstration, this is the EJB artifact. + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.ejb + jakarta.ejb-api + provided + + + org.jboss.ejb3 + jboss-ejb3-ext-api + provided + + + + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + org.apache.maven.plugins + maven-ejb-plugin + + 3.2 + true + + + + + diff --git a/ejb-security-jwt/app-one/ejb/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/appone/JWTSecurityEJBA.java b/ejb-security-jwt/app-one/ejb/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/appone/JWTSecurityEJBA.java new file mode 100644 index 0000000000..5871f8d564 --- /dev/null +++ b/ejb-security-jwt/app-one/ejb/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/appone/JWTSecurityEJBA.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of Apache License v2.0 which + * accompanies this distribution. + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * 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.jboss.as.quickstarts.ejb.security.jwt.appone; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.ejb.Remote; +import jakarta.ejb.SessionContext; +import jakarta.ejb.Stateless; +import org.jboss.ejb3.annotation.SecurityDomain; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import java.lang.reflect.Method; +import java.util.Hashtable; + +/** + * Implementation of {@link JWTSecurityEJBRemoteA} + * @author Lin Gao + */ +@Stateless +@Remote(JWTSecurityEJBRemoteA.class) +@SecurityDomain("jwt-app-domain") +@PermitAll +public class JWTSecurityEJBA implements JWTSecurityEJBRemoteA { + + @Resource + private SessionContext context; + + private InitialContext ctx; + + @PostConstruct + public void setup() { + try { + final Hashtable p = new Hashtable<>(); + p.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming"); + this.ctx = new InitialContext(p); + } catch (NamingException e) { + throw new RuntimeException("Could not initialize context", e); + } + } + + @PreDestroy + public void shutdown() { + try { + if (this.ctx != null) this.ctx.close(); + } catch (NamingException e) { + throw new RuntimeException("Failed to close the context", e); + } + } + + @Override + public String securityInfo(boolean recursive) { + StringBuilder sb = new StringBuilder("Security Info in JWTSecurityEJBA: \n\tCaller: ["); + String caller = principal(); + sb.append(caller).append("]\n"); + sb.append("\t\t").append(caller).append(" has user role: (").append(inRole("user")).append(")\n"); + sb.append("\t\t").append(caller).append(" has admin role: (").append(inRole("admin")).append(")\n"); + System.out.println("\nSecurity Info(A): \n" + sb + "\n"); + if (recursive) { + sb.append("\n=========== Below are invocation from remote EJB in app-two ===========\n"); + sb.append(securityInfoFromB()); + } + return sb.toString(); + } + + private String securityInfoFromB() { + try { + Object remote = lookupEJBB(false); + Method securityInfoMethod = remote.getClass().getDeclaredMethod("securityInfo"); + return securityInfoMethod.invoke(remote).toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Object lookupEJBB(boolean local) throws NamingException { + String app = local ? "ejb-security-jwt-app-one" : ""; + String lookup = "ejb:" + app + "/ejb-security-jwt-app-two/JWTSecurityEJBB!org.jboss.as.quickstarts.ejb.security.jwt.apptwo.JWTSecurityEJBRemoteB"; + return ctx.lookup(lookup); + } + + @Override + public String principal() { + return context.getCallerPrincipal() != null ? context.getCallerPrincipal().getName() : null; + } + + @Override + public boolean inRole(String role) { + return context.isCallerInRole(role); + } + + @Override + public String principalFromB(boolean local) { + try { + Object remote = lookupEJBB(local); + Method securityInfoMethod = remote.getClass().getDeclaredMethod("principal"); + return securityInfoMethod.invoke(remote).toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean inRoleFromB(String role, boolean local) { + try { + Object remote = lookupEJBB(local); + Method securityInfoMethod = remote.getClass().getDeclaredMethod("inRole", String.class); + return (Boolean)securityInfoMethod.invoke(remote, role); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/ejb-security-jwt/app-one/ejb/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/appone/JWTSecurityEJBRemoteA.java b/ejb-security-jwt/app-one/ejb/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/appone/JWTSecurityEJBRemoteA.java new file mode 100644 index 0000000000..ca385d030c --- /dev/null +++ b/ejb-security-jwt/app-one/ejb/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/appone/JWTSecurityEJBRemoteA.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of Apache License v2.0 which + * accompanies this distribution. + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * 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.jboss.as.quickstarts.ejb.security.jwt.appone; + +/** + * An EJB to returns current caller identity information and if it has a specific role. + * + * @author Lin Gao + */ +public interface JWTSecurityEJBRemoteA { + + /** + * @param recursive true if it invokes {@link org.jboss.as.quickstarts.ejb.security.jwt.apptwo.JWTSecurityEJBRemoteB} + * deployed in another server, false won't. + * @return the security information in the current security context + */ + String securityInfo(boolean recursive); + + /** + * @return the current caller principal. + */ + String principal(); + + /** + * @param role the role to check + * @return true if current identity has the role specified in the role parameter, false otherwise. + */ + boolean inRole(String role); + + /** + * @param local true if JWTSecurityEJBRemoteB should be look up in the same server, false in another server. + * @return the current caller principal from {@link org.jboss.as.quickstarts.ejb.security.jwt.apptwo.JWTSecurityEJBRemoteB} + */ + String principalFromB(boolean local); + + /** + * @param role the role to check + * @param local true if JWTSecurityEJBRemoteB should be look up in the same server, false in another server. + * @return true if current identity has the role specified in the role parameter from + * {@link org.jboss.as.quickstarts.ejb.security.jwt.apptwo.JWTSecurityEJBRemoteB}, false otherwise. + */ + boolean inRoleFromB(String role, boolean local); + +} diff --git a/ejb-security-jwt/app-one/pom.xml b/ejb-security-jwt/app-one/pom.xml new file mode 100644 index 0000000000..7ee5f14979 --- /dev/null +++ b/ejb-security-jwt/app-one/pom.xml @@ -0,0 +1,44 @@ + + + + 4.0.0 + + + org.wildfly.quickstarts + ejb-security-jwt + 31.0.0.Beta1-SNAPSHOT + ../pom.xml + + ejb-security-jwt-app-one + pom + Quickstart: ejb-security-jwt - app-one + A project has EJBA for the demonstration, this is the POM artifact. + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + ejb + ear + + diff --git a/ejb-security-jwt/app-two/pom.xml b/ejb-security-jwt/app-two/pom.xml new file mode 100644 index 0000000000..e102300e5b --- /dev/null +++ b/ejb-security-jwt/app-two/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + org.wildfly.quickstarts + ejb-security-jwt + 31.0.0.Beta1-SNAPSHOT + ../pom.xml + + ejb-security-jwt-app-two + ejb + Quickstart: ejb-security-jwt - app-two + This project has the EJBB for the demonstration + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + + + jakarta.annotation + jakarta.annotation-api + provided + + + + + jakarta.ejb + jakarta.ejb-api + provided + + + org.jboss.ejb3 + jboss-ejb3-ext-api + provided + + + + + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + ${project.build.finalName}.jar + + + + org.apache.maven.plugins + maven-ejb-plugin + + 3.2 + + true + + org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBB* + + + + + + + + diff --git a/ejb-security-jwt/app-two/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBB.java b/ejb-security-jwt/app-two/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBB.java new file mode 100644 index 0000000000..9f2db65a57 --- /dev/null +++ b/ejb-security-jwt/app-two/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBB.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of Apache License v2.0 which + * accompanies this distribution. + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * 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.jboss.as.quickstarts.ejb.security.jwt.apptwo; + +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.ejb.Remote; +import jakarta.ejb.SessionContext; +import jakarta.ejb.Stateless; +import org.jboss.ejb3.annotation.SecurityDomain; + +/** + * Implementation of {@link JWTSecurityEJBRemoteB}. + * + * @author Lin Gao + */ +@Stateless +@Remote(JWTSecurityEJBRemoteB.class) +@SecurityDomain("jwt-app-domain") +@PermitAll +public class JWTSecurityEJBB implements JWTSecurityEJBRemoteB { + + @Resource + private SessionContext context; + + @Override + public String securityInfo() { + StringBuilder sb = new StringBuilder("Security Info in JWTSecurityEJBB: \n\tCaller: ["); + String caller = principal(); + sb.append(caller).append("]\n"); + sb.append("\t\t").append(caller).append(" has user role: (").append(inRole("user")).append(")\n"); + sb.append("\t\t").append(caller).append(" has admin role: (").append(inRole("admin")).append(")\n"); + System.out.println("\nSecurity Info(B): \n" + sb + "\n"); + return sb.toString(); + } + + @Override + public String principal() { + return context.getCallerPrincipal() != null ? context.getCallerPrincipal().getName() : null; + } + + @Override + public boolean inRole(String role) { + return context.isCallerInRole(role); + } +} diff --git a/ejb-security-jwt/app-two/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBRemoteB.java b/ejb-security-jwt/app-two/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBRemoteB.java new file mode 100644 index 0000000000..9943ba70ca --- /dev/null +++ b/ejb-security-jwt/app-two/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/apptwo/JWTSecurityEJBRemoteB.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of Apache License v2.0 which + * accompanies this distribution. + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * 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.jboss.as.quickstarts.ejb.security.jwt.apptwo; + +/** + * An EJB to returns current caller identity information and if it has a specific role. + * + * @author Lin Gao + */ +public interface JWTSecurityEJBRemoteB { + + /** + * The security information contains the principal and if it has role of user and admin. + * + * @return the security information in the current security context + */ + String securityInfo(); + + /** + * @return the current caller principal. + */ + String principal(); + + /** + * @param role the role to check + * @return true if current identity has the role specified in the role parameter, false otherwise. + */ + boolean inRole(String role); +} diff --git a/ejb-security-jwt/client/pom.xml b/ejb-security-jwt/client/pom.xml new file mode 100644 index 0000000000..580ac20dff --- /dev/null +++ b/ejb-security-jwt/client/pom.xml @@ -0,0 +1,77 @@ + + + + 4.0.0 + + + org.wildfly.quickstarts + ejb-security-jwt + 31.0.0.Beta1-SNAPSHOT + ../pom.xml + + ejb-security-jwt-client + jar + Quickstart: ejb-security-jwt - client + This project is the remote EJB client application. + + + false + + + + + org.wildfly + wildfly-ejb-client-bom + pom + compile + + + jakarta.json + jakarta.json-api + + + org.eclipse.parsson + parsson + + + ${project.groupId} + ejb-security-jwt-app-one-ejb + ejb-client + + + + + + + org.codehaus.mojo + exec-maven-plugin + + java + ${project.build.directory}/exec-working-directory + + -Drecursive=${recursive} + -classpath + + org.jboss.as.quickstarts.ejb.security.jwt.client.RemoteEJBClient + + + + + + + diff --git a/ejb-security-jwt/client/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/client/RemoteEJBClient.java b/ejb-security-jwt/client/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/client/RemoteEJBClient.java new file mode 100644 index 0000000000..42da147475 --- /dev/null +++ b/ejb-security-jwt/client/src/main/java/org/jboss/as/quickstarts/ejb/security/jwt/client/RemoteEJBClient.java @@ -0,0 +1,96 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015, Red Hat, Inc. and/or its affiliates, and individual + * contributors by the @authors tag. See the copyright.txt in the + * distribution for a full listing of individual contributors. + * + * 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.jboss.as.quickstarts.ejb.security.jwt.client; + +import org.jboss.as.quickstarts.ejb.security.jwt.appone.JWTSecurityEJBRemoteA; +import org.wildfly.security.auth.client.AuthenticationConfiguration; +import org.wildfly.security.auth.client.AuthenticationContext; +import org.wildfly.security.auth.client.MatchRule; +import org.wildfly.security.credential.source.OAuth2CredentialSource; +import org.wildfly.security.sasl.SaslMechanismSelector; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import java.io.IOException; +import java.net.URL; +import java.util.Hashtable; + +/** + * A Java client used to invoke remote EJB. + */ +public class RemoteEJBClient { + + private static final String CLIENT_ID = "app"; + private static final String CLIENT_SECRET = "secret"; + private static final String TOKEN_URL = "http://localhost:8180/realms/jwt-realm/protocol/openid-connect/token"; + private static final String ADMIN_NAME = "admin"; + private static final String ADMIN_PASS = "admin"; + + private static final String DEFAULT_JNDI_PROVIDER_URL = "remote+http://localhost:8080"; + + public static void main(String[] args) throws Exception { + // recursive is used to indicate if JWTSecurityEJBRemoteA should call JWTSecurityEJBRemoteB + // it can only be set to true if JWTSecurityEJBRemoteA is deployed as an ear deployment. + boolean recursive = Boolean.getBoolean("recursive"); + AuthenticationContext.getContextManager().setThreadDefault(null); + invokeAppOne(recursive); + + System.out.println("\n\n* * * * * * * Below are invoked using admin account * * * * * *\n"); + switchToAdmin(); + invokeAppOne(recursive); + } + + private static void invokeAppOne(boolean recursive) throws NamingException { + JWTSecurityEJBRemoteA ejbRemoteA = lookupEJBRemoteA(recursive, DEFAULT_JNDI_PROVIDER_URL); + System.out.println("\n* * * * * * * * * * * recursive: " + recursive + " * * * * * * * * * * * * * * * * * * *\n"); + System.out.println(ejbRemoteA.securityInfo(recursive)); + System.out.println("\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n"); + } + + public static void switchToAdmin() throws IOException { + // now lets programmatically set up an authentication context to switch to admin user + AuthenticationConfiguration superUser = AuthenticationConfiguration.empty() + .setSaslMechanismSelector(SaslMechanismSelector.NONE.addMechanism("OAUTHBEARER")) + .useCredentials(OAuth2CredentialSource + .builder(new URL(TOKEN_URL)) + .clientCredentials(CLIENT_ID, CLIENT_SECRET) + .useResourceOwnerPassword(ADMIN_NAME, ADMIN_PASS) + .build()); + final AuthenticationContext authCtx = AuthenticationContext.empty().with(MatchRule.ALL, superUser); + AuthenticationContext.getContextManager().setThreadDefault(authCtx); + } + + public static JWTSecurityEJBRemoteA lookupEJBRemoteA(boolean ear, String providerURL) throws NamingException { + InitialContext context = jndiContext(providerURL); + try { + final String jndiName = ear ? + "ejb:ejb-security-jwt-app-one/ejb-security-jwt-app-one-ejb/JWTSecurityEJBA!" + JWTSecurityEJBRemoteA.class.getName() + : "ejb:/ejb-security-jwt-app-one-ejb/JWTSecurityEJBA!" + JWTSecurityEJBRemoteA.class.getName(); + return (JWTSecurityEJBRemoteA)context.lookup(jndiName); + } finally { + context.close(); + } + } + + private static InitialContext jndiContext(String providerURL) throws NamingException { + final Hashtable jndiProperties = new Hashtable<>(); + jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, "org.wildfly.naming.client.WildFlyInitialContextFactory"); + jndiProperties.put(Context.PROVIDER_URL, providerURL); + return new InitialContext(jndiProperties); + } +} diff --git a/ejb-security-jwt/client/src/main/resources/META-INF/wildfly-config.xml b/ejb-security-jwt/client/src/main/resources/META-INF/wildfly-config.xml new file mode 100644 index 0000000000..5139f875c0 --- /dev/null +++ b/ejb-security-jwt/client/src/main/resources/META-INF/wildfly-config.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ejb-security-jwt/configure-ejb-outbound-connection.cli b/ejb-security-jwt/configure-ejb-outbound-connection.cli new file mode 100644 index 0000000000..2786bf0121 --- /dev/null +++ b/ejb-security-jwt/configure-ejb-outbound-connection.cli @@ -0,0 +1,17 @@ +batch + +# The following part is needed for ejb from server a to server b +# Add the authentication configuration and authentication context that will be used for outbound connections into server A +/subsystem=elytron/authentication-configuration=ejb-outbound-configuration:add(security-domain=jwt-domain,sasl-mechanism-selector="OAUTHBEARER") +/subsystem=elytron/authentication-context=ejb-outbound-context:add(match-rules=[{authentication-configuration=ejb-outbound-configuration}]) + +# Add the outbound socket binding +/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ejb-outbound:add(host=localhost,port=${ejb-outbound-port:8280}) + +# Add the remote outbound connection +/subsystem=remoting/remote-outbound-connection=ejb-outbound-connection:add(outbound-socket-binding-ref=ejb-outbound, authentication-context=ejb-outbound-context) + +run-batch + +reload + diff --git a/ejb-security-jwt/configure-elytron.cli b/ejb-security-jwt/configure-elytron.cli new file mode 100644 index 0000000000..a1b68ddd5a --- /dev/null +++ b/ejb-security-jwt/configure-elytron.cli @@ -0,0 +1,23 @@ +batch + +# Add a new token security realm to elytron for authentication using oauth2 introspection endpoint +/subsystem=elytron/token-realm=jwt-realm:add(principal-claim="username", oauth2-introspection={client-id=app, client-secret=secret, introspection-url="http://localhost:8180/realms/jwt-realm/protocol/openid-connect/token/introspect"}) + +/subsystem=elytron/simple-role-decoder=roles-attribute:add(attribute="Roles") + +# Add a new security domain, which uses the jwt security realm +/subsystem=elytron/security-domain=jwt-domain:add(realms=[{realm=jwt-realm,role-decoder=roles-attribute}],permission-mapper=default-permission-mapper,default-realm=jwt-realm) + +# Create sasl authentication factory that uses SASL OAUTHBEARER +/subsystem=elytron/sasl-authentication-factory=jwt-sasl-authentication:add(security-domain=jwt-domain, sasl-server-factory=configured,mechanism-configurations=[{mechanism-name="OAUTHBEARER",mechanism-realm-configurations=[{realm-name="jwt-realm"}]}]) + +# Update remoting subsystem to use jwt-sasl-authentication created above +/subsystem=remoting/http-connector=http-remoting-connector:write-attribute(name=sasl-authentication-factory,value=jwt-sasl-authentication) + +# Configure the EJB subsystem to use jwt-domain +/subsystem=ejb3/application-security-domain=jwt-app-domain:add(security-domain=jwt-domain) + +run-batch + +reload + diff --git a/ejb-security-jwt/configure_server.adoc b/ejb-security-jwt/configure_server.adoc new file mode 100644 index 0000000000..e188da7182 --- /dev/null +++ b/ejb-security-jwt/configure_server.adoc @@ -0,0 +1,41 @@ +==== Configure the {productName} Server +Use the following CLI to configure the {productName} Server: + +[source,subs="+quotes,attributes+",options="nowrap"] +---- +$ __${jbossHomeName}__/bin/jboss-cli.sh -c {hostController} --file=configure-elytron.cli +---- + +It creates an elytron security realm called `jwt-realm` with the `username` as the principal claim, and the oauth2 introspection url to the one exposed by the Keycloak: + +.The `jwt-realm` information in standalone.xml +[source, xml] +---- + + + +---- + +It also creates an elytron security domain called `jwt-domain` with the default realm `jwt-realm` and role-decoder to `roles-attribute` which is a simple-role-decoder from attribute `Roles`. + +.The `jwt-domain` information in standalone.xml +[source, xml] +---- + + + +---- + +It also creates a `jwt-sasl-authentication` sasl-authentication-factory which uses the `jwt-domain` and `OAUTHBEARER` SASL mechanism. It updates remoting subsystem to use `jwt-sasl-authentication` created in previous step, and then creates a `application-security-domain` called `jwt-app-domain` in ejb3 subsystem, the latter matches the `@org.jboss.ejb3.annotation.SecurityDomain("jwt-app-domain")` in the EJB. + +.The `jwt-sasl-authentication` information in standalone.xml +[source, xml] +---- + + + + + + + +---- \ No newline at end of file diff --git a/ejb-security-jwt/keycloak/realm/realm-import.json b/ejb-security-jwt/keycloak/realm/realm-import.json new file mode 100644 index 0000000000..d86cf82271 --- /dev/null +++ b/ejb-security-jwt/keycloak/realm/realm-import.json @@ -0,0 +1,197 @@ +{ + "realm": "jwt-realm", + "enabled": true, + "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "requiredCredentials": [ + "password" + ], + "users": [ + { + "username": "quickstartUser", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "quickstartPwd1!" + } + ], + "realmRoles": [ + "user" + ] + }, + { + "username": "admin", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "admin" + } + ], + "realmRoles": [ + "user", + "admin" + ], + "clientRoles": { + "realm-management": [ + "realm-admin" + ] + } + }, + { + "username": "service-account-app", + "enabled": true, + "serviceAccountClientId": "app", + "clientRoles": { + "app" : ["uma_protection"] + } + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges" + }, + { + "name": "admin", + "description": "Administrator privileges" + }, + { + "name": "user_premium", + "description": "User Premium privileges" + } + ] + }, + "clients": [ + { + "clientId": "app", + "enabled": true, + "baseUrl": "http://localhost:8080/app", + "adminUrl": "http://localhost:8080/app", + "bearerOnly": false, + "redirectUris": [ + "http://localhost:8080/app/*" + ], + "webOrigins": [ + "+" + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "secret": "secret", + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:test:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "175c35de-cd3d-4abb-a114-d8230aecab76", + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "name" : "Default Policy", + "description" : "A policy that grants access to any user", + "type" : "role", + "logic" : "POSITIVE", + "decisionStrategy" : "AFFIRMATIVE", + "config" : { + "roles": "[{\"id\":\"user\",\"required\":false}]" + } + }, + { + "id": "e7b698eb-1684-4f92-a07e-1bc0dfde5570", + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:test:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [] + } + } + ], + "clientScopes": [ + { + "id": "009bdb24-a16a-4993-9ebf-7e54a4508ad7", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "bf1e081e-6482-4fcb-8e2b-3b63eec08b49", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "false", + "user.attribute": "foo", + "id.token.claim": "false", + "access.token.claim": "true", + "claim.name": "Roles", + "jsonType.label": "String" + } + }, + { + "id": "18c4afee-83be-4e1f-9437-2dc27d3da593", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "a8174d1e-8fc8-4269-9bc3-b5b87468f322", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ] +} diff --git a/ejb-security-jwt/pom.xml b/ejb-security-jwt/pom.xml new file mode 100644 index 0000000000..b6d5216717 --- /dev/null +++ b/ejb-security-jwt/pom.xml @@ -0,0 +1,169 @@ + + + + 4.0.0 + + org.wildfly.quickstarts + wildfly-quickstart-parent + + 5 + + + ejb-security-jwt + 31.0.0.Beta1-SNAPSHOT + pom + Quickstart: ejb-security-jwt + This project demonstrates how to access an EJB from a remote client using OAUTHBEARER SASL mechanism + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + 30.0.0.Final + ${version.server} + 5.0.0.Final + 4.2.0.Final + + + + + jboss-public-maven-repository + JBoss Public Maven Repository + https://repository.jboss.org/nexus/content/groups/public/ + + true + never + + + true + never + + default + + + redhat-ga-maven-repository + Red Hat GA Maven Repository + https://maven.repository.redhat.com/ga/ + + true + never + + + true + never + + default + + + + + jboss-public-maven-repository + JBoss Public Maven Repository + https://repository.jboss.org/nexus/content/groups/public/ + + true + + + true + + + + redhat-ga-maven-repository + Red Hat GA Maven Repository + https://maven.repository.redhat.com/ga/ + + true + + + true + + + + + + + + + org.wildfly.bom + wildfly-ee-with-tools + ${version.bom.ee} + pom + import + + + org.wildfly.quickstarts + ejb-security-jwt-client + ${project.version} + + + org.wildfly.quickstarts + ejb-security-jwt-app-one + ${project.version} + + + org.wildfly.quickstarts + ejb-security-jwt-app-one-ejb + ${project.version} + + + org.wildfly.quickstarts + ejb-security-jwt-app-one-ejb + ${project.version} + ejb-client + + + org.wildfly.quickstarts + ejb-security-jwt-app-one-ear + ${project.version} + + + org.wildfly.quickstarts + ejb-security-jwt-app-two + ${project.version} + + + + + + app-one + app-two + client + + + + + + org.wildfly.plugins + wildfly-maven-plugin + + true + + + + + diff --git a/ejb-security-jwt/restore-configuration.cli b/ejb-security-jwt/restore-configuration.cli new file mode 100644 index 0000000000..51319292e5 --- /dev/null +++ b/ejb-security-jwt/restore-configuration.cli @@ -0,0 +1,49 @@ +# Batch script to restore the configuration that was modified to run the quickstart + +# Remove the remote outbound connection +if (outcome == success) of /subsystem=remoting/remote-outbound-connection=ejb-outbound-connection:read-resource + /subsystem=remoting/remote-outbound-connection=ejb-outbound-connection:remove +end-if + +# Remove the outbound socket binding +if (outcome == success) of /socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ejb-outbound:read-resource + /socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ejb-outbound:remove +end-if + +# Remove the authentication context +if (outcome == success) of /subsystem=elytron/authentication-context=ejb-outbound-context:read-resource + /subsystem=elytron/authentication-context=ejb-outbound-context:remove +end-if + +# Remove authentication configuration +if (outcome == success) of /subsystem=elytron/authentication-configuration=ejb-outbound-configuration:read-resource + /subsystem=elytron/authentication-configuration=ejb-outbound-configuration:remove +end-if + +batch + +# Remove ejb3 application-security-domain +/subsystem=ejb3/application-security-domain=jwt-app-domain:remove + +# Sets sasl-authentication-factory of http-remoting-connector back to application-sasl-authentication +/subsystem=remoting/http-connector=http-remoting-connector:write-attribute(name=sasl-authentication-factory,value=application-sasl-authentication) + +# Remove jwt-sasl-authentication +/subsystem=elytron/sasl-authentication-factory=jwt-sasl-authentication:remove + +# Remove the security domain +/subsystem=elytron/security-domain=jwt-domain:remove + +# Remove the security realm +/subsystem=elytron/token-realm=jwt-realm:remove + +# Remove the role-decoder module +/subsystem=elytron/simple-role-decoder=roles-attribute:remove + +# Run the batch commands +run-batch + +# Reload the server configuration +reload + + diff --git a/pom.xml b/pom.xml index d315311f55..aa08a2ef86 100644 --- a/pom.xml +++ b/pom.xml @@ -307,6 +307,7 @@ ee-security ejb-multi-server ejb-security-context-propagation + ejb-security-jwt ejb-security-programmatic-auth ejb-throws-exception ejb-timer From 1bfa68600f1f835350fafb9326917ca42acede8e Mon Sep 17 00:00:00 2001 From: Lin Gao Date: Fri, 1 Dec 2023 22:37:15 +0800 Subject: [PATCH 2/2] [WFLY-17649] Adds integration tests and github action workflow for ejb-security-jwt quickstart --- .../quickstart_ejb-security-jwt_ci.yml | 18 ++++++ .../quickstart_ejb-security-jwt_ci_before.sh | 10 +++ ejb-security-jwt/README.adoc | 3 + ejb-security-jwt/app-one/ear/pom.xml | 44 +++++++++++++ ejb-security-jwt/client/pom.xml | 34 ++++++++++ .../ejb/security/jwt/EJBSecurityJWTIT.java | 62 +++++++++++++++++++ .../resources/META-INF/wildfly-config.xml | 40 ++++++++++++ 7 files changed, 211 insertions(+) create mode 100644 .github/workflows/quickstart_ejb-security-jwt_ci.yml create mode 100755 .github/workflows/quickstart_ejb-security-jwt_ci_before.sh create mode 100644 ejb-security-jwt/client/src/test/java/org/jboss/as/quickstarts/ejb/security/jwt/EJBSecurityJWTIT.java create mode 100644 ejb-security-jwt/client/src/test/resources/META-INF/wildfly-config.xml diff --git a/.github/workflows/quickstart_ejb-security-jwt_ci.yml b/.github/workflows/quickstart_ejb-security-jwt_ci.yml new file mode 100644 index 0000000000..3254c06b6c --- /dev/null +++ b/.github/workflows/quickstart_ejb-security-jwt_ci.yml @@ -0,0 +1,18 @@ +name: WildFly EJB Security JWT Quickstart CI + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'ejb-security-jwt/**' + - '.github/workflows/quickstart_ci.yml' + +jobs: + call-quickstart_ci: + uses: ./.github/workflows/quickstart_ci.yml + with: + QUICKSTART_PATH: ejb-security-jwt + TEST_PROVISIONED_SERVER: true + TEST_OPENSHIFT: false + MATRIX_OS: '"ubuntu-latest"' + DEPLOYMENT_DIR: app-one/ear diff --git a/.github/workflows/quickstart_ejb-security-jwt_ci_before.sh b/.github/workflows/quickstart_ejb-security-jwt_ci_before.sh new file mode 100755 index 0000000000..b8b6d7d2ed --- /dev/null +++ b/.github/workflows/quickstart_ejb-security-jwt_ci_before.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -x + +# Start keycloak with required configuration +docker run -d --rm --name "keycloak" \ + -p 8180:8080 \ + -e KEYCLOAK_ADMIN=admin \ + -e KEYCLOAK_ADMIN_PASSWORD=admin \ + -v ${GITHUB_WORKSPACE}/quickstarts/ejb-security-jwt/keycloak/realm:/opt/keycloak/data/import \ + quay.io/keycloak/keycloak:21.0.0 start-dev --import-realm diff --git a/ejb-security-jwt/README.adoc b/ejb-security-jwt/README.adoc index e29fcad9eb..e707be1941 100644 --- a/ejb-security-jwt/README.adoc +++ b/ejb-security-jwt/README.adoc @@ -312,6 +312,9 @@ We can see that the user `quickstartUser` has the `user` role, but does not have We can also see that the invocation from EJBA to EJBB uses the same authentication context as what is used in remote client calls EJBA. +// Server Distribution Testing +include::../shared-doc/run-integration-tests-with-server-distribution.adoc[leveloffset=+2] + == Undeploy the Archives To undeploy the components from the {productName} servers: diff --git a/ejb-security-jwt/app-one/ear/pom.xml b/ejb-security-jwt/app-one/ear/pom.xml index cf01f1bb7e..fc1d257732 100644 --- a/ejb-security-jwt/app-one/ear/pom.xml +++ b/ejb-security-jwt/app-one/ear/pom.xml @@ -22,6 +22,7 @@ org.wildfly.quickstarts ejb-security-jwt-app-one 31.0.0.Beta1-SNAPSHOT + ../pom.xml ejb-security-jwt-app-one-ear ear @@ -77,4 +78,47 @@ + + + provisioned-server + + + + org.wildfly.plugins + wildfly-maven-plugin + + + + org.wildfly:wildfly-galleon-pack:${version.server} + + + + cloud-server + ejb + + ${project.parent.artifactId}.ear + + + -Dejb-outbound-port=${ejb-outbound-port} + + + + + true + + + + + + + package + + + + + + + + + diff --git a/ejb-security-jwt/client/pom.xml b/ejb-security-jwt/client/pom.xml index 580ac20dff..a5890ab8a8 100644 --- a/ejb-security-jwt/client/pom.xml +++ b/ejb-security-jwt/client/pom.xml @@ -53,6 +53,13 @@ ejb-security-jwt-app-one-ejb ejb-client + + + + junit + junit + test + @@ -74,4 +81,31 @@ + + + integration-testing + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*IT + + + + + + integration-test + verify + + + + + + + + + diff --git a/ejb-security-jwt/client/src/test/java/org/jboss/as/quickstarts/ejb/security/jwt/EJBSecurityJWTIT.java b/ejb-security-jwt/client/src/test/java/org/jboss/as/quickstarts/ejb/security/jwt/EJBSecurityJWTIT.java new file mode 100644 index 0000000000..c73bda81b4 --- /dev/null +++ b/ejb-security-jwt/client/src/test/java/org/jboss/as/quickstarts/ejb/security/jwt/EJBSecurityJWTIT.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 JBoss by Red Hat. + * + * 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.jboss.as.quickstarts.ejb.security.jwt; + +import org.jboss.as.quickstarts.ejb.security.jwt.appone.JWTSecurityEJBRemoteA; +import org.jboss.as.quickstarts.ejb.security.jwt.client.RemoteEJBClient; +import org.junit.Assert; +import org.junit.Test; +import org.wildfly.security.auth.client.AuthenticationContext; + +import javax.naming.NamingException; +import java.io.IOException; + +/** + * Tests that + * + * @author Lin Gao + */ +public class EJBSecurityJWTIT { + private static final String DEFAULT_SERVER_HOST = "http://localhost:8080"; + + private String getProviderURl() { + final String serverHost = System.getProperty("server.host"); + return "remote+" + (serverHost != null ? serverHost : DEFAULT_SERVER_HOST); + } + + @Test + public void testRegular() throws NamingException { + AuthenticationContext.getContextManager().setThreadDefault(null); + JWTSecurityEJBRemoteA remoteA = RemoteEJBClient.lookupEJBRemoteA(true, getProviderURl()); + Assert.assertEquals("quickstartuser", remoteA.principal().toLowerCase()); + Assert.assertTrue(remoteA.inRole("user")); + Assert.assertFalse(remoteA.inRole("admin")); + Assert.assertTrue(remoteA.inRoleFromB("user", true)); + Assert.assertFalse(remoteA.inRoleFromB("admin", true)); + } + + @Test + public void testAdmin() throws IOException, NamingException { + RemoteEJBClient.switchToAdmin(); + JWTSecurityEJBRemoteA remoteA = RemoteEJBClient.lookupEJBRemoteA(true, getProviderURl()); + Assert.assertEquals("admin", remoteA.principal().toLowerCase()); + Assert.assertTrue(remoteA.inRole("user")); + Assert.assertTrue(remoteA.inRole("admin")); + Assert.assertTrue(remoteA.inRoleFromB("user", true)); + Assert.assertTrue(remoteA.inRoleFromB("admin", true)); + } + +} diff --git a/ejb-security-jwt/client/src/test/resources/META-INF/wildfly-config.xml b/ejb-security-jwt/client/src/test/resources/META-INF/wildfly-config.xml new file mode 100644 index 0000000000..5139f875c0 --- /dev/null +++ b/ejb-security-jwt/client/src/test/resources/META-INF/wildfly-config.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + +