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 11, 2020
1 parent 931a8f4 commit 931e789
Show file tree
Hide file tree
Showing 14 changed files with 544 additions and 83 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,14 @@ private TenantConfigContext getTenantConfigFromTenantResolver(RoutingContext con
}

boolean isBlocking(RoutingContext context) {
return getTenantConfigFromConfigResolver(context, false) == null;
TenantConfigContext resolver = getTenantConfigFromConfigResolver(context, false);

if (resolver != null) {
// we always run blocking if refresh token is enabled even if the tenant was already resolved
return resolver.oidcConfig.token.refreshTimeout.isPresent();
}

return resolver == null;
}

private TenantConfigContext getTenantConfigFromConfigResolver(RoutingContext context, boolean create) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
package io.quarkus.oidc.runtime;

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

import java.util.function.Consumer;
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.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniEmitter;
import io.vertx.core.AsyncResult;
Expand Down Expand Up @@ -86,7 +80,8 @@ public void handle(AsyncResult<AccessToken> event) {
JsonObject tokenJson = event.result().accessToken();
try {
uniEmitter.complete(
validateAndCreateIdentity(request, resolvedContext.oidcConfig, tokenJson));
validateAndCreateIdentity(request.getToken(), resolvedContext.oidcConfig,
tokenJson));
} catch (Throwable ex) {
uniEmitter.fail(ex);
}
Expand All @@ -110,43 +105,11 @@ private Uni<SecurityIdentity> validateTokenWithoutOidcServer(TokenAuthentication
return Uni.createFrom().failure(new AuthenticationFailedException());
} else {
try {
return Uni.createFrom().item(validateAndCreateIdentity(request, resolvedContext.oidcConfig, tokenJson));
return Uni.createFrom()
.item(validateAndCreateIdentity(request.getToken(), resolvedContext.oidcConfig, tokenJson));
} catch (Throwable ex) {
return Uni.createFrom().failure(ex);
}
}
}

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,27 @@ public void handle(AsyncResult<OAuth2Auth> event) {
});

auth = cf.join();

if (!ApplicationType.WEB_APP.equals(oidcConfig.applicationType)) {
if (oidcConfig.token.refreshTimeout.isPresent()) {
throw new RuntimeException(
"The logout path can only be enabled for " + ApplicationType.WEB_APP + " application types");
}
if (oidcConfig.logout.path.isPresent()) {
throw new RuntimeException(
"The logout path can only be enabled for " + ApplicationType.WEB_APP + " application types");
}
}

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

if (oidcConfig.logout.path.isPresent()) {
if (endSessionEndpoint == null) {
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.oidc.runtime;

class TokenEntry {

private volatile String token;

public TokenEntry(String token) {
this.token = token;
}

String getToken() {
return token;
}

void setToken(String token) {
this.token = token;
}
}
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=3
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 931e789

Please sign in to comment.