Apereo CAS - Simple Multifactor Authentication

Posted by Misagh Moayyed on July 29, 2023 · 14 mins read ·
Content Unavailable
Your browser is blocking content on this website. Please check your browser settings and try again.

Overview

The Apereo CAS portfolio presents support for an impressive number of multifactor authentication providers out of the box. One such option is to remove dependencies to an external vendor integration and let the CAS server itself become a provider. This is a rather simplified multifactor authentication solution where after primary authentication, CAS begins to issue time-sensitive tokens to end-users via pre-defined communication channels such as email or text messages.

In this tutorial, we are going to briefly review the steps required to turn on Simple Multifactor Authentication.

Our starting position is based on:

Configuration

Prepare your CAS overlay with the correct auto-configuration module. Next, we will first instruct CAS to trigger simple mfa for all requests and applications:

cas.authn.mfa.triggers.global.global-provider-id=mfa-simple
Why All?
This is done to keep things simple for purposes of this blog post. You are of course welcome to choose any multifactor trigger that works best for you. It should work all the same.

Then, let’s choose email as our preferred communication mechanism for sharing tokens. To do so, let’s teach CAS about our email server:

spring.mail.host=localhost
spring.mail.port=25000
spring.mail.testConnection=true
Why Spring?
Settings and properties that are directly controlled by the CAS platform always begin with the prefix cas. All other settings are controlled and provided to CAS via other underlying frameworks and may have their schemas, syntax and validation rules. In this case, the presence of the above settings will instruct Spring Boot to create the required components internally for sending an email and make them available to CAS.

Then, let’s instruct CAS to share tokens via email:

cas.authn.attribute-repository.stub.attributes.mail=misagh@somewhere.com

cas.authn.mfa.simple.mail.from=wolverine@example.org
cas.authn.mfa.simple.mail.subject=CAS MFA Token
cas.authn.mfa.simple.mail.text=Hello! Your requested CAS token is ${token}
cas.authn.mfa.simple.mail.attribute-name=mail

cas.authn.mfa.simple.time-to-kill-in-seconds=30

A few things to note:

  • The %s acts as a placeholder for the generated token in the body of the message.
  • The expiration of the generated token is set to 30 seconds.
  • User email addresses are expected to be found under a mail attribute. In this example, this is done as a static attribute via the stub attribute repository configuration.

At this point, we should be ready to test.

Test

Once you build and bring up the deployment, let’s simulate an authentication attempt from a made-up application, https://app.example.org, by submitting the following request:

https://sso.example.org/cas/login?service=https://app.example.org

After authentication, you should see the following entries in the CAS log:

- <Added ticket [CASMFA-004291] to registry.>
- <Successfully submitted token via SMS and/or email to [misagh]>

The screen should ask for the token:

If you check your email, you should have received a token:

Submit the generated token CASMFA-004291 back to CAS and you should be allowed to proceed.

Email Selection

In our present setup, user email addresses are expected to be found under a mail attribute. While in reality, email values can be produced from any attribute source, this is done as a static attribute via the stub attribute repository configuration in our example setup:

cas.authn.attribute-repository.stub.attributes.mail=misagh@somewhere.com

Let’s slightly alter the attribute repository configuration to simulate a scenario where the user might have multiple values for the mail attribute:

cas.authn.attribute-repository.stub.attributes.mail=misagh@fawnoos.com,misagh2@fawnoos.com

Now when you exercise the flow, you will be asked to select the email address(s) to which the token should be sent:

Token Management

Token generation management can be handled in several ways. You can let CAS issue and manage tokens, or of course you could outsource that task to an external REST API. Let’s review both options quickly.

Default

To control the length of the generated token, use:

cas.authn.mfa.simple.token.core.token-length=6

You can take direct control of the token generation logic by designing your own configuration component with the following bean in place:

@Bean
public UniqueTicketIdGenerator casSimpleMultifactorAuthenticationUniqueTicketIdGenerator() {
    return new MyUniqueTicketIdGenerator();
}

Implement the MyUniqueTicketIdGenerator as you see fit or better yet, use the GroovyUniqueTicketIdGenerator instead to hand off the implementation to an external Groovy script with the following body:

def run(Object... args) {
    def prefix = args[0]
    def logger = args[1]
    return ...
}

You can also control the token validation logic by supplying the following bean that should respond to credentials of type CasSimpleMultifactorTokenCredential:

@Bean
public AuthenticationHandler casSimpleMultifactorAuthenticationHandler() {
    return ...
}

Customizing the authentication handler as the parent component might be slightly overkill. Instead, you may opt into supplying your own multifactor authentication service directly:

@Bean
public CasSimpleMultifactorAuthenticationService casSimpleMultifactorAuthenticationService() {
    return ...
}

REST API

The operations that deal with token generation and management can also be entirely outsourced to an external REST API. This comes in handy in senarios where you might have your own token generation service that wants own the managament and maintenance tasks when it comes to tokens. To handle this, you need to instruct CAS to contact your REST API endpoint:

cas.authn.mfa.simple.token.rest.url=http://api.example.org/mfa
cas.authn.mfa.simple.token.rest.basic-auth-username=api-username
cas.authn.mfa.simple.token.rest.basic-auth-password=1234567890

The API needs to support the following endpoints and payloads:

const principal = {
    "@class": "org.apereo.cas.authentication.principal.SimplePrincipal",
    "id": "casuser"
};
let payload = {
    "/new": {
        "get": 'generated-token'
    },
    "/": {
        "post": 'generated-token'
    },
    "/:id": {
        "get": principal
    }
};

The /new endpoint is responsible for generating tokens via GET requests. The root path is tasked to actually save and store the generated token via a POST and finally, the /{id} endpoint should validate the given token and produce the corresponding payload once the token is deemed valid.

Rate Limiting

By default, if the user keeps asking for tokens without actually using them, CAS will continue to send the same unused token to the user so long as the token continues to be valid. This provides a useful defensive measure against too many token requests, but you may want to control the rate of the submitted requests as well. This can be done by forming a rate-limiting configuration plan to limit the number of requests:

cas.authn.mfa.simple.bucket4j.enabled=true
cas.authn.mfa.simple.bucket4j.blocking=true

cas.authn.mfa.simple.bucket4j.bandwidth[0].capacity=100
cas.authn.mfa.simple.bucket4j.bandwidth[0].duration=PT1M

cas.authn.mfa.simple.bucket4j.bandwidth[1].capacity=60
cas.authn.mfa.simple.bucket4j.bandwidth[1].duration=PT5S

We are allowed to define multiple rate-limting plans. For example, the above configuration handles requests in blocking mode, which means the client request will be blocked until resources eventually become available. Our plan allows for 100 requests per minute, but not no more often than 60 tokens per 5 seconds.

Specifying multiple bandwidths and configuration plans may be a very useful technique in protecting against clever attacks. For example, Bucket4j documentation provides the following scenario:

Suppose that you start with a limit of 10000 tokens / per 1 hour per user. A malicious attacker may send 9999 request within 10 seconds. This would correspond to 100 request per second which could seriously impact the system. A skilled attacker could stop at 9999 request per hour, and repeat every hour, which would make this attack impossible to detect because the limit would not be reached.

For additional details, please visit the Bucket4j reference documentation.

Need Help?

If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to send us a note and ask about consulting and support services.

So…

I hope this review was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. Please feel free to engage and contribute as best as you can.

Happy Coding,

Misagh Moayyed