Skip to content

Commit

Permalink
Support OIDC hybrid application type
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Sep 28, 2020
1 parent 64d4587 commit ea6a6a2
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class OidcTenantConfig {
* The application type, which can be one of the following values from enum {@link ApplicationType}.
*/
@ConfigItem(defaultValue = "service")
public ApplicationType applicationType;
public ApplicationType applicationType = ApplicationType.SERVICE;

/**
* The base URL of the OpenID Connect (OIDC) server, for example, 'https://host:port/auth'.
Expand Down Expand Up @@ -999,6 +999,13 @@ public static enum ApplicationType {
* RESTful Architectural Design. For this type of client, the Bearer Authorization method is defined as the preferred
* method for authenticating and authorizing users.
*/
SERVICE
SERVICE,

/**
* A combined {@code SERVICE} and {@code WEB_APP} client.
* For this type of client, the Bearer Authorization method will be used if the Authorization header is set
* and Authorization Code Flow - if not.
*/
HYBRID
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public Uni<SecurityIdentity> authenticate(RoutingContext context,
if (tenantContext.oidcConfig.tenantEnabled == false) {
return Uni.createFrom().nullItem();
}
return isWebApp(tenantContext) ? codeAuth.authenticate(context, identityProviderManager, resolver)
return isWebApp(context, tenantContext) ? codeAuth.authenticate(context, identityProviderManager, resolver)
: bearerAuth.authenticate(context, identityProviderManager, resolver);
}

Expand All @@ -43,7 +43,7 @@ public Uni<ChallengeData> getChallenge(RoutingContext context) {
if (tenantContext.oidcConfig.tenantEnabled == false) {
return Uni.createFrom().nullItem();
}
return isWebApp(tenantContext) ? codeAuth.getChallenge(context, resolver)
return isWebApp(context, tenantContext) ? codeAuth.getChallenge(context, resolver)
: bearerAuth.getChallenge(context, resolver);
}

Expand All @@ -55,7 +55,10 @@ private TenantConfigContext resolve(RoutingContext context) {
return tenantContext;
}

private boolean isWebApp(TenantConfigContext tenantContext) {
private boolean isWebApp(RoutingContext context, TenantConfigContext tenantContext) {
if (OidcTenantConfig.ApplicationType.HYBRID == tenantContext.oidcConfig.applicationType) {
return context.request().getHeader("Authorization") == null;
}
return OidcTenantConfig.ApplicationType.WEB_APP == tenantContext.oidcConfig.applicationType;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oi
options.setSite(authServerUrl);

if (!oidcConfig.discoveryEnabled) {
if (ApplicationType.WEB_APP.equals(oidcConfig.applicationType)) {
if (oidcConfig.applicationType != ApplicationType.SERVICE) {
if (!oidcConfig.authorizationPath.isPresent() || !oidcConfig.tokenPath.isPresent()) {
throw new OIDCException("'web-app' applications must have 'authorization-path' and 'token-path' properties "
+ "set when the discovery is disabled.");
Expand Down Expand Up @@ -298,7 +298,7 @@ public void accept(UniEmitter<? super OAuth2Auth> uniEmitter) {
@SuppressWarnings("deprecation")
private static TenantConfigContext createdTenantContextFromPublicKey(OAuth2ClientOptions options,
OidcTenantConfig oidcConfig) {
if (oidcConfig.applicationType == ApplicationType.WEB_APP) {
if (oidcConfig.applicationType != ApplicationType.SERVICE) {
throw new ConfigurationException("'public-key' property can only be used with the 'service' applications");
}
LOG.debug("'public-key' property for the local token verification is set,"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public String resolve(RoutingContext context) {
if (context.request().path().endsWith("/tenant-public-key")) {
return "tenant-public-key";
}
return context.request().path().split("/")[2];
String tenantId = context.request().path().split("/")[2];
if ("tenant-hybrid".equals(tenantId)) {
return context.request().getHeader("Authorization") != null ? "tenant-hybrid-service" : "tenant-hybrid-webapp";
}
return tenantId;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.it.keycloak;

import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.oidc.IdToken;

@Path("/tenants")
public class TenantHybridResource {
@Inject
@IdToken
JsonWebToken idToken;
@Inject
JsonWebToken accessToken;

@GET
@Path("/{tenant-hybrid}/api/user")
@RolesAllowed("user")
public String userNameService() {
return idToken.getName() != null ? (idToken.getName() + ":web-app") : (accessToken.getName() + ":service");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ quarkus.oidc.tenant-web-app2.credentials.secret=secret
quarkus.oidc.tenant-web-app2.application-type=web-app
quarkus.oidc.tenant-web-app2.roles.source=accesstoken

# Tenant Hybrid Service
quarkus.oidc.tenant-hybrid-service.auth-server-url=${keycloak.url}/realms/quarkus-hybrid
quarkus.oidc.tenant-hybrid-service.client-id=quarkus-app-hybrid
quarkus.oidc.tenant-hybrid-service.credentials.secret=secret
quarkus.oidc.tenant-hybrid-service.application-type=service

# Tenant Hybrid Web-App
quarkus.oidc.tenant-hybrid-webapp.auth-server-url=${keycloak.url}/realms/quarkus-hybrid
quarkus.oidc.tenant-hybrid-webapp.client-id=quarkus-app-hybrid
quarkus.oidc.tenant-hybrid-webapp.credentials.secret=secret
quarkus.oidc.tenant-hybrid-webapp.application-type=web-app

# Tenant Hybrid Web-App Service
quarkus.oidc.tenant-hybrid-webapp-service.auth-server-url=${keycloak.url}/realms/quarkus-hybrid
quarkus.oidc.tenant-hybrid-webapp-service.client-id=quarkus-app-hybrid
quarkus.oidc.tenant-hybrid-webapp-service.credentials.secret=secret
quarkus.oidc.tenant-hybrid-webapp-service.application-type=hybrid

# Custom header
quarkus.oidc.tenant-customheader.auth-server-url=${keycloak.url}/realms/quarkus-b
quarkus.oidc.tenant-customheader.client-id=quarkus-app-b
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.io.IOException;
Expand Down Expand Up @@ -69,6 +70,50 @@ public void testResolveTenantIdentifierWebApp2() throws IOException {
}
}

@Test
public void testHybridWebApp() throws IOException {
try (final WebClient webClient = createWebClient()) {
HtmlPage page = webClient.getPage("http://localhost:8081/tenants/tenant-hybrid/api/user");
assertNotNull(getStateCookie(webClient, "tenant-hybrid-webapp"));
assertEquals("Log in to quarkus-hybrid", page.getTitleText());
HtmlForm loginForm = page.getForms().get(0);
loginForm.getInputByName("username").setValueAttribute("alice");
loginForm.getInputByName("password").setValueAttribute("alice");
page = loginForm.getInputByName("login").click();
assertEquals("alice:web-app", page.getBody().asText());
webClient.getCookieManager().clearCookies();
}
}

@Test
public void testHybridService() {
RestAssured.given().auth().oauth2(getAccessToken("alice", "hybrid"))
.when().get("/tenants/tenant-hybrid/api/user")
.then()
.statusCode(200)
.body(equalTo("alice:service"));
}

@Test
public void testHybridWebAppService() throws IOException {
try (final WebClient webClient = createWebClient()) {
HtmlPage page = webClient.getPage("http://localhost:8081/tenants/tenant-hybrid-webapp-service/api/user");
assertNotNull(getStateCookie(webClient, "tenant-hybrid-webapp-service"));
assertEquals("Log in to quarkus-hybrid", page.getTitleText());
HtmlForm loginForm = page.getForms().get(0);
loginForm.getInputByName("username").setValueAttribute("alice");
loginForm.getInputByName("password").setValueAttribute("alice");
page = loginForm.getInputByName("login").click();
assertEquals("alice:web-app", page.getBody().asText());
webClient.getCookieManager().clearCookies();
}
RestAssured.given().auth().oauth2(getAccessToken("alice", "hybrid"))
.when().get("/tenants/tenant-hybrid-webapp-service/api/user")
.then()
.statusCode(200)
.body(equalTo("alice:service"));
}

@Test
public void testReAuthenticateWhenSwitchingTenants() throws IOException {
try (final WebClient webClient = createWebClient()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycl

@Override
public Map<String, String> start() {
for (String realmId : Arrays.asList("a", "b", "c", "d", "webapp", "webapp2")) {
for (String realmId : Arrays.asList("a", "b", "c", "d", "webapp", "webapp2", "hybrid")) {
RealmRepresentation realm = createRealm(KEYCLOAK_REALM + realmId);

realm.getClients().add(createClient("quarkus-app-" + realmId));
Expand Down Expand Up @@ -92,10 +92,10 @@ private static ClientRepresentation createClient(String clientId) {
client.setDirectAccessGrantsEnabled(true);
client.setEnabled(true);
client.setDefaultRoles(new String[] { "role-" + clientId });
if (clientId.startsWith("quarkus-app-webapp")) {
if (clientId.startsWith("quarkus-app-webapp") || clientId.equals("quarkus-app-hybrid")) {
client.setRedirectUris(Arrays.asList("*"));
}
if (clientId.equals("quarkus-app-webapp")) {
if (clientId.equals("quarkus-app-webapp") || clientId.equals("quarkus-app-hybrid")) {
// This instructs Keycloak to include the roles with the ID token too
client.setDefaultClientScopes(Arrays.asList("microprofile-jwt"));
}
Expand Down Expand Up @@ -123,7 +123,7 @@ private static UserRepresentation createUser(String username, String... realmRol

@Override
public void stop() {
for (String realmId : Arrays.asList("a", "b", "c", "d", "webapp", "webapp2")) {
for (String realmId : Arrays.asList("a", "b", "c", "d", "webapp", "webapp2", "hybrid")) {
RestAssured
.given()
.auth().oauth2(getAdminAccessToken())
Expand Down

0 comments on commit ea6a6a2

Please sign in to comment.