IPA and an external identity provider integration - idp objects
***************************************************************

IPA needs to store and manage IdP references. As a new IPA object, IdP
reference needs:

* creation of an LDAP object class and LDAP attribute types used to
  store IdP information

* creation of a new IPA API managing the IdP references

* definition of access control lists protecting access to the IdP
  references

* upgrade and backward compatibility.


LDAP Schema
===========


New attribute types
-------------------

The external IdP reference object needs to contain the following
information:

* name (string)

* authorization endpoint (URI mapped as a case-sensitive string),

* device authorization endpoint (URI mapped as a case-sensitive
  string), for instance https://oauth2.googleapis.com/device/code

* token endpoint (URI mapped as a case-sensitive string), for instance
  https://oauth2.googleapis.com/token

* userinfo endpoint (URI mapped as a case-sensitive string)

* JWKS endpoint (URI mapped as a case-sensitive string)

* OIDC URL (URI mapped as a case-sensitive string)

* "client_id" (case-sensitive string)

* "client_secret" (optional, may be an empty string, must be protected
  as a password)

* scope (case sensitive string)

* user subject attribute (case-sensitive string)

Additionally, an idp object needs to be referenced from a user object,
thus a link attribute has to be defined. This translates into the
following attribute type definitions:

   attributeTypes: (2.16.840.1.113730.3.8.23.15 NAME 'ipaIdpDevAuthEndpoint' DESC 'Identity Provider Device Authorization Endpoint' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.16 NAME 'ipaIdpAuthEndpoint' DESC 'Identity Provider Authorization Endpoint' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.17 NAME 'ipaIdpTokenEndpoint' DESC 'Identity Provider Token Endpoint' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.18 NAME 'ipaIdpClientId' DESC 'Identity Provider Client Identifier' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.19 NAME 'ipaIdpClientSecret' DESC 'Identity Provider Client Secret' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.20 NAME 'ipaIdpScope' DESC 'Identity Provider Scope' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.21 NAME 'ipaIdpConfigLink' DESC 'Corresponding Identity Provider Configuration link' SUP distinguishedName EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
   attributeTypes: (2.16.840.1.113730.3.8.23.22 NAME 'ipaIdpSub' DESC 'Identity Provider User Subject' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.23 NAME 'ipaIdpIssuerURL' DESC 'Identity Provider OIDC URL' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.24 NAME 'ipaIdpUserInfoEndpoint' DESC 'Identity Provider UserInfo Endpoint' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )
   attributeTypes: (2.16.840.1.113730.3.8.23.25 NAME 'ipaIdpKeysEndpoint' DESC 'Identity Provider JWKS Endpoint' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.9' )


New objectclasses for idp
-------------------------

In addition to IdP configuration object, a special object class is
added to be able to store IdP configuration link in a user object:

   objectClasses: (2.16.840.1.113730.3.8.24.6 NAME 'ipaIdP' SUP top STRUCTURAL DESC 'Identity Provider Configuration' MUST ( cn ) MAY ( ipaIdpDevAuthEndpoint $ ipaIdpAuthEndpoint $ ipaIdpTokenEndpoint $ ipaIdpUserInfoEndpoint $ ipaIdpKeysEndpoint $ ipaIdpClientId $ description $ ipaIdpClientSecret $ ipaIdpScope $ ipaIdpIssuerURL $ ipaIdpSub ) X-ORIGIN 'IPA v4.9' )
   objectClasses: (2.16.840.1.113730.3.8.24.7 NAME 'ipaIdpUser' SUP top AUXILIARY DESC 'User from an external Identity Provider ' MAY ( ipaIdpConfigLink $ ipaIdpSub ) X-ORIGIN 'IPA v4.9' )

In the current implementation, a user can be connected to a single IdP
reference. This is similar to RADIUS proxy references already used in
IPA. For both RADIUS and IdP cases IPA needs to know a particular user
identity within the context of the referred resource (IdP or RADIUS
server). Without associated IdP user information authentication of the
IPA user will not be triggered.


LDAP indices
------------

Indices need to be defined for all the attributes that can be used in
"ipa idp-find" command.

* The "cn" attribute is already indexed.

* The "ipaidpdevauthendpoint", "ipaidpauthendpoint",
  "idaidptokenendpoint", and "idpidpscope" attributes need indices
  (equality and substring indices).


IdP reference API
=================


Command Line Interface
----------------------

   idp-add   Add new external IdP server.
   idp-del   Delete an external IdP server.
   idp-find  Search for external IdP servers.
   idp-mod   Modify an external IdP server.
   idp-show  Display information about an external IdP server.

Following common options defined for the external IdP server object:

+----------------------------------------------------+----------------------------------------------------+
| Option                                             | Description                                        |
|====================================================|====================================================|
| "--dev-auth-uri"=URI                               | Device authorization endpoint                      |
+----------------------------------------------------+----------------------------------------------------+
| "--auth-uri"=URI                                   | General OAuth 2.0 authorization endpoint           |
+----------------------------------------------------+----------------------------------------------------+
| "--token-uri"=URI                                  | Token endpoint                                     |
+----------------------------------------------------+----------------------------------------------------+
| "--userinfo-uri"=URI                               | User information endpoint                          |
+----------------------------------------------------+----------------------------------------------------+
| "--keys-uri"=URI                                   | JWKS endpoint                                      |
+----------------------------------------------------+----------------------------------------------------+
| "--issuer-url"=URI                                 | The Identity Provider OIDC URL                     |
+----------------------------------------------------+----------------------------------------------------+
| "--client-id"=STR                                  | The client identifier issued by the IdP during     |
|                                                    | registration                                       |
+----------------------------------------------------+----------------------------------------------------+
| "--secret"                                         | The client secret, asked interactively             |
+----------------------------------------------------+----------------------------------------------------+
| "--scope"=STR                                      | OAuth 2.0 scopes of the access request             |
+----------------------------------------------------+----------------------------------------------------+
| "--idp-user-id"=STR                                | Attribute holding user identity in User info       |
+----------------------------------------------------+----------------------------------------------------+

"--scope" option expects space-separated list of OAuth 2.0 scopes.
Since the value includes space, it has to be enclosed in quotes:

   ipa idp-mod foo --scope="openid email"

Not all common options are required at the same time in the context of
different commands.

"ipa idp-add" command adds three more options:

+----------------------------------------------------+----------------------------------------------------+
| Option                                             | Description                                        |
|====================================================|====================================================|
| "--provider"=STR                                   | One of [google, github, microsoft, okta, keycloak] |
+----------------------------------------------------+----------------------------------------------------+
| "--org"=STR                                        | IdP-specific organization (tenant or realm) ID     |
+----------------------------------------------------+----------------------------------------------------+
| "--base-url"=URI                                   | IdP-specific base URL (e.g. "idp-                  |
|                                                    | host.$DOMAIN:$PORT/prefix")                        |
+----------------------------------------------------+----------------------------------------------------+

In order to ease the creation of idp, IPA pre-populates a set of
templates for well-known IdPs so that the user does not need to
provide the individual endpoint details.

For instance, for "google" provider, adding a new IdP would look like:

   ipa idp-add MyGoogleIdP --provider google \
       --client-id nZ8JDrV8Hklf3JumewRl2ke3ovPZn5Ho \

This call would be equivalent to the following:

   ipa idp-add MyGoogleIdP \
       --dev-auth-uri https://oauth2.googleapis.com/device/code \
       --token-uri https://oauth2.googleapis.com/token \
       --client-id nZ8JDrV8Hklf3JumewRl2ke3ovPZn5Ho \
       --scope "profile email"

As a consequence, "--provider" option and URI-specific options "--
auth-uri", "--dev-auth-uri", "--keys-uri", and "--token-uri" are
mutually exclusive.

"ipa idp-add" command does not currently use "--issuer-url" option URL
to dynamically look up other URIs through a well-known OIDC endpoints.
Some of the IdPs do not provide well-known OIDC endpoints URL at all.
This option can be used in "ipa idp-mod" to set "ipaIdpIssuerURL" LDAP
attribute. If this attribute is set, it will be passed to SSSD’s
"oidc_child" helper during actual authorization processing.

The "scope" is optional, and the "client_id" is mandatory.

The "client_secret" must not be displayed by "ipa idp-find" or "ipa
idp-show" commands unless permission ‘System: Read External IdP client
secret’ is assigned to the authenticated user running the command.


Pre-populated IdP templates
---------------------------

List of pre-populated IdP types is currently limited by the following
provider “names”:

* google

* github

* microsoft

* okta

* keycloak

Some IdP providers support parametrized URIs which include
organization or a realm name, or specific base URL, or both.

One notable omission in the pre-populated IdP types above is Gitlab.

FreeIPA only supports IdPs that implement OAuth 2.0 Device
authorization grant flow as defined by the RFC 8628. If required IdP
cannot be made to support Device authorization grant flow, it is
recommended to use OAuth 2.0 federation within an IdP that supports
this method. Gitlab does not support OAuth 2.0 Device authorization
grant flow and thus is not supported directly.

SSSD 2.7.0 implements Kerberos pre-authentication method "idp"
(registered as a pre-authentication type 152, PA-REDHAT-IDP-OAUTH2).
It relies on IPA KDB driver to provide a metadata in the Kerberos
principal entry to specify use of IdP. KDC side of the pre-
authentication method ‘idp’ then communicates with IPA "ipa-otpd"
daemon which reads external IdP reference object associated with the
user and spawns "oidc_child" helper from SSSD to complete
authorization.

"oidc_child" helper performs OAuth 2.0 device authorization grant
flow. Since it only supports a single authorization method, not all
endpoints are required. In order to authenticate users associated with
external IdPs to access IPA Web UI, a general OAuth 2.0 authorization
endpoint is required. Thus, this endpoint information is stored in the
object but not otherwise used by the Kerberos flow.


Google IdPs
~~~~~~~~~~~

Choosing "--provider=google" would expand to use the following
options:

+----------------------------------------------------+----------------------------------------------------+
| Option                                             | Value                                              |
|====================================================|====================================================|
| "--auth-uri"=URI                                   | "https://accounts.google.com/o/oauth2/auth"        |
+----------------------------------------------------+----------------------------------------------------+
| "--dev-auth-uri"=URI                               | "https://oauth2.googleapis.com/device/code"        |
+----------------------------------------------------+----------------------------------------------------+
| "--token-uri"=URI                                  | "https://oauth2.googleapis.com/token"              |
+----------------------------------------------------+----------------------------------------------------+
| "--userinfo-uri"=URI                               | "https://openidconnect.googleapis.com/v1/userinfo" |
+----------------------------------------------------+----------------------------------------------------+
| "--keys-uri"=URI                                   | "https://www.googleapis.com/oauth2/v3/certs"       |
+----------------------------------------------------+----------------------------------------------------+
| "--scope"=STR                                      | "openid email"                                     |
+----------------------------------------------------+----------------------------------------------------+
| "--idp-user-id"=STR                                | "email"                                            |
+----------------------------------------------------+----------------------------------------------------+


Github IdPs
~~~~~~~~~~~

Choosing "--provider=github" would expand to use the following
options:

+----------------------------------------------------+----------------------------------------------------+
| Option                                             | Value                                              |
|====================================================|====================================================|
| "--auth-uri"=URI                                   | "https://github.com/login/oauth/authorize"         |
+----------------------------------------------------+----------------------------------------------------+
| "--dev-auth-uri"=URI                               | "https://github.com/login/device/code"             |
+----------------------------------------------------+----------------------------------------------------+
| "--token-uri"=URI                                  | "https://github.com/login/oauth/access_token"      |
+----------------------------------------------------+----------------------------------------------------+
| "--userinfo-uri"=URI                               | "https://openidconnect.googleapis.com/v1/userinfo" |
+----------------------------------------------------+----------------------------------------------------+
| "--keys-uri"=URI                                   | "https://api.github.com/user"                      |
+----------------------------------------------------+----------------------------------------------------+
| "--scope"=STR                                      | "user"                                             |
+----------------------------------------------------+----------------------------------------------------+
| "--idp-user-id"=STR                                | "login"                                            |
+----------------------------------------------------+----------------------------------------------------+

Please note that Github explicitly states that a user login is not
unique and can be reused after a user account was deleted. The
configuration above aims for an easy setup for testing. If production
deployment with Github IdP would be required, it is recommended to
change "--idp-user-id" to a more unique subject like "id".
Unfortunately, Github UI does not give an easy way to discover a user
ID. Other IdPs also lack an easy way to resolve these internal
identifiers when not authorized by the user themselves.

For Github, user’s ID can be looked up without authentication through
the Users API. Assuming we have "curl" and "jq" utilities available, a
request to discover an ID of a Github user named "test" would look
like:

   $ curl --silent \
     -H "Accept: application/vnd.github.v3+json" \
     https://api.github.com/users/test | jq .id

   383316


Microsoft IdPs
~~~~~~~~~~~~~~

Microsoft Azure IdPs allow parametrization based on the Azure tenant
ID which can be specified with "--org" option to "ipa idp-add". If
support for live.com IdP is required, specify organization ID
"common", e.g.

   ipa idp-add LiveCom --provider microsoft --org common --client-id some-client-id

Choosing "--provider=microsoft" would expand to use the following
options. A string "${ipaidporg}" will be replaced by the value of "--
org" option.

+----------------------------------------------------+----------------------------------------------------+
| Option                                             | Value                                              |
|====================================================|====================================================|
| "--auth-uri"=URI                                   | "https://login.microsoftonline.com/${ipaidporg}/o  |
|                                                    | auth2/v2.0/authorize"                              |
+----------------------------------------------------+----------------------------------------------------+
| "--dev-auth-uri"=URI                               | "https://login.microsoftonline.com/${ipaidporg}/o  |
|                                                    | auth2/v2.0/devicecod e"                            |
+----------------------------------------------------+----------------------------------------------------+
| "--token-uri"=URI                                  | "https://login.microsoftonline.com/${ipaidporg}/o  |
|                                                    | auth2/v2.0/token"                                  |
+----------------------------------------------------+----------------------------------------------------+
| "--userinfo-uri"=URI                               | "https://graph.microsoft.com/oidc/userinfo"        |
+----------------------------------------------------+----------------------------------------------------+
| "--keys-uri"=URI                                   | "https://login.microsoftonline.com/common/discove  |
|                                                    | ry/v2.0/keys"                                      |
+----------------------------------------------------+----------------------------------------------------+
| "--scope"=STR                                      | "openid email"                                     |
+----------------------------------------------------+----------------------------------------------------+
| "--idp-user-id"=STR                                | "email"                                            |
+----------------------------------------------------+----------------------------------------------------+


Okta IdPs
~~~~~~~~~

Upon registration of a new organization in Okta, a new base URL is
associated with it. It can be specified with "--base-url" option to
"ipa idp-add":

   ipa idp-add MyOkta --provider okta --base-url dev-12345.okta.com --client-id some-client-id

Choosing "--provider=okta" would expand to use the following options.
A string "${ipaidpbaseurl}" will be replaced by the value of "--base-
url" option.

+----------------------------------------------------+----------------------------------------------------+
| Option                                             | Value                                              |
|====================================================|====================================================|
| "--auth-uri"=URI                                   | "https://${ipaidpbaseurl}/oauth2/v1/authorize"     |
+----------------------------------------------------+----------------------------------------------------+
| "--dev-auth-uri"=URI                               | "https://${ipaidpbaseurl}/oauth2/v1/device/author  |
|                                                    | ize"                                               |
+----------------------------------------------------+----------------------------------------------------+
| "--token-uri"=URI                                  | "https://${ipaidpbaseurl}/oauth2/v1/token"         |
+----------------------------------------------------+----------------------------------------------------+
| "--userinfo-uri"=URI                               | "https://${ipaidpbaseurl}/oauth2/v1/userinfo"      |
+----------------------------------------------------+----------------------------------------------------+
| "--scope"=STR                                      | "openid email"                                     |
+----------------------------------------------------+----------------------------------------------------+
| "--idp-user-id"=STR                                | "email"                                            |
+----------------------------------------------------+----------------------------------------------------+


Keycloak IdPs
~~~~~~~~~~~~~

Keycloak allows defining multiple realms (organizations). Since it is
often a part of a custom deployment, both base URL and realm ID are
required and can be specified with "--base-url" and "--org" options to
"ipa idp-add":

   ipa idp-add MySSO --provider keycloak \
       --org master --base-url keycloak.$DOMAIN:$PORT/prefix \
       --client-id some-client-id

Quarkus version of the Keycloak 17 or later has removed "/auth/" part
of the URI. If your deployment is using non-Quarkus distribution of
the Keycloak, "--base-url" would need to include "/auth/" component as
well.

Choosing "--provider=keycloak" would expand to use the following
options. A string "${ipaidpbaseurl}" will be replaced by the value of
"--base-url" option. A string "${ipaidporg}" will be replaced by the
value of "--org" option.

+----------------------------------------------------+----------------------------------------------------+
| Option                                             | Value                                              |
|====================================================|====================================================|
| "--auth-uri"=URI                                   | "https://${ipaidpbaseurl}/realms/${ipaidporg}/pro  |
|                                                    | tocol/openid- connect/auth"                        |
+----------------------------------------------------+----------------------------------------------------+
| "--dev-auth-uri"=URI                               | "https://${ipaidpbaseurl}/realms/${ipaidporg}/pro  |
|                                                    | tocol/openid- connect/auth/device"                 |
+----------------------------------------------------+----------------------------------------------------+
| "--token-uri"=URI                                  | "https://${ipaidpbaseurl}/realms/${ipaidporg}/pro  |
|                                                    | tocol/openid- connect/token"                       |
+----------------------------------------------------+----------------------------------------------------+
| "--userinfo-uri"=URI                               | "https://${ipaidpbaseurl}/realms/${ipaidporg}/pro  |
|                                                    | tocol/openid- connect/userinfo"                    |
+----------------------------------------------------+----------------------------------------------------+
| "--scope"=STR                                      | "openid email"                                     |
+----------------------------------------------------+----------------------------------------------------+
| "--idp-user-id"=STR                                | "email"                                            |
+----------------------------------------------------+----------------------------------------------------+


User-specific options
~~~~~~~~~~~~~~~~~~~~~

Similar to RADIUS proxy support, external IdP authentication only
triggered when the following three conditions are true:

* user entry contains external IdP reference

* user entry has external IdP user name (subject) set

* either global user authentication type or per-user authentication
  type includes "idp" method

Therefore, two additional attributes available for User object in IPA
API:

+----------------------------------------------------+----------------------------------------------------+
| Option                                             | Description                                        |
|====================================================|====================================================|
| "--idp"=STR                                        | External IdP object reference                      |
+----------------------------------------------------+----------------------------------------------------+
| "--idp-user-id"=STR                                | External IdP’s user name (subject)                 |
+----------------------------------------------------+----------------------------------------------------+

External IdP user name format is specific to individual IdPs. Most
OAuth 2.0 IdPs provide "email" scope to retrieve email associated with
the OAuth 2.0 resource owner (user) subject. Pre-populated IdP
templates described above include "email" scope by default. If
external IdP references were created with the help of these templates,
user’s email can be set to "--idp-user-id" to match the resource owner
subject. Please read Security section for details on when this is
inappropriate.

Below is an example how to associate a user account with a specific
IdP reference:

   ipa user-mod test --user-auth-type=idp --idp google --idp-user-id test@example.test


WebUI
-----

The IdP server objects need to be accessible in the WebUI. A new tab
for IdP will be created in the “Authentication” section, similar to
the existing *RADIUS servers* tab. It will contain a table with the
list of IdP objects, enabling to add or delete an object. Each object
must be clickable in order to be edited.

In the *settings* page for IdP objects, all the properties must be
editable but the client secret must not be displayed unless the
authenticated user permitted with the help of the "System: Read
External IdP client secret" permission.

In order to change the client secret, the admin will click on
*Actions:Reset secret*.

Web UI for user entry attributes have to be extended to include both
external IdP reference and IdP user subject.


Access control
==============

Specific permissions need to be created to protect access to the IdP
objects:

* System: Add External IdP

* System: Read External IdP (does not provide access to the client
  secret)

* System: Modify External IdP

* System: Delete External IdP

* System: Read External IdP client secret

Specific Privileges:

* External IdP Administrator: all the permissions related to external
  idp except *System: Read External IdP client secret*

Any user who is a member of the admins group will be able to manage
the IdP server objects.


Security
========

IPA objects representing external IdP references are OAuth 2.0
clients. The client ID and client secret details can be used to
impersonate OAuth 2.0 client. For public OAuth 2.0 client access to
Client ID is enough. It is not considered secure as it is typically a
part of the HTTP redirect in OAuth 2.0 authorization flows.

Client secret has to be protected as it is typically used for private
(trusted) OAuth 2.0 clients.

Kerberos pre-authentication method "idp" relies on the user subject
("ipaIdpSub" LDAP attribute) defined in the LDAP object entry to match
the Kerberos principal and the OAuth 2.0 resource owner. When "openid"
OAuth 2.0 scope is used, this typically maps to "sub" value. Since
there are no ways to pull this value for all users in advance, pre-
populated IdP templates set OAuth 2.0 scopes to include "email" and
then use "email" to map IdP subject where possible. There are some
well-known IdPs which allow reuse of user accounts and emails, this
applies to both Github and Gitlab. Since Gitlab does not support OAuth
2.0 Device authorization grant flow, it is not an issue in itself for
this project. However, for Github it is known that user accounts can
be recycled after their removal. In this case we would recommend to
use internal Github identifier instead.


Upgrade and backward compatibility
==================================

As the new object class and attribute types are added in the LDAP
schema, there is no need for a specific upgrade task. Upon normal
upgrade, the new schema is applied and replicated to the other
servers, even if they are installed with a version which does not
define the new object class / attribute types.

In a mixed topology containing old and new servers, the new API is
provided only by the new servers.
