KEYCLOAK-6055 Include X.509 certificate data in audit logs
Signed-off-by: Jan Lieskovsky <jlieskov@redhat.com> Co-authored-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
996ceb2ce8
commit
9eb400262f
7 changed files with 67 additions and 32 deletions
|
@ -70,4 +70,7 @@ public interface Details {
|
|||
|
||||
String EXISTING_USER = "previous_user";
|
||||
|
||||
String X509_CERTIFICATE_SERIAL_NUMBER = "x509_cert_serial_number";
|
||||
String X509_CERTIFICATE_SUBJECT_DISTINGUISHED_NAME = "x509_cert_subject_distinguished_name";
|
||||
String X509_CERTIFICATE_ISSUER_DISTINGUISHED_NAME = "x509_cert_issuer_distinguished_name";
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.bouncycastle.asn1.x500.style.BCStyle;
|
|||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -233,6 +234,29 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Saving some notes for audit to authSession as the event may not be necessarily triggered in this HTTP request where the certificate was parsed
|
||||
// For example if there is confirmation page enabled, it will be in the additional request
|
||||
protected void saveX509CertificateAuditDataToAuthSession(AuthenticationFlowContext context,
|
||||
X509Certificate cert) {
|
||||
context.getAuthenticationSession().setAuthNote(Details.X509_CERTIFICATE_SERIAL_NUMBER, cert.getSerialNumber().toString());
|
||||
context.getAuthenticationSession().setAuthNote(Details.X509_CERTIFICATE_SUBJECT_DISTINGUISHED_NAME, cert.getSubjectDN().toString());
|
||||
context.getAuthenticationSession().setAuthNote(Details.X509_CERTIFICATE_ISSUER_DISTINGUISHED_NAME, cert.getIssuerDN().toString());
|
||||
}
|
||||
|
||||
protected void recordX509CertificateAuditDataViaContextEvent(AuthenticationFlowContext context) {
|
||||
recordX509DetailFromAuthSessionToEvent(context, Details.X509_CERTIFICATE_SERIAL_NUMBER);
|
||||
recordX509DetailFromAuthSessionToEvent(context, Details.X509_CERTIFICATE_SUBJECT_DISTINGUISHED_NAME);
|
||||
recordX509DetailFromAuthSessionToEvent(context, Details.X509_CERTIFICATE_ISSUER_DISTINGUISHED_NAME);
|
||||
}
|
||||
|
||||
private void recordX509DetailFromAuthSessionToEvent(AuthenticationFlowContext context, String detailName) {
|
||||
String detailValue = context.getAuthenticationSession().getAuthNote(detailName);
|
||||
context.getEvent().detail(detailName, detailValue);
|
||||
}
|
||||
|
||||
|
||||
// Purely for unit testing
|
||||
public UserIdentityExtractor getUserIdentityExtractor(X509AuthenticatorConfigModel config) {
|
||||
return UserIdentityExtractorBuilder.fromConfig(config);
|
||||
|
|
|
@ -53,6 +53,9 @@ public class ValidateX509CertificateUsername extends AbstractX509ClientCertifica
|
|||
return;
|
||||
}
|
||||
|
||||
saveX509CertificateAuditDataToAuthSession(context, certs[0]);
|
||||
recordX509CertificateAuditDataViaContextEvent(context);
|
||||
|
||||
X509AuthenticatorConfigModel config = null;
|
||||
if (context.getAuthenticatorConfig() != null && context.getAuthenticatorConfig().getConfig() != null) {
|
||||
config = new X509AuthenticatorConfigModel(context.getAuthenticatorConfig());
|
||||
|
|
|
@ -66,6 +66,9 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif
|
|||
return;
|
||||
}
|
||||
|
||||
saveX509CertificateAuditDataToAuthSession(context, certs[0]);
|
||||
recordX509CertificateAuditDataViaContextEvent(context);
|
||||
|
||||
X509AuthenticatorConfigModel config = null;
|
||||
if (context.getAuthenticatorConfig() != null && context.getAuthenticatorConfig().getConfig() != null) {
|
||||
config = new X509AuthenticatorConfigModel(context.getAuthenticatorConfig());
|
||||
|
@ -261,6 +264,7 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif
|
|||
return;
|
||||
}
|
||||
if (context.getUser() != null) {
|
||||
recordX509CertificateAuditDataViaContextEvent(context);
|
||||
context.success();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
package org.keycloak.testsuite.x509;
|
||||
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.AfterClass;
|
||||
|
@ -524,10 +526,20 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe
|
|||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectLogin()
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectLogin()
|
||||
.user(userId)
|
||||
.detail(Details.USERNAME, attemptedUsername)
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.removeDetail(Details.REDIRECT_URI);
|
||||
|
||||
addX509CertificateDetails(expectedEvent)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
|
||||
protected AssertEvents.ExpectedEvent addX509CertificateDetails(AssertEvents.ExpectedEvent expectedEvent) {
|
||||
return expectedEvent
|
||||
.detail(Details.X509_CERTIFICATE_SERIAL_NUMBER, Matchers.not(Matchers.isEmptyOrNullString()))
|
||||
.detail(Details.X509_CERTIFICATE_SUBJECT_DISTINGUISHED_NAME, Matchers.startsWith("EMAILADDRESS=test-user@localhost"))
|
||||
.detail(Details.X509_CERTIFICATE_ISSUER_DISTINGUISHED_NAME, Matchers.startsWith("EMAILADDRESS=contact@keycloak.org"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.keycloak.testsuite.x509;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.util.PhantomJSBrowser;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
|
@ -67,28 +68,6 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
|
|||
replaceDefaultWebDriver(phantomJS);
|
||||
}
|
||||
|
||||
private void login(X509AuthenticatorConfigModel config, String userId, String username, String attemptedUsername) {
|
||||
AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig());
|
||||
String cfgId = createConfig(browserExecution.getId(), cfg);
|
||||
Assert.assertNotNull(cfgId);
|
||||
|
||||
loginConfirmationPage.open();
|
||||
|
||||
Assert.assertTrue(loginConfirmationPage.getSubjectDistinguishedNameText().startsWith("EMAILADDRESS=test-user@localhost"));
|
||||
Assert.assertEquals(username, loginConfirmationPage.getUsernameText());
|
||||
|
||||
loginConfirmationPage.confirm();
|
||||
|
||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectLogin()
|
||||
.user(userId)
|
||||
.detail(Details.USERNAME, attemptedUsername)
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void loginAsUserFromCertSubjectEmail() throws Exception {
|
||||
|
@ -376,13 +355,15 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
|
|||
|
||||
Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed."));
|
||||
|
||||
events.expectLogin()
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectLogin()
|
||||
.user((String) null)
|
||||
.session((String) null)
|
||||
.error("user_not_found")
|
||||
.detail(Details.USERNAME, "test-user@localhost")
|
||||
.removeDetail(Details.CONSENT)
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.removeDetail(Details.REDIRECT_URI);
|
||||
|
||||
addX509CertificateDetails(expectedEvent)
|
||||
.assertEvent();
|
||||
|
||||
// Continue with form based login
|
||||
|
@ -461,10 +442,13 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
|
|||
// the identity.
|
||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
events.expectLogin()
|
||||
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectLogin()
|
||||
.user(userId)
|
||||
.detail(Details.USERNAME, "test-user@localhost")
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.removeDetail(Details.REDIRECT_URI);
|
||||
|
||||
addX509CertificateDetails(expectedEvent)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.representations.AccessToken;
|
|||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.util.ContainerAssume;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.PhantomJSBrowser;
|
||||
|
@ -173,14 +174,16 @@ public class X509DirectGrantTest extends AbstractX509AuthenticationTest {
|
|||
|
||||
assertEquals(401, response.getStatusCode());
|
||||
|
||||
events.expectLogin()
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectLogin()
|
||||
.user((String) null)
|
||||
.session((String) null)
|
||||
.error("invalid_user_credentials")
|
||||
.client("resource-owner")
|
||||
.removeDetail(Details.CODE_ID)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.removeDetail(Details.REDIRECT_URI);
|
||||
|
||||
addX509CertificateDetails(expectedEvent)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
|
@ -308,7 +311,7 @@ public class X509DirectGrantTest extends AbstractX509AuthenticationTest {
|
|||
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
|
||||
events.expectLogin()
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectLogin()
|
||||
.client(clientId)
|
||||
.user(userId)
|
||||
.session(accessToken.getSessionState())
|
||||
|
@ -318,7 +321,9 @@ public class X509DirectGrantTest extends AbstractX509AuthenticationTest {
|
|||
.detail(Details.USERNAME, login)
|
||||
.removeDetail(Details.CODE_ID)
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.removeDetail(Details.CONSENT);
|
||||
|
||||
addX509CertificateDetails(expectedEvent)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue