My notes on CAS server installation when learning it. Hope it will be helpful to somebody.
-
Go to https://getcas.apereo.org/ui and download an overlay template project. You can use this example settings applied url (select version field 6.6.x if empty)
-
Generate a keystore and put it to
/etc/cas/keystore
(on Linux) orC:\etc\cas\keystore
(on Windows):You can do this two ways:
1. Automatic generation using the Gradle task defined in the project:
In the project, there's a gradle task that does this automatically, but the problem is, machine you're developing this on could be protected, so you need to run the command below as an "administrator"
./gradlew createKeystore
2. Manually: You can generate a keystore manually via the command below. Keystore password should be
changeit
keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048
Enter the asked fields as you see fit and put the generated keystore to
etc/cas/thekeystore
. -
Prepare configurations for project run to
/etc/cas/
(on Linux) orC:\etc\cas\
(onWindows)Again you can do this automatically or manually. Automatic gradle operation task can be called(as an "administrator") like:
./gradlew copyCasConfiguration
Or you can just copy the
etc/cas/config
on the project to your system's/etc/cas/config
(On Linux) orC:\etc\cas\config
(On Windows) -
Run the gradle wrapper command below to start basic cas server:
./gradlew run
- Open https://localhost:8443/cas/ and enter the credentials below:
Username: casuser
Password: Mellon
-
Check that you have successfully logged in.
-
Congrats! You have successfully entered the CAS World!
Add -DbaseDir=C:\\dev\\logs
to shut the log4j2 initial error! it made me mad!:) Also
debugging is a nightmare, always a zombie gradle process(something about the gradle deamon, IDK!)
stays and if you want to restart the app, you need to kill these gradle zombie processes (
java.exe ....) i added some vm args to the debug
gradle task to work, if you run the project via idea, use these vm args:
-Dorg.gradle.daemon=false
this disables the daemon for the current gradle task run-DbaseDir=C:\\dev\\logs
this makes sure the use a correct path for the log4j2
also, when starting the run configuration, check out the logs and make sure to click
to Attach debugger
link appears in the console to idea to start debugging the app. After
clicking
project will continue running its startup sequence. After that smooth sailing(with zombie
processes
of course!). For idea, i did define a run configuration of type Remote JVM debug, and set it up
that
before Remote JVM Debug launch, run the debug gradle task.
./gradlew run -DbaseDir=C:\\dev\\logs -Dorg.gradle.daemon=false
Default behaviour is setup on the projects etc/ dir and use the gradle script to copy to /etc or C: \etc before running the app. To persist settings to db and use it on startup:
First add the dependency on build.gradle
file under dependencies
:
implementation "org.apereo.cas:cas-server-support-configuration-cloud-jdbc"
And then, generate The Tables On Db(below is postgres):
create table if not exists CAS_SETTINGS_TABLE
(
ID serial
constraint cas_settings_pk primary key,
NAME TEXT UNIQUE NOT NULL,
VALUE TEXT NOT NULL,
DESCRIPTION TEXT DEFAULT ('açıklama giriniz') NOT NULL
);
Check and get the values from cas startup logs that you have run before or read the Cas docs to learn how to generate and get each of them.
INSERT INTO cas_settings_table (name, value, description)
VALUES ('cas.tgc.crypto.encryption.key', '<SOMEKEY>',
'Generated encryption key [] of size [256] for [Ticket-granting Cookie]. The generated key MUST be added to CAS settings');
INSERT INTO cas_settings_table (name, value, description)
VALUES ('cas.tgc.crypto.signing.key', '<SOMEKEY>', 'Generated signing key [] of size [512] for [Ticket-granting Cookie]. The generated key MUST be added to CAS settings:
');
INSERT INTO cas_settings_table (name, value, description)
VALUES ('cas.webflow.crypto.signing.key', '<SOMEKEY>',
'Generated signing key [] of size [512]. The generated key MUST be added to CAS settings:');
INSERT INTO cas_settings_table (name, value, description)
VALUES ('cas.webflow.crypto.encryption.key', '<SOMEKEY>',
'Generated encryption key [] of size [16]. The generated key MUST be added to CAS settings:');
INSERT INTO cas_settings_table (name, value, description)
VALUES ('cas.authn.accept.enabled', 'true',
'CAS is configured to accept a static list of credentials for authentication. While this is generally useful for demo purposes, it is STRONGLY recommended that you DISABLE this authentication method by setting ''cas.authn.accept.enabled=false'' and switch to a mode that is more suitable for production.');
INSERT INTO cas_settings_table (name, value, description)
VALUES ('spring.security.user.name', 'demo', 'Spring security secured username');
INSERT INTO cas_settings_table (name, value, description)
VALUES ('spring.security.user.password', 'demo', 'Spring security secured password');
After successful insertion, open the file <PROJECT_DIR>src/main/resources/application.yml
and add
these db connection information:
# Cas ayarlarının veritabanında alınmasını sağlamak adına aşağıdaki ayarları yapıyoruz.
cas:
spring:
cloud:
jdbc:
driver-class: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/<DBNAME>
user: <USERNAME>
password: <PASSWORD>
With the inserted rows, some warnings about key generation will not come again.
Project's default behaviour is to use the etc/cas/config/log4j2.xml
file at the project
root, if you copy this to /etc/cas/config
or C:/etc/cas/config
, it will override the config.
To use a config file at the project's classpath(src/main/resources
), simply add a
configuration to
the application.yml
:
logging:
config: 'classpath:config/logging/log4j2.xml'
you can modify default configurations according to your needs.
Project's default behaviour is a static user list, to disable it and authenticate users from a db :
-
For example we have user table this(on postgres):
CREATE TABLE users ( id bigint NOT NULL, disabled boolean, email character varying(40) COLLATE pg_catalog."default", first_name character varying(40) COLLATE pg_catalog."default", last_name character varying(40) COLLATE pg_catalog."default", expired boolean, password character varying(100) COLLATE pg_catalog."default", CONSTRAINT users_pkey PRIMARY KEY (id), CONSTRAINT users_unique_email UNIQUE (email) ); INSERT INTO users(id, disabled, email, first_name, last_name, expired, password) VALUES (1, false, '[email protected]', 'test', 'user1', false, 'wasd');
-
First add the necessary dependency to
build.gradle
:implementation "org.apereo.cas:cas-server-support-jdbc" implementation "org.apereo.cas:cas-server-support-jdbc-drivers"
-
Next, add the necessary user checking configurations to your settings. Below is written for a java properties file, but for this project it is in the cas_settings_table and these settings need to be put there.
# Authenticates a user by comparing the user password (which can be encoded with a password encoder) against the password on record determined by a configurable database query. # https://apereo.github.io/cas/6.6.x/authentication/Database-Authentication.html#query-database-authentication # Required Settings cas.authn.jdbc.query[0].driver-class=org.postgresql.Driver cas.authn.jdbc.query[0].url=jdbc:postgresql://localhost:5432/postgres cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.PostgreSQL95Dialect cas.authn.jdbc.query[0].user=postgres cas.authn.jdbc.query[0].password=postgres cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ? cas.authn.jdbc.query[0].field-password=password cas.authn.jdbc.query[0].password-encoder.type=NONE # Optional Settings cas.authn.jdbc.query[0].field-expired=expired cas.authn.jdbc.query[0].field-disabled=disabled
Örnek sql insert scripti:
INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].driver-class', 'org.postgresql.Driver', 'Kullanıcıların bakılacağı veritabanı JDBC bağlantı driver'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].dialect', 'org.hibernate.dialect.PostgreSQL95Dialect', 'Kullanıcıların bakılacağı veritabanı JDBC bağlantı SQL dialect'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].url', 'jdbc:postgresql://localhost:5432/postgres', 'Kullanıcıların bakılacağı veritabanı JDBC bağlantı URL'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].user', 'postgres', 'Kullanıcıların bakılacağı veritabanı JDBC bağlantı kullanıcı adı'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].password', 'postgres', 'Kullanıcıların bakılacağı veritabanı JDBC bağlantı şifresi'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].sql', 'SELECT * FROM users WHERE email = ?', 'Kullanıcıların bakılacağı veritabanı sorgu sql'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].field-password', 'password', 'Kullanıcıların bakılacağı veritabanı tablosundaki karşılaştırılacak şifrelerin bulunduğu kolon'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].password-encoder.type', 'NONE', 'Kullanıcıların bakılacağı veritabanı tablosu şifre kolonundaki değerlerin şifreleme bilgisi'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].field-expired', 'expired', 'Kullanıcıların bakılacağı veritabanı tablosunda, kullanıcının zaman aşımı(expired) olduğunu belirten kolon adı'); INSERT INTO cas_settings_table (name, value, description) VALUES ('cas.authn.jdbc.query[0].field-disabled', 'disabled', 'Kullanıcıların bakılacağı veritabanı tablosunda, kullanıcının devre dışı(disabled) olduğunu belirten kolon adı');
-
And lastly, restart your application via these commands: Reload dependencies and rebuild the project.
./gradlew clean build --refresh-dependencies
Run the project.
./gradlew run
When you go to to https://localhost:8443/cas/login and enter the credentials you've entered to the users table before, you should successfully be authenticated.
User passwords are clearly visible on the database for this example(check out the config key
cas.authn.jdbc.query[0].password-encoder.type
is set toNONE
), you should read and set up some kind of one-way encryption. Details are here.
To enable audit logs, first add the necessary dependency in build.gradle
:
implementation "org.apereo.cas:cas-server-support-audit-jdbc"
After that, configure the properties accordingly descibed here, for this example you can use the basic setup below:
cas.audit.engine.app-code=CAS
cas.audit.engine.excluded-actions=AUTHENTICATION_SUCCESS, AUTHENTICATION_EVENT_TRIGGERED,SERVICE_ACCESS_ENFORCEMENT_TRIGGERED
cas.audit.jdbc.date-formatter-pattern=dd-MM-yyyy
cas.audit.jdbc.ddl-auto=update
cas.audit.jdbc.dialect=org.hibernate.dialect.PostgreSQL95Dialect
cas.audit.jdbc.driver-class=org.postgresql.Driver
cas.audit.jdbc.max-age-days=30
cas.audit.jdbc.password=postgres
cas.audit.jdbc.select-sql-query-template=SELECT * FROM %s WHERE AUD_DATE>=TO_DATE('%s','dd-MM-yyyy') ORDER BY AUD_DATE DESC
cas.audit.jdbc.url=jdbc:postgresql://localhost:5432/postgres
cas.audit.jdbc.user=postgres
For this use case, here's the sql version:
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.engine.app-code', 'CAS');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.engine.excluded-actions',
'AUTHENTICATION_SUCCESS, AUTHENTICATION_EVENT_TRIGGERED,SERVICE_ACCESS_ENFORCEMENT_TRIGGERED');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.date-formatter-pattern', 'dd-MM-yyyy');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.ddl-auto', 'update');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.dialect', 'org.hibernate.dialect.PostgreSQL95Dialect');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.driver-class', 'org.postgresql.Driver');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.max-age-days', '30');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.password', 'postgres');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.select-sql-query-template',
'SELECT * FROM %s WHERE AUD_DATE>=TO_DATE(''%s'',''dd-MM-yyyy'') ORDER BY AUD_DATE DESC');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.url', 'jdbc:postgresql://localhost:5432/postgres');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.audit.jdbc.user', 'postgres');
To use Hazelcast for ticket stroge, add the necessary dependency first:
implementation "org.apereo.cas:cas-server-support-hazelcast-ticket-registry"
after that configure accordingly. (link)
cas.ticket.registry.hazelcast.cluster.core.instance-name=localhost
cas.ticket.registry.hazelcast.cluster.network.members=my.example.com.tr
cas.ticket.registry.hazelcast.cluster.network.port=5701
cas.ticket.registry.hazelcast.page-size=500
cas.ticket.st.number-of-uses=1
cas.ticket.st.time-to-kill-in-seconds=30
cas.ticket.tgt.core.onlyTrackMostRecentSession=false
cas.ticket.tgt.primary.max-time-to-live-in-seconds=28800
cas.ticket.tgt.primary.time-to-kill-in-seconds=3600
# Below Settings Are Fro Kubernetes Cluster Discovery:
# https://apereo.github.io/cas/6.6.x/ticketing/Hazelcast-Ticket-Registry-AutoDiscovery-Kubernetes.html
# cas.ticket.registry.hazelcast.cluster.discovery.enabled=true
# cas.ticket.registry.hazelcast.cluster.discovery.kubernetes.namespace=qa-cas
# cas.ticket.registry.hazelcast.cluster.discovery.kubernetes.service-name=cas-hz
# cas.ticket.registry.hazelcast.cluster.discovery.kubernetes.service-port=5701
and if your configuration is on db, add these configs as new rows on CAS_SETTINGS_TABLE
.
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.registry.hazelcast.cluster.core.instance-name', 'localhost');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.registry.hazelcast.cluster.network.members', 'my.example.com.tr');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.registry.hazelcast.cluster.network.port', '5701');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.registry.hazelcast.page-size', '500');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.st.number-of-uses', '1');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.st.time-to-kill-in-seconds', '30');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.tgt.core.onlyTrackMostRecentSession', 'false');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.tgt.primary.max-time-to-live-in-seconds', '28800');
INSERT INTO cas_settings_table (name, value)
VALUES ('cas.ticket.tgt.primary.time-to-kill-in-seconds', '3600');
Generallyt, we don't need to do this, but for a customized security configurations, this is good topic to be known. We can write our customized authentication handler, and register it to CAS to use it as explained here and here. Problem is, these explanations does not cover detailed operations like connecting to database or using the fields, etc. So we need to dig deeper, and learn to utilize already defined cas mechanisms to ease our problems.
-
First remove any Database authentication configurations defined before(like configuration entries starting with
cas.authn.jdbc.query[0]
). -
Add the necessary dependencies:
implementation "org.apereo.cas:cas-server-core-authentication-api"
-
After that we start to design the authentication handling operations via writing a class that extends the
AbstractUsernamePasswordAuthenticationHandler
. For example:
package tr.com.example.cas.config.auth.handler;
import java.security.GeneralSecurityException;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.credential.UsernamePasswordCredential;
import org.apereo.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
@Slf4j
public class DemoAuthHandler extends AbstractUsernamePasswordAuthenticationHandler {
protected DemoAuthHandler(String name, ServicesManager servicesManager,
PrincipalFactory principalFactory,
Integer order) {
super(name, servicesManager, principalFactory, order);
}
@Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
UsernamePasswordCredential credential, String originalPassword)
throws GeneralSecurityException, PreventedException {
// todo: implement your authentication logic here.
return null;
}
}
Example on the links i've written before are very basic and just gives the developer the starting point. Even this example is like leaving a person on a desert, alone! :) So how about a working example huh? For this example use case, i have written this handler below, that check given credentials according to the User db table i've mentioned before, checks if user exist, validates the password if user found, and checks the user if its expired or disabled:
Gradle CAS dependencies for the example to work.
implementation "org.apereo.cas:cas-server-core-authentication-api"
implementation "org.apereo.cas:cas-server-core-util-api"
implementation "org.apereo.cas:cas-server-support-jpa-util"
implementation "org.apereo.cas:cas-server-support-jdbc-authentication"
package tr.com.example.cas.config.auth.handler;
import com.google.common.collect.Maps;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.credential.UsernamePasswordCredential;
import org.apereo.cas.authentication.exceptions.AccountDisabledException;
import org.apereo.cas.authentication.exceptions.AccountPasswordMustChangeException;
import org.apereo.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.configuration.support.JpaBeans;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.util.CollectionUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.apereo.cas.adaptors.jdbc.AbstractJdbcUsernamePasswordAuthenticationHandler;
import tr.com.example.cas.config.password.encoder.CustomPasswordEncoder;
/**
* <p>
* Normalde tutorialler AbstractUsernamePasswordAuthenticationHandler sınıfından türetin diyor
* ancak, yapılacak db kontrolleri konusunda herşeyi developer'a bırakıyor. Bıraz karıştırarak,
* CAS'ın db bağlantı ayarlamalarını yapmasını sağlamak amacıyla
* AbstractJdbcUsernamePasswordAuthenticationHandler sınıfından türettim. Bu şekilde, jdbc
* authentication'daki config yapısına benzer şekilde, db bağlantı ayarlarını CAS'ın yoğurt
* yiyişinde alıp bağlantı sağlanmasını CAS'ın içinde bıraktım.
*
* @author Kambaa
*/
@Slf4j
public class CustomAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {
// Custom configuration for this custom auth handler to work.
private final CustomAuthenticationProperties properties;
// Custom password endcoder for this custom auth handler to work.
private final CustomPasswordEncoder passwordEncoder;
// Constructor.
public CustomAuthenticationHandler(
final CustomAuthenticationProperties properties,
final ServicesManager servicesManager,
final PrincipalFactory principalFactory,
CustomPasswordEncoder passwordEncoder
) {
super(properties.getName(), servicesManager, principalFactory, properties.getOrder(),
JpaBeans.newDataSource(properties));
this.properties = properties;
this.passwordEncoder = passwordEncoder;
}
// Custom authentication method.
@Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
final UsernamePasswordCredential credential,
final String originalPassword)
throws GeneralSecurityException, PreventedException {
// check the sql configuration of this custom auth handler
if (StringUtils.isBlank(properties.getSql()) || getJdbcTemplate() == null) {
throw new GeneralSecurityException("Authentication handler is not configured correctly");
}
val attributes =
Maps.<String, List<Object>>newHashMap();
val username = credential.getUsername();
try {
val values = performSqlQuery(username);
// use the custom encoder to encode the password(or salt+password, or custom data+password, if you catch my drift :) CAS can do password+salt internally with given config, so write your custom handlers if you need absolutely necessary)
val encoded = passwordEncoder.encode(credential.getPassword());
// check password
if (!values.get(properties.getPasswordFieldName()).equals(encoded)) {
throw new FailedLoginException("Password does not match value on record.");
}
// check user is expired
if (StringUtils.isNotBlank(properties.getExpiredFieldName()) && values.containsKey(
properties.getExpiredFieldName())) {
val dbExpired = values.get(properties.getExpiredFieldName()).toString();
if (BooleanUtils.toBoolean(dbExpired) || "1".equals(dbExpired)) {
throw new AccountPasswordMustChangeException("Password has expired");
}
}
// check user is disabled
if (StringUtils.isNotBlank(properties.getDisabledFieldName()) && values.containsKey(
properties.getDisabledFieldName())) {
val dbDisabled = values.get(properties.getDisabledFieldName()).toString();
if (BooleanUtils.toBoolean(dbDisabled) || "1".equals(dbDisabled)) {
throw new AccountDisabledException("Account has been disabled");
}
}
// save the user data in db to User Attributes(OPTIONAL and NOT ADVISED, for this example project only)
values.forEach((key, names) -> {
attributes.put(key, List.of(CollectionUtils.wrap(names)));
});
return createHandlerResult(credential,
this.principalFactory.createPrincipal(username, attributes), new ArrayList<>(0));
} catch (final IncorrectResultSizeDataAccessException e) {
if (e.getActualSize() == 0) {
throw new AccountNotFoundException(username + " not found with SQL query");
}
throw new FailedLoginException("Multiple records found for " + username);
} catch (final DataAccessException e) {
throw new PreventedException(e);
}
}
// Gets the necessary user information for custom auth from User db table.
protected Map<String, Object> performSqlQuery(final String username) {
return getJdbcTemplate().queryForMap(properties.getSql(), username);
}
}
I reused and extended the AbstractJpaProperties
properties class to use set up
configuration in CAS's way and utilize the db operations of CAS internals:
package tr.com.example.cas.config.auth.handler;
import com.fasterxml.jackson.annotation.JsonFilter;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apereo.cas.configuration.model.support.jdbc.authn.BaseJdbcAuthenticationProperties;
import org.apereo.cas.configuration.model.support.jpa.AbstractJpaProperties;
import org.apereo.cas.configuration.support.RequiredProperty;
import org.apereo.cas.configuration.support.RequiresModule;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* This is {@link CustomAuthenticationProperties}.
*
* @author Kambaa
*/
@RequiresModule(name = "cas-server-support-jdbc-authentication")
@Getter
@Setter
@Accessors(chain = true)
@JsonFilter("CustomAuthenticationProperties")
@ConfigurationProperties(value = "custom")
@RefreshScope
public class CustomAuthenticationProperties extends AbstractJpaProperties {
private static final long serialVersionUID = 123456L;
/**
* SQL query to execute and look up accounts. Example:
* {@code SELECT * FROM table WHERE username=?}.
*/
@RequiredProperty
private String sql;
/**
* Password column name.
*/
private String passwordFieldName = "password";
/**
* Field/column name that indicates the username.
*/
@RequiredProperty
private String usernameFieldName = "email";
/**
* Column name that indicates whether account is expired.
*/
private String expiredFieldName;
/**
* Column name that indicates whether account is disabled.
*/
private String disabledFieldName;
private String name = "CUSTOM-AUTHENTICATION";
/**
* Order of the authentication handler in the chain.
*/
private int order = Integer.MAX_VALUE;
}
And here's a very basic CustomPasswordEncoder that does nothing, no-encoding, just returns it :).
package tr.com.example.cas.password.encoder;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Custom password encoder class.
* <p>
* For CAS to use this encoder on db authentication, check the configuration:<br/>
* <em>"cas.authn.jdbc.query[0].password-encoder.type"</em><br/>
* on <em>"cas_settings_table"</em>
* <p>
*
* @author Kambaa
*/
public class CustomPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return encode(charSequence).equals(s);
}
}
After that it's time to enter configs like this(if you set up db config like written above, enter these there):
custom.driver-class=org.postgresql.Driver
custom.dialect=org.hibernate.dialect.PostgreSQL95Dialect
custom.url=jdbc:postgresql://localhost:5432/postgres
custom.user=postgres
custom.password=postgres
custom.passwordFieldName=password
custom.sql=SELECT * FROM users WHERE email = ?
custom.disabledFieldName=disabled
custom.expiredFieldName=expired
this is the design phase of custom authentication building. Now comes the registration part.
Registering the authentication handler: To register the handler we'we written, we write a spring
confgiguration class that extends AuthenticationEventExecutionPlanConfigurer
package tr.com.example.cas.config.auth.handler;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tr.com.example.cas.config.password.encoder.CustomPasswordEncoder;
@Configuration("CustomAuthenticationEventExecutionPlanConfiguration")
@EnableConfigurationProperties(
{CasConfigurationProperties.class, CustomAuthenticationProperties.class})
public class CustomAuthenticationEventExecutionPlanConfiguration implements
AuthenticationEventExecutionPlanConfigurer {
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
private CustomAuthenticationProperties customAuthenticationProperties;
@Bean
public CustomPasswordEncoder customPasswordEncoder() {
return new CustomPasswordEncoder();
}
@Autowired
@Qualifier("jdbcPrincipalFactory")
private PrincipalFactory principalFactory;
@Bean
public AuthenticationHandler myAuthHandler() {
final CustomAuthenticationHandler handler =
new CustomAuthenticationHandler(
customAuthenticationProperties
, null,
principalFactory,
customPasswordEncoder()
);
return handler;
}
@Override
public void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) {
plan.registerAuthenticationHandler(myAuthHandler());
}
}
This configuration prepares the necessary custom handling classes and registers it to CAS. What
made my head hurt was finding necessary dependant classes and initiate on the config class as
spring beans(via method @Bean'ing and @Autowiring whatever it takes :) ). I looked too much
at CasJdbcAuthenticationConfiguration
and QueryAndEncodeDatabaseAuthenticationHandler
classes
to understand the innerworkings of an authentication handler.
And for the last step, we need to tell spring to add our config class to register via openning the
file src/main/resources/META-INF/spring.factories
and adding our configuration class, we can add
multiple so no worries:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.apereo.cas.config.CasOverlayOverrideConfiguration,\
tr.com.example.cas.config.auth.handler.CustomAuthenticationEventExecutionPlanConfiguration
Lastly, on our handler method(at the return
line, attributes variable being used), we've added the
attributes to the principal object to use, so that authentication handler does copy the db fields
to authenticated user (Principal) object.
To enable mfa, first add the dependencies tobuild.gradle
:
implementation "org.apereo.cas:cas-server-support-simple-mfa"
And then add these settings.
cas.authn.mfa.simple.mail.attribute-name=email
cas.authn.mfa.simple.mail.from[email protected]
cas.authn.mfa.simple.mail.subject=cas.mfa.email.key
cas.authn.mfa.simple.mail.text=Your key is: ${token}
cas.authn.mfa.simple.sms.attribute-name=phone
cas.authn.mfa.simple.sms.from=EXAMPLE
cas.authn.mfa.simple.sms.text=Your key is: ${token}
cas.authn.mfa.simple.token.core.timeToKillInSeconds=90
cas.authn.mfa.simple.token.core.tokenLength=6
cas.authn.mfa.triggers.principal.global-principal-attribute-name-triggers=mfa
cas.authn.mfa.triggers.principal.global-principal-attribute-value-regex=mfa-simple
cas.authn.mfa.trusted.core.device-registration-enabled=false
cas.authn.mfa.simple.bypass.authentication-attribute-name=mfa-bypass
cas.authn.mfa.simple.bypass.authentication-attribute-value=.*
and of course, if your configuration is on db, add these configs as new rows on CAS_SETTINGS_TABLE
.
With these settings, basic mfa setup is now complete, but we still need to do stuff for the actual mfa code sending implementation, we can configure cas email sending settings of mfa(defining smtp and stuff like written here (Read the config items in the "Email Server" tab), but for the sms side, we still need to use external libraries and configure them. For this learning project's sake, let's learn how to customize both email and sms sending operations.
To customize CAS MFA mail/sms sending capability,
we need to write out own class that implements the CommunicationsManager
and register as a @Bean
as
explained here.
The explanation on CAS documentation did not say about which module this class belong to, so i
have to searched it so that you don't... We need to add this as a dependency on our project. To
start with MFA, First add this dependency to build.gradle
:
implementation "org.apereo.cas:cas-server-core-notifications"
For simplicity's sake, i used a free and open source notification service
named [ntfy.sh](https://ntfy.sh/app)
to simulate both email and sms sending operations. To do this
first go to https://ntfy.sh/app and click the menu item at
the left named Subscribe to topic
. After that a popup appears and wants to name your topic. Enter something, like:
cas-learning-mfa-demo
for your topic name name and press Subscribe
button. This topic name is used in the class
written next phase, you should write like this, or you can change topic name but remember update in
the class below, accordingly. You can test if notifications are working by calling this command:
curl -d 'Hello, is this working?' https://ntfy.sh/cas-learning-mfa-demo
if you hear a sound and see the Hello, is this working?
at ntfy.sh browser
tab, you're ready! Of course this is not an actual implementation of sending emails and sms's, but Just think
that these notifications are SMS and EMAILs sent to your user.
Now let's implement our CustomCustomCommunicationsManager
class:
package tr.com.example.cas.config.mfa;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.bval.constraints.Email;
import org.apache.commons.collections.CollectionUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.notifications.CommunicationsManager;
import org.apereo.cas.notifications.mail.EmailCommunicationResult;
import org.apereo.cas.notifications.mail.EmailMessageRequest;
import org.apereo.cas.notifications.sms.SmsRequest;
import org.apereo.cas.util.http.HttpClient;
@Slf4j
public class CustomCustomCommunicationsManager implements CommunicationsManager {
@Override
public boolean isMailSenderDefined() {
return false;
}
@Override
public boolean isSmsSenderDefined() {
return true;
}
@Override
public boolean isNotificationSenderDefined() {
return false;
}
@Override
public boolean notify(Principal principal, String title, String body) {
return false;
}
// Sends the email (NTFY example).
@Override
public EmailCommunicationResult email(EmailMessageRequest emailRequest) {
var username = emailRequest.getPrincipal().getId();
String email;
// We need to understand this method is called from a password resetting operation or MFA operation.
// When password resetting operation, this method will be invoked with emailRequest.getTo() a.k.a `to` field exists. So use it accordingly.
// If not, it's a MFA request and you can get the necessary email address from principal
if (emailRequest.getTo() != null) {
// şifre sıfırlama sırasında
email = emailRequest.getTo().get(0);
} else {
// login işlemi sırasında
email = emailRequest.getPrincipal().getAttributes().get("email").get(0).toString();
}
String body = emailRequest.getBody();
if (body != null) {
Map<String, String> result =
sendToNtfy("EMAIL", email, body);
boolean emailSent = "200".equals(result.get("code"));
if (emailSent) {
LOGGER.info("Sending mail to {} user is successful.", email);
}
return EmailCommunicationResult.builder().success(emailSent).build();
}
// i've just return here positive sent outcome. Do not forget to change and handle the negative cases!
return EmailCommunicationResult.builder().success(true).build();
}
// Send SMS(NTFY Example!)
@Override
public boolean sms(SmsRequest smsRequest) {
Map result = sendToNtfy("SMS", smsRequest.getTo(), smsRequest.getText());
System.out.println("Response Status code: " + result.get("code"));
System.out.println("Response Body: " + result.get("body"));
return result.get("code").equals("200");
}
@Override
public boolean validate() {
return false;
}
private Map<String, String> sendToNtfy(String type, String to, String body) {
// https://www.baeldung.com/apache-httpclient-cookbook
// https://docs.ntfy.sh/publish/
HttpPost request = new HttpPost("https://ntfy.sh/cas-learning-mfa-demo");
request.addHeader("Priority", "high");
request.addHeader("X-Tags", "policeman");
request.addHeader("Markdown", "yes");
request.addHeader("Title", "CAS MFA");
StringBuilder sb = new StringBuilder();
sb.append("Type: ").append(type).append("\n");
sb.append("To: ").append(to).append("\n");
sb.append("Message Body: \n").append(body);
try {
request.setEntity(
new StringEntity(sb.toString())
);
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = httpClient.execute(request);
Map<String, String> out = new HashMap<>();
out.put("code", String.valueOf(response.getStatusLine().getStatusCode()));
out.put("body", String.valueOf(EntityUtils.toString(response.getEntity())));
return out;
} catch (UnsupportedEncodingException | ClientProtocolException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// run this method check the ntfy sending demo yourself! :)
public static void main(String[] args) {
CustomCustomCommunicationsManager cm = new CustomCustomCommunicationsManager();
Map<String, String> result = cm.sendToNtfy("DEMO", "ME", "Hello from CAS MFA DEMO");
System.out.println("Response Status code: " + result.get("code"));
System.out.println("Response Body: " + result.get("body"));
}
}
imporant note here is that isXXXXSenderDefined
methods on the beginning of the class toggles the sending of these methods, and
email
, sms
and notify
methods do the actual sending operations. So i wrote the ntfy
integration on a private method called sendToNtfy
and use it on both email and sms.
Lastly, we need to register this class as a Spring bean, so basically add it to the
configuration class(i.e: CasOverlayOverrideConfiguration
in our overlay project) like this:
@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
@Bean
public CommunicationsManager communicationsManager() {
return new CustomCommunicationsManager();
}
To see the effect on the login, we have defined on the configuration that, any login attempt that
has a mfa
attribute with a value of mfa-simple
will enter the mfa operation cycle. Below
configurations you have entered already(look at the first mfa config list, you should see these!).
These are the configurations for this functionality:
cas.authn.mfa.triggers.principal.global-principal-attribute-name-triggers=mfa
cas.authn.mfa.triggers.principal.global-principal-attribute-value-regex=mfa-simple
To test this functionality on this learning project, revisit the CustomAuthenticationHandler
class.
In the authenticateUsernamePasswordInternal
method, before returning, add the line below to enable
mfa for the users:
attributes.put("mfa",List.of("mfa-simple"));
after that, run the project and see when you enter your password, you will see that it goes to another page for mfa key and at the same time(approximately :) ) your ntfy.sh website tab will notify you that cas mfa token key. After entering the key, you will be logged in successfully.
To further your knowledge, read up on the triggering section of MFA on CAS documentation: https://apereo.github.io/cas/6.6.x/mfa/Configuring-Multifactor-Authentication-Triggers.html
From start to here, all we did was logging in by using CAS Login screens, which is not all CAS can
do. Other applications need security by authenticating their access points. In CAS these type of
applications are called services
and there's some configurations to handle these services
.
By default, CAS tries to open the /etc/config/services directory, and loads the service definition
JSON files. These configurations looks like this:
HTTPSandIMAPS-10000001.json
{
"@class": "org.apereo.cas.services.CasRegisteredService",
"serviceId": "^(https|imaps)://.*",
"name": "HTTPS and IMAPS",
"id": 10000001,
"description": "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
"evaluationOrder": 10000
}
I wrote an example application(todo: add the link here) that uses CAS, and to register this as a CAS service i wrote the service registry file like this:
{
"@class": "org.apereo.cas.services.CasRegisteredService",
"id": 1000,
"name": "local",
"description": "For localhost only development services",
"logo": "https://mythemeshop.com/wp-content/uploads/2020/07/What-Exactly-is-Localhost.jpg",
"informationUrl": "http://localhost:8900/",
"serviceId": "http://localhost:8900/login/cas",
"evaluationOrder": 10,
"multifactorPolicy": {
"@class": "org.apereo.cas.services.DefaultRegisteredServiceMultifactorPolicy",
"multifactorAuthenticationProviders": [
"java.util.LinkedHashSet",
[
"mfa-simple"
]
],
"bypassEnabled": true,
"forceExecution": true
}
}
To summarize the json file structure briefly(from what i understand):
@class
is the type of service that CAS understandsserviceId
is typically a regex that the service will be calling from(Be careful and always double check before setting this value)name
andid
information should be used as both the json file name and identifying the service uniquely.description
is describing the service briefly.evaluationOrder
is for when multiple url expressions is used the same service, which will be used.
There's more properties in this JSON definition, you can enhance your knowledge about this topic here
An example defining MFA per service registry json config like this:
...
"multifactorPolicy" : {
"@class": "org.apereo.cas.services.DefaultRegisteredServiceMultifactorPolicy",
"multifactorAuthenticationProviders": ["java.util.LinkedHashSet", ["mfa-simple"]], // in our example we defined simple so we use its name here.
"bypassEnabled": true, // enable or disable the MFA
"forceExecution": true // force executing MFA flow
}
...
- multiFactorPolicy config
- bypassing configuration via this
- configuring for principle attributes via this
- configure what to do when MFA failure happens via this
Now, configuring these service registries in a json file is good for the start, but not very good for production use, because for every update maintainer will need to update their codebase/json folder, so, for that reason, we can move all of these configuration to database. Here are the necessary steps:
-
Add the necessary dependencies(for jpa use the
jpa-service-registry
):// JSON Service Registry: https://apereo.github.io/cas/6.6.x/services/JSON-Service-Management.html // implementation "org.apereo.cas:cas-server-support-json-service-registry" // JPA Service Registry: https://apereo.github.io/cas/6.6.x/services/JPA-Service-Management.html implementation "org.apereo.cas:cas-server-support-jpa-service-registry"
-
Use these settings:
cas.service-registry.core.init-from-json=false cas.service-registry.json.watcher-enabled=false # i changed default cas behaviour to look at the `/src/main/resources/config/services` dir when learning :) cas.service-registry.json.location=classpath:config/services cas.service-registry.jpa.driver-class=org.postgresql.Driver cas.service-registry.jpa.dialect=org.hibernate.dialect.PostgreSQL95Dialect cas.service-registry.jpa.url=jdbc:postgresql://localhost:5432/postgres cas.service-registry.jpa.user=postgres cas.service-registry.jpa.password=postgres cas.service-registry.jpa.ddl-auto=update # PT15S=15 seconds is set for dev purposes! cas.service-registry.schedule.repeat-interval=PT15S
and of course for the db config people :) :
INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.core.init-from-json', 'false'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.jpa.ddl-auto', 'update'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.jpa.dialect', 'org.hibernate.dialect.PostgreSQL95Dialect'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.jpa.driver-class', 'org.postgresql.Driver'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.jpa.password', 'postgres'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.jpa.url', 'jdbc:postgresql://localhost:5432/postgres'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.jpa.user', 'postgres'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.json.location', 'classpath:config/services'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.json.watcher-enabled', 'false'); INSERT INTO cas_settings_table (name, value) VALUES ('cas.service-registry.schedule.repeat-interval', 'PT15S');
-
After that, when you restart CAS, you'll see a brand new table, named
registered_services
. The json files will be converted in this table, which looks bad, but it's easy to do. I think devs did not do a full json->column conversion and you can almost copy the json text to thebody
column of the table, and set the other columnsevaluation_order
,evaluation_priority
,name
andservice_id
as the same in the json file. As conversion goes, my example client app's CAS Service Registry Row became like this:INSERT INTO public.registered_services ( id, body, evaluation_order, evaluation_priority, name, service_id) VALUES ( 1000, '{ "@class": "org.apereo.cas.services.CasRegisteredService", "description": "For localhost only development services", "logo": "https://mythemeshop.com/wp-content/uploads/2020/07/What-Exactly-is-Localhost.jpg", "informationUrl":"http://localhost:8900/", "serviceId": "http://localhost:8900/login/cas", "evaluationOrder": 10, "multifactorPolicy" : { "@class" : "org.apereo.cas.services.DefaultRegisteredServiceMultifactorPolicy", "multifactorAuthenticationProviders" : [ "java.util.LinkedHashSet", [ "mfa-simple" ] ], "bypassEnabled": true, "forceExecution": true }}', 10, 1000, 'local', 'http://localhost:8900/login/cas');
-
For the last step, i configured the
cas.service-registry.schedule.repeat-interval
property to make the CAS refresh its service registry list. For dev purposes, i did set this 15 seconds to see the effects quickly, for production this should be more reasonable value. -
To see its effects, I changed the
bypassEnabled
value on the body column, and waited for CAS to reload the service registries from db. You'll see the logs saying :<Loaded [1] service(s) from [JpaServiceRegistry].>)
and see that MFA became toggle-able by service.
-
Important to think about these configurations is that, for our application which have a web interface and callable rest apis, we could set up entries (with proper execution order and priority) that don't require MFA's for the rest endpoints, and MFA's for the web interfaces.
this page tells important enablings of service registry configurations.
-
enabled
: by adding this in your configuration, you basically disable your service to use CAS, and CAS starts to showApplication Not Authorized to Use CAS
error:"accessStrategy" : { "@class" : "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy", "enabled" : false }
-
ssoEnabled
: by adding this in your configuration, even if you logged in to CAS from another browser tab, you still need to re-login your application through CAS when entered:"accessStrategy" : { "@class" : "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy", "ssoEnabled" : false }
-
Others: there are other configurations, which should be good to know. Check out the documentation at:
https://apereo.github.io/cas/6.6.x/services/Configuring-Service-Access-Strategy.html
-
For advanced usage, resolving, accessing, transforming and releasing user attributes (metadata about logged in user, can be fetched anywhere) and redirecting these attribute datas to the client/service applications is another good-to-know topic.
First add these dependencies in the build.gradle:
implementation "org.apereo.cas:cas-server-support-gauth"
implementation "org.apereo.cas:cas-server-support-gauth-jpa"
After that, use these configurations:
cas.authn.mfa.gauth.core.issuer=CASIssuer
cas.authn.mfa.gauth.core.label=CASLabel
cas.authn.mfa.gauth.core.multiple-device-registration-enabled=false
cas.authn.mfa.gauth.core.trusted-device-enabled=false
cas.authn.mfa.gauth.core.window-size=3
cas.authn.mfa.gauth.crypto.encryption.key={SOME-KEY}
cas.authn.mfa.gauth.crypto.signing.key={SOME-KEY}
cas.authn.mfa.gauth.jpa.driver-class=org.postgresql.Driver
cas.authn.mfa.gauth.jpa.dialect=org.hibernate.dialect.PostgreSQL95Dialect
cas.authn.mfa.gauth.jpa.url=jdbc:postgresql://localhost:5432/postgres
cas.authn.mfa.gauth.jpa.user=postgres
cas.authn.mfa.gauth.jpa.password=postgres
cas.authn.mfa.gauth.jpa.ddl-auto=none
cas.authn.mfa.gauth.jpa.pool.maximum-lifetime=PT20M
cas.authn.mfa.gauth.jpa.idle-timeout=PT10M
cas.authn.mfa.gauth.jpa.pool.max-size=18
cas.authn.mfa.gauth.jpa.pool.min-size=6
cas.authn.mfa.gauth.jpa.pool.name=GAUTH-CONN-POOL
After that you need to add the necessary tables:
-- holds the user's gauth registration secret key
create table if not exists google_authenticator_registration_record
(
id bigint not null
primary key,
last_used_date_time varchar(255),
name varchar(255) not null,
registration_date timestamp,
secret_key varchar(2048) not null,
username varchar(255) not null,
validation_code integer not null,
constraint ukbfdvvfhi8n022v1yket0jajih
unique (username, name)
);
-- table that holds the used gauth tokens
create table if not exists google_authenticator_token
(
id bigint not null
primary key,
issued_date_time timestamp not null,
token integer not null,
user_id varchar(255) not null
);
-- holds the one time usage codes for gauth is unavailable.
create table if not exists scratch_codes
(
id bigint not null
constraint fkmneuc3ux4ho26jqepo36wfoj9
references public.google_authenticator_registration_record,
scratch_codes numeric not null
);
And with that, initial setup is complete.
For users to register the gauth:
First generate a static web page to show user is successfully registered their gauth. Add a service registry to force gauth authentication for the users to register with, for example(check out the multifactorAuthenticationProviders section):
{
"serviceId":"https://kayit-prp.mycompany.com.tr/activate-gauth",
"@class": "org.apereo.cas.services.CasRegisteredService",
"name": "Epiaş",
"description": "Tüm Google Authenticator Kayıt Servisi",
"id": 10000000,
"evaluationOrder": 99
// The default strategy allows one to configure a service with the following properties:
// https://apereo.github.io/cas/6.6.x/services/Service-Access-Strategy-Basic.html
"accessStrategy":
{
"@class": "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy",
// Flag to toggle whether the entry is active; a disabled entry produces behavior equivalent to a non-existent entry.
"enabled": true,
// Set to false to force users to authenticate to the service regardless of protocol flags (e.g. renew=true).
"ssoEnabled": true
}
"multifactorPolicy" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceMultifactorPolicy",
"multifactorAuthenticationProviders" : [ "java.util.LinkedHashSet", [ "mfa-gauth" ] ],
"bypassEnabled": true,
"forceExecution": true
}
}
After that, you can use this cas login link anywhere(on account preferences page or profile page your choice), with service query parameter value as this url: https://{CAS-URL}/cas/login?service=https://kayit-prp.mycompany.com.tr/activate-gauth
You can add a flag column(i.e: gauth_enabled col) on your users table, and after successfull registration, you can set this flag to true after the activation complete(make a request to another application/microservice, to set the users flag column to true) You can trigger gauth varius ways, as explained in the cas docs. For simplicity, i will mention one way with two different approaches. As mentioned, you can add a custom column on users table, check it on your authentication handler and if that column is true, trigger gauth by adding an attribute on the user principal attribute. Or you can write a code that in the auhtn handler, that if there's a row in the google_authenticator_registration_record table for the logged in user, add an attribute on the user principal that forces gauth authn.
For a user to disable/uninstall/reset their google authentication on their account, CAS does not provide a mechanism about this. But we know that if a user is registered to use google authenticator, there's a row on the google_authenticator_registration_record with username on it, and with this row's id, there's scratch codes on the scratch_codes table. so, we can write a custom service that deletes these rows and with that(if you use the user table flag column, don't forget to reset the flag value) , we can remove user from the gauth registration tables.
There's a problem arising with enableding MFA if you're also enabling CAS rest enpoints that generates and validates the TGT/ST. You need to set your authn handler that if the request comes from rest endpoints, do not trigger a MFA flow. Otherwise your rest calls will not succeed. For example in your authn handler:
// Eğer gelen istek REST'ten ise MFA'ya girmesini engellemek gerekmektedir.
boolean requestComesFromRest =
HttpRequestUtils.getHttpServletRequestFromRequestAttributes().getRequestURL().toString()
.endsWith("/cas/v1/tickets");
if (requestComesFromRest) {
attributes.put(MFA_BYPASS_ATTR_NAME, List.of("true"));
} else {
...
To change the default theme, use this setting below:
```
cas.theme.default-theme-name
```
To write a custom theme for CAS run this gradle command to setup the base structure for the overlay project:
```
./gradlew createTheme -Ptheme=new-theme-name
```
After that you'll see a structure like this written in the overlay project's readme file:
```
├── new-theme-name.properties
├── static
│ └── themes
│ └── new-theme-name
│ ├── css
│ │ └── cas.css
│ └── js
│ └── cas.js
└── templates
└── new-theme-name
└── fragments
```
new-theme-name.properties
file contains theme configs(enable sidebar, display hero banner, web page title text configs etc).static/themes/new-theme-name/
this folder contains static assets(images, fonts,css, js and other)templates/new-theme-name/
this folder contains the customization of the default CAS webflowThymeleaf
html files.
You can use the cas-server-support-thymleaf
dependency to see all the necessary html files to customize your furher needs.
Check out the screenshots below to understand the basic 'cheat sheet' of the CAS theming folder system.
What you need to do on your template is to copy the necessary files exactly the same structure on your templates/new-theme-name
folder.
Example settings:
spring.security.user.name=user
spring.security.user.password=somepassword
management.endpoint.info.enabled=true
management.endpoint.health.enabled=true
management.endpoint.health.show-details=when_authorized
management.endpoint.health.probes.enabled=true
management.endpoint.heapdump.enabled=true
management.endpoint.metrics.enabled=true
management.endpoint.scheduledtasks.enabled=true
management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true
management.endpoint.quartz.enabled=true
management.endpoint.refresh.enabled=true
cas.monitor.endpoints.endpoint.health.access=AUTHENTICATED
cas.monitor.endpoints.endpoint.heapdump.access=AUTHENTICATED
cas.monitor.endpoints.endpoint.metrics.access=AUTHENTICATED
cas.monitor.endpoints.endpoint.scheduledtasks.access=AUTHENTICATED
cas.monitor.endpoints.endpoint.prometheus.access=AUTHENTICATED
cas.monitor.endpoints.endpoint.quartz.access=AUTHENTICATED
cas.monitor.endpoints.endpoint.refresh.access=AUTHENTICATED
cas.monitor.endpoints.endpoint.info.access=AUTHENTICATED
management.endpoints.web.exposure.include=health,info,metrics,scheduledtasks,prometheus,quartz,heapdump,refresh
These settings setup, secure and let users to fiddle with actuator:
- To secure the endpoints with basic encryption, use these settings: spring.security.user.name and spring.security.user.password ( remember to use the encoder in the project, if a custom encoder is used)
- management.endpoint.* settings lets you enable/disable the actuator modules
- cas.monitor.endpoints.endpoint.* settings lets you setup security around your actuator modules
- management.endpoints.web.exposure.include settings lets you enable the module endpoints.
Some example actuator endpoints:
- Health: https://{CAS-URL}/cas/actuator/health
- Probes(k8s readiness için): https://{CAS-URL}/cas/actuator/health/readiness
- Probes(k8s liveness için): https://{CAS-URL}/cas/actuator/health/liveness
- Server Infos: https://{CAS-URL}/cas/actuator/info
- Metrics: https://{CAS-URL}/cas/actuator/metrics https://{CAS-URL}/cas/actuator/metrics/{requiredMetricName}
- Metrics for Prometheus/Graphana: https://{CAS-URL}/cas/actuator/prometheus
- Scheduled Jobs: https://{CAS-URL}/cas/actuator/scheduledtasks
- Quartz Scheduler: https://{CAS-URL}/cas/actuator/quartz https://{CAS-URL}/cas/actuator/quartz/{jobsOrTriggers}, https://{CAS-URL}/cas/actuator/quartz/{jobsOrTriggers}/{group}, https://{CAS-URL}/cas/actuator/quartz/{jobsOrTriggers}/{group}/{name},
- Context Refresh(Canlıda kullanılması tavsiye edilmez!): https://{CAS-URL}/cas/actuator/refresh
- Heap Dump Download: https://{CAS-URL}/cas/actuator/heapdump
For details, read up on these links: