changeset 334:ff7d48d8d51a default tip

added Resources/Samples/SampleHttpAuthentication.py
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 21 Aug 2026 16:32:47 +0200
parents 10bc736775b1
children
files NEWS Resources/Samples/SampleHttpAuthentication.py
diffstat 2 files changed, 180 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Aug 21 15:14:47 2026 +0200
+++ b/NEWS	Fri Aug 21 16:32:47 2026 +0200
@@ -4,11 +4,13 @@
 => Maximum SDK version: 1.13.0 (default) <=
 => Minimum SDK version: 1.7.2 <=
 
-* Wrapped OrthancPluginRegisterHttpAuthentication() as orthanc.RegisterHttpAuthentication()
+* Wrapped "OrthancPluginRegisterHttpAuthentication()" as
+  "orthanc.RegisterHttpAuthentication()", for which a sample is
+  available in "./Resources/Samples/SampleHttpAuthentication.py"
 * Give access to the authentication payload in the REST callbacks,
   which requires Orthanc SDK 1.12.9
-* Wrapped DicomInsante.GetInstanceRemoteIp() and DicomInsante.GetInstanceCalledAet(),
-  which requires Orthanc SDK 1.13.1 (not released yet)
+* Wrapped "DicomInstance.GetInstanceRemoteIp()" and "DicomInstance.GetInstanceCalledAet()",
+  which require Orthanc SDK 1.13.1 (not released yet)
 
 
 Version 7.1 (2026-04-07)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/SampleHttpAuthentication.py	Fri Aug 21 16:32:47 2026 +0200
@@ -0,0 +1,175 @@
+# SPDX-FileCopyrightText: 2020-2023 Osimis S.A., 2024-2026 Orthanc Team SRL, 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+##
+## Python plugin for Orthanc
+## Copyright (C) 2020-2023 Osimis S.A., Belgium
+## Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+## Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+##
+## This program is free software: you can redistribute it and/or
+## modify it under the terms of the GNU Affero General Public License
+## as published by the Free Software Foundation, either version 3 of
+## the License, or (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+## Affero General Public License for more details.
+##
+## You should have received a copy of the GNU Affero General Public License
+## along with this program. If not, see <http://www.gnu.org/licenses/>.
+##
+
+
+#
+# Sample Python plugin illustrating how to implement basic
+# cookie-based user authentication.
+#
+# This sample uses plain session cookies. A more realistic
+# implementation would use JWT-based authentication.
+#
+
+
+import json
+import orthanc
+import urllib.parse
+
+
+CREDENTIALS = {
+    'admin' : 'admin',
+}
+
+
+ROOT = '/authentication-sample'
+COOKIE_LOGGED_USER = 'logged_user'
+COOKIE_BAD_CREDENTIALS = 'bad_credentials'
+
+
+HTML_PAGE_LOGIN = '''
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Orthanc sample authentication</title>
+</head>
+<body>
+<h1>Orthanc sample authentication</h1>
+%s
+<form action="do-login" method="POST">
+<p>Username: <input type="text" name="username"></input></p>
+<p>Password: <input type="password" name="password"></input></p>
+<button type="submit">Log in</button>
+</form>
+</body>
+</html>'''
+
+
+HTML_PAGE_LOGGED = '''
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Orthanc sample authentication</title>
+</head>
+<body>
+<h1>Orthanc sample authentication</h1>
+<p>You are logged as: <b>%s</b></p>
+<form action="do-logout" method="POST">
+<button type="submit">Log out</button>
+</form>
+<p><a href=".." target="_blank"><button>Open Orthanc Explorer</button></a></p>
+</body>
+</html>
+'''
+
+def SetSessionCookie(output, cookie, value):
+    output.SetHttpHeader('Set-Cookie', '%s=%s; HttpOnly; SameSite=Lax; Secure; Path=/' % (cookie, value))
+
+
+def ClearSessionCookie(output, cookie):
+    output.SetHttpHeader('Set-Cookie', '%s=; HttpOnly; SameSite=Lax; Secure; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' % cookie)
+
+
+def Login(output, uri, **request):
+    authentication = json.loads(request['authentication_payload'])
+
+    if 'username' in authentication:
+        output.AnswerBuffer(HTML_PAGE_LOGGED % authentication['username'], 'text/html')
+    else:
+        if authentication.get('bad_credentials') == 'true':
+            bad_credentials = '<p><b>Bad credentials were provided</b></p>'
+            ClearSessionCookie(output, COOKIE_BAD_CREDENTIALS)  # Only warn once about bad credentials
+        else:
+            bad_credentials = ''
+
+        output.AnswerBuffer(HTML_PAGE_LOGIN % bad_credentials, 'text/html')
+
+
+def DoLogin(output, uri, **request):
+    body = dict(urllib.parse.parse_qsl(request['body'].decode('utf-8')))
+    username = body.get('username')
+    password = body.get('password')
+
+    if (username != None and
+        password != None and
+        CREDENTIALS.get(username) == password):
+        # Correct credentials
+        SetSessionCookie(output, COOKIE_LOGGED_USER, username)
+        ClearSessionCookie(output, COOKIE_BAD_CREDENTIALS)
+    else:
+        # Wrong credentials
+        ClearSessionCookie(output, COOKIE_LOGGED_USER)
+        SetSessionCookie(output, COOKIE_BAD_CREDENTIALS, 'true')
+
+    output.SetHttpHeader('Location', 'index.html')
+    output.SendHttpStatusCode(302)  # Redirection
+
+
+def DoLogout(output, uri, **request):
+    ClearSessionCookie(output, COOKIE_LOGGED_USER)
+    ClearSessionCookie(output, COOKIE_BAD_CREDENTIALS)
+    output.SetHttpHeader('Location', 'index.html')
+    output.SendHttpStatusCode(302)  # Redirection
+
+
+def DoAuthentication(uri, ip, headers, get):
+    authentication_payload = {
+        # It is a good practice to inform other plugins (such as
+        # orthanc-wsi) about the plugin that generated the
+        # authentication payload
+        'source' : 'orthanc-python-sample-authentication',
+    }
+    is_logged = False
+
+    for (key, value) in headers.items():
+        if key == 'cookie':
+            for value in value.split(';'):
+                cookie = value.split('=')
+                cookieName = cookie[0].strip()
+                cookieValue = cookie[1].strip()
+                if cookieName == COOKIE_LOGGED_USER:
+                    authentication_payload['username'] = cookieValue
+                    is_logged = True
+                elif cookieName == COOKIE_BAD_CREDENTIALS:
+                    authentication_payload['bad_credentials'] = cookieValue
+
+    authentication_payload = json.dumps(authentication_payload).encode('utf-8')  # From JSON string to bytes
+
+    if uri.startswith(ROOT):
+        # Always grant access to the routes related to login/logout
+        return (orthanc.HttpAuthenticationStatus.GRANTED, authentication_payload, None)
+    elif is_logged:
+        # You could return orthanc.HttpAuthenticationStatus.FORBIDDEN
+        # if the logged user tries to access a resource for which the
+        # credentials are not sufficient
+        return (orthanc.HttpAuthenticationStatus.GRANTED, authentication_payload, None)
+    else:
+        # Request authentication is the user is not logged in yet
+        return (orthanc.HttpAuthenticationStatus.REDIRECT, authentication_payload, '%s/index.html' % ROOT)
+
+
+orthanc.RegisterHttpAuthenticationCallback(DoAuthentication)
+orthanc.RegisterRestCallback('%s/do-login' % ROOT, DoLogin)
+orthanc.RegisterRestCallback('%s/do-logout' % ROOT, DoLogout)
+orthanc.RegisterRestCallback('%s/index.html' % ROOT, Login)