Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
aab4750
extended configuration manager with optional OIDC sections
sjanssen2 Mar 20, 2024
49b0448
flake8
sjanssen2 Mar 20, 2024
2840601
also provide a label for a speaking name of the identity provider
sjanssen2 Mar 20, 2024
f1c9149
start implementing the OIDC dance
sjanssen2 Mar 20, 2024
2eb6d08
modal not necessary, if only one provider was defined
sjanssen2 Mar 20, 2024
48ca02a
error handling of provider not in config file
sjanssen2 Mar 20, 2024
dc4bd20
adding pycurl package to enable tornado curl_httpclients
sjanssen2 Mar 20, 2024
e1f3c13
a new method to create a user, if information do not need to be enter…
sjanssen2 Mar 20, 2024
48f09a5
full OIDC dance implemented
sjanssen2 Mar 20, 2024
baf40df
add an admin page to activate users which requested authorization thr…
sjanssen2 Mar 20, 2024
670a55a
flake8
sjanssen2 Mar 20, 2024
091ffc6
adding menu entry for user authorization
sjanssen2 Mar 20, 2024
1feefc0
do not expose traditional qiita internal user authentication, if OIDC…
sjanssen2 Mar 21, 2024
29ce7dd
use Qiita typical modal for OIDC login
sjanssen2 Mar 21, 2024
2ca5bb8
wrong menu entrie affected
sjanssen2 Mar 21, 2024
1b787cb
always allow logout
sjanssen2 Mar 21, 2024
88319b2
improved error handling
sjanssen2 Mar 21, 2024
02d9af0
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Mar 22, 2024
b1e1b6b
revert: let user change their profile, but not password - if provided…
sjanssen2 Mar 22, 2024
a7d3b84
speaking button names + move into correct div to always get displayed
sjanssen2 Mar 22, 2024
125835a
use email from config + loop user_info from OIDC to fill DB
sjanssen2 Mar 22, 2024
5f28092
use OIDC info to prefil user information
sjanssen2 Mar 22, 2024
19b4d7b
drop admin user authorization
sjanssen2 Apr 4, 2024
33f2879
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Apr 4, 2024
c9d413a
using the well-known json dict instead of manually providing multiple…
sjanssen2 Jun 5, 2024
6bfafcb
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Jun 5, 2024
9a5e7cc
flake8
sjanssen2 Jun 5, 2024
b2fc279
flake8
sjanssen2 Jun 5, 2024
5cc0896
add ability to display OIDC logos
sjanssen2 Jun 5, 2024
949084d
add OIDC logo
sjanssen2 Jun 5, 2024
c3b040b
revert to dev branch
sjanssen2 Jun 5, 2024
d96bbae
fixing config manager tests
sjanssen2 Jun 5, 2024
a491870
Merge pull request #7 from jlab/auth_oidc_wellknown
sjanssen2 Jun 5, 2024
b1baece
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Jun 6, 2024
81fdcbf
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Jun 20, 2024
e0c4002
add missing template
sjanssen2 Jun 20, 2024
bb9c685
Merge branch 'add_admin_purge_template' of github.com:jlab/qiita into…
sjanssen2 Jun 20, 2024
79e794a
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Jun 21, 2024
c9aacec
Merge branch 'master' of github.com:qiita-spots/qiita into auth_oidc_…
sjanssen2 Mar 4, 2025
a5deb83
Merge pull request #10 from jlab/auth_oidc_merged
sjanssen2 Mar 4, 2025
7693c5e
extended configuration manager with optional OIDC sections
sjanssen2 Mar 20, 2024
b4ab605
flake8
sjanssen2 Mar 4, 2025
baa7230
also provide a label for a speaking name of the identity provider
sjanssen2 Mar 20, 2024
52e57ca
start implementing the OIDC dance
sjanssen2 Mar 20, 2024
4061373
modal not necessary, if only one provider was defined
sjanssen2 Mar 20, 2024
51307d1
error handling of provider not in config file
sjanssen2 Mar 20, 2024
7a0ec9f
adding pycurl package to enable tornado curl_httpclients
sjanssen2 Mar 20, 2024
0c365a1
a new method to create a user, if information do not need to be enter…
sjanssen2 Mar 20, 2024
e993a99
full OIDC dance implemented
sjanssen2 Mar 20, 2024
ca5f7f6
add an admin page to activate users which requested authorization thr…
sjanssen2 Mar 20, 2024
4d5c6a2
flake8
sjanssen2 Mar 20, 2024
fd6d15e
adding menu entry for user authorization
sjanssen2 Mar 20, 2024
9c8b824
do not expose traditional qiita internal user authentication, if OIDC…
sjanssen2 Mar 21, 2024
a654e48
use Qiita typical modal for OIDC login
sjanssen2 Mar 21, 2024
27f6d35
always allow logout
sjanssen2 Mar 21, 2024
85bf1fa
improved error handling
sjanssen2 Mar 21, 2024
8a504cc
revert: let user change their profile, but not password - if provided…
sjanssen2 Mar 22, 2024
ef05eed
speaking button names + move into correct div to always get displayed
sjanssen2 Mar 22, 2024
a5270a0
use email from config + loop user_info from OIDC to fill DB
sjanssen2 Mar 22, 2024
2efb70f
use OIDC info to prefil user information
sjanssen2 Mar 22, 2024
c8b1198
drop admin user authorization
sjanssen2 Apr 4, 2024
3d6f718
using the well-known json dict instead of manually providing multiple…
sjanssen2 Jun 5, 2024
3957030
flake8
sjanssen2 Jun 5, 2024
648f2f9
flake8
sjanssen2 Jun 5, 2024
73f92b9
add ability to display OIDC logos
sjanssen2 Jun 5, 2024
0dc243d
add OIDC logo
sjanssen2 Jun 5, 2024
9b81163
fixing config manager tests
sjanssen2 Jun 5, 2024
bb03167
Merge branch 'auth_oidc' of github.com:jlab/qiita into auth_oidc
sjanssen2 Mar 4, 2025
a76288b
Merge branch 'master' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Mar 11, 2025
8cc718f
Merge branch 'dev' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Mar 11, 2025
89cab41
Merge branch 'master' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Mar 11, 2025
1a61930
Merge branch 'dev' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Mar 12, 2025
d022665
Merge branch 'master' of github.com:jlab/qiita into auth_oidc
sjanssen2 Aug 29, 2025
1d673e0
Merge branch 'dev' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Sep 13, 2025
670e432
Merge branch 'auth_oidc' of github.com:jlab/qiita into auth_oidc
sjanssen2 Sep 13, 2025
3061d94
discriminate nginx and direct requests and either use nginx or tornad…
sjanssen2 Sep 17, 2025
cfe606a
codestyle
sjanssen2 Sep 17, 2025
71526bb
don't complain about overwriting files IF in test mode
sjanssen2 Sep 18, 2025
c7924ca
don't read config from file, but from DB
sjanssen2 Sep 18, 2025
ae888a8
allow to push whole directories to qiita main
sjanssen2 Sep 18, 2025
81cb696
extended FetchFileFromCentralHandler to directories, but very limited…
sjanssen2 Nov 6, 2025
8ce0cdb
assert presence/absence of directory transfer flag
sjanssen2 Nov 6, 2025
736d1fd
codestyle
sjanssen2 Nov 6, 2025
f853f56
Merge pull request #13 from jlab/tornado_FetchFileFromCentralHandler_…
sjanssen2 Nov 6, 2025
c6f8357
adding debug information
sjanssen2 Nov 7, 2025
1ff9892
move debug to individual files
sjanssen2 Nov 7, 2025
4bd4a18
more debug when composing zip
sjanssen2 Nov 7, 2025
14d43f1
debug
sjanssen2 Nov 7, 2025
fc367bb
modify nginx file list for ZIP
sjanssen2 Nov 7, 2025
043c6be
use correct function
sjanssen2 Nov 7, 2025
3cb9cf7
fix path computation
sjanssen2 Nov 7, 2025
2cc787d
clean up
sjanssen2 Nov 7, 2025
9441772
clean download.py
sjanssen2 Nov 7, 2025
c446bf9
Merge pull request #14 from jlab/tornado_FetchFileFromCentralHandler_…
sjanssen2 Nov 7, 2025
017b150
add ability to delete files/dirs through API, but only in qiita test …
sjanssen2 Nov 12, 2025
7604d84
also delete non managed dirs
sjanssen2 Nov 12, 2025
50a1788
avoid deleting BIOM sub-dir :-/
sjanssen2 Nov 12, 2025
d3cf5f5
Merge pull request #15 from jlab/tornado_FetchFileFromCentralHandler_…
sjanssen2 Nov 12, 2025
dcedd02
also allow downloading of html summary directories
sjanssen2 Nov 14, 2025
f85c390
autoremove files created during testing
antgonza Jan 28, 2026
e2a6c51
artifact.id <= 9
antgonza Jan 28, 2026
45f0d52
Merge branch 'dev' of https://github.com/qiita-spots/qiita into torna…
sjanssen2 Feb 10, 2026
0673566
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Feb 10, 2026
7d9210b
add missing import back in
sjanssen2 Feb 10, 2026
48ba46a
Merge branch 'tornado_FetchFileFromCentralHandler' of github.com:jlab…
sjanssen2 Feb 11, 2026
57d8ea9
Merge branch 'autoremove-files-created-during-testing' of https://git…
sjanssen2 Feb 11, 2026
41fc336
use OIDC style to log out user globally, i.e. not only from qiita but…
sjanssen2 Feb 16, 2026
14c4505
Merge branch 'dev' of github.com:jlab/qiita into auth_oidc
sjanssen2 Feb 17, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/qiita-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
# Setting up main qiita conda environment
conda config --add channels conda-forge
conda deactivate
conda create --quiet --yes -n qiita python=3.9 pip libgfortran numpy nginx cython redis
conda create --quiet --yes -n qiita python=3.9 pip libgfortran numpy nginx cython redis pycurl
conda env list
conda activate qiita
pip install -U pip
Expand Down
69 changes: 68 additions & 1 deletion qiita_core/configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ class ConfigurationManager(object):
The email address a user should write to when asking for help
sysadmin_email : str
The email address, Qiita sends internal notifications to a sys admin
None (=internal user authentication) or one or several 'oidc_' sections
to use external identity providers (IdP) with following values:
client_id : str
The name you registered Qiita with at the external IdP
client_secret : str
A secret string with which Qiita identifies at the external IdP (not
all IdPs need a secret)
redirect_endpoint : str
The internal Qiita endpoint the IdP shall redirect the user after
logging in
wellknown_uri : str
The URL of the well-known json document, specifying how API end points
like 'authorize', 'token' or 'userinfo' are defined. See e.g.
https://swagger.io/docs/specification/authentication/
openid-connect-discovery/
label : str
A speaking label for the Identity Provider
scope : str
The scope, i.e. fields about a user, which Qiita requests from the
Identity Provider, e.g. "profile email eduperson_orcid".
Will be automatically extended by the scope "openid", to enable the
"authorize_code" OIDC flow.
logo : str
Optional. Name of a file in qiita_pet/static/img that shall be
displayed for login through Service Provider, instead of a plain
button

Raises
------
Expand Down Expand Up @@ -160,6 +186,7 @@ def __init__(self):
self._get_vamps(config)
self._get_portal(config)
self._iframe(config)
self._get_oidc(config)

def _get_main(self, config):
"""Get the configuration of the main section"""
Expand Down Expand Up @@ -412,4 +439,44 @@ def _get_portal(self, config):
raise ValueError(msg % (name, val, "larger than 180°"))

def _iframe(self, config):
self.iframe_qiimp = config.get("iframe", "QIIMP", fallback=None)
self.iframe_qiimp = config.get('iframe', 'QIIMP', fallback=None)

def _get_oidc(self, config):
"""Get the configuration of the open ID connect section(s)
User can provide multiple sections with naming schema oidc_foo where
foo is the name of an Identity Provider - Qiita can handle multiple
Identity Providers simultaneously.
"""
PREFIX = 'oidc_'
self.oidc = dict()
for section_name in config.sections():
if section_name.startswith(PREFIX):
provider = dict()
provider['client_id'] = config.get(
section_name, 'CLIENT_ID', fallback=None)
provider['client_secret'] = config.get(
section_name, 'CLIENT_SECRET', fallback=None)
provider['redirect_endpoint'] = config.get(
section_name, 'REDIRECT_ENDPOINT')
if provider['redirect_endpoint']:
if not provider['redirect_endpoint'].startswith('/'):
provider['redirect_endpoint'] = '/%s' % provider[
'redirect_endpoint']
if provider['redirect_endpoint'].endswith('/'):
provider['redirect_endpoint'] = provider[
'redirect_endpoint'][:-1]
provider['wellknown_uri'] = config.get(
section_name, 'WELLKNOWN_URI')
provider['label'] = config.get(section_name, 'LABEL')
if not provider['label']:
# fallback, if no label is provided
provider['label'] = section_name[len(PREFIX):]
self.oidc[section_name[len(PREFIX):]] = provider
provider['scope'] = config.get(
section_name, 'SCOPE', fallback=None)
if not provider['scope']:
provider['scope'] = 'openid'
if 'openid' not in provider['scope']:
provider['scope'] = 'openid %s' % provider['scope']
provider['logo'] = config.get(
section_name, 'LOGO', fallback=None)
65 changes: 65 additions & 0 deletions qiita_core/support_files/config_test.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,68 @@ STATS_MAP_CENTER_LONGITUDE =
# On May 2024, we removed QIIMP from the code base but we will leave this
# section in case we need to add access to another iframe in the future; note
# that the qiita-terms are also accessed via iframe but this is internal

# --------------------- External Identity Provider settings --------------------
# user authentication happens per default within Qiita, i.e. when a user logs in,
# the stored password hash and email address is compared against what a user
# just provided. You might however, use an external identity provider (IdP) to
# authenticate the user like
# google: https://developers.google.com/identity/protocols/oauth2 or
# github: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps or
# self hosted keycloak: https://www.keycloak.org/
# Thus, you don't have to deal with user verification, reset passwords, ...
# Authorization (i.e. if the authorized user is allowed to use Qiita or which
# user level he/she gets assigned is an independent process. You can even use
# multiple independent external identity providers!
# Qiita currently only support the "open ID connect" protocol with the implicit flow.
# Each identity provider comes as its own config section [oidc_foo] and needs
# to specify the following five fields:
#
# Typical identity provider manage multiple "realms" and specific "clients" per realm
# You need to contact your IdP and register Qiita as a new "client". The IdP will
# provide you with the correct values.
#
# The authorization protocol requires three steps to obtain user information:
# 1) you identify as the correct client and ask the IdP for a request code
# You have to forward the user to the login page of your IdP. To let the IdP
# know how to come back to Qiita, you need to provide a redirect URL
# 2) you exchange the code for a user token
# 3) you obtain information about the user for the obtaines user token
# Typically, each step is implemented as a separate URL endpoint
#
# To activate IdP: comment out the following config section

#[oidc_academicid]
#
## client ID for Qiita as registered at your Identity Provider of choice
#CLIENT_ID = gi-qiita-prod
#
## client secret to verify Qiita as the correct client. Not all IdPs require
## a client secret!
#CLIENT_SECRET = verySecretString
#
## redirect URL (end point in your Qiita instance), to which the IdP redirects
## after user types in his/her credentials. If you don't want to change code in
## qiita_pet/webserver.py the URL must follow the pattern:
## base_URL/auth/login_OIDC/foo where foo is the name of this config section
## without the oidc_ prefix!
#REDIRECT_ENDPOINT = /auth/login_OIDC/localkeycloak
#
## The URL of the well-known json document, specifying how API end points
## like 'authorize', 'token' or 'userinfo' are defined. See e.g.
## https://swagger.io/docs/specification/authentication/
## openid-connect-discovery/
#WELLKNOWN_URI = https://keycloak.sso.gwdg.de/.well-known/openid-configuration
#
## a speaking label for the Identity Provider. Section name is used if empty.
#LABEL = GWDG Academic Cloud
#
## The scope, i.e. fields about a user, which Qiita requests from the
## Identity Provider, e.g. "profile email eduperson_orcid".
## Will be automatically extended by the scope "openid", to enable the
## "authorize_code" OIDC flow.
#SCOPE = openid
#
##Optional. Name of a file in qiita_pet/static/img that shall be
##displayed for login through Service Provider, instead of a plain button
#LOGO = oidc_lifescienceAAI.png
85 changes: 85 additions & 0 deletions qiita_core/tests/test_configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,55 @@ def test_get_portal_latlong(self):
obs._get_portal(self.conf)
self.assertEqual(obs.stats_map_center_longitude, -105.24827)

def test_get_oidc(self):
SECTION_NAME = 'oidc_academicid'
obs = ConfigurationManager()
self.assertTrue(len(obs.oidc), 1)
self.assertTrue(obs.oidc.keys(), [SECTION_NAME])

# assert endpoint starts with /
self.conf.set(SECTION_NAME, 'REDIRECT_ENDPOINT', 'auth/something')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['redirect_endpoint'],
'/auth/something')

# assert endpoint does not end with /
self.conf.set(SECTION_NAME, 'REDIRECT_ENDPOINT', 'auth/something/')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['redirect_endpoint'],
'/auth/something')

self.conf.set(SECTION_NAME, 'CLIENT_ID', 'foo')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['client_id'], "foo")

self.assertTrue('gwdg.de' in obs.oidc['academicid']['wellknown_uri'])

self.assertEqual(obs.oidc['academicid']['label'],
'GWDG Academic Cloud')
# test fallback, if no label is provided
self.conf.set(SECTION_NAME, 'LABEL', '')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['label'], 'academicid')

self.assertEqual(obs.oidc['academicid']['scope'], 'openid')
# test fallback, if no scope is provided
self.conf.set(SECTION_NAME, 'SCOPE', '')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['scope'], 'openid')

# test if scope will be automatically extended with 'openid'
self.conf.set(SECTION_NAME, 'SCOPE', 'email affiliation')
obs._get_oidc(self.conf)
self.assertTrue('openid' in obs.oidc['academicid']['scope'].split())

self.assertEqual(obs.oidc['academicid']['logo'],
'oidc_lifescienceAAI.png')
# test fallback, if no scope is provided
self.conf.remove_option(SECTION_NAME, 'LOGO')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['logo'], None)


CONF = """
# ------------------------------ Main settings --------------------------------
Expand Down Expand Up @@ -472,6 +521,42 @@ def test_get_portal_latlong(self):

# ----------------------------- iframes settings ---------------------------
[iframe]

# ------------------- External Identity Provider settings ------------------
[oidc_academicid]

# client ID for Qiita as registered at your Identity Provider of choice
CLIENT_ID = gi-qiita-prod

# client secret to verify Qiita as the correct client. Not all IdPs require
# a client secret.
CLIENT_SECRET = verySecretString

# redirect URL (end point in your Qiita instance), to which the IdP redirects
# after user types in his/her credentials. If you don't want to change code in
# qiita_pet/webserver.py the URL must follow the pattern:
# base_URL/auth/login_OIDC/foo where foo is the name of this config section
# without the oidc_ prefix!
REDIRECT_ENDPOINT = /auth/login_OIDC/academicid

# The URL of the well-known json document, specifying how API end points
# like 'authorize', 'token' or 'userinfo' are defined. See e.g.
# https://swagger.io/docs/specification/authentication/
# openid-connect-discovery/
WELLKNOWN_URI = https://keycloak.sso.gwdg.de/.well-known/openid-configuration

# a speaking label for the Identity Provider. Section name is used if empty.
LABEL = GWDG Academic Cloud

# The scope, i.e. fields about a user, which Qiita requests from the
# Identity Provider, e.g. "profile email eduperson_orcid".
# Will be automatically extended by the scope "openid", to enable the
# "authorize_code" OIDC flow.
SCOPE = openid

# Optional. Name of a file in qiita_pet/static/img that shall be
# displayed for login through Service Provider, instead of a plain button
LOGO = oidc_lifescienceAAI.png
"""

if __name__ == "__main__":
Expand Down
48 changes: 48 additions & 0 deletions qiita_db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,54 @@ def create(cls, email, password, info=None):

return cls(email)

@classmethod
def create_oidc(cls, email, user_info, idp):
"""Creates a new user with information obtained from an external
identity provider

Parameters
----------
email : str
The user's email fetched from the User Info of the Identity
Provider upon successful authentication.
user_info : dict
User information provided by the external identity provider
idp : str
The label of the external identity provider as set in config file

Raises
------
IncorrectEmailError
Email string given is not a valid email
"""
if not validate_email(email):
raise IncorrectEmailError("Bad email given: %s" % email)
info = {}
# email and password are minimal needed information, password indicates
# OIDC user registration purely for admins
info['email'] = email
info['password'] = "not_necessary_due_to_OIDC"
# verify code is necessary to manually authorize users on the admin
# page
info['user_verify_code'] = idp
# check if we got useful information from the IdP
if 'name' in user_info.keys():
info['name'] = user_info['name']

qdb.util.check_table_cols(info, cls._table)
columns = info.keys()
values = [info[col] for col in columns]

# create user
sql = "INSERT INTO qiita.{0} ({1}) VALUES ({2})".format(
cls._table, ','.join(columns), ','.join(['%s'] * len(values)))

qdb.sql_connection.TRN.add(sql, values)

qdb.sql_connection.TRN.execute()

return cls(email)

@classmethod
def verify_code(cls, email, code, code_type):
"""Verify that a code and email match
Expand Down
Loading
Loading