Skip to content

Commit

Permalink
feat: Draft to support access restrictions in credential reset flow
Browse files Browse the repository at this point in the history
see #121
  • Loading branch information
sventorben committed Nov 16, 2022
1 parent 37a375b commit fce43f6
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 36 deletions.
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ services:
volumes:
- ./target/keycloak-restrict-client-auth.jar:/opt/keycloak/providers/keycloak-restrict-client-auth.jar
- ./src/test/resources/:/tmp/import
mail:
container_name: mail
image: maildev/maildev
ports:
- 1080:1080
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.sventorben.keycloak.authorization.client;

import de.sventorben.keycloak.authorization.client.access.AccessProvider;
import de.sventorben.keycloak.authorization.client.access.AccessProviderResolver;
import de.sventorben.keycloak.authorization.client.access.role.ClientRoleBasedAccessProviderFactory;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
Expand Down Expand Up @@ -31,7 +32,7 @@ public void authenticate(final AuthenticationFlowContext context) {
final ClientModel client = context.getSession().getContext().getClient();
final RestrictClientAuthConfig config = new RestrictClientAuthConfig(context.getAuthenticatorConfig());

final AccessProvider access = getAccessProvider(context, config);
final AccessProvider access = new AccessProviderResolver(config).resolve(context);

if (!access.isRestricted(client)) {
context.success();
Expand All @@ -51,37 +52,6 @@ public void authenticate(final AuthenticationFlowContext context) {
}
}

private AccessProvider getAccessProvider(AuthenticationFlowContext context, RestrictClientAuthConfig config) {
final String accessProviderId = config.getAccessProviderId();

if (accessProviderId != null) {
AccessProvider accessProvider = context.getSession().getProvider(AccessProvider.class, accessProviderId);
if (accessProvider == null) {
LOG.warnf(
"Configured access provider '%s' in authenticator config '%s' does not exist.",
accessProviderId, config.getAuthenticatorConfigAlias());
} else {
LOG.tracef(
"Using access provider '%s' in authenticator config '%s'.",
accessProviderId, config.getAuthenticatorConfigAlias());
return accessProvider;
}
}

final AccessProvider defaultProvider = context.getSession().getProvider(AccessProvider.class);
if (defaultProvider != null) {
LOG.debugf(
"No access provider is configured in authenticator config '%s'. Using server-wide default provider '%s'",
config.getAuthenticatorConfigAlias(), defaultProvider);
return defaultProvider;
}

LOG.infof(
"Neither an access provider is configured in authenticator config '%s' nor has a server-wide default provider been set. Using '%s' as a fallback.",
config.getAuthenticatorConfigAlias(), ClientRoleBasedAccessProviderFactory.PROVIDER_ID);
return context.getSession().getProvider(AccessProvider.class, ClientRoleBasedAccessProviderFactory.PROVIDER_ID);
}

private Response errorResponse(AuthenticationFlowContext context, RestrictClientAuthConfig config) {
Response response;
if (MediaTypeMatcher.isHtmlRequest(context.getHttpRequest().getHttpHeaders())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public final class RestrictClientAuthAuthenticatorFactory implements Authenticat

@Override
public String getDisplayType() {
return "Restrict user authentication on clients";
return "Restrict user authentication on clients (via Authenticator)";
}

@Override
Expand All @@ -57,7 +57,7 @@ public boolean isUserSetupAllowed() {

@Override
public String getHelpText() {
return "Restricts user authentication on clients based on an access provider";
return "Restricts user authentication on clients based on an access provider. Access will be denied during authenticator execution.";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public final class RestrictClientAuthConfig {

private final AuthenticatorConfigModel authenticatorConfigModel;

RestrictClientAuthConfig(AuthenticatorConfigModel configModel) {
public RestrictClientAuthConfig(AuthenticatorConfigModel configModel) {
this.authenticatorConfigModel = configModel;
}

Expand All @@ -31,7 +31,7 @@ public String getAccessProviderId() {
.orElse(ClientRoleBasedAccessProviderFactory.PROVIDER_ID);
}

String getAuthenticatorConfigAlias() {
public String getAuthenticatorConfigAlias() {
return Optional.ofNullable(authenticatorConfigModel)
.map(AuthenticatorConfigModel::getAlias)
.orElse(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package de.sventorben.keycloak.authorization.client.access;

import de.sventorben.keycloak.authorization.client.RestrictClientAuthConfig;
import de.sventorben.keycloak.authorization.client.access.role.ClientRoleBasedAccessProviderFactory;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;

public final class AccessProviderResolver {

private static final Logger LOG = Logger.getLogger(AccessProviderResolver.class);

private final RestrictClientAuthConfig config;

public AccessProviderResolver(RestrictClientAuthConfig config) {
this.config = config;
}

public AccessProvider resolve(AuthenticationFlowContext context) {
final String accessProviderId = config.getAccessProviderId();

if (accessProviderId != null) {
AccessProvider accessProvider = context.getSession().getProvider(AccessProvider.class, accessProviderId);
if (accessProvider == null) {
LOG.warnf(
"Configured access provider '%s' in authenticator config '%s' does not exist.",
accessProviderId, config.getAuthenticatorConfigAlias());
} else {
LOG.tracef(
"Using access provider '%s' in authenticator config '%s'.",
accessProviderId, config.getAuthenticatorConfigAlias());
return accessProvider;
}
}

final AccessProvider defaultProvider = context.getSession().getProvider(AccessProvider.class);
if (defaultProvider != null) {
LOG.debugf(
"No access provider is configured in authenticator config '%s'. Using server-wide default provider '%s'",
config.getAuthenticatorConfigAlias(), defaultProvider);
return defaultProvider;
}

LOG.infof(
"Neither an access provider is configured in authenticator config '%s' nor has a server-wide default provider been set. Using '%s' as a fallback.",
config.getAuthenticatorConfigAlias(), ClientRoleBasedAccessProviderFactory.PROVIDER_ID);
return context.getSession().getProvider(AccessProvider.class, ClientRoleBasedAccessProviderFactory.PROVIDER_ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package de.sventorben.keycloak.authorization.client.requiredactions;

import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;

import javax.ws.rs.core.Response;

public class RestrictAccessRequiredAction implements RequiredActionProvider {

@Override
public InitiatedActionSupport initiatedActionSupport() {
return InitiatedActionSupport.SUPPORTED;
}

@Override
public void evaluateTriggers(RequiredActionContext requiredActionContext) {
}

@Override
public void requiredActionChallenge(RequiredActionContext requiredActionContext) {
requiredActionContext.challenge(htmlErrorResponse(requiredActionContext));
}

@Override
public void processAction(RequiredActionContext requiredActionContext) {
}

private Response htmlErrorResponse(RequiredActionContext context) {
AuthenticationSessionModel authSession = context.getAuthenticationSession();
return context.form()
.setError(Messages.ACCESS_DENIED, authSession.getAuthenticatedUser().getUsername(),
authSession.getClient().getClientId())
.createErrorPage(Response.Status.FORBIDDEN);
}

@Override
public void close() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package de.sventorben.keycloak.authorization.client.requiredactions;

import de.sventorben.keycloak.authorization.client.common.OperationalInfo;
import org.keycloak.Config;
import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;

import javax.ws.rs.core.Response;
import java.util.Map;

public class RestrictAccessRequiredActionFactory implements RequiredActionFactory, ServerInfoAwareProviderFactory {

static final String PROVIDER_ID = "RESTRICT_CLIENT_AUTH_DENY_ACCESS";

@Override
public String getDisplayText() {
return "Restrict user authentication on clients";
}

@Override
public RequiredActionProvider create(KeycloakSession keycloakSession) {
return new RestrictAccessRequiredAction();
}

@Override
public void init(Config.Scope scope) {

}

@Override
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {

}

@Override
public void close() {

}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public Map<String, String> getOperationalInfo() {
return OperationalInfo.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package de.sventorben.keycloak.authorization.client.requiredactions;

import de.sventorben.keycloak.authorization.client.RestrictClientAuthConfig;
import de.sventorben.keycloak.authorization.client.access.AccessProvider;
import de.sventorben.keycloak.authorization.client.access.AccessProviderResolver;
import de.sventorben.keycloak.authorization.client.common.OperationalInfo;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.authenticators.resetcred.AbstractSetRequiredActionAuthenticator;
import org.keycloak.events.Errors;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.utils.MediaTypeMatcher;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Map;

public final class RestrictClientAuthRequiredActionAuthenticator extends AbstractSetRequiredActionAuthenticator implements ServerInfoAwareProviderFactory {

private static final Logger LOG = Logger.getLogger(RestrictClientAuthRequiredActionAuthenticator.class);

private static final String PROVIDER_ID = "restrict-client-auth-action-auth";

public RestrictClientAuthRequiredActionAuthenticator() {}

@Override
public void authenticate(final AuthenticationFlowContext context) {

if (context.getExecution().isRequired()) {

final ClientModel client = context.getSession().getContext().getClient();
final RestrictClientAuthConfig config = new RestrictClientAuthConfig(context.getAuthenticatorConfig());

final AccessProvider access = new AccessProviderResolver(config).resolve(context);

final UserModel user = context.getUser();
if (access.isRestricted(client) && !access.isPermitted(client, user)) {
context.getAuthenticationSession().addRequiredAction(RestrictAccessRequiredActionFactory.PROVIDER_ID);
}
}

context.success();
}

@Override
public void action(AuthenticationFlowContext context) {
LOG.warn("Action called!");
context.failure(AuthenticationFlowError.ACCESS_DENIED);
}

@Override
public boolean requiresUser() {
return true;
}

@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}

@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}

@Override
public void close() {
}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public Map<String, String> getOperationalInfo() {
return OperationalInfo.get();
}

@Override
public String getDisplayType() {
return "Restrict user authentication on clients (via required action)";
}

@Override
public String getHelpText() {
return "Restricts user authentication on clients based on an access provider. Should be used in reset credentials flow. Access will be denied during required action evaluation.";
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
de.sventorben.keycloak.authorization.client.RestrictClientAuthAuthenticatorFactory
de.sventorben.keycloak.authorization.client.requiredactions.RestrictClientAuthRequiredActionAuthenticator
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
de.sventorben.keycloak.authorization.client.requiredactions.RestrictAccessRequiredActionFactory

0 comments on commit fce43f6

Please sign in to comment.