If you’re using Auth0 to authenticate and authorize your users across several existing applications, you may want to integrate your next web application with Auth0.
There are several ways to do that, for example, if you want to integrate Jenkins with Auth0, you could use SAML v2; this blog post explains it pretty well.
If your application does not support SAML v2 or make it an enterprise paying feature you could want to use the OAuth2 (or OIDC) integration.
Let’s take the example of the Open Source monitoring solution Grafana, and let’s integrate it with Auth0.
Contents
Authenticate Grafana users with Auth0: just read the documentation
The official Grafana documentation will explain you how to:
- set the
root_url
option of[server]
for the callback URL to be correct - create a new client in Auth0, set the allowed callback Urls to
https://<grafana domain>/login/generic_oauth
- configure Grafana with a similar configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
; not mandatory, but super useful to debug OAuth interactions with Auth0 [log] level = debug [server] root_url = https://<grafana domain>/ [auth.generic_oauth] enabled = true allow_sign_up = true team_ids = allowed_organizations = name = Auth0 client_id = <client id> client_secret = <client secret> scopes = openid profile email auth_url = https://<domain>/authorize token_url = https://<domain>/oauth/token api_url = https://<domain>/userinfo |
The problem is… you won’t get any type of authorization with that; all your Auth0 users will be able to login to Grafana, but will be assigned the Viewer
role by default. That’s because Grafana needs to receive extra information about the role of the logged in user from Auth0.
1 2 3 |
t=2020-04-14T11:39:03+0000 lvl=dbug msg="Received user info response" logger=oauth.generic_oauth raw_json="{\"sub\":\"auth0|5e87486a85dd980c68d912c4\",\"nickname\":\"anthony\",\"name\":\"anthony@host.net\",\"picture\":\"https://s.gravatar.com/avatar/79033b96a632e4ea71b59fe9554c53a2?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fan.png\",\"updated_at\":\"2020-04-14T11:39:02.862Z\",\"email\":\"anthony@host.net\",\"email_verified\":false}" data="Name: anthony@host.net, Displayname: , Login: , Username: , Email: anthony@host.net, Upn: , Attributes: map]" t=2020-04-14T11:39:03+0000 lvl=dbug msg="User info result" logger=oauth.generic_oauth result="&{Id: Name:anthony@host.net Email:anthony@host.net Login:anthony@host.net Company: Role: Groups:]}" t=2020-04-14T11:39:03+0000 lvl=dbug msg="OAuthLogin got user info" logger=oauth userInfo="&{Id: Name:anthony@host.net Email:anthony@host.net Login:anthony@host.net Company: Role: Groups:]}" |
If you look at the Grafana debug logs above, you’ll see that the user was logged in, but since no role was mapped, the user was assigned the Viewer
role
Authorization in Auth0: install the extension, then set groups and roles
In Auth0, you first need to add the Authorization extension, you’ll then be prompted to configure the extension:
Once it’s done (make sure to enable Groups and Roles, and then to Rotate and press publish rule) you can then create some groups
You can then add a user to the Admin group
If you go back to Auth0, more precisely the rules panel, you’ll see that the extension added and activated a new rule:
Unfortunately, this is not enough: we need to have Auth0 enrich the userinfo
it sends back to Grafana; in the previous chapter, we saw the Grafana debug logs showed to us:
1 2 3 4 5 6 7 8 9 |
{ "sub": "auth0|5e87486a85dd980c68d912c4", "nickname": "anthony", "name": "anthony@host.net", "picture": "https://s.gravatar.com/avatar/79033b96a632e4ea71b59fe9554c53a2?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fan.png", "updated_at": "2020-04-14T11:39:02.862Z", "email": "anthony@host.net", "email_verified": false } |
So to enrich this json object with group info, we need to create another rule, that will enrich the user profile; let’s create a new rule (I’ve named it add-groups
) and add the following code:
1 2 3 4 5 |
function addAttributes(user, context, callback) { const namespace = 'https://dahanne.net/'; context.idToken[namespace + 'groups'] = user.groups; callback(null, user, context); } |
We should now have 2 rules applied to our Auth0 tenant:
If you log back in to Grafana now, you won’t see any change to your Grafana profile; but if you look at the logs, in particular at the raw_json
from the userinfo
object, you’ll notice a new field that was added by our rules:
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "sub": "auth0|5db0908a8bc0400c5c05604e", "nickname": "anthony", "name": "anthony@host.net", "picture": "https://s.gravatar.com/avatar/79033b96a632e4ea71b59fe9554c53a2?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fan.png", "updated_at": "2020-04-13T22:49:58.965Z", "email": "anthony@host.net", "email_verified": true, "https://dahanne.net/groups": [ "Admin" ] } |
Now we need to instruct Grafana how to read this new field and use it to assign a group to our user profile.
Back to Grafana, use JMESPath to retrieve the user role from Auth0 response
We’ll need first to read the documentation for Grafana JMESPath
From the documentation, we can deduct that we need such a mapping:
1 |
role_attribute_path = contains("https://dahanne.net/groups"[*], 'Admin') && 'Admin' || contains("https://dahanne.net/groups"[*], 'Editor') && 'Editor' || 'Viewer' |
Now, if you log back into Grafana, and have a look at the debug logs, you’ll see the new field from Auth0:
1 |
lvl=dbug msg="User info result" logger=oauth.generic_oauth result="&{Id: Name:anthony@host.net Email:anthony@host.net Login:anthony@host.net Company: Role:Admin Groups:]}" |
And of course, your user profile in Grafana is now updated:
Final words
While the authentication integration is well documented, I had trouble figuring out the authorization part… At first, I tried enriching the user
object in the Auth0 rules, but only enriching the context idToken
would work (thanks to my colleague Brett for helping me out with that); and even more, the namespace being an URL is mandatory too!
On the Grafana side though, everything worked pretty well out of the box; the debug logs were really helping!