Skip to content

Commit

Permalink
[fixes quarkusio#4481] - RP-Initiated Logout and session verification
Browse files Browse the repository at this point in the history
  • Loading branch information
pedroigor committed Apr 10, 2020
1 parent 39a989e commit efe5153
Show file tree
Hide file tree
Showing 12 changed files with 441 additions and 87 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package io.quarkus.oidc.runtime;

import static io.quarkus.oidc.runtime.OidcUtils.validateAndCreateIdentity;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Supplier;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;

import io.quarkus.oidc.OIDCException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
Expand Down Expand Up @@ -82,7 +76,8 @@ public void handle(AsyncResult<AccessToken> event) {
}
JsonObject tokenJson = event.result().accessToken();
try {
result.complete(validateAndCreateIdentity(request, resolvedContext.oidcConfig, tokenJson));
result.complete(validateAndCreateIdentity(request.getToken(), resolvedContext.oidcConfig,
tokenJson));
} catch (Throwable ex) {
result.completeExceptionally(ex);
}
Expand All @@ -109,44 +104,11 @@ private CompletableFuture<SecurityIdentity> validateTokenWithoutOidcServer(Token
result.completeExceptionally(new AuthenticationFailedException());
} else {
try {
result.complete(validateAndCreateIdentity(request, resolvedContext.oidcConfig, tokenJson));
result.complete(validateAndCreateIdentity(request.getToken(), 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ public void handle(AsyncResult<OAuth2Auth> event) {
});

auth = cf.join();

String endSessionEndpoint = OAuth2AuthProviderImpl.class.cast(auth).getConfig().getLogoutPath();

if (endSessionEndpoint == null && oidcConfig.logout.path.isPresent()) {
throw new RuntimeException(
"The application supports RP-Initiated Logout but the OpenID Provider does not advertise the end_session_endpoint");
}

break;
} catch (Throwable throwable) {
while (throwable instanceof CompletionException && throwable.getCause() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ public class OidcTenantConfig {
@ConfigItem
Tls tls = new Tls();

/**
* Logout configuration
*/
@ConfigItem
Logout logout = new Logout();

@ConfigGroup
public static class Tls {
public enum Verification {
Expand All @@ -119,6 +125,42 @@ public enum Verification {

}

@ConfigGroup
public static class Logout {

/**
* The relative path of the logout endpoint at the application. If provided, the application is able to initiate the
* logout through this
* endpoint in conformance with the OpenID Connect RP-Initiated Logout specification.
*/
@ConfigItem
Optional<String> path;

/**
* Relative path of the application endpoint where the user should be redirected to after logging out from the OpenID
* Connect Provider.
* This {@code URI} must be properly registered at the OpenID Connect Provider as a valid redirect URI.
*/
@ConfigItem
Optional<String> postLogoutPath;

public void setPath(Optional<String> path) {
this.path = path;
}

public String getPath() {
return path.get();
}

public void setPostLogoutPath(Optional<String> postLogoutPath) {
this.postLogoutPath = postLogoutPath;
}

public Optional<String> getPostLogoutPath() {
return postLogoutPath;
}
}

public Optional<Duration> getConnectionDelay() {
return connectionDelay;
}
Expand Down Expand Up @@ -215,6 +257,14 @@ public void setProxy(Proxy proxy) {
this.proxy = proxy;
}

public void setLogout(Logout logout) {
this.logout = logout;
}

public Logout getLogout() {
return logout;
}

@ConfigGroup
public static class Credentials {

Expand Down Expand Up @@ -475,6 +525,14 @@ public static Token fromAudience(String... audience) {
@ConfigItem
public Optional<String> principalClaim = Optional.empty();

/**
* The time, in seconds, that tokens should be silently refreshed so that tokens with no active session at the
* OpenID Provider are invalidated and users are logged out. This option is only valid when the application is of type
* {@link ApplicationType#WEB_APP}}.
*/
@ConfigItem
public Optional<Integer> refreshTimeout = Optional.empty();

public Optional<String> getIssuer() {
return issuer;
}
Expand Down Expand Up @@ -506,6 +564,14 @@ public Optional<String> getPrincipalClaim() {
public void setPrincipalClaim(String principalClaim) {
this.principalClaim = Optional.of(principalClaim);
}

public Optional<Integer> getRefreshTimeout() {
return refreshTimeout;
}

public void setRefreshTimeout(Optional<Integer> refreshTimeout) {
this.refreshTimeout = refreshTimeout;
}
}

@ConfigGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@
import java.util.regex.Pattern;

import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;

import io.quarkus.oidc.OIDCException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.credential.TokenCredential;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

Expand Down Expand Up @@ -113,4 +120,36 @@ private static List<String> convertJsonArrayToList(JsonArray claimValue) {
}
return list;
}

static QuarkusSecurityIdentity validateAndCreateIdentity(TokenCredential credential,
OidcTenantConfig config, JsonObject tokenJson) {
try {
OidcUtils.validateClaims(config.getToken(), tokenJson);
} catch (OIDCException e) {
throw new AuthenticationFailedException(e);
}

QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.addCredential(credential);

JsonWebToken jwtPrincipal;
try {
JwtClaims jwtClaims = JwtClaims.parse(tokenJson.encode());
jwtClaims.setClaim(Claims.raw_token.name(), credential.getToken());
jwtPrincipal = new OidcJwtCallerPrincipal(jwtClaims, credential,
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();
}
}
5 changes: 5 additions & 0 deletions integration-tests/oidc-code-flow/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
<artifactId>htmlunit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public String resolve(RoutingContext context) {
}

String path = context.request().path();

if (path.contains("tenant-logout")) {
return "tenant-logout";
}

return path.contains("callback-") ? "tenant-1" : path.contains("/web-app2") ? "tenant-2" : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public String getNameCallbackAfterRedirect() {
return "callback:" + getName();
}

@GET
@Path("tenant-logout")
public String getTenantLogout() {
return "Tenant Logout";
}

@GET
@Path("access")
public String getAccessToken() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.it.keycloak;

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

import io.quarkus.security.Authenticated;

@Path("/tenant-logout")
public class TenantLogout {

@Authenticated
@GET
public String getTenantLogout() {
return "Tenant Logout";
}

@GET
@Path("post-logout")
public String postLogout() {
return "You were logged out";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,24 @@ quarkus.oidc.tenant-3.authentication.redirect-path=/some/other/path
quarkus.oidc.tenant-3.authentication.restore-path-after-redirect=false
quarkus.oidc.tenant-3.application-type=web-app

quarkus.oidc.tenant-logout.auth-server-url=${keycloak.url}/realms/logout-realm
quarkus.oidc.tenant-logout.client-id=quarkus-app
quarkus.oidc.tenant-logout.credentials.secret=secret
quarkus.oidc.tenant-logout.application-type=web-app
quarkus.oidc.tenant-logout.authentication.cookie-path=/tenant-logout
quarkus.oidc.tenant-logout.logout.path=/tenant-logout/logout
quarkus.oidc.tenant-logout.logout.post-logout-path=/tenant-logout/post-logout
quarkus.oidc.tenant-logout.token.refresh-timeout=5
quarkus.oidc.tenant-logout.token.expiration-grace=120

quarkus.http.auth.permission.roles1.paths=/index.html
quarkus.http.auth.permission.roles1.policy=authenticated

quarkus.http.auth.permission.logout.paths=/tenant-logout
quarkus.http.auth.permission.logout.policy=authenticated

quarkus.http.auth.permission.post-logout.paths=/tenant-logout/post-logout
quarkus.http.auth.permission.post-logout.policy=permit

quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".level=TRACE
#quarkus.log.category."org.apache.http".level=TRACE
Loading

0 comments on commit efe5153

Please sign in to comment.