Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added CodeFlowAuthorizationTest #14173

Merged
merged 1 commit into from
Mar 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions integration-tests/oidc-wiremock/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@
<artifactId>jakarta.servlet-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.eclipse.jetty</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.it.keycloak;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;

@Path("/code-flow")
@Authenticated
public class CodeFlowResource {

@Inject
SecurityIdentity identity;

@GET
public String access() {
return identity.getPrincipal().getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.it.keycloak;

import javax.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.TenantResolver;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantResolver implements TenantResolver {

@Override
public String resolve(RoutingContext context) {
String path = context.normalisedPath();
if (path.endsWith("code-flow")) {
return "code-flow";
}
return null;
}
}
cemnura marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=secret
quarkus.oidc.token.principal-claim=email
quarkus.oidc.authentication.scopes=profile,email,phone
cemnura marked this conversation as resolved.
Show resolved Hide resolved

quarkus.oidc.code-flow.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.code-flow.client-id=quarkus-web-app
quarkus.oidc.code-flow.credentials.secret=secret
quarkus.oidc.code-flow.application-type=web-app
quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".min-level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".level=TRACE

quarkus.oidc.token.audience=https://service.example.com

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.it.keycloak;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;

import org.junit.jupiter.api.Test;

import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
@QuarkusTestResource(KeycloakTestResource.class)
public class CodeFlowAuthorizationTest {

@Test
public void testCodeFlow() throws IOException {
try (final WebClient webClient = createWebClient()) {
webClient.getOptions().setRedirectEnabled(true);
HtmlPage page = webClient.getPage("http://localhost:8081/code-flow");

HtmlForm form = page.getFormByName("form");
form.getInputByName("username").type("alice");
form.getInputByName("password").type("alice");

page = form.getInputByValue("login").click();

assertEquals("alice", page.getBody().asText());
}
}

private WebClient createWebClient() {
WebClient webClient = new WebClient();
webClient.setCssErrorHandler(new SilentCssErrorHandler());
return webClient;
}

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

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.joining;

import java.util.Collections;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

Expand All @@ -18,9 +22,11 @@

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
import com.google.common.collect.ImmutableSet;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import io.smallrye.jwt.build.Jwt;

public class KeycloakTestResource implements QuarkusTestResourceLifecycleManager {

Expand All @@ -31,7 +37,10 @@ public class KeycloakTestResource implements QuarkusTestResourceLifecycleManager
@Override
public Map<String, String> start() {

server = new WireMockServer(wireMockConfig().dynamicPort());
server = new WireMockServer(
wireMockConfig()
.extensions(new ResponseTemplateTransformer(false))
.dynamicPort());
server.start();

server.stubFor(
Expand All @@ -41,10 +50,15 @@ public Map<String, String> start() {
.withBody("{\n" +
" \"jwks_uri\": \"" + server.baseUrl()
+ "/auth/realms/quarkus/protocol/openid-connect/certs\",\n" +
" \"token_introspection_endpoint\": \"" + server.baseUrl()
+ "/auth/realms/quarkus/protocol/openid-connect/token/introspect\",\n" +
" \"authorization_endpoint\": \"" + server.baseUrl() + "/auth/realms/quarkus\"," +
" \"token_endpoint\": \"" + server.baseUrl() + "/auth/realms/quarkus/token\"," +
" \"issuer\" : \"https://server.example.com\"," +
" \"introspection_endpoint\": \"" + server.baseUrl()
+ "/auth/realms/quarkus/protocol/openid-connect/token/introspect\",\n"
+ "\"issuer\" : \"https://server.example.com\""
+ "}")));
+ "/auth/realms/quarkus/protocol/openid-connect/token/introspect\""
+
"}")));

server.stubFor(
get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/certs"))
Expand Down Expand Up @@ -72,8 +86,45 @@ public Map<String, String> start() {
// Invalid
defineInvalidIntrospectionMockTokenStubForUserWithRoles("expired", emptySet());

// Code Flow Authorization Mock
defineCodeFlowAuthorizationMockTokenStub();

// Login Page
server.stubFor(
get(urlPathMatching("/auth/realms/quarkus"))
.willReturn(aResponse()
.withHeader("Content-Type", MediaType.TEXT_HTML)
.withBody("<html>\n" +
"<body>\n" +
" <form action=\"/login\" name=\"form\">\n" +
" <input type=\"text\" id=\"username\" name=\"username\"/>\n" +
" <input type=\"password\" id=\"password\" name=\"password\"/>\n" +
" <input type=\"hidden\" id=\"state\" name=\"state\" value=\"{{request.query.state}}\"/>\n"
+
" <input type=\"hidden\" id=\"redirect_uri\" name=\"redirect_uri\" value=\"{{request.query.redirect_uri}}\"/>\n"
+
" <input type=\"submit\" id=\"login\" value=\"login\"/>\n" +
"</form>\n" +
"</body>\n" +
"</html> ")
.withTransformers("response-template")));

// Login Request
server.stubFor(
sberyozkin marked this conversation as resolved.
Show resolved Hide resolved
get(urlPathMatching("/login"))
.willReturn(aResponse()
.withHeader("Location",
"{{request.query.redirect_uri}}?state={{request.query.state}}&code=58af24f2-9093-4674-a431-4a9d66be719c.50437113-cd78-48a2-838e-b936fe458c5d.0ac5df91-e044-4051-bd03-106a3a5fb9cc")
.withStatus(302)
.withTransformers("response-template")));

LOG.infof("Keycloak started in mock mode: %s", server.baseUrl());
return Collections.singletonMap("quarkus.oidc.auth-server-url", server.baseUrl() + "/auth/realms/quarkus");
Map<String, String> conf = new HashMap<>();
conf.put("quarkus.oidc.auth-server-url", server.baseUrl() + "/auth/realms/quarkus");
conf.put("quarkus.oidc.code-flow.auth-server-url", server.baseUrl() + "/auth/realms/quarkus");
conf.put("keycloak-url", server.baseUrl());
sberyozkin marked this conversation as resolved.
Show resolved Hide resolved

return conf;
}

private void defineValidIntrospectionMockTokenStubForUserWithRoles(String user, Set<String> roles) {
Expand All @@ -100,6 +151,30 @@ private void defineInvalidIntrospectionMockTokenStubForUserWithRoles(String user
+ "\",\"iat\":1562315654,\"exp\":1,\"expires_in\":1,\"client_id\":\"my_client_id\"}")));
}

private void defineCodeFlowAuthorizationMockTokenStub() {
server.stubFor(WireMock.post("/auth/realms/quarkus/token")
.withRequestBody(containing("authorization_code"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON)
.withBody("{\n" +
" \"access_token\": \""
+ getAccessToken("alice", new HashSet<>(Arrays.asList("user", "admin"))) + "\",\n" +
" \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\",\n" +
" \"id_token\": \"" + getAccessToken("alice", new HashSet<>(Arrays.asList("user", "admin")))
+ "\"\n" +
"}")));
}

private String getAccessToken(String userName, Set<String> groups) {
return Jwt.preferredUserName(userName)
.groups(groups)
.issuer("https://server.example.com")
.audience("https://service.example.com")
.jws()
.keyId("1")
.sign("privateKey.jwk");
}

@Override
public synchronized void stop() {
if (server != null) {
Expand Down