Skip to content

Commit

Permalink
support confirming risky authentication attempts
Browse files Browse the repository at this point in the history
  • Loading branch information
mmoayyed committed Sep 15, 2023
1 parent 44821a7 commit 0e36d69
Show file tree
Hide file tree
Showing 45 changed files with 828 additions and 264 deletions.
3 changes: 3 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Security Vulnerability Response
Please review [this](https://apereo.github.io/cas/developer/Sec-Vuln-Response.html).
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.apereo.cas.ticket.TicketGrantingTicket;

import lombok.Builder;
import lombok.Getter;
import lombok.experimental.SuperBuilder;

import java.util.LinkedHashMap;
Expand Down Expand Up @@ -44,6 +45,7 @@ public class AuditableContext {
private final Object httpResponse;

@Builder.Default
@Getter
private Map<String, Object> properties = new LinkedHashMap<>(0);

public Optional<Service> getService() {
Expand Down Expand Up @@ -81,8 +83,4 @@ public Optional<AuthenticationResult> getAuthenticationResult() {
public Optional<TicketGrantingTicket> getTicketGrantingTicket() {
return Optional.ofNullable(ticketGrantingTicket);
}

public Map<String, Object> getProperties() {
return properties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.apereo.cas.configuration.model.support.email.EmailProperties;
import org.apereo.cas.configuration.model.support.sms.SmsProperties;
import org.apereo.cas.configuration.support.DurationCapable;
import org.apereo.cas.configuration.support.RequiredProperty;
import org.apereo.cas.configuration.support.RequiresModule;

Expand Down Expand Up @@ -180,6 +181,14 @@ public static class Response implements Serializable {
*/
private String riskyAuthenticationAttribute = "triggeredRiskBasedAuthentication";

/**
* Control the expiration window of the verification token
* that can be used to verify and confirm risky authentication
* attempts.
*/
@DurationCapable
private String riskVerificationTokenExpiration = "PT5M";

/**
* Email settings for notifications,
* If an authentication attempt is deemed risky.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public class CasRiskyAuthenticationDetectedEvent extends AbstractCasEvent {
private final Object score;


public CasRiskyAuthenticationDetectedEvent(final Object source, final Authentication authentication,
public CasRiskyAuthenticationDetectedEvent(final Object source,
final Authentication authentication,
final RegisteredService service,
final Object riskScore,
final ClientInfo clientInfo) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.apereo.cas.support.events.authentication.adaptive;

import org.apereo.cas.support.events.AbstractCasEvent;
import lombok.Getter;
import lombok.ToString;
import org.apereo.inspektr.common.web.ClientInfo;
import java.io.Serial;

/**
* This is {@link CasRiskyAuthenticationVerifiedEvent}.
*
* @author Misagh Moayyed
* @since 5.1.0
*/
@ToString(callSuper = true)
@Getter
public class CasRiskyAuthenticationVerifiedEvent extends AbstractCasEvent {

@Serial
private static final long serialVersionUID = 291168297497263298L;

private final String riskToken;

public CasRiskyAuthenticationVerifiedEvent(final Object source,
final ClientInfo clientInfo,
final String riskToken) {
super(source, clientInfo);
this.riskToken = riskToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.ToString;
import org.apereo.inspektr.common.web.ClientInfo;

import java.io.Serial;
import java.time.ZonedDateTime;


Expand All @@ -20,7 +21,8 @@
@Getter
public abstract class AbstractCasTicketGrantingTicketEvent extends AbstractCasEvent {

public static final long serialVersionUID = 5815205609847140811L;
@Serial
private static final long serialVersionUID = 5815205609847140811L;

private final TicketGrantingTicket ticketGrantingTicket;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,8 @@ public class CasTicketGrantingTicketCreatedEvent extends AbstractCasTicketGranti
@Serial
private static final long serialVersionUID = -1862937393590213844L;

/**
* Instantiates a new CAS sso session established event.
*
* @param source the source
* @param ticketGrantingTicket the ticket granting ticket
*/
public CasTicketGrantingTicketCreatedEvent(final Object source, final TicketGrantingTicket ticketGrantingTicket, final ClientInfo clientInfo) {
public CasTicketGrantingTicketCreatedEvent(final Object source, final TicketGrantingTicket ticketGrantingTicket,
final ClientInfo clientInfo) {
super(source, ticketGrantingTicket, clientInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
*/
public interface CasWebflowConfigurer extends Ordered {

/**
* Flow id for risk-based authentication's verification.
*/
String FLOW_ID_RISK_VERIFICATION = "riskauthverify";

/**
* Flow id for delegated authentication redirect.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,10 @@ public interface CasWebflowConstants {
* State id 'viewRegistrationWebAuthn'.
*/
String STATE_ID_WEBAUTHN_VIEW_REGISTRATION = "viewRegistrationWebAuthn";
/**
* State id 'checkRiskVerificationToken'.
*/
String STATE_ID_RISK_AUTHENTICATION_TOKEN_CHECK = "checkRiskVerificationToken";

/**
* State id 'validateWebAuthnToken'.
Expand Down Expand Up @@ -2043,7 +2047,10 @@ public interface CasWebflowConstants {
* Action id 'baseSpnegoClientAction'.
*/
String ACTION_ID_SPNEGO_CLIENT_BASE = "baseSpnegoClientAction";

/**
* Action id 'riskAuthenticationTokenCheckAction'.
*/
String ACTION_ID_RISK_AUTHENTICATION_TOKEN_CHECK = "riskAuthenticationCheckTokenAction";
/**
* The action id 'spnego'.
*/
Expand All @@ -2052,4 +2059,5 @@ public interface CasWebflowConstants {
* The action id 'syncopePrincipalProvisionerAction'.
*/
String ACTION_ID_SYNCOPE_PRINCIPAL_PROVISIONER_ACTION = "syncopePrincipalProvisionerAction";

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"conditions": {
"docker": "true"
},

"properties": [
"--cas.server.name=https://localhost:8443",
"--cas.server.prefix=${cas.server.name}/cas",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
------------------------------------
Dear ${authentication.principal.id}.
------------------------------------

link=${verificationUrl}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const puppeteer = require('puppeteer');
const cas = require('../../cas.js');
const assert = require("assert");

(async () => {
const browser = await puppeteer.launch(cas.browserOptions());
const page = await cas.newPage(browser);
let service = "https://localhost:9859/anything/adaptive";
await cas.goto(page, `https://localhost:8443/cas/login?service=${service}`);
await page.waitForTimeout(1000);
await cas.loginWith(page, "casuser", "Mellon");
await page.waitForTimeout(1000);
await cas.assertInnerTextContains(page, "#loginErrorsPanel p", "authentication attempt is determined to be risky");

await cas.goto(page, "http://localhost:8282");
await page.waitForTimeout(5000);
await cas.click(page, "table tbody td a");
await page.waitForTimeout(1000);
let body = await cas.textContent(page, "div[name=bodyPlainText] .well");
console.log(`Email message body is: ${body}`);
const link = body.substring(body.indexOf("link=") + 5);
await cas.logg(`Verification link is ${link}`);
let response = await cas.goto(page, link);
console.log(`${response.status()} ${response.statusText()}`);
assert(response.ok());
await cas.assertInnerText(page, "#content h2", "Risky Authentication attempt is confirmed.");
await browser.close();
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"dependencies": "events-memory,electrofence,geolocation",
"conditions": {
"docker": "true"
},
"properties": [
"--cas.server.name=https://localhost:8443",
"--cas.server.prefix=${cas.server.name}/cas",

"--cas.authn.attribute-repository.stub.attributes.phone=13477464523",
"--cas.authn.attribute-repository.stub.attributes.mail=casuser@example.org",

"--cas.http-client.host-name-verifier=none",
"--cas.audit.slf4j.use-single-line=true",

"--cas.authn.adaptive.risk.core.threshold=0.2",
"--cas.authn.adaptive.risk.ip.enabled=true",

"--cas.service-registry.core.init-from-json=true",

"--spring.mail.host=localhost",
"--spring.mail.port=25000",

"--cas.webflow.crypto.signing.key=v43dwqO_GbGSVsFqgPFpVdwdMSEunMUzc4QSF13x18kInHPeRuvntleljO5Y5cKqDGAFe1vv10mM4tpyoKyBBA",
"--cas.webflow.crypto.encryption.key=2PikjfOKY6n8Bbux2cy-Hg",

"--cas.tgc.crypto.encryption.key=u696jJnPvm1DHLR7yVCSKMMzzoPoFxJZW4-MP1CkM5w",
"--cas.tgc.crypto.signing.key=zPdNCd0R1oMR0ClzEqZzapkte8rO0tNvygYjmHoUhitAu6CBscwMC3ZTKy8tleTKiQ6GVcuiQQgxfd1nSKxf7w",

"--cas.authn.adaptive.risk.response.mail.html=false",
"[email protected]",
"--cas.authn.adaptive.risk.response.mail.subject=CasRiskyAuthN",
"--cas.authn.adaptive.risk.response.mail.text=file:${PWD}/ci/tests/puppeteer/scenarios/${SCENARIO}/email.gtemplate"
],
"jvmArgs": "-Djava.net.preferIPv4Addresses=true",
"initScript": "${PWD}/ci/tests/mail/run-mail-server.sh,${PWD}/ci/tests/httpbin/run-httpbin-server.sh"
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.apereo.cas.support.events.ticket.CasTicketGrantingTicketCreatedEvent;
import org.apereo.cas.support.events.ticket.CasTicketGrantingTicketDestroyedEvent;
import org.apereo.cas.util.DateTimeUtils;
import org.apereo.cas.util.function.FunctionUtils;
import org.apereo.cas.util.http.HttpRequestUtils;
import org.apereo.cas.util.text.MessageSanitizer;
import lombok.Getter;
Expand Down Expand Up @@ -46,15 +47,13 @@ private CasEvent prepareCasEvent(final AbstractCasEvent event) {
val dt = DateTimeUtils.zonedDateTimeOf(Instant.ofEpochMilli(event.getTimestamp()));
dto.setCreationTime(dt.toString());
val clientInfo = event.getClientInfo();
if (clientInfo != null) {
FunctionUtils.doIfNotNull(clientInfo, __ -> {
dto.putClientIpAddress(clientInfo.getClientIpAddress());
dto.putServerIpAddress(clientInfo.getServerIpAddress());
dto.putAgent(clientInfo.getUserAgent());
val location = determineGeoLocationFor(clientInfo);
dto.putGeoLocation(location);
} else {
LOGGER.trace("No client information is available. The final event cannot track client location, user agent or IP addresses");
}
});
return dto;
}

Expand Down
Loading

0 comments on commit 0e36d69

Please sign in to comment.