Skip to content

Commit

Permalink
frontend/oidc: add OIDC_USERNAME_CLAIM
Browse files Browse the repository at this point in the history
The current OpenID Connect code assumes the existence of a
non-standard field in the UserInfo returned by the provider, limiting
the ability to use non-Ipsilon OIDC providers; however, the
standard forbids using corresponding standardised field
(preferred_username) as a unique user identifier, a property on which
Copr relies.

This commit allows the administrator to specify which claim from
UserInfo should be used as a username; they may, for instance, choose
preferred_username if they know that the values from their OIDC
provider will be unique.  The upshot of this change is that
administrators can now use OIDC providers like Gitea/Forgejo.

Fixes #3008.
  • Loading branch information
Bob131 authored and FrostyX committed Dec 19, 2023
1 parent 22df66d commit bcf645d
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 3 deletions.
15 changes: 15 additions & 0 deletions frontend/coprs_frontend/config/copr.conf
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ HIDE_IMPORT_LOG_AFTER_DAYS = 14
# OIDC_SCOPES = "" # e.g. "openid username profile email"
# OIDC_TOKEN_AUTH_METHOD="client_secret_post" # possible: client_secret_post, client_secret_basic, none

# The "claim" (for our purposes: a piece of information in the
# UserInfo returned by an OIDC provider) to use as a unique
# username. There are two supported values:
#
# - "username" (the default): a non-standard Ipsilon extension
# that provides unique user names.
#
# - "preferred_username": a claim that is specified in the OpenID
# Connect standard, but one that the standard forbids Relying
# Parties from using for this purpose since they are not
# guaranteed to be unique. This option is offered regardless,
# since oftentimes this claim is unique to a specific user in
# practice.
# OIDC_USERNAME_CLAIM = "username"

# We have supported two types of OIDC client register
# 1. dynamic register
# 2. static register
Expand Down
4 changes: 3 additions & 1 deletion frontend/coprs_frontend/coprs/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from coprs import app
from coprs.exceptions import CoprHttpException, AccessRestricted
from coprs.logic.users_logic import UsersLogic
from coprs.oidc import oidc_username_from_userinfo


class UserAuth:
Expand Down Expand Up @@ -391,8 +392,9 @@ def user_from_userinfo(userinfo):

zoneinfo = userinfo['zoneinfo'] if 'zoneinfo' in userinfo \
and userinfo['zoneinfo'] else None
username = oidc_username_from_userinfo(app.config, userinfo)

user = UserAuth.get_or_create_user(userinfo['username'], userinfo['email'], zoneinfo)
user = UserAuth.get_or_create_user(username, userinfo['email'], zoneinfo)
GroupAuth.update_user_groups(user, OpenIDConnect.groups_from_userinfo(userinfo))
return user

Expand Down
15 changes: 15 additions & 0 deletions frontend/coprs_frontend/coprs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,21 @@ class Config(object):
# OIDC is opt-in
OIDC_LOGIN = False

# The "claim" (for our purposes: a piece of information in the
# UserInfo returned by an OIDC provider) to use as a unique
# username. There are two supported values:
#
# - "username" (the default): a non-standard Ipsilon extension
# that provides unique user names.
#
# - "preferred_username": a claim that is specified in the OpenID
# Connect standard, but one that the standard forbids Relying
# Parties from using for this purpose since they are not
# guaranteed to be unique. This option is offered regardless,
# since oftentimes this claim is unique to a specific user in
# practice.
OIDC_USERNAME_CLAIM = "username"

PACKAGES_COUNT = False

EXTRA_BUILDCHROOT_TAGS = []
Expand Down
21 changes: 21 additions & 0 deletions frontend/coprs_frontend/coprs/oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,34 @@ def oidc_enabled(config):
app.logger.warning("OIDC_SCOPES is empty, using default method: client_secret_basic")
config["OIDC_TOKEN_AUTH_METHOD"] = "client_secret_basic"

username_claim = config.get("OIDC_USERNAME_CLAIM")
if username_claim and \
not username_claim in ("username", "preferred_username"):
app.logger.error(
f"Invalid setting {repr(username_claim)} for OIDC_USERNAME_CLAIM, " +
"expected one of: \"username\", \"preferred_username\""
)
return False

return config.get("OIDC_METADATA") or (
config.get("OIDC_AUTH_URL")
and config.get("OIDC_TOKEN_URL")
and config.get("OIDC_USERINFO_URL")
)


def oidc_username_from_userinfo(config, userinfo):
"""
Return a unique user name from UserInfo
"""
try:
return userinfo[config.get("OIDC_USERNAME_CLAIM", "username")]
except KeyError as exc:
raise RuntimeError(
"Can't get unique username, see OIDC_USERNAME_CLAIM configuration docs"
) from exc


def init_oidc_app(app):
"""
Init a openID connect client using configs
Expand Down
4 changes: 2 additions & 2 deletions frontend/coprs_frontend/coprs/views/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from coprs.exceptions import ObjectNotFound
from coprs.measure import checkpoint_start
from coprs.auth import FedoraAccounts, UserAuth, OpenIDConnect
from coprs.oidc import oidc_enabled
from coprs.oidc import oidc_enabled, oidc_username_from_userinfo
from coprs import oidc

@app.before_request
Expand Down Expand Up @@ -115,7 +115,7 @@ def oidc_auth():
oidc.copr.authorize_access_token()
userinfo = oidc.copr.userinfo()
user = OpenIDConnect.user_from_userinfo(userinfo)
flask.session["oidc"] = userinfo['username']
flask.session["oidc"] = oidc_username_from_userinfo(app.config, userinfo)
return do_create_or_login(user)


Expand Down

0 comments on commit bcf645d

Please sign in to comment.