chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# SQLPage CAS Client
|
||||
|
||||
This is a demonstration of how to implement a
|
||||
[Central Authentication Service (CAS)](https://apereo.github.io/cas/)
|
||||
client in a SQLPage application.
|
||||
|
||||
CAS is a single sign-on protocol that allows users to authenticate once and access multiple applications without having to log in again. It is primarily used in academic institutions and research organizations.
|
||||
|
||||
The protocol is based on a ticketing system, where the user logs in once and receives a ticket that can be used to access other applications without having to log in again. The ticket is validated by the CAS server, which then returns the user's information to the application.
|
||||
|
||||
This can be implemented in SQLPage with two `.sql` files:
|
||||
- [`login.sql`](login.sql): This just redirects the user to the CAS server's login page.
|
||||
- [`redirect_handler.sql`](redirect_handler.sql): This is the page where the CAS server redirects the user after login. It validates the ticket by sending a request to the CAS server and if the ticket is valid, it creates a session for the user in the SQLPage application.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use this CAS client in your own SQLPage application, you need to follow these steps:
|
||||
|
||||
1. Configure your CAS server to allow your SQLPage application to authenticate users. You will need to create a new service in the CAS server with the following information:
|
||||
- **Service URL**: The URL of your `redirect_handler.sql` page. For example, `https://example.com/redirect_handler.sql`.
|
||||
- **Service Name**: A descriptive name for your service. This can be anything you want.
|
||||
- **Service Type**: `CAS 3.0`.
|
||||
2. In your SQLPage application, set the following environment variable:
|
||||
- `CAS_ROOT_URL`: The URL of your CAS server. For example, `https://cas.example.com/cas`.
|
||||
|
||||
> Environment variables are global variables that can be made available to a program.
|
||||
> Using environment variables is a good practice for storing sensitive information and configuration settings,
|
||||
> so that they are not hard-coded in the code and are easy to change without modifying the code.
|
||||
> You can set an environment variable by running `export VARIABLE_NAME=value` in the terminal before starting your SQLPage application.
|
||||
> If you are running your application as a [systemd](https://en.wikipedia.org/wiki/Systemd) service,
|
||||
> you can set environment variables in the service configuration file, like this:
|
||||
> ```ini
|
||||
> [Service]
|
||||
> Environment="VARIABLE_NAME=value"
|
||||
> ```
|
||||
>
|
||||
> Alternatively, you could store the CAS root URL inside your database and replace
|
||||
> `sqlpage.environment_variable('CAS_ROOT_URL')` with `(SELECT cas_root_url FROM cas_config)`
|
||||
> in the `login.sql` and `redirect_handler.sql` files.
|
||||
|
||||
## CAS v3 Authentication Flow, step by step
|
||||
|
||||
### Login
|
||||
The client (usually a web browser) requests a resource from the application (client service).
|
||||
The application redirects the client to the CAS server with a service URL (the URL to which CAS should return the user after authentication).
|
||||
|
||||
### CAS Server Authentication
|
||||
The CAS server presents a login form.
|
||||
The user submits their credentials (username and password).
|
||||
Upon successful authentication, the CAS server redirects the user back to the application with a service ticket (ST) appended to the service URL.
|
||||
|
||||
### Service Ticket Validation
|
||||
The application receives the service ticket and makes a back-channel request to the CAS server to validate the service ticket.
|
||||
The CAS server responds with a success or failure. If successful, it also returns the user's attributes (such as username, email, etc.).
|
||||
|
||||
### User Session
|
||||
Upon successful validation, the application creates a session for the user and grants access to the requested resource.
|
||||
|
||||
### CAS v3 Pseudocode Implementation
|
||||
|
||||
```plaintext
|
||||
function authenticateUser(serviceUrl):
|
||||
if userNotLoggedIn():
|
||||
redirectToCasServer(serviceUrl)
|
||||
|
||||
function redirectToCasServer(serviceUrl):
|
||||
casLoginUrl = "https://cas.example.com/login?service=" + urlEncode(serviceUrl)
|
||||
redirect(casLoginUrl)
|
||||
|
||||
function casCallback(request):
|
||||
serviceTicket = request.getParameter("ticket")
|
||||
if serviceTicket is not None:
|
||||
validationUrl = "https://cas.example.com/serviceValidate?ticket=" + serviceTicket + "&service=" + urlEncode(serviceUrl)
|
||||
validationResponse = httpRequest(validationUrl)
|
||||
if validateResponse(validationResponse):
|
||||
userAttributes = extractAttributes(validationResponse)
|
||||
createUserSession(userAttributes)
|
||||
redirectToService(serviceUrl)
|
||||
else:
|
||||
authenticationFailed()
|
||||
else:
|
||||
error("Invalid service ticket.")
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This implementation uses the CAS 3.0 protocol. If your CAS server uses a different version of the protocol, you may need to modify the code (the ticket validation URL in redirect_handler.sql in particular).
|
||||
- This implementation does not handle single sign-out (SLO) or proxy tickets. These features can be added by extending the code in `redirect_handler.sql`.
|
||||
- This implementation assumes that the CAS server returns the user's email address in the `mail` attribute of the user's profile. If your CAS server uses a different attribute to store the email address, or does not return the email address at all, you will need to modify the code to extract the email address from the user's profile in `redirect_handler.sql`.
|
||||
@@ -0,0 +1,4 @@
|
||||
set user_email = (select email from user_sessions where session_id = sqlpage.cookie('session_id'));
|
||||
|
||||
select 'text' as component, 'You are not authenticated. [Log in](login.sql).' as contents_md where $user_email is null;
|
||||
select 'text' as component, 'Welcome, ' || $user_email || '. You can now [log out](logout.sql).' as contents_md where $user_email is not null;
|
||||
@@ -0,0 +1,5 @@
|
||||
select
|
||||
'redirect' as component,
|
||||
sqlpage.environment_variable('CAS_ROOT_URL')
|
||||
|| '/login?service=' || sqlpage.protocol() || '://' || sqlpage.header('host') || '/cas/redirect_handler.sql'
|
||||
as link;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- remove the session cookie
|
||||
select 'cookie' as component, 'session_id' as name, true as remove;
|
||||
-- remove the session from the database
|
||||
delete from user_sessions where session_id = sqlpage.cookie('session_id');
|
||||
-- log the user out of the cas server
|
||||
select
|
||||
'redirect' as component,
|
||||
sqlpage.environment_variable('CAS_ROOT_URL')
|
||||
|| '/logout?service=' || sqlpage.protocol() || '://' || sqlpage.header('host') || '/cas/redirect_handler.sql'
|
||||
as link;
|
||||
@@ -0,0 +1,41 @@
|
||||
-- The CAS server will redirect the user to this URL after the user has authenticated
|
||||
-- This page will be loaded with a ticket parameter in the query string, which we can read in the variable $ticket
|
||||
|
||||
-- If we don't have a ticket, go back to the CAS login page
|
||||
select 'redirect' as component, '/cas/' as link where $ticket is null;
|
||||
|
||||
-- We must then validate the ticket with the CAS server
|
||||
-- CAS v3 specifies the following URL for ticket validation (see https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-Specification.html#28-p3servicevalidate-cas-30)
|
||||
-- https://cas.example.org/p3/serviceValidate?ticket=ST-1856339-aA5Yuvrxzpv8Tau1cYQ7&service=http://myclient.example.org/myapp&format=JSON
|
||||
set ticket_url =
|
||||
sqlpage.environment_variable('CAS_ROOT_URL')
|
||||
|| '/p3/serviceValidate'
|
||||
|| '?ticket=' || sqlpage.url_encode($ticket)
|
||||
|| '&service=' || sqlpage.protocol() || '://' || sqlpage.header('host') || '/cas/redirect_handler.sql'
|
||||
|| '&format=JSON';
|
||||
|
||||
-- We must then make a request to the CAS server to validate the ticket
|
||||
set validation_response = sqlpage.fetch($ticket_url);
|
||||
|
||||
-- If the ticket is invalid, the CAS server will return a 200 OK response with a JSON object like this:
|
||||
-- { "serviceResponse": { "authenticationFailure": { "code": "INVALID_TICKET", "description": "..." } } }
|
||||
select 'redirect' as component,
|
||||
'/cas/login.sql' as link
|
||||
where $validation_response->'serviceResponse'->'authenticationFailure' is not null;
|
||||
|
||||
-- If the ticket is valid, the CAS server will return a 200 OK response with a JSON object like this:
|
||||
-- { "serviceResponse": { "authenticationSuccess": { "user": "username", "attributes": { "attribute": "value" } } } }
|
||||
-- You can use the following SQL code to inspect what the CAS server returned:
|
||||
-- select 'debug' as component, $validation_response;
|
||||
insert into user_sessions(session_id, user_id, email, oidc_token)
|
||||
values(
|
||||
sqlpage.random_string(32),
|
||||
$validation_response->'serviceResponse'->'authenticationSuccess'->>'user', -- The '->' operator extracts a JSON object field as JSON, while the '->>' operator extracts a JSON object field as text
|
||||
$validation_response->'serviceResponse'->'authenticationSuccess'->'attributes'->>'mail',
|
||||
$ticket
|
||||
)
|
||||
returning
|
||||
'cookie' as component, 'session_id' as name, session_id as value;
|
||||
|
||||
-- Redirect the user to the home page
|
||||
select 'redirect' as component, '/cas/' as link;
|
||||
Reference in New Issue
Block a user