10BC0 Allow configuring the logout page (#5514) · holoviz/panel@a640d70 · GitHub
[go: up one dir, main page]

Skip to content

Commit a640d70

Browse files
authored
Allow configuring the logout page (#5514)
1 parent 4496012 commit a640d70

File tree

10 files changed

+274
-16
lines changed

10 files changed

+274
-16
lines changed
44.8 KB
Loading

doc/how_to/authentication/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ Discover how to configure OAuth from the commandline.
3636
A list of OAuth providers and how to configure them.
3737
:::
3838

39+
:::{grid-item-card} {octicon}`file;2.5em;sd-mr-1 sd-animate-grow50` Templates
40+
:link: templates
41+
:link-type: doc
42+
43+
Discover how to configure error and logout templates to match the design of your application.
44+
:::
45+
3946
:::{grid-item-card} {octicon}`person;2.5em;sd-mr-1 sd-animate-grow50` User Information
4047
:link: user_info
4148
:link-type: doc
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Authentication Templates
2+
3+
Authentication flows have multiple stages and in certain scenarios Panel has to serve pages that provide you with information when authentication has not succeeded or allow you the option of logging back into the application. Panel includes default templates for these cases, specifically it has:
4+
5+
- Error Template: The template that is displayed if the authentication errored for any reason, e.g. the user was not authorized to access the application.
6+
- Logout Template: The template served when a user hits the `/logout` endpoint.
7+
8+
Both templates use Jinja2 syntax to render certain variables.
9+
10+
## Error Template
11+
12+
The error template is used to display errors when authentication errored out. This can occur for any number of reasons, e.g. authentication is misconfigured or the user is not authorized to access the application.
13+
14+
The template can be configured on the commandline using the `--oauth-error-template` option. The provided value must be a valid HTML file relative to the current working directory or an absolute path:
15+
16+
```bash
17+
panel serve app.py ... --oauth-error-template error.html
18+
```
19+
20+
When using `panel.serve` to dynamically serve the application you can configure the template with the `oauth_error_template` argument:
21+
22+
```python
23+
pn.serve(..., oauth_error_template='error.html')
24+
```
25+
26+
The error template may be a completely static file or use Jinja2 templating syntax with the following variables:
27+
28+
`npm_cdn`
29+
: The CDN to load NPM resources from.
30+
31+
`title`
32+
: The HTML page title.
33+
34+
`error_type`
35+
: The type of error being raised.
36+
37+
`error`
38+
: A short description of the error.
39+
40+
`error_msg`
41+
: The full error message providing additional context.
42+
43+
## Logout Template
44+
45+
The logout template is rendered when a user hits the `/logout` endpoint with authentication enabled. It is meant to be a simple static page that confirms that the logout process was completed and optionally allows the user to log back in.
46+
47+
The default template looks like this:
48+
49+
![Basic Auth Login Form](../../_static/images/logout_template.png)
50+
51+
The template can be configured on the commandline using the `--logout-template` option. The provided value must be a valid HTML file relative to the current working directory or an absolute path:
52+
53+
```bash
54+
panel serve app.py ... --logout-template logout.html
55+
```
56+
57+
When using `panel.serve` to dynamically serve the application you can configure the template with the `logout_template` argument:
58+
59+
```python
60+
pn.serve(..., logout_template=logout.html')
61+
```
62+
63+
The template may be a completely static HTML file or use Jinja2 templating syntax with the following variables being provided:
64+
65+
`PANEL_CDN`
66+
: The URL of the CDN (or local path) that Panel resources will be loaded from.

panel/_templates/logout.html

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<!-- Uses the template from https://github.com/bokeh/bokeh/tree/branch-3.2/examples/server/app/server_auth -->
2+
<!DOCTYPE html>
3+
<html>
4+
<head>
5+
<meta charset="utf-8">
6+
<meta content="width=device-width, initial-scale=1.0" name="viewport">
7+
<meta content="en" name="docsearch:language">
8+
<title>Panel App | Logout</title>
9+
<link rel="icon" type="image/x-icon" href="{{ PANEL_CDN }}images/favicon.ico">
10+
<style>
11+
* {
12+
box-sizing: border-box;
13+
margin:0;
14+
padding: 0;
15+
}
16+
html {
17+
height: 100%;
18+
}
19+
body {
20+
font-family: 'Segoe UI', sans-serif;;
21+
font-size: 1em;
22+
height: 100%;
23+
line-height: 1.6;
24+
}
25+
p {
26+
padding-bottom: 5px;
27+
}
28+
.wrap {
29+
align-items: center;
30+
background: #fafafa;
31+
display: flex;
32+
height: 100%;
33+
justify-content: center;
34+
width: 100%;
35+
}
36+
.login-form {
37+
background: #ffffff;
38+
border: 1px solid #ddd;
39+
margin: 0 auto;
40+
padding: 2em;
41+
width: 350px;
42+
}
43+
.form-input {
44+
background: #fafafa;
45+
border: 1px solid #eeeeee;
46+
padding: 12px;
47+
width: 100%;
48+
}
49+
.form-group {
50+
margin: 1em 0;
51+
}
52+
.form-button {
53+
background: #107bba;
54+
border: 1px solid #ddd;
55+
color: #ffffff;
56+
padding: 10px;
57+
text-transform: uppercase;
58+
width: 100%;
59+
}
60+
.form-button:hover {
61+
background: #0072b5;
62+
}
63+
.form-header {
64+
text-align: center;
65+
}
66+
.form-footer {
67+
text-align: center;
68+
}
69+
#logo {
70+
margin-top: 2em;
71+
}
72+
#error-message {
73+
text-align: center;
74+
margin-bottom: 0em;
75+
}
76+
</style>
77+
</head>
78+
<body>
79+
<div class="wrap">
80+
<form class="login-form" action="./login" method="get">
81+
<div class="form-header">
82+
<h3><img id="logo" src="{{ PANEL_CDN }}images/logo_stacked.png" width="150" height="120"></h3>
83+
<br>
84+
<p> Successfully logged out.</p>
85+
</div>
86+
<div class="form-group">
87+
<button class="form-button" type="submit">Login</button>
88+
</div>
89+
<div><small></small></div>
90+
</form>
91+
</div>
92+
</body>
93+
</html>

Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .entry_points import entry_points_for
2020
from .io import state
2121
from .io.resources import (
22-
BASIC_LOGIN_TEMPLATE, CDN_DIST, ERROR_TEMPLATE, _env,
22+
BASIC_LOGIN_TEMPLATE, CDN_DIST, ERROR_TEMPLATE, LOGOUT_TEMPLATE, _env,
2323
)
2424
from .util import base64url_decode, base64url_encode
2525

@@ -236,7 +236,7 @@ def set_state_cookie(self, state):
236236
)
237237

238238
def get_state(self):
239-
next_url = original_next_url = self.get_argument('next', None)
239+
next_url = original_next_url = self.get_argument('next', self.request.uri.replace('/login', ''))
240240
if next_url:
241241
# avoid browsers treating \ as /
242242
next_url = next_url.replace('\\', urlparse.quote('\\'))
@@ -251,7 +251,7 @@ def get_state(self):
251251
"Ignoring next_url %r, using %r", original_next_url, next_url
252252
)
253253
return _serialize_state(
254-
{'state_id': uuid.uuid4().hex, 'next_url': next_url}
254+
{'state_id': uuid.uuid4().hex, 'next_url': next_url or '/'}
255255
)
256256

257257
async def get(self):
@@ -267,6 +267,7 @@ async def get(self):
267267
'redirect_uri': redirect_uri,
268268
'client_id': config.oauth_key,
269269
}
270+
270271
# Some OAuth2 backends do not correctly return code
271272
next_arg = self.get_argument('next', {})
272273
if next_arg:
@@ -780,22 +781,30 @@ class GoogleLoginHandler(OAuthIDTokenLoginHandler, OAuth2Mixin):
780781

781782
class LogoutHandler(tornado.web.RequestHandler):
782783

784+
_logout_handler = LOGOUT_TEMPLATE
785+
783786
def get(self):
784787
self.clear_cookie("user")
785788
self.clear_cookie("id_token")
786789
self.clear_cookie("access_token")
787790
self.clear_cookie(STATE_COOKIE_NAME)
788-
self.redirect(self.request.uri.replace('/logout', '/login'))
791+
html = self._logout_template.render(PANEL_CDN=CDN_DIST)
792+
self.write(html)
789793

790794

791795
class OAuthProvider(AuthProvider):
792796

793-
def __init__(self, error_template=None):
797+
def __init__(self, error_template=None, logout_template=None):
794798
if error_template is None:
795799
self._error_template = ERROR_TEMPLATE
796800
else:
797801
with open(error_template) as f:
798802
self._error_template = _env.from_string(f.read())
803+
if logout_template is None:
804+
self._logout_template = LOGOUT_TEMPLATE
805+
else:
806+
with open(logout_template) as f:
807+
self._logout_template = _env.from_string(f.read())
799808
super().__init__()
800809

801810
@property
@@ -806,10 +815,7 @@ def get_user(request_handler):
806815

807816
@property
808817
def login_url(self):
809-
if config.oauth_redirect_uri is None:
810-
return '/login'
811-
else:
812-
return urlparse.urlparse(config.oauth_redirect_uri).path + '/login'
818+
return '/login'
813819

814820
@property
815821
def login_handler(self):
@@ -824,6 +830,8 @@ def logout_url(self):
824830

825831
@property
826832
def logout_handler(self):
833+
if self._logout_template:
834+
LogoutHandler._logout_template = self._logout_template
827835
return LogoutHandler
828836

829837

@@ -886,13 +894,13 @@ def set_current_user(self, user):
886894

887895
class BasicProvider(OAuthProvider):
888896

889-
def __init__(self, basic_login_template=None):
897+
def __init__(self, basic_login_template=None, logout_template=None):
890898
if basic_login_template is None:
891899
self._basic_login_template = BASIC_LOGIN_TEMPLATE
892900
else:
893901
with open(basic_login_template) as f:
894902
self._basic_login_template = _env.from_string(f.read())
895-
super().__init__()
903+
super().__init__(logout_template=logout_template)
896904

897905
@property
898906
def login_url(self):

Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ class Serve(_BkServe):
148148
type = str,
149149
help = "Template to serve when user is unauthenticated."
150150
)),
151+
('--logout-template', dict(
152+
action = 'store',
153+
type = str,
154+
help = "Template to serve logout page."
155+
)),
151156
('--basic-login-template', dict(
152157
action = 'store',
153158
type = str,
@@ -425,6 +430,12 @@ def customize_kwargs(self, args, server_kwargs):
425430
)
426431
config.auth_template = str(authpath.absolute())
427432

433+
434+
if args.logout_template:
435+
logout_template = str(pathlib.Path(args.logout_template).absolute())
436+
else:
437+
logout_template = None
438+
428439
if args.basic_auth and config.basic_auth:
429440
raise ValueError(
430441
"Turn on Basic authentication using environment variable "
@@ -443,8 +454,10 @@ def customize_kwargs(self, args, server_kwargs):
443454
)
444455
else:
445456
basic_login_template = None
457+
446458
kwargs['auth_provider'] = BasicProvider(
447-
basic_login_template=basic_login_template
459+
basic_login_template=basic_login_template,
460+
logout_template=logout_template
448461
)
449462

450463
if args.cookie_secret and config.cookie_secret:
@@ -554,7 +567,10 @@ def customize_kwargs(self, args, server_kwargs):
554567
error_template = config.auth_template
555568
else:
556569
error_template = None
557-
kwargs['auth_provider'] = OAuthProvider(error_template=error_template)
570+
571+
kwargs['auth_provider'] = OAuthProvider(
572+
error_template=error_template, logout_template=logout_template
573+
)
558574

559575
if args.oauth_redirect_uri and config.oauth_redirect_uri:
560576
raise ValueError(

Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def conffilter(value):
8383
INDEX_TEMPLATE = _env.get_template('convert_index.html')
8484
BASE_TEMPLATE = _env.get_template('base.html')
8585
ERROR_TEMPLATE = _env.get_template('error.html')
86+
LOGOUT_TEMPLATE = _env.get_template('logout.html')
8687
BASIC_LOGIN_TEMPLATE = _env.get_template('basic_login.html')
8788
DEFAULT_TITLE = "Panel Application"
8889
JS_RESOURCES = _env.get_template('js_resources.html')

Original file line numberDiff line numberDiff line change
@@ -982,8 +982,10 @@ def get_server(
982982
oauth_secret: Optional[str] = None,
983983
oauth_redirect_uri: Optional[str] = None,
984984
oauth_extra_params: Mapping[str, str] = {},
985+
oauth_error_template: Optional[str] = None,
985986
cookie_secret: Optional[str] = None,
986987
oauth_encryption_key: Optional[str] = None,
988+
logout_template: Optional[str] = None,
987989
session_history: Optional[int] = None,
988990
**kwargs
989991
) -> Server:
@@ -1038,11 +1040,16 @@ def get_server(
10381040
Overrides the default OAuth redirect URI
10391041
oauth_extra_params: dict (optional, default={})
10401042
Additional information for the OAuth provider
1043+
oauth_error_template: str (optional, default=None)
1044+
Jinja2 template used when displaying authentication errors.
10411045
cookie_secret: str (optional, default=None)
10421046
A random secret string to sign cookies (required for OAuth)
10431047
oauth_encryption_key: str (optional, default=False)
10441048
A random encryption key used for encrypting OAuth user
10451049
information and access tokens.
1050+
logout_template: str (optional, default=None)
1051+
Jinja2 template served when viewing the logout endpoint when
1052+
authentication is enabled.
10461053
session_history: int (optional, default=None)
10471054
The amount of session history to accumulate. If set to non-zero
10481055
and non-None value will launch a REST endpoint at
@@ -1151,11 +1158,17 @@ def get_server(
11511158
from ..auth import BasicProvider
11521159
server_config['basic_auth'] = basic_auth
11531160
basic_login_template = kwargs.pop('basic_login_template', None)
1154-
opts['auth_provider'] = BasicProvider(basic_login_template)
1161+
opts['auth_provider'] = BasicProvider(
1162+
basic_login_template,
1163+
logout_template=logout_template
1164+
)
11551165
elif oauth_provider:
11561166
from ..auth import OAuthProvider
11571167
config.oauth_provider = oauth_provider # type: ignore
1158-
opts['auth_provider'] = OAuthProvider()
1168+
opts['auth_provider'] = OAuthProvider(
1169+
error_template=oauth_error_template,
1170+
logout_template=logout_template
1171+
)
11591172
if oauth_key:
11601173
config.oauth_key = oauth_key # type: ignore
11611174
if oauth_secret: