Introduce profile 'feature' for step-up authentication enabled by default

Closes #10315
This commit is contained in:
mposolda 2022-03-02 16:08:20 +01:00 committed by Marek Posolda
parent 48565832d4
commit d394e51674
6 changed files with 47 additions and 6 deletions

View file

@ -65,7 +65,8 @@ public class Profile {
MAP_STORAGE("New store", Type.EXPERIMENTAL), MAP_STORAGE("New store", Type.EXPERIMENTAL),
PAR("OAuth 2.0 Pushed Authorization Requests (PAR)", Type.DEFAULT), PAR("OAuth 2.0 Pushed Authorization Requests (PAR)", Type.DEFAULT),
DECLARATIVE_USER_PROFILE("Configure user profiles using a declarative style", Type.PREVIEW), DECLARATIVE_USER_PROFILE("Configure user profiles using a declarative style", Type.PREVIEW),
DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL); DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL),
STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT);
private String label; private String label;
private final Type typeProject; private final Type typeProject;

View file

@ -21,13 +21,15 @@ import java.util.List;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowCallbackFactory; import org.keycloak.authentication.AuthenticationFlowCallbackFactory;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.common.Profile;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ProviderConfigurationBuilder;
public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthenticatorFactory, AuthenticationFlowCallbackFactory { public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthenticatorFactory, AuthenticationFlowCallbackFactory, EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "conditional-level-of-authentication"; public static final String PROVIDER_ID = "conditional-level-of-authentication";
private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = new AuthenticationExecutionModel.Requirement[]{ private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = new AuthenticationExecutionModel.Requirement[]{
@ -105,4 +107,9 @@ public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthentica
// NOP - instance created in create() method // NOP - instance created in create() method
return null; return null;
} }
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.STEP_UP_AUTHENTICATION);
}
} }

View file

@ -884,7 +884,13 @@ public class TokenManager {
token.setNonce(clientSessionCtx.getAttribute(OIDCLoginProtocol.NONCE_PARAM, String.class)); token.setNonce(clientSessionCtx.getAttribute(OIDCLoginProtocol.NONCE_PARAM, String.class));
token.setScope(clientSessionCtx.getScopeString()); token.setScope(clientSessionCtx.getScopeString());
token.setAcr(getAcr(clientSession)); if (Profile.isFeatureEnabled(Profile.Feature.STEP_UP_AUTHENTICATION)) {
token.setAcr(getAcr(clientSession));
} else {
// Backwards compatibility behaviour prior step-up authentication was introduced
String acr = AuthenticationManager.isSSOAuthentication(clientSession) ? "0" : "1";
token.setAcr(acr);
}
String authTime = session.getNote(AuthenticationManager.AUTH_TIME); String authTime = session.getNote(AuthenticationManager.AUTH_TIME);
if (authTime != null) { if (authTime != null) {

View file

@ -31,11 +31,13 @@ import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator; import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory; import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory;
import org.keycloak.common.Profile;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.models.AuthenticationExecutionModel.Requirement; import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -44,6 +46,7 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.ClaimsRepresentation; import org.keycloak.representations.ClaimsRepresentation;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -51,9 +54,12 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory; import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
import org.keycloak.testsuite.client.KeycloakTestingClient; import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginTotpPage; import org.keycloak.testsuite.pages.LoginTotpPage;
@ -66,6 +72,7 @@ import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE; import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
/** /**
@ -617,6 +624,26 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
} }
@Test
@DisableFeature(value = Profile.Feature.STEP_UP_AUTHENTICATION, skipRestart = true)
public void testDisableStepupFeatureTest() {
BrowserFlowTest.revertFlows(testRealm(), "browser - Level of Authentication FLow");
// Login normal way - should return 1 (backwards compatibility before step-up was introduced)
loginPage.open();
authenticateWithUsernamePassword();
authenticateWithTotp(); // OTP required due the user has it
assertLoggedInWithAcr("1");
// SSO login - should return 0 (backwards compatibility before step-up was introduced)
oauth.openLoginForm();
assertLoggedInWithAcr("0");
// Flow is needed due the "after()" method
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow("browser - Level of Authentication FLow"));
}
public void openLoginFormWithAcrClaim(boolean essential, String... acrValues) { public void openLoginFormWithAcrClaim(boolean essential, String... acrValues) {
openLoginFormWithAcrClaim(oauth, essential, acrValues); openLoginFormWithAcrClaim(oauth, essential, acrValues);
} }

View file

@ -932,7 +932,7 @@
<kc-tooltip>{{:: 'require-pushed-authorization-requests.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'require-pushed-authorization-requests.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group clearfix block" data-ng-show="!clientEdit.bearerOnly && protocol == 'openid-connect'"> <div class="form-group clearfix block" data-ng-show="!clientEdit.bearerOnly && protocol == 'openid-connect' && serverInfo.featureEnabled('STEP_UP_AUTHENTICATION')">
<label class="col-md-2 control-label" for="newAcr">{{:: 'acr-loa-map' | translate}}</label> <label class="col-md-2 control-label" for="newAcr">{{:: 'acr-loa-map' | translate}}</label>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="input-group input-map" ng-repeat="(acr, loa) in acrLoaMap"> <div class="input-group input-map" ng-repeat="(acr, loa) in acrLoaMap">
@ -953,7 +953,7 @@
<kc-tooltip>{{:: 'acr-loa-map-client.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'acr-loa-map-client.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol == 'openid-connect'"> <div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol == 'openid-connect' && serverInfo.featureEnabled('STEP_UP_AUTHENTICATION')">
<label class="col-md-2 control-label" for="newDefaultAcrValue">{{:: 'default-acr-values' | translate}}</label> <label class="col-md-2 control-label" for="newDefaultAcrValue">{{:: 'default-acr-values' | translate}}</label>
<div class="col-sm-6"> <div class="col-sm-6">

View file

@ -72,7 +72,7 @@
</div> </div>
<kc-tooltip>{{:: 'sslRequired.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'sslRequired.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group clearfix block"> <div class="form-group clearfix block" data-ng-show="serverInfo.featureEnabled('STEP_UP_AUTHENTICATION')">
<label class="col-md-2 control-label" for="newAcr">{{:: 'acr-loa-map' | translate}}</label> <label class="col-md-2 control-label" for="newAcr">{{:: 'acr-loa-map' | translate}}</label>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="input-group input-map" ng-repeat="(acr, loa) in acrLoaMap"> <div class="input-group input-map" ng-repeat="(acr, loa) in acrLoaMap">