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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:57 +08:00
commit d718c5a372
986 changed files with 74597 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
# SQLPage Single Sign-On demo
This project demonstrates how to implement
external authentication (Single Sign-On) in a SQLPage application using SQLPage's built-in OIDC support.
It demonstrates the implementation of two external authentication protocols:
- [OpenID Connect (OIDC)](https://openid.net/connect/)
- [Central Authentication Service (CAS)](https://apereo.github.io/cas/)
Depending on your use case, you can choose the appropriate protocol for your application.
## Screenshots
| Home Page | Login Page | User Info |
| --- | --- | --- |
| ![Home Page](assets/homepage.png) | ![Login Page](assets/login_page.png) | ![User Info](assets/logged_in.png) |
## Running the Demo
To run the demo, you just need docker and docker-compose installed on your machine. Then, run the following commands:
```bash
docker compose up --watch
```
This will start a Keycloak server and a SQLPage server. You can access the SQLPage application at http://localhost:8080.
The credentials for the demo are:
- **Username: `demo`**
- **Password: `demo`**
The credentials to the keycloak admin console accessible at http://localhost:8180 are `admin/admin`.
## CAS Client
This example also contains a CAS (Central Authentication Service) client
in the [`cas`](./cas) directory that demonstrates how to authenticate users using
the [CAS protocol](https://apereo.github.io/cas/) (version 3.0), which is mostly used in academic institutions.
## OIDC Client
OIDC is an authentication protocol that allows users to authenticate with a third-party identity provider and then access applications without having to log in again. This is useful for single sign-on (SSO) scenarios where users need to access multiple applications with a single set of credentials.
OIDC can be used to implement a "Login with Google" or "Login with Facebook" button in your application, since these providers support the OIDC protocol.
SQLPage has built-in support for OIDC authentication since v0.35.
This project demonstrates how to use it with the free and open source [Keycloak](https://www.keycloak.org/) OIDC provider.
You can easily replace Keycloak with another OIDC provider, such as Google, or your enterprise OIDC provider, by following the steps in the [Configuration](#configuration) section.
### Public and Protected Pages
By default, SQLPage's built-in OIDC support protects the entire website. However, you can configure it to have a mix of public and protected pages using the `oidc_protected_paths` option in your `sqlpage.json` file.
This allows you to create a public-facing area (like a homepage with a login button) and a separate protected area for authenticated users.
### Configuration
To use OIDC authentication in your own SQLPage application,
you need to configure it in your `sqlpage.json` file:
```json
{
"oidc_issuer_url": "https://your-keycloak-server/auth/realms/your-realm",
"oidc_client_id": "your-client-id",
"oidc_client_secret": "your-client-secret",
"host": "localhost:8080",
"oidc_protected_paths": ["/protected"]
}
```
The configuration parameters are:
- `oidc_issuer_url`: The base URL of your OIDC provider
- `oidc_client_id`: The ID that identifies your SQLPage application to the OIDC provider
- `oidc_client_secret`: The secret key for your SQLPage application
- `host`: The web address where your application is accessible
### Accessing User Information
Once OIDC is configured, you can access information about the authenticated user in your SQL files using these functions:
- `sqlpage.user_info(claim_name)`: Get a specific claim about the user (like name or email)
- `sqlpage.user_info_token()`: Get the entire identity token as JSON
Example:
```sql
select 'text' as component, 'Welcome, ' || sqlpage.user_info('name') || '!' as contents_md;
```
### Implementation Details
The demo includes several SQL files that demonstrate different aspects of OIDC integration:
1. `index.sql`: A public page that shows a welcome message and a login button. If the user is logged in, it displays their email and a link to the protected page.
2. `protected.sql`: A page that is only accessible to authenticated users. It displays the user's information.
3. `logout.sql`: Logs the user out by removing the authentication cookie and redirecting to the OIDC provider's logout page.
### Docker Setup
The demo uses Docker Compose to set up both SQLPage and Keycloak. The configuration includes:
- SQLPage service with:
- Volume mounts for the web root and configuration
- CAS configuration for optional CAS support
- Debug logging enabled
- Keycloak service with:
- Pre-configured realm and users
- Health checks to ensure it's ready before SQLPage starts
- Admin credentials for management
## References
- [SQLPage OIDC Documentation](https://sql-page.com/sso)
- [OpenID Connect](https://openid.net/connect/)
- [Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

@@ -0,0 +1,16 @@
# Single Sign-On with OpenID Connect
Welcome to this demonstration of how to implement *OpenID Connect* (OIDC) authentication in a SQLPage application.
[OIDC](https://openid.net/connect/) is a standard authentication protocol that allows users to authenticate with a third-party identity provider and then access applications without having to log in again. This is useful for single sign-on (SSO) scenarios where users need to access multiple applications with a single set of credentials. OIDC can be used to implement a "Login with Google" or "Login with Facebook" button in your application, since these providers support the OIDC protocol.
To test this application, click the login button on the top right corner of the page.
You will be redirected to the identity provider's login page, where you can login with the following credentials:
- **Username: `demo`**
- **Password: `demo`**
After logging in, you will be redirected back to this page, and you will see the user information that was returned by the identity provider.
This example also contains a CAS (Central Authentication Service) client that demonstrates how to authenticate users using the CAS protocol (version 3.0), which is mostly used in academic institutions. [Log in with CAS](/cas/login.sql)
![closed](/assets/closed.jpeg)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

+89
View File
@@ -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`.
+4
View File
@@ -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;
+5
View File
@@ -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;
+10
View File
@@ -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;
@@ -0,0 +1,45 @@
# This file lets you run the example with a single command: docker-compose up
# Download docker here: https://www.docker.com/products/docker-desktop
#
# This docker compose starts two services:
# 1. a SQLPage service that serves a simple page with a login button
# 2. a Keycloak service that acts as an OpenID Connect provider (manages users and authentication)
#
services:
sqlpage:
image: lovasoa/sqlpage:main # Use the latest development version of SQLPage
build:
context: ../..
volumes:
- .:/var/www
- ./sqlpage:/etc/sqlpage
environment:
# CAS (central authentication system) configuration
# (you can ignore this if you're only using OpenID Connect)
- CAS_ROOT_URL=http://localhost:8181/realms/sqlpage_demo/protocol/cas
# SQLPage configuration
- RUST_LOG=sqlpage=debug
network_mode: host
depends_on:
keycloak:
condition: service_healthy
develop:
watch:
- action: restart
path: ./sqlpage/
keycloak:
build:
context: .
dockerfile: keycloak.Dockerfile
volumes:
- ./keycloak-configuration.json:/opt/keycloak/data/import/realm.json
network_mode: host
healthcheck:
test: ["CMD-SHELL", "/opt/keycloak/bin/kcadm.sh get realms/sqlpage_demo --server http://localhost:8181 --realm master --user admin --password admin || exit 1"]
interval: 10s
timeout: 2s
retries: 5
start_period: 5s
@@ -0,0 +1,39 @@
# This file lets you run the example with a single command: docker compose up
# Download docker here: https://www.docker.com/products/docker-desktop
#
# This docker compose starts two services:
# 1. a SQLPage service that serves a simple page with a login button
# 2. a Keycloak service that acts as an OpenID Connect provider (manages users and authentication)
#
services:
sqlpage:
image: lovasoa/sqlpage:main
volumes:
- .:/var/www
- ./sqlpage:/etc/sqlpage
environment:
- CAS_ROOT_URL=http://localhost:8181/realms/sqlpage_demo/protocol/cas
- RUST_LOG=sqlpage=debug
network_mode: host
depends_on:
keycloak:
condition: service_healthy
develop:
watch:
- action: restart
path: ./sqlpage/
keycloak:
build:
context: .
dockerfile: keycloak.Dockerfile
volumes:
- ./keycloak-configuration.json:/opt/keycloak/data/import/realm.json
network_mode: host
healthcheck:
test: ["CMD-SHELL", "/opt/keycloak/bin/kcadm.sh get realms/sqlpage_demo --server http://localhost:8181 --realm master --user admin --password admin || exit 1"]
interval: 10s
timeout: 2s
retries: 5
start_period: 5s
+27
View File
@@ -0,0 +1,27 @@
SELECT 'shell' as component, 'My public app' as title;
set email = sqlpage.user_info('email');
-- For anonymous users
SELECT 'hero' as component,
'/protected' as link,
'Log in' as link_text,
'Welcome' as title,
'You are currently browsing as a guest. Log in to access the protected page.' as description,
'/protected/public/hello.jpeg' as image
WHERE $email IS NULL;
-- For logged-in users
SELECT 'text' as component,
'Welcome back, ' || sqlpage.user_info('name') || '!' as title,
'You are logged in as ' || sqlpage.user_info('email') ||
'. You can now access the [protected page](/protected) or [log out](' ||
-- Secure OIDC logout with CSRF protection
-- This redirects to /sqlpage/oidc_logout which:
-- 1. Verifies the CSRF token
-- 2. Removes the auth cookies
-- 3. Redirects to the OIDC provider's logout endpoint
-- 4. Finally redirects back to the homepage
sqlpage.oidc_logout_url()
|| ').' as contents_md
WHERE $email IS NOT NULL;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
FROM keycloak/keycloak:26.6.2
ADD --chown=1000:0 https://github.com/jacekkow/keycloak-protocol-cas/releases/download/26.6.2/keycloak-protocol-cas-26.6.2.jar \
/opt/keycloak/providers/keycloak-protocol-cas.jar
COPY ./keycloak-configuration.json /opt/keycloak/data/import/realm.json
CMD ["start-dev", "--import-realm", "--http-port", "8181"]
@@ -0,0 +1,20 @@
set user_email = sqlpage.user_info('email');
select 'shell' as component, 'My secure app' as title,
json_object(
'title', 'Log Out',
'link', sqlpage.oidc_logout_url()
) as menu_item;
select 'text' as component,
'You''re in, '|| sqlpage.user_info('name') || ' !' as title,
'You are logged in as *`' || $user_email || '`*.
You have access to this protected page.
![open door](/assets/welcome.jpeg)'
as contents_md;
select 'list' as component;
select key as title, value as description
from json_each(sqlpage.user_info_token());
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,8 @@
-- Table to store user sessions
CREATE TABLE user_sessions(
session_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
email TEXT NOT NULL,
oidc_token TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
@@ -0,0 +1,6 @@
oidc_issuer_url: http://localhost:8181/realms/sqlpage_demo # Given by keycloak as the "OpenID Endpoint Configuration" url.
oidc_client_id: sqlpage # configured in keycloak (http://localhost:8181/admin/master/console/#/sqlpage_demo/clients/a2bec2b8-f850-405e-9f26-59063ffa6f08/settings)
oidc_client_secret: qiawfnYrYzsmoaOZT28rRjPPRamfvrYr # For a safer setup, use environment variables to store this
oidc_protected_paths: ["/protected"] # Makes the website root is publicly accessible, requiring authentication only for the /protected path
oidc_public_paths: ["/protected/public"] # Adds an exception for the /protected/public path, which is publicly accessible too
oidc_additional_trusted_audiences: [] # For increased security, reject any token that has more than just the client ID in the "aud" claim
+50
View File
@@ -0,0 +1,50 @@
GET http://localhost:8080
HTTP 200
[Asserts]
header "Content-Type" contains "text/html"
body contains "Welcome"
body contains "Log in"
body htmlUnescape contains "/protected"
body not contains "An error occurred"
GET http://localhost:8080/protected
HTTP 303
[Captures]
login_url: header "Location"
[Asserts]
header "Location" contains "http://localhost:8181/realms/sqlpage_demo/protocol/openid-connect/auth"
GET {{login_url}}
HTTP 200
[Captures]
login_action: xpath "string(//form[@id='kc-form-login']/@action)"
[Asserts]
xpath "//form[@id='kc-form-login']" exists
xpath "//input[@name='username']" exists
xpath "//input[@name='password']" exists
body htmlUnescape contains "client_id=sqlpage"
POST {{login_action}}
[FormParams]
username: demo
password: demo
credentialId:
HTTP 302
[Captures]
callback_url: header "Location"
[Asserts]
header "Location" contains "http://localhost:8080/sqlpage/oidc_callback"
GET {{callback_url}}
HTTP 303
[Asserts]
header "Location" == "/protected"
GET http://localhost:8080/protected/
HTTP 200
[Asserts]
body htmlUnescape contains "You're in, John Smith"
body contains "demo@example.com"
body contains "You have access to this protected page."
body htmlUnescape contains "/assets/welcome.jpeg"
body not contains "An error occurred"