Skip to content

Commit 9aa89fe

Browse files
cursoragentlovasoa
andcommitted
feat: Add OIDC logout functionality
This commit introduces the `oidc_logout_url` function, allowing users to securely log out of OIDC-authenticated applications. It includes CSRF protection and handles redirection to the OIDC provider's logout endpoint. Co-authored-by: contact <[email protected]>
1 parent ce8539e commit 9aa89fe

File tree

4 files changed

+275
-78
lines changed

4 files changed

+275
-78
lines changed

examples/official-site/sqlpage/migrations/61_oidc_functions.sql

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,126 @@ VALUES
169169
'claim',
170170
'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.',
171171
'TEXT'
172+
);
173+
174+
INSERT INTO
175+
sqlpage_functions (
176+
"name",
177+
"introduced_in_version",
178+
"icon",
179+
"description_md"
180+
)
181+
VALUES
182+
(
183+
'oidc_logout_url',
184+
'0.41.0',
185+
'logout',
186+
'# Secure OIDC Logout
187+
188+
The `sqlpage.oidc_logout_url` function generates a secure logout URL for users authenticated via [OIDC Single Sign-On](/sso).
189+
190+
When a user visits this URL, SQLPage will:
191+
1. Remove the authentication cookie
192+
2. Redirect the user to the OIDC provider''s logout endpoint (if available)
193+
3. Finally redirect back to the specified `redirect_uri`
194+
195+
## Security Features
196+
197+
This function provides protection against **Cross-Site Request Forgery (CSRF)** attacks:
198+
- The generated URL contains a cryptographically signed token
199+
- The token includes a timestamp and expires after 10 minutes
200+
- The token is signed using your OIDC client secret
201+
- Only relative URLs (starting with `/`) are allowed as redirect targets
202+
203+
This means that malicious websites cannot trick your users into logging out by simply including an image or link to your logout URL.
204+
205+
## How to Use
206+
207+
```sql
208+
select ''button'' as component;
209+
select
210+
''Logout'' as title,
211+
sqlpage.oidc_logout_url(''/'') as link,
212+
''logout'' as icon,
213+
''red'' as outline;
214+
```
215+
216+
This creates a logout button that, when clicked:
217+
1. Logs the user out of your SQLPage application
218+
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))
219+
3. Redirects the user back to your homepage (`/`)
220+
221+
## Examples
222+
223+
### Logout Button in Navigation
224+
225+
```sql
226+
select ''shell'' as component,
227+
''My App'' as title,
228+
json_array(
229+
json_object(
230+
''title'', ''Logout'',
231+
''link'', sqlpage.oidc_logout_url(''/''),
232+
''icon'', ''logout''
233+
)
234+
) as menu_item;
235+
```
236+
237+
### Logout with Return to Current Page
238+
239+
```sql
240+
select ''button'' as component;
241+
select
242+
''Sign Out'' as title,
243+
sqlpage.oidc_logout_url(sqlpage.path()) as link;
244+
```
245+
246+
### Conditional Logout Link
247+
248+
```sql
249+
select ''button'' as component
250+
where sqlpage.user_info(''sub'') is not null;
251+
select
252+
''Logout '' || sqlpage.user_info(''name'') as title,
253+
sqlpage.oidc_logout_url(''/'') as link
254+
where sqlpage.user_info(''sub'') is not null;
255+
```
256+
257+
## Requirements
258+
259+
- OIDC must be [configured](/sso) in your `sqlpage.json`
260+
- If OIDC is not configured, this function returns NULL
261+
- The `redirect_uri` must be a relative path starting with `/`
262+
263+
## Provider Support
264+
265+
The logout behavior depends on your OIDC provider:
266+
267+
| Provider | Full Logout Support |
268+
|----------|-------------------|
269+
| Keycloak | ✅ Yes |
270+
| Auth0 | ✅ Yes |
271+
| Google | ❌ No (local logout only) |
272+
| Azure AD | ✅ Yes |
273+
| Okta | ✅ Yes |
274+
275+
When the provider doesn''t support RP-Initiated Logout, SQLPage will still remove the local authentication cookie and redirect to your specified URI.
276+
'
277+
);
278+
279+
INSERT INTO
280+
sqlpage_function_parameters (
281+
"function",
282+
"index",
283+
"name",
284+
"description_md",
285+
"type"
286+
)
287+
VALUES
288+
(
289+
'oidc_logout_url',
290+
1,
291+
'redirect_uri',
292+
'The relative URL path where the user should be redirected after logout. Must start with `/`. Defaults to `/` if not provided.',
293+
'TEXT'
172294
);

examples/single sign on/logout.sql

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
-- remove the session cookie
2-
select
3-
'cookie' as component,
4-
'sqlpage_auth' as name,
5-
true as remove;
1+
-- Secure OIDC logout with CSRF protection
2+
-- This redirects to /sqlpage/oidc_logout which:
3+
-- 1. Verifies the CSRF token
4+
-- 2. Removes the auth cookies
5+
-- 3. Redirects to the OIDC provider's logout endpoint
6+
-- 4. Finally redirects back to the homepage
67

78
select
89
'redirect' as component,
9-
sqlpage.link('http://localhost:8181/realms/sqlpage_demo/protocol/openid-connect/logout', json_object(
10-
'post_logout_redirect_uri', 'http://localhost:8080/',
11-
'id_token_hint', sqlpage.cookie('sqlpage_auth')
12-
)) as link;
10+
sqlpage.oidc_logout_url('/') as link;

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ super::function_definition_macro::sqlpage_functions! {
3535
headers((&RequestInfo));
3636
hmac(data: Cow<str>, key: Cow<str>, algorithm: Option<Cow<str>>);
3737

38+
oidc_logout_url((&RequestInfo), redirect_uri: Option<Cow<str>>);
39+
3840
user_info_token((&RequestInfo));
3941
link(file: Cow<str>, parameters: Option<Cow<str>>, hash: Option<Cow<str>>);
4042

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

863+
async fn oidc_logout_url<'a>(
864+
request: &'a RequestInfo,
865+
redirect_uri: Option<Cow<'a, str>>,
866+
) -> anyhow::Result<Option<String>> {
867+
let Some(oidc_state) = &request.app_state.oidc_state else {
868+
return Ok(None);
869+
};
870+
871+
let redirect_uri = redirect_uri.as_deref().unwrap_or("/");
872+
873+
if !redirect_uri.starts_with('/') || redirect_uri.starts_with("//") {
874+
anyhow::bail!(
875+
"oidc_logout_url: redirect_uri must be a relative path starting with '/'. Got: {redirect_uri}"
876+
);
877+
}
878+
879+
let logout_url = crate::webserver::oidc::create_logout_url(
880+
redirect_uri,
881+
&request.app_state.config.site_prefix,
882+
&oidc_state.config.client_secret,
883+
);
884+
885+
Ok(Some(logout_url))
886+
}
887+
861888
/// Returns a specific claim from the ID token.
862889
async fn user_info<'a>(
863890
request: &'a RequestInfo,

0 commit comments

Comments
 (0)