Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Draft to support access restrictions in credential reset flow #123

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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