Single-Sign-On (SSO) ist gerade im Enterprise Kontext wo eine zentrale Benutzerverwaltung stattfindet relevant. In WildFly gibt es schon seit vielen Jahren eine Unterstützung zur Authentifikation mittels OIDC, zum Beispiel in Zusammenspiel mit dem Keycloak Server aus demselben Hause. Seit dem letzten Blogpost zu diesem Thema hat es der Keycloak Adapter, der manuell hinzugefügt werden musste, als integriertes Subsystem in den WildFly Server geschafft. Wir verschaffen uns in diesem Beitrag einen Überblick über den aktuellen Stand und testen die Authentifizierung an einem einfachen Beispielprojekt.
Unsere Anwendung verfügt über eine REST-API und über eine Nutzeroberfläche. Die Nutzeroberfläche verwendet ein selbst definiertes Servlet sowie Jakarta Faces. Wir möchten gerne alle diese Schnittstellen über OIDC absichern. Dazu kann die Konfiguration über die Datei src/main/webapp/WEB-INF/oidc.json
vorgenommen werden. Wir setzen hier Testwerte die optional über Umgebungsvariablen angepasst werden können.
{
"client-id": "${env.OIDC_CLIENT_ID:demo-app}",
"provider-url" : "${env.OIDC_PROVIDER_URL:http://localhost:9090/realms/demo}",
"ssl-required": "external",
"credentials": {
"secret": "${env.OIDC_CLIENT_SECRET:QjfVYgcISONI4RTJSSyvqEf0D4NK1zmV}"
},
"principal-attribute" : "preferred_username"
}
Zusätzlich muss in der Datei src/main/webapp/WEB-INF/web.xml
die Login-Methode OIDC aktiviert werden.
<login-config>
<auth-method>OIDC</auth-method>
</login-config>
Absichern der Schnittstellen
Schauen wir uns zunächst die Faces Seiten. Es gibt eine öffentliche Startseite und einem authentifizierten Bereich im Pfad /auth
. Die Zugriffsrechte und Beschränkung der Rollen können wir hierfür auch in der web.xml
Datei anpassen. In diesem Fall konfigurieren wir einen Zugriff für alle authentifizierten Nutzer unabhängig von der Rolle. Hier ist aber natürlich auch die Beschränkung auf bestimmte Rollen möglich. Es können auch unterschiedliche Seiten für unterschiedliche Rollen freigegeben werden.
<security-role>
<role-name>*</role-name>
</security-role>
<security-constraint>
<web-resource-collection>
<web-resource-name>Protected Web Services</web-resource-name>
<url-pattern>/auth/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
</security-constraint>
Unser @WebServlet
ist unter /protectedServlet
erreichbar. Es soll nur von Nutzern mit der Rolle demo-role
aufgerufen werden können. Das können wir über die @ServletSecurity
Annotation erreichen. Wir haben hier auch die Möglichkeit über den jakarta.security.enterprise.SecurityContext
auf Attribute wie den Nutzernamen zuzugreifen.
@WebServlet("/protectedServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "demo-role"))
public class ProtectedServlet extends HttpServlet {
@Inject
private SecurityContext securityContext;
@Override
public void doGet(
HttpServletRequest request, HttpServletResponse response)
throws IOException {
try (var writer = response.getWriter()) {
writer.write("This is a protected servlet\n");
writer.write("User: " + securityContext.getCallerPrincipal().getName() + "\n");
writer.write("Is user in role demo-role?: " + securityContext.isCallerInRole("demo-role") + "\n");
}
}
}
Schauen wir uns nun die REST-API an. Jakarta Security definiert grundsätzlich eine @RolesAllowed
Annotation. Damit RESTEasy diese Annotationen verarbeitet, muss dies zunächst in der web.xml
Datei konfiguriert werden.
<context-param>
<param-name>resteasy.role.based.security</param-name>
<param-value>true</param-value>
</context-param>
Jetzt können wir mithilfe der Annotation den Zugriff auf bestimmte Endpunkte auf spezifische Rollen beschränken. In diesem Fall haben wir einen öffentlichen Endpunkt /api/demo/public
und einen abgesicherten /api/demo/protected
. Über SecurityContext
erhalten wir Informationen über den angemeldeten Benutzer.
@RequestScoped
@Path("/demo")
public class DemoEndpoint {
@Inject
private jakarta.security.enterprise.SecurityContext securityContext;
@RolesAllowed("demo-role")
@GET
@Path("protected")
@Produces("text/plain")
public String demoProtected() {
return "securityContext User " + securityContext.getCallerPrincipal().getName();
}
@GET
@Path("public")
@Produces("text/plain")
public String demoPublic() {
return "Hello " + securityContext.getCallerPrincipal().getName() + "!";
}
}
Das Beispielprojekt gibt es wie immer auch auf GitHub.
Aber gibt’s da nicht auch was vom Standard?
Seit Jakarta EE 10 gibt es die Annotation @OpenIdAuthenticationMechanismDefinition
über die Applikationen mit OIDC Authentifizierung versehen werden können. Die Annotation muss an einer z.B. über @ApplicationScoped
bereitgestellten Bean vorhanden sein und wird damit applikationsweit aktiviert. Die Verwendung ist aber an einen browserbasierten Authentifizerungsweg gebunden und eine REST Authentifikation über ein Bearer Token im Authorization Header, wie es dort üblicherweise eingesetzt wird, wird nicht unterstützt.
Für reine REST-Anwendungen ist MicroProfile-JWT eine mögliche Alternative. Über diesen mit Jakarta EE eng zusammenhängenden Standard lassen sich REST-Applikationen mit relativ wenig Aufwand absichern.
Problematisch ist in Jakarta EE 10 allerdings noch die Kombination dieser beiden Wege. Beide implementieren einen HttpAuthenticationMechanism
und die Anwendung führt diese nicht einfach zusammen. Hier verspricht Jakarta EE 11 allerdings bereits Besserung und die Verwendung mehrerer Authentifikationsmechanismen soll vereinfacht werden. Diese Unterstützung ist in WildFly zum aktuellen Zeitpunkt leider noch nicht gegeben. Die Änderung sollte damit aber mittelfristig auch die kombinierte Absicherung einer UI- und REST-Schnittstelle mit Standardmitteln ermöglichen.