OpenID Connect Federation on IBM Bluemix Private Cloud
- What is Keystone Federation?
- What is OpenID Connect (OIDC)?
- How does OpenID Protocol work?
- Setting up OIDC
- Managing Groups
- Managing Mappings
- Using Horizon
- Using OAuth API
- Using OAuth CLI
- Upgrading python-openstackclient package
- Additional Notes
What is Federation?
Keystone federation enables identities from an Identity Provider (IDP) to be used on a Service Provider (SP). The different protocols and formats supported are OpenIDConnect (OIDC) and Security Assertion Markup Language (SAML).
The IDP stores and manages the user’s credentials and sends claims or assertions to the SP. This allows the customer to use their enterprise credentials to authenticate to a Bluemix Private Cloud without sending their password to their Bluemix Private Cloud. Keystone is usually always the SP since it consumes identities from external resources.
What is OpenID Connect (OIDC)?
OpenID
is a protocol for authentication.
OAuth
is a protocol for authorization.
OpenID Connect
does both.
OpenID Connect is a simple identity layer on top of the OAuth 2.0 protocol. It lets Clients verify the identity of an End-User, based on the authentication performed by an Authorization Server. It also lets Clients obtain basic profile information about the End-User, in an interoperable and REST-like manner.
This document describes a mechanism for an OpenID Connect relying party to discover the End-User’s OpenID Provider, and to obtain information needed to interact with it, including its OAuth 2.0 endpoint locations.
How does OpenID Protocol work?
- The relying party sends the request to the OpenID provider to authenticate the End-User.
- The OpenID provider authenticates the user.
- The OpenID provider sends the ID token and access token to the relying party.
- The relying party sends a request to the user info endpoint with the access token received from OpenID provider.
- The user info endpoint returns the claims.
Setting up OIDC
A Blue Box operator will need to work with an IDP integration engineer from the customer’s company (for example purposes, Company XYZ
) to set up OIDC.
The Bluemix Private Cloud operator will need the following information:
- Authorized Redirect/Callback URL
- Client ID and Client Secret
- User Claim - OIDC Claim
- The well-known/configuration URL which provides all the information needed for access to the provider
- Default/initial Mapping - The documentation on mappings can be found here: http://docs.openstack.org/developer/keystone/federation/federated_identity.html#mapping-combination
The Bluemix Private Cloud operator should work with the integration engineer to create the initial/default mapping. The mapping provides the way in which the remote users on the IDP are mapped to local groups on the SP. The rest of this document may help clarify what a mapping is and how it’s used.
After the Bluemix Private Cloud operator is finished integrating Company XYZ IDP
with Keystone, a mapping called mapping-for-company-xyz-idp
should be created (or a similar mapping name with the IDP name in it).
The steps for managing the mapping is:
1. Manage groups (Federated users will have the same access rights as the group)
2. Manage the mappings
Managing Groups
Keystone Federation works by federating Identity Provider users to a group on the Service Provider. The group is granted access by assigning that group a role on a project. The available roles are as follows:
project_admin
- Theadmin
role for the project.cloud_admin
- Theadmin
role for the cloud instance._member_
- A member of a project.heat_stack_owner
- An owner for heat stacks.
To list groups:
$ openstack group list
To create a group:
$ openstack group create --help
$ openstack group create <new group name here>
To list roles:
$ openstack role list
To list role assignments:
$ openstack role assignment list --name
To list projects:
$ openstack project list
For help looking at adding role assignment:
$ openstack role add --help
To add a role assignment for a group to have a role on a project:
$ openstack role add <role> --project <project> --group <group>
Federated users are mapped into groups on the Service Provider. Here’s the order of operations for granting access for groups:
-
First, create or edit the group.
-
Then, create or edit the role assignments for groups on projects.
Example 1. Creating a company_xyz_admin_group
group.
$ source cloud_adminrc # Your openstack credentials file
$ openstack group list
$ openstack group create company_xyz_admin_group --or-show
$ openstack role assignment list --name --group company_xyz_admin_group
Next, create the role assignment if it doesn’t exist.
These commands add the role cloud_admin
for the group cloud_admin
for the “demo” project.
$ openstack role add cloud_admin --group company_xyz_admin_group --project demo
$ openstack role assignment list --name --group company_xyz_admin_group
After executing the openstack role assignment list
command, you should see an output similar to this one:
+-------------+------+---------------------------------+--------------+--------+-----------+
| Role | User | Group | Project | Domain | Inherited |
+-------------+------+---------------------------------+--------------+--------+-----------+
| cloud_admin | | company_xyz_admin_group@Default | demo@Default | | False |
+-------------+------+---------------------------------+--------------+--------+-----------+
Example 2. Creating a group with member access.
$ source cloud_adminrc #Your openstack credentials file
$ openstack group list
$ openstack group create company_xyz_member_group --or-show
$ openstack role assignment list --name --group company_xyz_member_group
$ openstack role add _member_ --group company_xyz_member_group --project demo
$ openstack role assignment list --name --group company_xyz_member_group
After entering the openstack role assignment list
command you should see an output similar to this one:
+----------+------+----------------------------------+--------------+--------+-----------+
| Role | User | Group | Project | Domain | Inherited |
+----------+------+----------------------------------+--------------+--------+-----------+
| _member_ | | company_xyz_member_group@Default | demo@Default | | False |
+----------+------+----------------------------------+--------------+--------+-----------+
Managing Mappings
Below, we are using the example of Company XYZ IDP
and supposing that a mapping for it already exists. A user with cloud_admin
access (user or role) will be able to edit the mapping. The user will not be able to create or delete mappings.
Mappings can be managed on Horizon in the Identity Dashboard -> Federation -> Mappings -> Edit
The openstack mapping list
command shows a list of mappings. It should show the mapping-for-company-xyz-idp
mapping.
The openstack mapping show mapping-for-company-xyz-idp -f json
command shows the details of the given mapping. It should contain a json
listing of the rules for the mapping.
The openstack mapping set mapping-for-company-xyz-idp --rules newmappings.json
command updates and sets the mapping.
The mappings file should follow the format given here:
[
{
"local": [
<group>
],
"remote": [
<condition>
]
}
]
The rules file should contain a list of rules that map the remote users to a local group. The local section contains a rule specifying the group that the remote users should map to. The local group is required.
{
"group": {
"domain": {
"name": "Default"
},
"name": "company_xyz_admin_group"
}
}
The remote section includes rules that specify which remote users should be allowed access.
{
"type": "group",
"any_one_of": [
"admin_group"]
}
The possible user attributes types that can be given in the remote rules are these:
The possible actions to filter on remote attributes are:
empty
any_ony_of
not_any_of
blacklist
whitelist
To set the username we can use {0} in the local section to indicate to use a field from the remote section. Then we can additional information to the new Federated username. We can reference the remote section in the local section by index (“{i}”, where i is the index of the remote field):
[
{
"local": [
{
"user": {
"name": "CompanyXYZ/{0}"
}
},
...
],
"remote": [
{
"type": "HTTP_OIDC_CLAIM_EMAIL"
},
...
]
}
]
The section that follows contains some example scenarios:
Example 1. Using an email address to create a username on Keystone
In this example, we are mapping users from the Company XYZ IDP and allowing any user from the admin_group
. The user’s email address will be used to create a user on Keystone. If the user’s email is test@test.ibmcloud.com
, then the resulting username would be CompanyXYZ/test@test.ibmcloud.com
.
[
{
"local": [
{
"user": {
"name": "CompanyXYZ/{0}"
}
},
{
"group": {
"domain": {
"name": "Default"
},
"name": "company_xyz_admin_group"
}
}
],
"remote": [
{
"type": "HTTP_OIDC_CLAIM_EMAIL"
},
{
"type": "HTTP_OIDC_CLAIM_GROUP",
"any_one_of": [
"admin_group"
]
}
]
}
]
Example 2. Multiple Groups
In the following example we are using multiple groups.
[
{
"local": [
{
"user": {
"name": "CompanyXYZ/{0}"
}
},
{
"group": {
"domain": {
"name": "Default"
},
"name": "company_xyz_admin_group"
}
}
],
"remote": [
{
"type": "HTTP_OIDC_CLAIM_EMAIL"
},
{
"type": "HTTP_OIDC_CLAIM_GROUP",
"any_one_of": [
"admin_group"
]
}
]
},
{
"local": [
{
"user": {
"name": "CompanyXYZ/{0}"
}
},
{
"group": {
"domain": {
"name": "Default"
},
"name": "company_xyz_member_group"
}
}
],
"remote": [
{
"type": "HTTP_OIDC_CLAIM_EMAIL"
},
{
"type": "HTTP_OIDC_CLAIM_GROUP",
"any_one_of": [
"member_group"
]
}
]
}
]
Using Horizon
The OpenStack dashboard on the Identity Provider should have a drop-down menu labeled Authenticate with SSO.
-
The dropdown menu should show a label for the IdP. Select the IDP and click Connect.
-
You will then be redirected to the IDP log in page.
-
After successfully authenticating with the IDP, you should be redirected to the Horizon page.
Using OAuth API
User credentials are never sent to Keystone directly, but they are sent to the OAuth provider endpoints.
Steps to run the following code:
-
Install Pre-Requisites:
request-oauthlib
, Keystone client, and Nova client -
Source OIDC stackrc file
-
python filename
Example 3. OIDC stackrc
export OS_AUTH_URL=https://<fqdn>:5000/v3
export OS_IDENTITY_PROVIDER=<identity_provider name>
export OS_PROTOCOL=<protocol_name>
export OS_REDIRECT_URI=https://<fqdn>:5000/v3/auth/OS-FEDERATION/websso/oidc/redirect
export OS_CLIENT_ID=<client_ID>
export OS_CLIENT_SECRET=<client_secret>
export OS_DISCOVERY_ENDPOINT=https://<FQDN-OP>/.well-known/openid-configuration
export OS_TOKEN_ENDPOINT=https://<FQDN-OP>/oauth/token
export OS_AUTHORIZATION_ENDPOINT=https://<FQDN-OP>/authorize
export OS_PROJECT_ID=<project_id>
Example 4. Using API for authorization code flow
This file is oidc-authflow.py
from keystoneauth1.identity.v3 import oidc
from keystoneauth1 import session
from requests_oauthlib import OAuth2Session
from keystoneauth1.identity.v3 import Token
from novaclient import client
import os
def main():
auth_url = os.environ.get('OS_AUTH_URL')
identity_provider = os.environ.get('OS_IDENTITY_PROVIDER')
protocol = 'oidc'
redirect_uri = os.environ.get('OS_REDIRECT_URI')
scope = ['openid','profile','email']
client_id = os.environ.get('OS_CLIENT_ID')
client_secret = os.environ.get('OS_CLIENT_SECRET')
discovery_endpoint = os.environ.get('OS_DISCOVERY_ENDPOINT')
access_token_endpoint = os.environ.get('OS_TOKEN_ENDPOINT')
authorization_endpoint = os.environ.get('OS_AUTHORIZATION_ENDPOINT')
project_id = os.environ.get('OS_PROJECT_ID')
oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope)
authorization_url, state = oauth.authorization_url(authorization_endpoint,
access_type='offline',
approval_prompt='force')
print( 'Please go to %s and authorize access' % authorization_url)
authorization_code = raw_input('Enter the authorization code here: ')
s = session.Session(verify=False)
oidc_plugin = oidc.OidcAuthorizationCode(auth_url, identity_provider,
protocol, client_id=client_id,
client_secret=client_secret,
access_token_endpoint=access_token_endpoint,
discovery_endpoint=discovery_endpoint,
access_token_type='access_token',
redirect_uri=redirect_uri,
code=authorization_code)
unscoped_auth_token = oidc_plugin.get_unscoped_auth_ref(s)
scoped_token = Token(auth_url=auth_url,
token=unscoped_auth_token._auth_token,
project_id=project-id,
reauthenticate=False)
federated_session = session.Session(auth=scoped_token)
nova = client.Client(version=2, session=federated_session)
for server in nova.servers.list():
print server.name
if __name__ == '__main__':
main()
Using API for password flow
This file is oidc-passflow.py
from keystoneauth1 import session
from keystoneauth1.identity.v3 import oidc
from keystoneauth1.identity.v3 import Token
from keystoneclient.v3.client import Client
from novaclient import client
import os
auth_url = os.environ.get('OS_AUTH_URL')
identity_provider = os.environ.get('OS_IDENTITY_PROVIDER')
protocol = 'oidc'
redirect_uri = os.environ.get('OS_REDIRECT_URI')
scope = ['openid','profile','email']
client_id = os.environ.get('OS_CLIENT_ID')
client_secret = os.environ.get('OS_CLIENT_SECRET')
discovery_endpoint = os.environ.get('OS_DISCOVERY_ENDPOINT')
access_token_endpoint = os.environ.get('OS_TOKEN_ENDPOINT')
authorization_endpoint = os.environ.get('OS_AUTHORIZATION_ENDPOINT')
project_id = os.environ.get('OS_PROJECT_ID')
def get_project_and_federated_session():
s = session.Session(verify=False)
# get an access token
payload = {'client_id': client_id, 'grant_type': 'password',
'username': '<username>',
'password': '<password>', 'scope': 'openid',
'connection':'Username-Password-Authentication'}
response = s.post('https://<FQDN-OP>/oauth/ro',
data=payload,
authenticated=False)
print(response.json())
access_token = response.json()['access_token']
oidc_plugin = oidc.OidcAccessToken(auth_url, identity_provider, protocol, access_token)
response = oidc_plugin.get_unscoped_auth_ref(s)
scoped_token = Token(auth_url=auth_url,
token=response._auth_token,
project_id=project_id,
reauthenticate=False)
federated_session = session.Session(auth=scoped_token)
return federated_session
def main():
scoped_session = get_project_and_federated_session()
nova = client.Client(version=2, session=scoped_session)
for server in nova.servers.list():
print server.name
if __name__ == '__main__':
main()
Using OAuth CLI
Authorization code flow for Command line
Required: version of python-openstackclient > 3.0.0
Check the version by running the following command :
pip freeze | grep openstackclient
Steps to use the OpenStack client with authorization code:
-
Create a file called
oidcrc
-
Paste the following contents into the file. Update the parameters based on your environment.
OIDC stackrc
export OS_TOKEN_ENDPOINT=https://<replace>/oauth/token export OS_CLIENT_SECRET=<client secret> export OS_CLIENT_ID=<client_id> export OS_DISCOVERY_ENDPOINT=https://<replace>/.well-known/openid-configuration export OS_IDENTITY_PROVIDER=auth0 export OS_REDIRECT_URI=<redirect_uri> export OS_IDENTITY_API_VERSION=3 export OS_DOMAIN=Default export OS_PROTOCOL=oidc export OS_AUTH_URL=https://<FQDN>:5000/v3 export OS_USERNAME=<username> export OS_AUTHORIZATION_ENDPOINT=https://<replace>/authorize
-
Get an authorization code from your Identity Manager.
-
The following command fetches a Keystone token that you can use to perform any command till it expires:
openstack --os-auth-type v3oidcauthcode --os-code <auth-code> --os-project-name <project-name> --os-project-domain-name Default token issue
Example:
root@allinone-multiple:~# openstack --os-auth-type v3oidcauthcode --os-code 6LxfzO7GvCjmmcRa --os-project-name admin --os-project-domain-name Default token issue
+------------+----------------------------------+
| Field | Value |
+------------+----------------------------------+
| expires | 2017-01-18 17:02:32.432419+00:00 |
| id | 7c43310f774f4401b5ec6abee8aa3d77 |
| project_id | 2abdd5e144ad490294f20f9efb9968e7 |
| user_id | 6f81cf8c63fc49d1a12f8fd4f75cab82 |
+------------+----------------------------------+
Perform user list
command using the token just generated.
openstack --os-auth-type v3token --os-token <token id from previous request> --os-project-name <project-name> --os-project-domain-name Default user list
NOTE:
Client ID and Secret can be passed as parameters in the OpenStack CLI instead of saving it in the rc
file by using the parameters –os-client-secret and –os-client-id
Upgrading python-openstackclient version
Run the following commands:
sudo su
pip install python-openstackclient --upgrade
Additional Notes
Currently an issue exists with federated users and with the trustor/trustee feature in Keystone, which prevents
the federated Heat user from delegating their heat_stack_owner
role.
You’ll need to use a workaround for the trust delegation by assigning the heat_stack_owner
role to the user directly. This is the user that is created during federated log in. The user’s domain is federated, and it could have the value of None (Federation=None
).
$ openstack user list
The federated user should have a domain of None: openstack user show *federated user*
Here we require the user ID:
$ openstack role add heat_stack_owner --user *federated user ID* --project *project*
Another known issue is that a federated user is presented with the option to change their password in the settings page. They will not be able to change their password.
Also, please note that federated users should not create local users. Federated admins may not be able to change a local user’s password.