Skip to content

Commit

Permalink
Merge pull request #12198 from sberyozkin/code_authentication_listener
Browse files Browse the repository at this point in the history
Add OIDC SecurityEvent
  • Loading branch information
sberyozkin authored Oct 1, 2020
2 parents 70b7d81 + a0f4eac commit 109af54
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,31 @@ If UserInfo is the source of the roles then set `quarkus.oidc.authentication.use

Additionally a custom `SecurityIdentityAugmentor` can also be used to add the roles as documented link:security#security-identity-customization[here].

== Listening to important authentication events

One can register `@ApplicationScoped` bean which will observe important OIDC authentication events. The listener will be updated when a user has logged in for the first time or re-authenticated, as well as when the session has been refreshed. More events may be reported in the future. For example:

[source, java]
----
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import io.quarkus.oidc.IdTokenCredential;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class SecurityEventListener {
public void event(@Observes SecurityEvent event) {
String tenantId = event.getSecurityIdentity().getAttribute("tenant-id");
RoutingContext vertxContext = event.getSecurityIdentity().getCredential(IdTokenCredential.class).getRoutingContext();
vertxContext.put("listener-message", String.format("event:%s,tenantId:%s", event.getEventType().name(), tenantId));
}
}
----

== Single Page Applications

Please check if implementing SPAs the way it is suggested in the link:security-openid-connect#single-page-applications[Single Page Applications for Service Applications] section can meet your requirements.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package io.quarkus.oidc.deployment;

import java.util.Collection;
import java.util.function.BooleanSupplier;

import javax.inject.Singleton;

import org.eclipse.microprofile.jwt.Claim;
import org.jboss.jandex.DotName;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.BuildExtension;
import io.quarkus.arc.processor.ObserverInfo;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
Expand All @@ -18,6 +24,7 @@
import io.quarkus.deployment.builditem.EnableAllSecurityServicesBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.runtime.DefaultTenantConfigResolver;
import io.quarkus.oidc.runtime.OidcAuthenticationMechanism;
import io.quarkus.oidc.runtime.OidcBuildTimeConfig;
Expand All @@ -35,6 +42,7 @@
import io.smallrye.jwt.build.impl.JwtProviderImpl;

public class OidcBuildStep {
public static final DotName DOTNAME_SECURITY_EVENT = DotName.createSimple(SecurityEvent.class.getName());

OidcBuildTimeConfig buildTimeConfig;

Expand Down Expand Up @@ -90,6 +98,18 @@ public SyntheticBeanBuildItem setup(
.done();
}

@BuildStep(onlyIf = IsEnabled.class)
@Record(ExecutionTime.RUNTIME_INIT)
public ValidationErrorBuildItem findSecurityEventObservers(
OidcRecorder recorder,
ValidationPhaseBuildItem validationPhase) {
Collection<ObserverInfo> observers = validationPhase.getContext().get(BuildExtension.Key.OBSERVERS);
boolean isSecurityEventObserved = observers.stream()
.anyMatch(observer -> observer.asObserver().getObservedType().name().equals(DOTNAME_SECURITY_EVENT));
recorder.setSecurityEventObserved(isSecurityEventObserved);
return new ValidationErrorBuildItem();
}

static class IsEnabled implements BooleanSupplier {
OidcBuildTimeConfig config;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1011,9 +1011,8 @@ public static class Proxy {

public static enum ApplicationType {
/**
* A {@code WEB_APP} is a client that server pages, usually a frontend application. For this type of client the
* Authorization Code Flow is
* defined as the preferred method for authenticating users.
* A {@code WEB_APP} is a client that serves pages, usually a frontend application. For this type of client the
* Authorization Code Flow is defined as the preferred method for authenticating users.
*/
WEB_APP,

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.quarkus.oidc;

import io.quarkus.security.identity.SecurityIdentity;

/**
* Security event.
*
*/
public class SecurityEvent {
public enum Type {
/**
* OIDC Login event which is reported after the first user authentication but also when the user's session
* has expired and the user has re-authenticated at the OIDC provider site.
*/
OIDC_LOGIN,
/**
* OIDC Session refreshed event is reported if it has been detected that an ID token will expire shortly and the session
* has been successfully auto-refreshed without the user having to re-authenticate again at the OIDC site.
*/
OIDC_SESSION_REFRESHED,
/**
* OIDC Session expired and refreshed event is reported if a session has expired but been successfully refreshed
* without the user having to re-authenticate again at the OIDC site.
*/
OIDC_SESSION_EXPIRED_AND_REFRESHED,
/**
* OIDC Logout event is reported when the current user has started an RP-initiated OIDC logout flow.
*/
OIDC_LOGOUT_RP_INITIATED
}

private final Type eventType;
private final SecurityIdentity securityIdentity;

public SecurityEvent(Type eventType, SecurityIdentity securityIdentity) {
this.eventType = eventType;
this.securityIdentity = securityIdentity;
}

public Type getEventType() {
return eventType;
}

public SecurityIdentity getSecurityIdentity() {
return securityIdentity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@
import io.smallrye.mutiny.Uni;

abstract class AbstractOidcAuthenticationMechanism {
protected DefaultTenantConfigResolver resolver;

protected Uni<SecurityIdentity> authenticate(IdentityProviderManager identityProviderManager,
TokenCredential token) {
return identityProviderManager.authenticate(new TokenAuthenticationRequest(token));
}

void setResolver(DefaultTenantConfigResolver resolver) {
this.resolver = resolver;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMec
null, null);

public Uni<SecurityIdentity> authenticate(RoutingContext context,
IdentityProviderManager identityProviderManager,
DefaultTenantConfigResolver resolver) {
IdentityProviderManager identityProviderManager) {
String token = extractBearerToken(context, resolver.resolve(context, false).oidcConfig);

// if a bearer token is provided try to authenticate
Expand All @@ -29,7 +28,7 @@ public Uni<SecurityIdentity> authenticate(RoutingContext context,
return Uni.createFrom().nullItem();
}

public Uni<ChallengeData> getChallenge(RoutingContext context, DefaultTenantConfigResolver resolver) {
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return Uni.createFrom().item(UNAUTHORIZED_CHALLENGE);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.quarkus.oidc.OidcTenantConfig.Credentials;
import io.quarkus.oidc.OidcTenantConfig.Credentials.Secret;
import io.quarkus.oidc.RefreshToken;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.AuthenticationRedirectException;
Expand Down Expand Up @@ -80,8 +81,7 @@ public Uni<Boolean> apply(Permission permission) {
}

public Uni<SecurityIdentity> authenticate(RoutingContext context,
IdentityProviderManager identityProviderManager,
DefaultTenantConfigResolver resolver) {
IdentityProviderManager identityProviderManager) {

Cookie sessionCookie = context.request().getCookie(
getSessionCookieName(resolver.resolve(context, false)));
Expand All @@ -100,6 +100,7 @@ public Uni<SecurityIdentity> authenticate(RoutingContext context,
@Override
public SecurityIdentity apply(SecurityIdentity identity) {
if (isLogout(context, configContext)) {
fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity);
throw redirectToLogoutEndpoint(context, configContext, idToken);
}

Expand Down Expand Up @@ -130,12 +131,16 @@ public SecurityIdentity apply(Throwable throwable) {
if (identity == null) {
LOG.debug("SecurityIdentity is null after a token refresh");
throw new AuthenticationCompletionException();
} else {
fireEvent(SecurityEvent.Type.OIDC_SESSION_EXPIRED_AND_REFRESHED, identity);
}
} else {
identity = trySilentRefresh(configContext, refreshToken, context, identityProviderManager);
if (identity == null) {
LOG.debug("ID token can no longer be refreshed, using the current SecurityIdentity");
identity = ((TokenAutoRefreshException) throwable).getSecurityIdentity();
} else {
fireEvent(SecurityEvent.Type.OIDC_SESSION_REFRESHED, identity);
}
}
return identity;
Expand Down Expand Up @@ -164,7 +169,7 @@ private boolean shouldAutoRedirect(TenantConfigContext configContext, RoutingCon
: true;
}

public Uni<ChallengeData> getChallenge(RoutingContext context, DefaultTenantConfigResolver resolver) {
public Uni<ChallengeData> getChallenge(RoutingContext context) {

TenantConfigContext configContext = resolver.resolve(context, true);
removeCookie(context, configContext, getSessionCookieName(configContext));
Expand Down Expand Up @@ -390,6 +395,13 @@ private void processSuccessfulAuthentication(RoutingContext context,
maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds();
}
createCookie(context, configContext, getSessionCookieName(configContext), cookieValue, maxAge);
fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity);
}

private void fireEvent(SecurityEvent.Type eventType, SecurityIdentity securityIdentity) {
if (resolver.isSecurityEventObserved()) {
resolver.getSecurityEvent().fire(new SecurityEvent(eventType, securityIdentity));
}
}

private String getRedirectPath(TenantConfigContext configContext, RoutingContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;

import org.jboss.logging.Logger;

import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.TenantResolver;
import io.vertx.ext.web.RoutingContext;
Expand All @@ -33,6 +35,11 @@ public class DefaultTenantConfigResolver {
@Inject
TenantConfigBean tenantConfigBean;

@Inject
Event<SecurityEvent> securityEvent;

private volatile boolean securityEventObserved;

@PostConstruct
public void verifyResolvers() {
if (tenantConfigResolver.isResolvable()) {
Expand Down Expand Up @@ -91,6 +98,18 @@ boolean isBlocking(RoutingContext context) {
|| resolver.oidcConfig.authentication.userInfoRequired);
}

boolean isSecurityEventObserved() {
return securityEventObserved;
}

void setSecurityEventObserved(boolean securityEventObserved) {
this.securityEventObserved = securityEventObserved;
}

Event<SecurityEvent> getSecurityEvent() {
return securityEvent;
}

private TenantConfigContext getTenantConfigFromConfigResolver(RoutingContext context, boolean create) {
if (tenantConfigResolver.isResolvable()) {
OidcTenantConfig tenantConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Collections;
import java.util.Set;

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

Expand All @@ -23,18 +24,25 @@ public class OidcAuthenticationMechanism implements HttpAuthenticationMechanism

@Inject
DefaultTenantConfigResolver resolver;

private BearerAuthenticationMechanism bearerAuth = new BearerAuthenticationMechanism();
private CodeAuthenticationMechanism codeAuth = new CodeAuthenticationMechanism();

@PostConstruct
public void init() {
bearerAuth.setResolver(resolver);
codeAuth.setResolver(resolver);
}

@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context,
IdentityProviderManager identityProviderManager) {
TenantConfigContext tenantContext = resolve(context);
if (tenantContext.oidcConfig.tenantEnabled == false) {
return Uni.createFrom().nullItem();
}
return isWebApp(context, tenantContext) ? codeAuth.authenticate(context, identityProviderManager, resolver)
: bearerAuth.authenticate(context, identityProviderManager, resolver);
return isWebApp(context, tenantContext) ? codeAuth.authenticate(context, identityProviderManager)
: bearerAuth.authenticate(context, identityProviderManager);
}

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

private TenantConfigContext resolve(RoutingContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public Uni<SecurityIdentity> authenticate(TokenAuthenticationRequest request,
AuthenticationRequestContext context) {
OidcTokenCredential credential = (OidcTokenCredential) request.getToken();
RoutingContext vertxContext = credential.getRoutingContext();
vertxContext.put(AuthenticationRequestContext.class.getName(), context);
return Uni.createFrom().deferred(new Supplier<Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> get() {
Expand Down Expand Up @@ -159,6 +160,8 @@ public String getName() {
if (userInfo != null) {
OidcUtils.setSecurityIdentityRoles(builder, resolvedContext.oidcConfig, userInfo);
}
OidcUtils.setBlockinApiAttribute(builder, vertxContext);
OidcUtils.setTenantIdAttribute(builder, resolvedContext.oidcConfig);
uniEmitter.complete(builder.build());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import org.jboss.logging.Logger;

import io.quarkus.arc.Arc;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
Expand Down Expand Up @@ -333,4 +334,9 @@ protected static Optional<ProxyOptions> toProxyOptions(OidcTenantConfig.Proxy pr
}
return Optional.of(new ProxyOptions(jsonOptions));
}

public void setSecurityEventObserved(boolean isSecurityEventObserved) {
DefaultTenantConfigResolver bean = Arc.container().instance(DefaultTenantConfigResolver.class).get();
bean.setSecurityEventObserved(isSecurityEventObserved);
}
}
Loading

0 comments on commit 109af54

Please sign in to comment.