Skip to content

Commit

Permalink
Merge pull request #7750 from sberyozkin/oidc_public_key
Browse files Browse the repository at this point in the history
Support for OIDC public-key property
  • Loading branch information
gastaldi authored Mar 12, 2020
2 parents f02e1c0 + 37ccd8b commit 1fc4f71
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 82 deletions.
10 changes: 9 additions & 1 deletion extensions/oidc/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
<systemPropertyVariables>
<keycloak.not.required/>
</systemPropertyVariables>
<includes>
<include>ServicePublicKeyTestCase.java</include>
</includes>
</configuration>
</plugin>
</plugins>
Expand All @@ -121,6 +126,9 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>false</skip>
<excludes>
<exclude>ServicePublicKeyTestCase.java</exclude>
</excludes>
<systemPropertyVariables>
<keycloak.url>${keycloak.url}</keycloak.url>
</systemPropertyVariables>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,25 @@ public class KeycloakDevModeRealmResourceManager implements QuarkusTestResourceL

@Override
public Map<String, String> start() {

try {

RealmRepresentation realm = createRealm(KEYCLOAK_REALM);

realm.getClients().add(createClient("client-dev-mode"));
realm.getUsers().add(createUser("alice-dev-mode", "user"));

RestAssured
.given()
.auth().oauth2(getAdminAccessToken())
.contentType("application/json")
.body(JsonSerialization.writeValueAsBytes(realm))
.when()
.post(KEYCLOAK_SERVER_URL + "/admin/realms").then()
.statusCode(201);
} catch (IOException e) {
throw new RuntimeException(e);
if (System.getProperty("keycloak.not.required") == null) {
try {

RealmRepresentation realm = createRealm(KEYCLOAK_REALM);

realm.getClients().add(createClient("client-dev-mode"));
realm.getUsers().add(createUser("alice-dev-mode", "user"));

RestAssured
.given()
.auth().oauth2(getAdminAccessToken())
.contentType("application/json")
.body(JsonSerialization.writeValueAsBytes(realm))
.when()
.post(KEYCLOAK_SERVER_URL + "/admin/realms").then()
.statusCode(201);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return Collections.emptyMap();
}
Expand Down Expand Up @@ -113,11 +114,12 @@ private static UserRepresentation createUser(String username, String... realmRol

@Override
public void stop() {

RestAssured
.given()
.auth().oauth2(getAdminAccessToken())
.when()
.delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).thenReturn().prettyPrint();
if (System.getProperty("keycloak.not.required") == null) {
RestAssured
.given()
.auth().oauth2(getAdminAccessToken())
.when()
.delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).thenReturn().prettyPrint();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.oidc.test;

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

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.security.Authenticated;

@Path("/service")
@Authenticated
public class ServiceProtectedResource {

@Inject
JsonWebToken accessToken;

@GET
public String getName() {
return accessToken.getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.quarkus.oidc.test;

import java.io.IOException;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import io.smallrye.jwt.build.Jwt;

public class ServicePublicKeyTestCase {

private static Class<?>[] testClasses = {
ServiceProtectedResource.class
};

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(testClasses)
.addAsResource("privateKey.pem")
.addAsResource("application-service-public-key.properties", "application.properties"));

@Test
public void testAccessTokenInjection() throws IOException, InterruptedException {
String jwt = Jwt.claims().preferredUserName("alice").sign();
Assertions.assertEquals("alice", RestAssured.given().auth()
.oauth2(jwt)
.get("/service").getBody().asString());
}

@Test
public void testModifiedSignature() throws IOException, InterruptedException {
String jwt = Jwt.claims().preferredUserName("alice").sign();
// the last section of the jwt token is a signature
Response r = RestAssured.given().auth()
.oauth2(jwt + "1")
.get("/service");
Assertions.assertEquals(403, r.getStatusCode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
quarkus.oidc.client-id=test
quarkus.oidc.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
smallrye.jwt.sign.key-location=/privateKey.pem
28 changes: 28 additions & 0 deletions extensions/oidc/deployment/src/test/resources/privateKey.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
f3cg+fr8aou7pr9SHhJlZCU=
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.oauth2.AccessToken;
import io.vertx.ext.auth.oauth2.impl.OAuth2AuthProviderImpl;
import io.vertx.ext.jwt.JWT;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
Expand Down Expand Up @@ -53,13 +56,22 @@ public SecurityIdentity get() {
return authenticate(request, vertxContext);
}

@SuppressWarnings("deprecation")
private CompletableFuture<SecurityIdentity> authenticate(TokenAuthenticationRequest request,
RoutingContext vertxContext) {
CompletableFuture<SecurityIdentity> result = new CompletableFuture<>();
TenantConfigContext resolvedContext = tenantResolver.resolve(vertxContext, true);
OidcTenantConfig config = resolvedContext.oidcConfig;

if (resolvedContext.oidcConfig.publicKey.isPresent()) {
return validateTokenWithoutOidcServer(request, resolvedContext);
} else {
return validateTokenWithOidcServer(request, resolvedContext);
}
}

@SuppressWarnings("deprecation")
private CompletableFuture<SecurityIdentity> validateTokenWithOidcServer(TokenAuthenticationRequest request,
TenantConfigContext resolvedContext) {

CompletableFuture<SecurityIdentity> result = new CompletableFuture<>();
resolvedContext.auth.decodeToken(request.getToken().getToken(),
new Handler<AsyncResult<AccessToken>>() {
@Override
Expand All @@ -68,42 +80,73 @@ public void handle(AsyncResult<AccessToken> event) {
result.completeExceptionally(new AuthenticationFailedException(event.cause()));
return;
}
AccessToken token = event.result();
try {
OidcUtils.validateClaims(config.getToken(), token.accessToken());
} catch (OIDCException e) {
result.completeExceptionally(new AuthenticationFailedException(e));
return;
}

QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.addCredential(request.getToken());

JsonWebToken jwtPrincipal;
try {
JwtClaims jwtClaims = JwtClaims.parse(token.accessToken().encode());
jwtClaims.setClaim(Claims.raw_token.name(), request.getToken().getToken());
jwtPrincipal = new OidcJwtCallerPrincipal(jwtClaims, request.getToken(),
config.token.principalClaim.isPresent() ? config.token.principalClaim.get() : null);
} catch (InvalidJwtException e) {
result.completeExceptionally(new AuthenticationFailedException(e));
return;
}
builder.setPrincipal(jwtPrincipal);
JsonObject tokenJson = event.result().accessToken();
try {
String clientId = config.getClientId().isPresent() ? config.getClientId().get() : null;
for (String role : OidcUtils.findRoles(clientId, config.getRoles(), token.accessToken())) {
builder.addRole(role);
}
} catch (Exception e) {
result.completeExceptionally(new ForbiddenException(e));
return;
result.complete(validateAndCreateIdentity(request, resolvedContext.oidcConfig, tokenJson));
} catch (Throwable ex) {
result.completeExceptionally(ex);
}

result.complete(builder.build());
}
});

return result;
}

private CompletableFuture<SecurityIdentity> validateTokenWithoutOidcServer(TokenAuthenticationRequest request,
TenantConfigContext resolvedContext) {
CompletableFuture<SecurityIdentity> result = new CompletableFuture<>();

OAuth2AuthProviderImpl auth = ((OAuth2AuthProviderImpl) resolvedContext.auth);
JWT jwt = auth.getJWT();
JsonObject tokenJson = null;
try {
tokenJson = jwt.decode(request.getToken().getToken());
} catch (Throwable ex) {
result.completeExceptionally(new AuthenticationFailedException(ex));
return result;
}
if (jwt.isExpired(tokenJson, auth.getConfig().getJWTOptions())) {
result.completeExceptionally(new AuthenticationFailedException());
} else {
try {
result.complete(validateAndCreateIdentity(request, resolvedContext.oidcConfig, tokenJson));
} catch (Throwable ex) {
result.completeExceptionally(ex);
}
}
return result;
}

private QuarkusSecurityIdentity validateAndCreateIdentity(TokenAuthenticationRequest request,
OidcTenantConfig config, JsonObject tokenJson)
throws Exception {
try {
OidcUtils.validateClaims(config.getToken(), tokenJson);
} catch (OIDCException e) {
throw new AuthenticationFailedException(e);
}

QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.addCredential(request.getToken());

JsonWebToken jwtPrincipal;
try {
JwtClaims jwtClaims = JwtClaims.parse(tokenJson.encode());
jwtClaims.setClaim(Claims.raw_token.name(), request.getToken().getToken());
jwtPrincipal = new OidcJwtCallerPrincipal(jwtClaims, request.getToken(),
config.token.principalClaim.isPresent() ? config.token.principalClaim.get() : null);
} catch (InvalidJwtException e) {
throw new AuthenticationFailedException(e);
}
builder.setPrincipal(jwtPrincipal);
try {
String clientId = config.getClientId().isPresent() ? config.getClientId().get() : null;
for (String role : OidcUtils.findRoles(clientId, config.getRoles(), tokenJson)) {
builder.addRole(role);
}
} catch (Exception e) {
throw new ForbiddenException(e);
}
return builder.build();
}
}
Loading

0 comments on commit 1fc4f71

Please sign in to comment.