2. Authenticating with your OIDC client and fetching collab user info
Requirement
You should read this documentation to understand the concept of Authentication and Authorization with OIDC and OAuth2 before trying to implement it.
Abstract
In order to create an OIDC client, see 1. Registering an OIDC client. After creating the OIDC client, you have a corresponding access token and secret.
For the example below, we consider the case of someone wanting to provide access to https://www.getpostman.com as an app for Collaboratory users to access from their collabs. You should replace that URL by the one of your own app.
The redirect_uri is set with the URL of your application to which your users will be redirected after having been authenticated by their EBRAINS account. For example when you login to this wiki, the redirect URI is https://wiki.ebrains.eu/*
The whole authentication flow presented here is based on the official OAuth2 RFC described in the section 4.1.
https://tools.ietf.org/html/rfc6749#section-4.1
Authentication flow
Authorization Code Request
The first step of the authentication protocol is to fetch an authorization code for your client and your user. This is done by directing your users to the URL of the EBRAINS login page (IAM) where they can enter their username and password.
Request
The authorization code is fetched by an HTTP request:
/GET: https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/auth
with the following parameters:
- response_type=code
- login=true
- client_id=community-apps-tutorial
- redirect_uri=https://www.getpostman.com/oauth2/callback
- scope=openid+group+team
with the italics indicating the fields you customize for your own app. The URL will look like:
https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/auth?response_type=code&login=true&client_id=community-apps-tutorial&redirect_uri=https://www.getpostman.com/oauth2/callback&scope=openid+group+team
The scope parameter can include a combination of several values. Each user will be asked to consent to sharing that scope with your app upon first access.
- openid: This scope is required because we use the OIDC protocol. It will give your app access to the user's basic information such as username, email and full name.
- profile (optional): More information on user if provided by the user
- email (optional): The verified email of the user, should be add in addition of openid and/or profile to get the email.
- group (optional): If you request this scope, the future access token generated will authorize your app to identify which units and groups the user belongs to.
- team (optional): This scope is like the group scope lets your app identify the permissions of the user, but by identifying what collabs the user has access to and with what roles.
- clb.wiki.read (optional): access to GET Collab API
- clb.wiki.write (optional): access to DELETE/PUT/POST Collab API
- collab.drive (optional): access to GET/POST/PUT/DELETE drive API
- offline_access (optional): See description here
- quota : User's quota usage around EBRAINS services
The group and team scopes are a simple way for your app to grant permissions to its services and resources when you want to grant access to a very few units, groups, or collab teams. For more complex permission management, contact support.
Response
Once the user has logged in, your app gets an HTTP 301 redirection followed by an HTTP 200 success response with an authorization code inside. A typical response might look like:
https://www.getpostman.com/oauth2/callback?session_state=a0ff8a68-2654-43ef-977a-6c15ce343546&code=f3f04f93-hbp-482d-ac3d-demo.turtorial.7122c1d9-3f7e-4d80-9c4f-dcd244bc2ec7
The authorization code is the part in bold in the response above.
Access Token Request
Now that your app has the authorization code for a user, it can fetch the user ID Token and Access Token
Request
/POST: https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/token
with the following parameters:
- grant_type: authorization_code
- code: f3f04f93-hbp-482d-ac3d-demo.turtorial.7122c1d9-3f7e-4d80-9c4f-dcd244bc2ec7
- redirect_uri: https://www.getpostman.com/oauth2/callback
- client_id: community-apps-tutorial
- client_secret: your client secret obtained during client creation
The image below shows a sample POST request generated from the Postman tool. [The fact that this page is based on getpostman.com as an example is pure coincidence.]
Response
200 OK
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi...pP5vaNwvvsaNGEA",
"expires_in": 604773,
"refresh_expires_in": 604773,
"refresh_token": "eyJh...vC5eIR1rNhRJ4d8",
"token_type": "bearer",
"id_token": "eyJ...YOwdQ",
"not-before-policy": 0,
"session_state": "76e553bf-ba2e-45b6-8c6c-c867772b40ec",
"scope": "openid"
}
Your app gets a response containing the access token, the refresh token, the id token and other information. The ID Token should be use by developer on their backend to read user informations such as username, first name, last name etc. The ID Token should be use internally, into your app only, the app which triggered the authentication. The access token will be use to reach APIs, the access token can be see as a card to access an ATM. ID Token is for Authentication, Access token is for Authorization. Refresh token is to re-ask a valid access token after expiration.
Access user info
Now that your app has the access token of a user, it can fetch the user's info.
Request
/GET: https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/userinfo
with the following parameters:
- Authorization: the access token preceded by the string "Bearer "
The image below shows a sample GET request generated from the Postman tool. [The fact that this page is based on getpostman.com as an example is pure coincidence.]
Response
As response your app receives a JSON with all the information about the logged user
{
"sub": "fa2db206-3...0ebaba98e1",
"preferred_username": "demoaccount",
"unit": [
"/all/institutions/switzerland/epfl",
"/all/projects/hbp/consortium/SGA2/SP05",
"/all/projects/hbp/consortium/SGA3/WP6/T6_11"
],
"roles": {
"jupyterhub": [
"feature:authenticate"
],
"xwiki": [
"feature:authenticate"
],
"team": [
"collab-collaboratory-community-apps-editor"
],
"group": [
"group-collaboratory-developers",
"unit-all-projects-hbp-consortium-sga2-sp05-administrator"
]
},
"mitreid-sub": "30...62"
}
The unit field above lists Collaboratory Units which the user is a member of, with the unit name using slashes instead of the colons you see in the Collaboratory UI.
jupyterhub and xwiki are OIDC clients with more advanced permission management.
The team field above lists Collaboratory Teams which the user is a member of, in the form "collab-collabname-role" where role is one of admin, editor, or viewer according to the user's role in collab collabname.
The group field above lists Collaboratory Groups which the user is a member of, in the form "group-groupname". It also lists Collaboratory Units which the user is an admin of, in the form "unit-unitname-administrator" with unitname using dashes instead of the colons you see in the Collaboratory UI.
Which unique identifier should you use for your users
We recommend that you use the "preferred_username" field (i.e. the EBRAINS username) to identify your users for every new user in your service. The "preferred_username" is used as a unique identifier in The Collaboratory, it's the more user friendly way to reference users and interact easily with the Identity Manager (IDM) API. We are guarantying its unicity.
You can also use the "sub" field provided by Keycloak which is the OIDC/OAuth2 standard unique identifier for a user and might be considered as a good choice. If in addition you store the username associated to the sub, you will still be able to easily use the Collaboratory IDM API; otherwise, you might need to perform an additional API request to get the User object from a given sub in the IDM API
You can find additional information about userinfo fields and the difference with previous Collaboratory 1 here. This will also help you if you want to keep compatibility with your previous OIDC implementation in Collaboratory 1.
OIDC Plugin configuration example
You will probably not reinvent the wheel and use an existing OIDC Library to establish the authentication. Often theses libraries do all the above flow for you; you just have to fill a property file.
OIDC Plugin Properties: useful link
All the links you need to fill the property file of your OIDC plugin are visible here:
https://iam.ebrains.eu/auth/realms/hbp/.well-known/openid-configuration
OIDC Plugin for Python: mozilla-django-oidc
We are using this plugin with our Django based project, a full documentation is available here
https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html
Github demo project using mozilla-django-oidc
You can find a very basic empty Django project using mozilla-django-oidc on this github:
https://github.com/HumanBrainProject/mozilla-django-oidc-demo
Clone it and follow the README instructions. In a few minutes, if you already have an OIDC clientId and clientSecret available, you should be able to login with this demo application. Once you have validated that your client works well, you can compare with your own code and debug your own code to fix potential authentication issues.
In the future, we will add a branch in this github project with advanced usage examples such as overriding the default mozilla-django-oidc class and to fetch the teams, groups and units of a user.
For now, you can find more advanced usage in the CodeJam#12 presentation regarding OIDC integration for Python.
You can see every function of the original mozilla-django-oidc on that github project, so you can better understand what you can override and how:
https://github.com/mozilla/mozilla-django-oidc/blob/master/mozilla_django_oidc/auth.py
mozilla-django-oidc sample config
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/auth"
OIDC_OP_TOKEN_ENDPOINT = "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/token"
OIDC_OP_USER_ENDPOINT = "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/userinfo"
OIDC_RP_SIGN_ALGO ="RS256"
OIDC_OP_JWKS_ENDPOINT="https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/certs"
LOGIN_REDIRECT_URL = '/'
OIDC_STORE_ACCESS_TOKEN = True
# Store secret somewhere on your system
OIDC_RP_SCOPES="openid profile email"
OIDC_RP_CLIENT_ID = os.environ['OIDC_RP_CLIENT_ID']
OIDC_RP_CLIENT_SECRET = os.environ['OIDC_RP_CLIENT_SECRET']
# Theses one are situational, you problably don't need them, it's to use in the case you want to override an action in a python class, for exemple execute some code after the callback
OIDC_CALLBACK_CLASS = 'clb_auth.views.OIDCAuthenticationCallbackView'
OIDC_DRF_AUTH_BACKEND = 'clb_auth.auth.OIDCBearerAuthenticationBackend'
OIDC_OP_LOGOUT_URL_METHOD = 'clb_auth.auth.logout_url'
OIDC Plugin for Python: flask-oidc
https://flask-oidc.readthedocs.io/en/latest/
You will need at the root of your project a "client_secrets.json" file.
client_secrets.json
"web": {
"client_id": "yourClientId",
"client_secret": "yourClientSecret",
"redirect_uris": [
"http://localhost:8081/",
"https://yourappurl.url/"
],
"auth_uri": "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/auth",
"token_uri": "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/token",
"userinfo_uri": "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/userinfo",
"issuer": "https://iam.ebrains.eu/auth/realms/hbp"
}
}