Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions examples/official-site/sqlpage/migrations/61_oidc_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,126 @@ VALUES
'claim',
'The name of the user information to retrieve. Common values include ''name'', ''email'', ''picture'', ''sub'', ''preferred_username'', ''given_name'', and ''family_name''. The exact values available depend on your OIDC provider and configuration.',
'TEXT'
);

INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'oidc_logout_url',
'0.41.0',
'logout',
'# Secure OIDC Logout

The `sqlpage.oidc_logout_url` function generates a secure logout URL for users authenticated via [OIDC Single Sign-On](/sso).

When a user visits this URL, SQLPage will:
1. Remove the authentication cookie
2. Redirect the user to the OIDC provider''s logout endpoint (if available)
3. Finally redirect back to the specified `redirect_uri`

## Security Features

This function provides protection against **Cross-Site Request Forgery (CSRF)** attacks:
- The generated URL contains a cryptographically signed token
- The token includes a timestamp and expires after 10 minutes
- The token is signed using your OIDC client secret
- Only relative URLs (starting with `/`) are allowed as redirect targets

This means that malicious websites cannot trick your users into logging out by simply including an image or link to your logout URL.

## How to Use

```sql
select ''button'' as component;
select
''Logout'' as title,
sqlpage.oidc_logout_url(''/'') as link,
''logout'' as icon,
''red'' as outline;
```

This creates a logout button that, when clicked:
1. Logs the user out of your SQLPage application
2. Logs the user out of the OIDC provider (if the provider supports [RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html))
3. Redirects the user back to your homepage (`/`)

## Examples

### Logout Button in Navigation

```sql
select ''shell'' as component,
''My App'' as title,
json_array(
json_object(
''title'', ''Logout'',
''link'', sqlpage.oidc_logout_url(''/''),
''icon'', ''logout''
)
) as menu_item;
```

### Logout with Return to Current Page

```sql
select ''button'' as component;
select
''Sign Out'' as title,
sqlpage.oidc_logout_url(sqlpage.path()) as link;
```

### Conditional Logout Link

```sql
select ''button'' as component
where sqlpage.user_info(''sub'') is not null;
select
''Logout '' || sqlpage.user_info(''name'') as title,
sqlpage.oidc_logout_url(''/'') as link
where sqlpage.user_info(''sub'') is not null;
```

## Requirements

- OIDC must be [configured](/sso) in your `sqlpage.json`
- If OIDC is not configured, this function returns NULL
- The `redirect_uri` must be a relative path starting with `/`

## Provider Support

The logout behavior depends on your OIDC provider:

| Provider | Full Logout Support |
|----------|-------------------|
| Keycloak | ✅ Yes |
| Auth0 | ✅ Yes |
| Google | ❌ No (local logout only) |
| Azure AD | ✅ Yes |
| Okta | ✅ Yes |

When the provider doesn''t support RP-Initiated Logout, SQLPage will still remove the local authentication cookie and redirect to your specified URI.
'
);

INSERT INTO
sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES
(
'oidc_logout_url',
1,
'redirect_uri',
'The relative URL path where the user should be redirected after logout. Must start with `/`. Defaults to `/` if not provided.',
'TEXT'
);
16 changes: 7 additions & 9 deletions examples/single sign on/logout.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
-- remove the session cookie
select
'cookie' as component,
'sqlpage_auth' as name,
true as remove;
-- 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

select
'redirect' as component,
sqlpage.link('http://localhost:8181/realms/sqlpage_demo/protocol/openid-connect/logout', json_object(
'post_logout_redirect_uri', 'http://localhost:8080/',
'id_token_hint', sqlpage.cookie('sqlpage_auth')
)) as link;
sqlpage.oidc_logout_url('/') as link;
27 changes: 27 additions & 0 deletions src/webserver/database/sqlpage_functions/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ super::function_definition_macro::sqlpage_functions! {
headers((&RequestInfo));
hmac(data: Cow<str>, key: Cow<str>, algorithm: Option<Cow<str>>);

oidc_logout_url((&RequestInfo), redirect_uri: Option<Cow<str>>);

user_info_token((&RequestInfo));
link(file: Cow<str>, parameters: Option<Cow<str>>, hash: Option<Cow<str>>);

Expand Down Expand Up @@ -858,6 +860,31 @@ async fn user_info_token(request: &RequestInfo) -> anyhow::Result<Option<String>
Ok(Some(serde_json::to_string(claims)?))
}

async fn oidc_logout_url<'a>(
request: &'a RequestInfo,
redirect_uri: Option<Cow<'a, str>>,
) -> anyhow::Result<Option<String>> {
let Some(oidc_state) = &request.app_state.oidc_state else {
return Ok(None);
};

let redirect_uri = redirect_uri.as_deref().unwrap_or("/");

if !redirect_uri.starts_with('/') || redirect_uri.starts_with("//") {
anyhow::bail!(
"oidc_logout_url: redirect_uri must be a relative path starting with '/'. Got: {redirect_uri}"
);
}

let logout_url = crate::webserver::oidc::create_logout_url(
redirect_uri,
&request.app_state.config.site_prefix,
&oidc_state.config.client_secret,
);

Ok(Some(logout_url))
}

/// Returns a specific claim from the ID token.
async fn user_info<'a>(
request: &'a RequestInfo,
Expand Down
Loading