#3016 Support OIDC token for API authentication and authorisation
Opened 2 years ago by cverna. Modified 2 years ago
cverna/pagure oidc_api_support  into  master

file modified
+15

@@ -751,6 +751,21 @@ 

  (IdP-specific user id, can be a nickname, email or a numeric ID

  depending on identity provider).

  

+ OIDC_API_TOKEN

+ ^^^^^^^^^^^^^^

+ 

+ When set to ``True``, pagure REST APIs are using OIDC tokens to validate ACLs.

+ it requires ``PAGURE_AUTH`` to be set to ``OIDC`` to work.

+ Default to ``False``.

+ 

+ OIDC_SCOPES_BASENAME

+ ^^^^^^^^^^^^^^^^^^^^

+ 

+ Configurable part of the OIDC scopes. For example ``https://pagure.io/oidc`` is the

+ base name of ``https://pagure.io/oidc/create_issue`` scope.

+ By changing the value of this field you can register different instance of pagure to

+ the same identity provider.

+ 

  IP_ALLOWED_INTERNAL

  ~~~~~~~~~~~~~~~~~~~

  

file modified
+93 -5

@@ -70,6 +70,7 @@ 

      EINVALIDTOK = 'Invalid or expired token. Please visit %s to get or '\

          'renew your API token.'\

          % urljoin(pagure_config['APP_URL'], 'settings#api-keys')

+     EINVALIDOIDCTOK = 'Invalid or expired OIDC token'

      ENOISSUE = 'Issue not found'

      EISSUENOTALLOWED = 'You are not allowed to view this issue'

      EPULLREQUESTSDISABLED = 'Pull-Request have been deactivated for this '\

@@ -162,7 +163,74 @@ 

          return jsonout

  

  

- def api_login_required(acls=None):

+ def check_oidc_token(scopes, optional=False):

+     ''' Check if the provided oidc token has the required scopes

+     and allow the user to access the desired enpoint.

+     '''

+     from pagure.ui.oidc_login import oidc

+     valid = False

+     if 'Authorization' in flask.request.headers:

+         token = flask.request.headers.get("Authorization").strip()

+         if token.startswith('Bearer '):

+             token = token[len('Bearer '):].strip()

+             valid = oidc.validate_token(token=token, scopes_required=scopes)

+ 

+     if valid is True:

+         try:

+             flask.g.fas_user = pagure.lib.get_user(

+                 flask.g.session, flask.g.oidc_token_info['sub'])

+             flask.g.authenticated = True

+             flask.g.fas_user.cla_done = True

+         except pagure.exceptions.PagureException:

+             output = {

+                 'error_code': APIERROR.EDBERROR.name,

+                 'error': APIERROR.EDBERROR.value}

+             jsonout = flask.jsonify(output)

+             jsonout.status_code = 404

+             return jsonout

+ 

+     elif optional is True:

+         return

+ 

+     else:

+         output = {

+             'error_code': APIERROR.EINVALIDOIDCTOK.name,

+             'error': APIERROR.EINVALIDOIDCTOK.value}

+         jsonout = flask.jsonify(output)

+         jsonout.status_code = 401

+         return jsonout

+ 

+ 

+ def create_oidc_scopes(acls, kwargs):

+     ''' Create the oidc scopes from the acls '''

+     # Convert default acls to OIDC scopes

+     # ie acl: create_issue becomes

+     # https://pagure.io/oidc/create_issue

+     scopes = []

+     if acls is not None:

+         scopes = [pagure_config.get('OIDC_SCOPES_BASENAME') + acl

+                   for acl in acls]

+ 

+         # Build the name of the scope in function of repo, username

+         # or namespace specific api call.

+         # https://pagure.io/oidc/create_issue(username/namespace/repo)

+         repo = None

+         if kwargs.get('repo'):

+             repo = kwargs.get('repo')

+         if kwargs.get('namespace'):

+             repo = '{}/{}'.format(kwargs.get('namespace'), repo)

+         if kwargs.get('username'):

+             repo = '{}/{}'.format(kwargs.get('username'), repo)

+ 

+         if repo is not None:

+             copy_scopes = list(scopes)

+             for scope in copy_scopes:

+                 scopes.append(scope + '({})'.format(repo))

+ 

+     return scopes

+ 

+ 

+ def api_login_required(acls=None, optional=False):

      ''' Decorator used to indicate that authentication is required for some

      API endpoint.

      '''

@@ -174,9 +242,20 @@ 

          def decorated_function(*args, **kwargs):

              ''' Actually does the job with the arguments provided. '''

  

+             # First verify pagure's API token, if the token is not valid

+             # then check if this is an OIDC token.

              response = check_api_acls(acls)

-             if response:

-                 return response

+             if response is not None:

+                 if pagure_config.get('OIDC_API_TOKEN', False):

+ 

+                     scopes = create_oidc_scopes(acls, kwargs)

+                     oidc_response = check_oidc_token(scopes)

+ 

+                     if oidc_response is not None:

+                         return oidc_response

+                 else:

+                     return response

+ 

              return function(*args, **kwargs)

  

          return decorated_function

@@ -196,9 +275,18 @@ 

          def decorated_function(*args, **kwargs):

              ''' Actually does the job with the arguments provided. '''

  

+             # First verify pagure's API token, if the token is not valid

+             # then check if this is an OIDC token.

              response = check_api_acls(acls, optional=True)

-             if response:

-                 return response

+             if response is not None:

+                 if pagure_config.get('OIDC_API_TOKEN', False):

+                     scopes = ['openid']

+                     oidc_response = check_oidc_token(scopes, optional=True)

+                     if oidc_response is not None:

+                         return oidc_response

+                 else:

+                     return response

+ 

              return function(*args, **kwargs)

  

          return decorated_function

@@ -212,6 +212,12 @@ 

  # (IdP-specific user id, can be a nickname, email or a numeric ID

  #  depending on IdP).

  # OIDC_PAGURE_USERNAME_FALLBACK = 'email'

+ # When this is set to True, pagure is using OIDC token to authenticate and

+ # authorize the API usage. When set to False it is using pagure's generated

+ # token

+ # OIDC_API_TOKEN = False

+ # Base namespace used for the OIDC API token scopes.

+ # OIDC_SCOPES_BASENAME = 'https://pagure.io/oidc/'

  

  # When this is set to True, the session cookie will only be returned to the

  # server via ssl (https). If you connect to the server via plain http, the

file modified
+5 -2

@@ -4,9 +4,12 @@ 

  To access some endpoints, you need to login to Pagure using API token. You

  can generate one in the project setting page.

  

+ In the case where you are using OIDC tokens. You need to obtain a token form

+ your identity provider.

+ 

  When sending HTTP request, include an ``Authorization`` field in the header

  with value ``token $your-api-token``, where ``$your-api-token`` is the

- API token generated in the project setting page.

+ API token generated in the project setting page or an OIDC token.

  

  So the result should look like:

  

@@ -14,7 +17,7 @@ 

  

      Authorization: token abcdefghijklmnop

  

- Where ``abcdefghijklmnop`` is the API token provided by pagure.

+ Where ``abcdefghijklmnop`` is the API token.

  

  Anyone with the token can access the APIs on your behalf, so please be

  sure to keep it private and safe.

@@ -119,6 +119,7 @@ 

      </div>

      {% endif %}

  

+     {% if not config.get('OIDC_API_TOKEN', False) %}

      <div class="col-md-8 col-md-offset-2">

        <div class="card">

          <div class="card-header">

@@ -224,6 +225,7 @@ 

          </div>

        </div>

      </div>

+     {% endif %}

  

      <div class="col-md-8 col-md-offset-2">

        <div class="card">

@@ -160,6 +160,7 @@ 

      {% endif %}

    </div>

  

+   {% if not config.get('OIDC_API_TOKEN', False) %}

    <div class="col-md-8 col-md-offset-2">

      <div class="card" id="api-keys">

        <div class="card-header">

@@ -250,7 +251,7 @@ 

        </div>

      </div>

    </div>

- 

+   {% endif %}

  </div>

  

  {% endblock %}

Add support to use OIDC token for pagure's API.

1 new commit added

  • Create the OIDC scopes for the token.
2 years ago

1 new commit added

  • Fix OICD to OIDC
2 years ago

1 new commit added

  • Support specific project scopes
2 years ago

rebased onto 56bb1f24f4b887517bacb7a21e852c3ccb7a5a79

2 years ago

2 new commits added

  • We need a specific OIDC token error message
  • Rework how we validate the token.
2 years ago

1 new commit added

  • Update the documentation
2 years ago

rebased onto a68b3545c7d8bc85f0eb88520b1d705a1219737e

2 years ago

Note that for OIDC (rather OAuth2), you want the prefix to be "Bearer ".
So "Authorization: Bearer <sometoken>".

I would suggest allowing both "legacy" and OIDC/OAuth2 tokens at the same time, just not allow creating new legacy tokens.
This way, there's a simple migration path, where legacy tokens will be replaced with OAuth2 ones as they expire.

I wanted to keep compatibility with the existing token system so that we don't have to update the documentation etc ...

I would suggest allowing both "legacy" and OIDC/OAuth2 tokens at the same time, just not allow creating new legacy tokens.
This way, there's a simple migration path, where legacy tokens will be replaced with OAuth2 ones as they expire.

Sure that's a good idea :thumbsup:

rebased onto 2dfb832

2 years ago

@cverna Looks like that infra issue is done?

@ngompa , yes unfortunately to test this feature, we also need a patch to ipsilon.

@ngompa unfortunately no, for this PR to work we still need change on the ipsilon side and I honestly don't think I will have time to look at it.

I have opened a ticket in ipsilon in order to track what is required (https://pagure.io/ipsilon/issue/307). If someone wants to do it :)