Hello, in this article I will discuss how to use the single sign-on authentication system, CAS server, in the .Net MVC architecture.
First of all, CAS (Central Authentication Service) is a central authentication system. It is an open-source project. It is used to allow users to access multiple applications or systems with a single sign-on. CAS is an authentication method known as Single Sign-On (SSO).
Since it is a Single Sign-On (SSO) method, when the user logs in with CAS, the same session information can be used by other applications. Once the user logs in, they can access other CAS-supported applications without re-authenticating, as long as they do not log out. It supports authentication mechanisms such as LDAP, SQL databases, and Active Directory. The CAS protocol can also be adapted to different authentication mechanisms.
CAS Protocol Working Principle
Client Request
A request is made to the project working with the CAS authentication structure by the user.
If the user has not yet authenticated, they are redirected to the CAS server.
Redirect
The user is redirected to the CAS server, and the CAS server asks the user to enter their credentials (username and password).
Authentication
The CAS server verifies the user’s credentials. If authentication is successful, the CAS server gives the user a “ticket.”
Ticket Process
This ticket is sent via an HTTP GET request to the specified endpoint on the server side through the query string. (The information about which endpoint to send it to is specified in the service parameter when redirecting to the CAS login page.)
On the server side, depending on the CAS server version (for CAS 5, it is the “serviceValidate” endpoint), a request is made to the endpoint to validate the ticket on the CAS server. If authentication is successful, the response includes user identity information and, if CAS server configuration settings are done, a session id value with the identityNo tag. This session id value can be stored for session information.
If successful, a Ticket-Granting Ticket (TGT) is created. TGT is stored as a cookie in the user’s browser. This maintains the user’s session. We’ll discuss TGT details shortly.
Once the authentication process is completed, the client is redirected to the index page.
Ticket-Granting Ticket (TGT) Principle
When a client authenticated with the CAS protocol wants to access another CAS-supported application, a new service ticket is requested from the CAS server using the TGT.
A new ticket is obtained from the CAS server using the TGT stored in the browser without passing the user through the authentication process again, and it is sent to the user. Then, for the application that is to be accessed, the server makes another request to the serviceValidate endpoint using this ticket to re-authenticate.
> Example of Authentication with CAS Protocol
Our example is based on a simple .Net Core MVC application. First, instead of setting up a CAS server, let’s run the CAS server through Docker using the open-source apereo/cas Docker image. The Apereo CAS image is version 5. The latest version, CAS 6, was released in 2021. The serviceValidate and logout endpoints are also available in version 5. When we start the compose file, it will automatically pull the image if it is not already available.
Compose file:
version: '3.3' services: cas: image: apereo/cas:latest container_name: cas ports: - "8443:8443" volumes: - C:\etc\cas\thekeystore:/etc/cas/thekeystore - C:\etc\cas\cas.properties:/etc/cas/config/cas.properties restart: unless-stopped
We are pulling the Apereo CAS image with the latest tag from Docker Hub. We set the container name as cas. We redirected port 8443 on the host machine to port 8443 inside the container, as Apereo CAS typically runs over HTTPS.
The CAS server requires the thekeystore and cas.properties files to function. Therefore, we mounted these two locally created files to the corresponding files inside the CAS container in the volume section. To ensure the container restarts automatically if it stops unexpectedly, we added unless-stopped to the compose file.
The Apereo CAS server needs the thekeystore file. This file is a Java Keystore (JKS) used to store SSL/TLS certificates and private keys. This file allows the CAS server to establish secure connections (HTTPS) and manage security certificates. Now, let’s create the thekeystore file. Using the command line (cmd), we execute:
`keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048`
This command generates a keystore file in the current directory. The keystore file created by this command is intended for use in applications like Apereo CAS, to secure SSL/TLS certificates and HTTPS connections.
Then, in the same directory where the keystore is located, run the following command in cmd:
keytool -importkeystore -srckeystore thekeystore -destkeystore "C:\Program Files\Java\jdk-11\lib\security\cacerts
Make sure to point to the correct path for the cacerts file, based on where the JDK is installed on your system. The password should be changeit, as specified in the command. Since Apereo CAS is a Java-based application, you need to have JDK installed on your computer.
Now, let’s look at the cas.properties file. The cas.properties file is a configuration file where we set the configuration settings for the Apereo CAS (Central Authentication Service) server. This file is used to configure the runtime behavior and features of the CAS server. It contains configuration keys that set up authentication, authorization, caching, protocols, and various other features of the CAS server.
- cas.propertiescas.server.prefix: Specifies the base path in the URL of the CAS server.
- cas.server.login-url: Specifies the CAS server login URL where users can log in. We set it to http://localhost:8443/cas/login. We handle these URLs in the code to automatically redirect the client side.
- **cas.server.logout-url: **The CAS server logout URL where users can log out.
- cas.authn.attribute-repository.attribute-names: Specifies additional attributes to be retrieved during authentication.
- cas.authn.accept.users: Specifies the accepted users and their passwords for authentication. Normally, this could be provided via a JSON file or similar, but since we are creating a demo project, we define a single account in the CAS server that we will start with Docker. The user casuser can authenticate with the password casTestPassword.
- cas.service-registry.core.init-from-json: This setting specifies whether the CAS server should initialize the service registry from a JSON file. Services in the CAS server can be thought of as projects where CAS authentication will be used. For example, if we want to use CAS authentication on an x website, we must register the default URL where that project will run in the CAS server, otherwise, we will receive a “Service not allowed” warning when trying to use the CAS server. The URL of the application to be used must be registered as a service in the CAS server.
- cas.service-registry.json.location: Specifies the location of the JSON file containing the service definitions.
- cas.logout.followServiceRedirects: Allows redirects to the logout URLs of registered services (applications) during the logout process from the CAS server. This ensures that when users log out from the CAS server, they are also automatically logged out from all registered applications.
The service-registry.json file should be located at the path specified in the cas.service-registry.json.location setting in the cas.properties file.
Since our demo project is a single application, we registered only it in the service-registry.json file. This application runs locally on port 7108. Thus, we can perform authentication operations for this application using the CAS server. If we do not register the application, the CAS server will not recognize it, and tickets will not be sent to the application, so we cannot perform authentication operations using the CAS protocol.
The directories for the cas.properties and thekeystore files must be specified correctly in the compose file since they are mounted to the corresponding files in the CAS server.
Now, let’s start our compose file.
docker-compose -f casCompose.yaml up
We have started the CAS server using Docker. Let’s check if our cas.properties and thekeystore files have been successfully mounted in the container named cas.
docker exec -it cas /bin/bash -c "cd /etc/cas && exec /bin/bash"
With the command, we accessed the /etc/cas directory inside the running container from cmd. We entered the container using bin/bash and navigated to the /etc/cas directory with cd.
As seen, our thekeystore file is present in the running container’s files.
We mounted the cas.properties file to the /etc/cas/config/cas.properties path, so let’s check that as well.
Our cas.properties file. Let’s read its contents using the cat command.
We have the cas.properties file on our local machine, and we have confirmed that the CAS server running through Docker has successfully mounted the specified files from our local system.
Now we can move on to the application demo project code.
The .Net MVC Core project is open.
Here are the LoginController.cs codes:
using Microsoft.AspNetCore.Mvc;
using CasExample.Common; namespace CasExample.Controllers
{ public class LoginController : Controller { private readonly IHttpContextAccessor _httpContextAccessor; private readonly string _defaultUrl; public LoginController(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; var httpRequest = _httpContextAccessor.HttpContext.Request; _defaultUrl = $"{httpRequest.Scheme}://{httpRequest.Host}{httpRequest.PathBase}"; } [HttpGet] public IActionResult Index() { if (Request.IsHttps) { var casServiceUrl = $"{_defaultUrl}/Login/ValidateTicketWithCasProtocol"; return Redirect($"https://localhost:8443/cas/login?service={casServiceUrl}"); } return View(); } [HttpGet] public IActionResult ValidateTicketWithCasProtocol(string ticket) { var casServiceUrl = $"{_defaultUrl}/Login/ValidateTicketWithCasProtocol"; var result = CasAuthentication.ValidateTicket(ticket, casServiceUrl); return result.IsValid ? RedirectToAction("Index", "Home") : RedirectToAction("Index", "Login"); } }
}
Authentication processes on the CAS server do not work for projects running over HTTP. However, it is possible to configure it to work over HTTP by modifying the source code. Since we are not modifying the source code, we added a condition to check Request.IsHttps. If the application is not running over HTTPS, it will proceed with the normal login system; otherwise, it will continue with CAS authentication.
The default URL considered for the application running on the server is the URL we registered in the cas.properties file. Therefore, the default URL for our demo project when launched via Visual Studio is https://localhost:7108/. As seen, we provide casServiceUrl as a service parameter. With this service parameter, we specify the endpoint to which the CAS server will send the ticket after logging in from the CAS login page.
After entering login information on the CAS login page, the ticket is not immediately sent to the ValidateTicketWithCasProtocol method. First, the information is validated on the CAS server. If correct, the ticket is sent to the ValidateTicketWithCasProtocol method via the query string.
Here is the code for CasAuthentication.cs:
using CasExample.Models;
using System.Diagnostics;
using System.Net;
using System.Xml; namespace CasExample.Common
{ public static class CasAuthentication { /// <summary> /// ValidateTicket for authentication with CAS protocol. /// </summary> /// <param name="ticket"></param> /// <param name="service"></param> /// <returns></returns> public static CasUserModel ValidateTicket(string ticket, string service) { var casValidateTicketUrl = "https://localhost:8443/cas/serviceValidate"; if (string.IsNullOrEmpty(ticket)) { return new CasUserModel { IsValid = false }; } var validateEndpoint = $"{casValidateTicketUrl}?service={service}&ticket={ticket}"; try { var handler = new HttpClientHandler(); using (HttpClient client = new HttpClient(handler)) { var response = client.GetAsync(validateEndpoint).Result; response.EnsureSuccessStatusCode(); var responseBody = response.Content.ReadAsStringAsync().Result; var casUser = ExtractCasUser(responseBody); if (string.IsNullOrEmpty(casUser.Username)) { return new CasUserModel { IsValid = false }; } casUser.Ticket = ticket; casUser.IsValid = true; return casUser; } } catch (HttpRequestException exception) { EventLog.WriteEntry("CasDemoApplication", exception.Message); return new CasUserModel { IsValid = false }; } } private static CasUserModel ExtractCasUser(string xmlResponse) { var document = new XmlDocument(); document.LoadXml(xmlResponse); var xmlNamespaceManager = new XmlNamespaceManager(document.NameTable); var casNamespaceNode = document.DocumentElement; var casNamespace = casNamespaceNode.GetNamespaceOfPrefix("cas"); xmlNamespaceManager.AddNamespace("cas", casNamespace); var userNode = document.SelectSingleNode("//cas:serviceResponse/cas:authenticationSuccess/cas:user", xmlNamespaceManager); var identityNoNode = document.SelectSingleNode("//cas:serviceResponse/cas:authenticationSuccess/cas:identityNo", xmlNamespaceManager); return new CasUserModel { Username = userNode?.InnerText, Id = identityNoNode?.InnerText }; } }
}
The ValidateTicket method is used to authenticate a user by providing a CAS ticket and the service URL. The expected service parameter here is the address of the method where the ticket is sent. For our demo project, this address is https://localhost:44365/Login/ValidateTicketWithCasProtocol.
We assigned the address of the serviceValidate endpoint to the casValidateTicketUrl variable, which is used to validate the user’s ticket on the CAS server. Since our server is running on port 8443 via Docker, we provide the serviceValidate endpoint address of the CAS application running on that port.
In reality, the ticket is not null; when CAS login occurs, the ticket is sent to the ValidateTicketWithCasProtocol method specified in the service parameter. However, we still added a validation step. We set the validateEndpoint variable with the service and ticket parameters to determine the validateUrl. The serviceValidate endpoint requires both the ticket and service parameters as mandatory. The pgtr and renew parameters are optional. The ticket parameter contains the user’s ticket, and the service parameter is the endpoint address where the ticket is sent. The service parameter could also be encoded. We send an HTTP GET request to the validateEndpoint address to receive the user authentication response. If the user is successfully authenticated:
We receive a response as follows if successful:
If the authentication fails:
we receive an XML response.
In the ExtractCasUser method, we handle the returned XML response. If authentication is successful, we retrieve the username and the session ID as identityNo. If authentication fails (i.e., if we cannot obtain the username), we handle this scenario appropriately. Different methods can be applied here; for example, checking if the XML response contains an authenticationFailure tag or if the code is INVALID_TICKET, etc.
If authentication fails, we set the IsValid property of the CasUserModel model to false in the ValidateTicket method. Although this property defaults to false since it’s a boolean variable, we explicitly set it to make the scenario clearer for developers.
> Let’s start the application:
https://localhost:8443/cas/login?service=https://localhost:44365/Login/ValidateTicketWithCasProtocol
If the application is running over HTTPS, the application starts from the URL mentioned above. The service parameter is for the address of the endpoint to which the ticket will be sent after logging in from the CAS login page. When the application is launched, the initial page is present. We specified the CAS login URL in the cas.properties file. Additionally, we specified the default URL of the application to be used as the service parameter in both the CAS server configuration files cas.properties and service-registry.json. If we had not specified these, we would not see the following view and would receive a “Application Not Authorized to Use CAS” error from the CAS server because the application was not registered as a service.
When we start our project, if we have not registered our demo application as a service, we would encounter the error shown above when redirected to the CAS login page. However, since we have registered our demo application as a service, we will encounter the screen shown below.
After entering the correct credentials, we logged in. The ticket parameter was sent to the ValidateTicketWithCasProtocol method specified in the service. We validated the ticket by sending an HTTP GET request to the serviceValidate endpoint of the CAS application. The response is as follows:
As seen, authenticationSuccess and user information were returned. The attributes present here may vary depending on the CAS server configuration settings.
As seen above, in our demo project, we used the username value from the XML response returned by the CAS server API endpoint to create a model. This demonstrates that authentication for the user “casuser” with the CAS protocol on the CAS server is complete. Since we have received the authenticationSuccess response, we can now redirect the client to the index page.
If the project has other authentication systems in place, we can automatically authenticate the user through those systems and handle session, authorization, etc. This shows that the CAS authentication mechanism discussed at the beginning of this article is compatible with various authentication mechanisms already used in the project.
The ticket is sent specifically to the service (the URL of the project where CAS authentication will be used) registered in the cas.properties file by the CAS server. The ticket has an expiration time. If an attempt is made to authenticate the user with an expired ticket using the serviceValidate endpoint, we would receive an authenticationFailure response with the INVALID_TICKET code, even if the login credentials are correct.
For example, let’s try to authenticate a user with an expired ticket by sending it to the serviceValidate endpoint.
When we sent a request to the serviceValidate endpoint, we received an authenticationFailure response because the ticket had expired. Thus, the client could not authenticate and should be redirected to the CAS login page again.
If the application has a session timeout duration, it can be set to match the ticket duration configured on the CAS server. When the ticket expires and returns an authenticationFailure response, the application can automatically end the session and redirect the user back to the login screen.
References
https://hub.docker.com/r/apereo/cas/
https://github.com/apereo/cas-overlay-template/
https://medium.com/swlh/install-cas-server-with-db-authentication-8ff52234f52
apereo documents