Category: Integration

Best Django Openidc Package

I think we know the security benefits and the development benefits of using a delegated authentication protocol like OpenIDC or SAML.
However, actually doing the integration in the application can be difficult at times.

There is a lack of documentation and guidance on the best modules or packages to use for the various frameworks.
In this post I will be looking at the options available for Django when authenticating with an OpenIDC provider - Keycloak.

Using Django as an OpenIDC provider is not what I want - although it is possible with the django openidc provider package

First port of call is an anonymous web search and checking django packages authentication. Django packages seems to put everything in the auth basket where we specifically want to look at OpenIDC clients.

What we are Looking For

  • Supports or is compatible with Django 3
  • Explicit OpenIDC support (Oauth 2 is not good enough we also need the identity)
  • Good Documentation
  • A client that is not closely bound to a specific provider - but that is closely bound to the protocol specification.
  • Integrated with django admin - ie. logging in from django admin redirects
  • A client that can use django's existing permissions and a nice way to integrate with the provider maybe by way of groups...

OpenIDC Django Packages we are reviewing

If there is no decent documentation the package is discarded from review

There are ways to integrate apache and nginx with OpenIDC - the problem is making use of django's permission. Will that still be possible.

Method of Testing

We going to create a blank django project, add a model (Beer) and 2 django groups. The first group "customers" can view the beers - as staff on the admin site. The second group "creators" can add, view and change beers on the admin site.

Django Admin SSO

Decent - although documentation is lacking and it defaults to google SSO. You have to check the example settings and change it to keycloak.
Initially it did not get the id token as scope was set to email.
I had to edit the code and change the scope to opendic.
Then it would only progress is email_verified was True...so I had to edit that code as well.

Weirdly in the changelog it mentions:

Using OpenID is now deprecated and OpenID support will be removed in a future release

OpenIDC is the future.

This package contains migrations that create the assignments table.

Problems

The problem with this library is that it forces assignments to be created before users are allowed to login. An assignment maps an OpenIDC identity to a local user - that must already exist.

This increases the administrative burden and does not do what openIDC intends - delegating auth (and groups) to the identity provider.

Conclusion

A decent lightweight choice - purely for admin login. Just a bit of admin on setting up a user and an assignment.

Django Social Registration

Requires 'django.contrib.sites'.

Conclusion

Excluded. Too old and not compatible with Django 3.

Python Social Auth

The repo looks very old in terms of recent updates on github.
However the social-core is where most of the updates happen.

Social auth core is a dependecy of the project:

social-auth-app-django==4.0.0
social-auth-core==4.1.0

The docs are not updated for keycloak settings but they are documented in the code:

AUTHENTICATION_BACKENDS = (
    'social_core.backends.keycloak.KeycloakOAuth2',
    'django.contrib.auth.backends.ModelBackend',
)

SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['username', 'first_name', 'email']

# OpenIDC URL
SOCIAL_AUTH_KEYCLOAK_KEY = 'test-django-oidc'
SOCIAL_AUTH_KEYCLOAK_SECRET = 'a7a41-245e-...'
SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = \
    'MIIBIjANBxxxdSD'
SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = \
    'https://iam.example.com/auth/realms/voxcloud-staff/protocol/openid-connect/auth'
SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = \
    'https://iam.example.com/auth/realms/voxcloud-staff/protocol/openid-connect/token'

SOCIAL_AUTH_KEYCLOAK_ID_KEY = 'email'

Problems

I was getting an issues where the client_id (audience) set up on django was not present in the aud key of the id_token JWT. Creating the error: Invalid audience.

The workaround to solve the client_id not in the audiences is on stackoverflow.

The gist of it is in you client's mappers tab - create an audience mapper for your client_id.

You also have to override the django tempalte for admin login to add the link for keycloak login.

Conclusion

Overall a good quality package. Works well. Now it is just about ensuring the user gets staff status pulled through with the correct permissions from keycloak mapped to django groups.

To give a user the is_staff and relevant group assignments - the flow with python social auth is to create a pipeline as mentioned in this github issue. Here are the docs for extending the pipeline.

So we can modify the pipelines to match our requirements. If we want users to be automatically created or not etc. The default pipeline can be overriden by this setting:

SOCIAL_AUTH_PIPELINE = (
    'social_core.pipeline.social_auth.social_details',
    'social_core.pipeline.social_auth.social_uid',
    'social_core.pipeline.social_auth.auth_allowed',
    'social_core.pipeline.social_auth.social_user',
    'social_core.pipeline.social_auth.associate_user',
    'social_core.pipeline.social_auth.load_extra_data',
    'social_core.pipeline.user.user_details',
    'my_module.pipeline.my_custom_pipeline'
)

Pipelines can also be defined per backend...for example: SOCIAL_AUTH_TWITTER_PIPELINE

We just need to be clear on the use of groups and roles in keycloak. Groups define the types of users in an organisation. Composite roles are for managing the application side.

In other words, groups are only an entity on keycloak side - they are not ever sent to client applications. Only the roles they assign are.

Overall I think this is a great choice

Django Boss SSO 2

Leverages other libraries namely drf-oidc-auth and mozilla-django-oidc to provide openidc auth with keycloak for django and django rest framework.

The readme has inconsistencies and does not lsit all required packages. There is too many libraries that are too similar used in my opinion.

django-oidc is used but not listed and is not django > 2 compliant.

Yes, it is not at the level required.

I do not recommend this package

Django Keycloak Auth

Specifically for drf - django rest framework.

Each viewset has to be explicity given roles:

keycloak_roles = {
    'GET': ['judge'],
}

I don't like this - I would prefer leveraging of djangos groups and permissions.

I am going to skip reviewing this package.

Django AllAuth

I have an issue where the response was the encoded JWT but the keycloak backend was expecting a json response.

So raised an issue and will look at this again when there is a change...

Turns out this was a problem on the settings on the provider side (keycloak) - the Userinfo signed response algorithm must be unisgned. I had it as RS256.

signed-userinfo-endpoint-keycloak

Mozzila Django OpenIDC

Very straight forward in setup.
We need to do a bit of nigly work to get the login link on the admin page shown below.

Remember the link the initialise the redirect code flow is:

{% translate 'Login with SSO Provider' %}

Then similar to the pipelines in python-social-auth you need to define a function for extra stuff like linking groups up and making people superusers - first names and last names etc. Although not as eligant as social auth does it...

You end up having to inherit from OIDCAuthenticationBackend and heavily customise the backend to your liking.
More so that necessary with python-social-auth.

A decent library choice buy architecturally python-social-auth is better.

Tutorial on how to add a Login with OpenIDC link to the Admin Page

  1. Look for templates in your project folder templates directory by adding to settings.py in the TEMPLATES part:

    [os.path.join(BASE_DIR, ' ,'templates'), ]

    Remember to import os

  2. Now add the template for the login page for that folder - copy the content from env/lib/../django/contrib/admin/templates/admin/login.html into <your_project>/templates/admin/custom_login.html

    Make the changes to the template (a link to OpenIDC login)

  3. Now tell the admin site to use that template - in urls.py

    admin.site.login_template = 'admin/custom_login.html'

Keycloak OIDC

An enhancement on top of mozilla-django-openidc - adding the ability to link keycloak roles to groups on django.

Still not 100% now yet - as teh is_staff and is_superuser will have to be handled in a custom manner.

Using Keycloak as the OpenIDC Identity Provider (to Login) to Hashicorp Vault

I like Keycloak a lot.
A single place to manage your authentication for many systems that you can use to federate users from an existing directory of users.

For more information look at my Keycloak Essentials fixes blog

There are a host of benefits but mainly it lets applications focus on their goals and not be distracted by the complex task of authentication.

If you are making use of keycloak and vault in your environment, then I would say it is worth looking at providing auth into vault via keycloak. Even if your users are in directory that can be accessed with LDAP - federating those users with keycloak and using OpenIDC auth is better than directly using LDAP - as credentials will be entered into only a single place ever.

Before jumping in, it is worthwhile taking a look at Vault1.1: OpenIDC with vault youtube tutorial

Key terminology:

  • Claims: key/value pairs in the JWT id token - a OpenIDC JWT term
  • Client Scopes: A keycloak term, tedious to configure protocol and role scope mappings for each client. These are shared scope mappers. By default the SAML client uses the roles_list and openidc has profile, email, address, phone, offline_access, roles, web-origins and microprofile-jwt

Prerequisites

Ensure you have setup the following:

  • keycloak (I used version 9.x)
  • Vault 1.1 or higher (I used Vault v1.3.4)

Getting Started

Keycloak Client Setup

Hashicorp provides decent docs and have a section on setting up your OIDC provider. We will use that to set up the hashicorp client in your keycloak realm.

  1. Log into keycloak and use the correct realm
  2. Create a client
    • Client Protocol: openid-connect
    • Access Type: confidential
    • Standard Flow Enabled: On
    • Configure Valid Redirect URIs
  3. Make note of the Client ID and Secret

Also ensure that valid redirect URL's are used:

valid-redirect-urls-vault

https://<vault_server_address>:8200/ui/vault/auth/oidc/oidc/callback,
https://<vault_server_address>:8250/oidc/callback

Vault Info

Vault provides information about the JWT/Open ID Connect auth method.

A JWT (said jot) is a JSON web token - this is a token containing authentication information that is signed by the identify provider - made available to the client.
That is how the client knows the identity of the user.

Vault allows login using the authorization code flow (via browser) with keycloak - or direct entry of the JWT.

Key Concepts

Remember that sometimes we want to maintain order and not just authorize any user on keycloak to login. In that case, Bound Claims are used.
These are the required keys and values expected to be in the JWT in order for that JWT to be authorized.

For example only user's with the attributes in the client scope of the JWT like the following:

{
        'department': 'implementations',
        'region': 'za'
}

Are allowed in.

Apparently the mapping is done on vault side with claim_mappings...and this must exist in the JWT other wise auth fails.

Note: the metadata key name "role" is reserved and may not be used for claim mappings.

Redirect URI's

Different from other integrations - vault requires setting the redirect URI. On other integrations usually you just set the OpenIDC endpoint...but in vault's case you need to set the redirect urls correctly as well.

The allowed_redirect_uris is specified per role.

If you plan to support authnetication via vault login -method=oidc, then a localhost redirect must be set http://localhost:8250/oidc/callback.

If you are allowing logging in via the vault ui you would use https://{host:port}/ui/vault/auth/{path}/oidc/callback.

The example given is: https://vault.example.com:8200/ui/vault/auth/oidc/oidc/callback?namespace=my_ns

So I am assuming this redirect url is the one on the vault side?...we'll figure this out

Logging in Via UI

Once everything is setup the login with openidc should look like this:

login-openidc

More info in the docs...on login via CLI and troubleshooting

Vault Setup

I am following this tutorial: Tutorial for Implementing OpenIDC Access

Ensure you have all the required details on hand:

  • keycloak baseurl (or realm url)
  • client-id
  • client-secret

Create the Policies for the Demo

Every client token has policies attached to it to control its secret access

Create a secret reader and a secret manager policy:

manager.hcl:

# Manage k/v secrets
path "/secret/*" {
    capabilities = ["create", "read", "update", "delete", "list"]
}

reader.hcl:

# Read permission on the k/v secrets
path "/secret/*" {
    capabilities = ["read", "list"]
}

I'm going to do most of this stuff using the UI - but as always with vault you can use the API or the CLI as well

  1. Go to https://<yourvault-instance>:8200/ui/vault/policies/acl
  2. Click Create ACL policy
  3. Upload the files, or just paste the contents of the .hcl's above

Enable OIDC Auth Method

  1. In the Web UI, select Access
  2. Slick Enable new method
  3. Select OIDC
  4. Enable method
  5. In configuraton add the OIDC discovery URL: https://<keycloak-url>/auth/realms/<realm>
  6. Enter reader in the Default_role field
  7. Add the client-id and client-secret in the OIDC options tab
  8. Save

Now a reader role needs to be created that binds the client and sets the allowed redirect urls:

vault write auth/oidc/role/reader \
        bound_audiences="<client-id>" \
        allowed_redirect_uris="http://<vault_server_address>:8200/ui/vault/auth/oidc/oidc/callback" \
        allowed_redirect_uris="http://<vault_server_address>:8250/oidc/callback" \
        user_claim="sub" \
        policies="reader"

All the above should match the setup redirect url's in keycloak.

From a keycloak perspective the discovery URL will always be the URL of the endpoint configuration on the realm level...but with everything after and including /.well-known removed

Login with OpenIDC

Logout and then sign in with openIDC provider.
A popup will redirect to keycloak for auth, and then close when completed.

vault-login-openidc-keycloak

Creating the Role for the Manager on Keycloak

By default, users will be secret readers.

On keycloak create a new role and add a user to that group, call is kv-manager for example.

In keycloak - roles by default are added to the client scope - so clients can view the roles of a particular user with the JWT.

  1. To create a global role Roles -> add Role or a client level role got to the client and roles
  2. Add the secret_manager role
  3. Optionally set other attributes
  4. Add a user to that role

Creating the Vault Manager Group

Concept: When you login, vault will see the roles claim groups_claim="roles" in the id token coming from the keycloak default openidc client scope. It is going to iterate over the list of values it finds. If it finds any that matches a group alias eg. secret_manager it will add that user to that group - if they are not already a member. As a result, that token they use to login with have that group's policies included.

So we are using the role secret_manager on keycloak.

The manager policy has already been created.

The original role will be extended to add a groups_claim.

Then we create a vault (external) group - that is attached to the manager policy and save its id.

Then we create a group alias that is attached to that vault external group we just created - bound to the oidc authentication method that can have any named alias.

Create the External Group:

Create the secret_manager role

Looks like this has to be done via CLI

vault write auth/oidc/role/secret_manager \
        bound_audiences="<client-id>" \
        allowed_redirect_uris="http://<vault_server_address>/ui/vault/auth/oidc/oidc/callback" \
        allowed_redirect_uris="http://<vault_server_address>:8250/oidc/callback" \
        user_claim="sub" \
        policies="reader" \
        groups_claim="roles"

I think the above policy should maybe be manager, but will have to see

Create an external group named manager linked to the manager policy

vault write identity/group name="manager" type="external" \
        policies="manager" \
        metadata=responsibility="Manage K/V Secrets"

Retain the returned ID:

export GROUP_ID="17..."

Now we need to link the manager group to the secret_manager role:

# Get the mount accessor value of the oidc auth method and save it in accessor.txt file
$ vault auth list -format=json  \
        | jq -r '."oidc/".accessor' > accessor.txt

# Create a group alias named "kv-mgr"
$ vault write identity/group-alias name="secret_manager" \
        mount_accessor=$(cat accessor.txt) \
        canonical_id="$GROUP_ID"

Try Login now

Now sign out and log in again.
Set the role as secret_manager and then login with OIDC Provider.

If everything goes according to plan, you should login and have the manager policies attached.

This was not the case for me and I got the following error:

Token verification failed. error validating claims: aud claim does not match any bound audience

vault-openidc-manager-aud-claim

I think the reason for the error is because the roles claim is not found...so the id token returned from keycloak didn't have the roles key in it.

Adding Roles to the ID Token

Addng the roles to the id token is done using a client protocol mapper

Remember vault uses the id token not the access token so the access token debug won't be as useful -
You can debug the access token within keycloak by going to: Vault Client -> Client Scopes -> Evaluate then use the secret manager user.

What you have to do is ensure that the client or realm roles is setup and added to the id token - you do that by going to the client mapper -> Add built IN and selecting the client or realm roles.

After that click the mapper and ensure Add to ID token is set to True.

add-to-id-token-keycloak-client-roles

My role was on the id token but it looked like this:

  "resource_access": {
    "vault-dev": {
      "roles": [
        "secret_manager"
      ]
    },

So that is a few more levels deep than expected.
So with a nested groups claim, you can make use of a json pointer.

In this case:

"/resource_access/vault-dev/roles"
vault write auth/oidc/role/secret_manager \
        bound_audiences="<client-id>" \
        allowed_redirect_uris="http://<vault_server_address>/ui/vault/auth/oidc/oidc/callback" \
        allowed_redirect_uris="http://<vault_server_address>:8250/oidc/callback" \
        user_claim="sub" \
        policies="reader" \
        groups_claim="/resource_access/vault-dev/roles"

So I update that, and try again - if you get this error it means the role was not found on the id token:

"/resource_access/vault-dev/roles" claim not found in token

Login Success

Now login should work successfully...

So a bit of extra work figuring things out but now things are looking good. I don't really like the flow of having to specifify the role you want to log in as explicitly but I'm sure there is a reason for this.

As for the next steps, I want to figure out why you have to speciify the role to use when logging in. Why doesn't vault get your associated roles and groups and apply the merged polcies. Perhaps that is the issue - if the roles conflict.

Using Keycloak Identity Provider for Rancher SSO

In Rancher 2.1.0 they added support for SAML authentication with Keycloak. What this means is Rancher will use a Keycloak realm to authenticate users.

This means that there is one place to manage users for a host of your applications. It also means that if they have logged on to the realm with their browser they can have single sign on to rancher - ie. they will already be logged into rancher.

The documentation on Rancher setting up Keycloak auth is already pretty good, however certain things can get tricky espescially if you are using Keycloak 7 and up.

So let's jump in and set it up...

Prerequisites

  • A Keycloak Identity Provider Setup
  • A Rancher Instance

Setting up the Rancher Client on Keycloak

In keycloak you already have a realm and user's eith local or federated from LDAP.

So now we need to create the client for Rancher.

Go to Clients -> Create:

keycloak-rancher-saml-add-client

Now add the following extra settings (replace the white box with your Rancher URL):

keycloak-saml-client-for-rancher-config-1keycloak-saml-client-for-rancher-config-2Boom, we've setup the keycloak side. Now we just need to export this config to load onto the rancher side.

Usually this would be done on the Installation tab of the client. However rancher wants the SAML Metadata IDPSSODescriptor but Keycloak 7 does not provide that.

The way to get it is to go to this URL:


https://{KEYCLOAK-URL}/auth/realms/{REALM-NAME}/protocol/saml/descriptor

and save that XML to a file.

You then need to copy all the properties of EntitiesDescriptor and add them to the first EntityDescriptor.  Then delete the EntitiesDescriptor element.

Now save that and it will be the config to import.

Setting up Keycloak Auth on Rancher

Open Rancher on the Global scope and select Security -> Authentication:

rancher-global-security-authentication

You will get a few authentication / identity provider options:

rancher-2.3-authentication-options

Select Keycloak and you will need the following information.

Field Description
Display Name Field The AD attribute that contains the display name of users.
User Name Field The AD attribute that contains the user name/given name.
UID Field An AD attribute that is unique to every user.
Groups Field Make entries for managing group memberships.
Rancher API Host The URL for your Rancher Server.
Private Key / Certificate A key/certificate pair to create a secure shell between Rancher and your IdP.
IDP-metadata The metadata.xml file that you exported from your IdP server.

These AD Attributes are set on the Keycloak side at Client -> Mappers, the best thing to do is add builtin on the rancher side:

That will add the defualt attributes and it is best to add those, however for access to the groups of the user you need to map a group list attribute.

builtin-attrbutes-mapper-saml-keycloak

You would use the protocol mapper below, to map the member attribute on rancher side to a user's group list.

 

group-list-attribute-saml-keycloak

So to view the SAML attribute name just open one of the mappers and use the Friendly Name.

In my case:

Display Name Field: givenName
User Name Field: email
UID Field: email
Rancher API Host: https://yourRancherHostURL:5443
Groups Field: member

rancher-auth-keycloak-success

If successful you will be presented with this screen. If you are not successful and you have the invalid SAML attributes error, then you need to fix your attribute names.

If you only want to allow members of certain groups on the keycloak realm then you can change the options below:

rancher-keycloak-groups-authorization

So How has Our Lives Improved Since Implementing SSO

  • A central place for authentication, you don't need to manage much about auth on your application side.
  • Deletage auth and turn Time-based One Time Pin, Add terms and conditions and giving access with a simple button switch.
  • You get SSO (single sign on): sign on to the realm and now you can simply press the login button on any other linked client and you will be logged in.
  • Single sign off - I tested this and it did not work. Logging out of one client kept you in the other client.

Single Sign Out

During my testing single sign out was not working. However I must mention my setup was 3 parts:

  • Keycloak on a VM accessible only on the local network
  • A Django App in a docker container on my local machine
  • Rancher also accessible on the local network

So my guess is that when using the front channel (browser) for the authentication step on the django client, it works because the redirect from the client goes to the docker container.

However when the logout is sent on the backchannel, it can't access 0.0.0.0:8000 as there isn't a proper DNS entry.

What I am going to do is deploy the docker container on Openshift (god bless my soul)...and set an ip or dns so that keycloak can access it on the back channel.

 

 

Sources