Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How would one go about accessing RBAC permissions #23

Closed
ed-sparkes opened this issue May 23, 2019 · 27 comments
Closed

How would one go about accessing RBAC permissions #23

ed-sparkes opened this issue May 23, 2019 · 27 comments

Comments

@ed-sparkes
Copy link

Hi,

Would be great to be able to access RBAC (Authorization Core) permissions in the SPA so as to be able to extend the PrivateRoute component to restrict routes based on permission.

Can see how to get the permissions included in the access token for my API but unsure how to get access to them in the SPA.

Many thanks, Ed

@luisrudge
Copy link
Contributor

Let's discuss this in the beta community post: https://community.auth0.com/t/access-rbac-permissions/25629

@andrejohansson
Copy link

andrejohansson commented Jul 1, 2019

I'm also interested in this topic but I am not allowed to access the above topic.

@luisrudge, how can I get access to the above beta community topic?
@ed-sparkes did you get any further with this?

@luisrudge
Copy link
Contributor

@andrejohansson not sure why it was removed. Maybe because we are out of beta.. In any case, we don't provide RBAC integration out of the box, but you have access to all the claims that are inside your id_token

@andrejohansson
Copy link

@luisrudge ok, thanks.

For anyone else interested, here is my extended react-auth0-spa.tsx which an added token property that is decoded. From that I can read the scope and permissions which is enough for me. I don´t know if there is an option to add role information to the tokens in auth0 aswell.

import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "@auth0/auth0-spa-js";
import Auth0Client from "@auth0/auth0-spa-js/dist/typings/Auth0Client";

interface Auth0Context {
    isAuthenticated: boolean;
    user: any;
    token: any,
    loading: boolean;
    popupOpen: boolean;
    loginWithPopup(options: PopupLoginOptions): Promise<void>;
    handleRedirectCallback(): Promise<RedirectLoginResult>;
    getIdTokenClaims(o?: getIdTokenClaimsOptions): Promise<IdToken>;
    loginWithRedirect(o: RedirectLoginOptions): Promise<void>;
    getTokenSilently(o?: GetTokenSilentlyOptions): Promise<string | undefined>;
    getTokenWithPopup(o?: GetTokenWithPopupOptions): Promise<string | undefined>;
    logout(o?: LogoutOptions): void;
}
interface Auth0ProviderOptions {
    children: React.ReactElement;
    onRedirectCallback?(result: RedirectLoginResult): void;
}

const DEFAULT_REDIRECT_CALLBACK = () =>
    window.history.replaceState({}, document.title, window.location.pathname);

export const Auth0Context = React.createContext<Auth0Context | null>(null);
export const useAuth0 = () => useContext(Auth0Context)!;
export const Auth0Provider = ({
                                  children,
                                  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
                                  ...initOptions
                              }: Auth0ProviderOptions & Auth0ClientOptions) => {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [user, setUser] = useState();
    const [token, setToken] = useState();
    const [auth0Client, setAuth0] = useState<Auth0Client>();
    const [loading, setLoading] = useState(true);
    const [popupOpen, setPopupOpen] = useState(false);

    useEffect(() => {
        const initAuth0 = async () => {
            const auth0FromHook = await createAuth0Client(initOptions);
            setAuth0(auth0FromHook);

            if (window.location.search.includes("code=")) {
                const { appState } = await auth0FromHook.handleRedirectCallback();
                onRedirectCallback(appState);
            }

            const isAuthenticated = await auth0FromHook.isAuthenticated();

            setIsAuthenticated(isAuthenticated);

            if (isAuthenticated) {
                const user = await auth0FromHook.getUser();
                const token = await getDecodedToken(auth0FromHook);
                setUser(user);
                setToken(token);
            }

            setLoading(false);
        };
        initAuth0();
        // eslint-disable-next-line
    }, []);

    const loginWithPopup = async (o: PopupLoginOptions) => {
        setPopupOpen(true);
        try {
            await auth0Client!.loginWithPopup(o);
        } catch (error) {
            console.error(error);
        } finally {
            setPopupOpen(false);
        }
        const user = await auth0Client!.getUser();
        setUser(user);
        setIsAuthenticated(true);
    };

    const handleRedirectCallback = async () => {
        setLoading(true);
        const result = await auth0Client!.handleRedirectCallback();
        const user = await auth0Client!.getUser();
        const token = await getDecodedToken(auth0Client!);
        setLoading(false);
        setIsAuthenticated(true);
        setUser(user);
        setToken(token);
        return result;
    };

    const getDecodedToken = async (client: Auth0Client) => {
      const jwtToken = await client!.getTokenSilently();
      let jwtData = jwtToken.split('.')[1];
      let decodedJwtJsonData = window.atob(jwtData);
      let decodedJwtData = JSON.parse(decodedJwtJsonData);
      return decodedJwtData;
    };

    return (
        <Auth0Context.Provider
            value={{
                isAuthenticated,
                user,
                token,
                loading,
                popupOpen,
                loginWithPopup,
                handleRedirectCallback,
                getIdTokenClaims: (o: getIdTokenClaimsOptions | undefined) =>
                    auth0Client!.getIdTokenClaims(o),
                loginWithRedirect: (o: RedirectLoginOptions) =>
                    auth0Client!.loginWithRedirect(o),
                getTokenSilently: (o: GetTokenSilentlyOptions | undefined) =>
                    auth0Client!.getTokenSilently(o),
                getTokenWithPopup: (o: GetTokenWithPopupOptions | undefined) =>
                    auth0Client!.getTokenWithPopup(o),
                logout: (o: LogoutOptions | undefined) => auth0Client!.logout(o)
            }}
        >
            {children}
        </Auth0Context.Provider>
    );
};

@luisrudge
Copy link
Contributor

@andrejohansson decoding an access token is considered a bad practice. Access tokens are considered ‘opaque strings’, which means you shouldn't assume they're in any given format.

@andrejohansson
Copy link

@luisrudge forgive me if I'm naive, but how else am I going to get the claims then?

@vibronet
Copy link

vibronet commented Jul 2, 2019

hey @andrejohansson - to expand a bit on the topic. Access tokens are meant to be seen only by their intended recipient, in this case the API. If you write code on the client that peeks inside the access token, you might end up being broken if something changes. More detailed explanation in http://www.cloudidentity.com/blog/2018/04/20/clients-shouldnt-peek-inside-access-tokens/

If your frontend needs to have access to information from the authorization server, the standard mechanism devised by OpenID Connect is to place that information in the id_token. Today Auth0 doesn't have any way of automatically including that info (tho we are looking into adding features to that effect) but you can achieve that by adding custom rules.

There are many architectural considerations that come into play in this scenario (coupling between front-end and backend, the API having more context for authorization than the frontend, changing policies and claims semantic, etc etc) that we will describe in an upcoming blog post, but in the meanwhile I hope this unblocked you.

@Satyam
Copy link

Satyam commented Aug 26, 2019

I have found a rule somewhere (I'd love to credit whoever posted it, but I lost track of the many open tabs I had and lost track of the author) that gives the rbac permissions in a regular call to getUser:

function (user, context, callback) {
  var map = require('array-map');
  var ManagementClient = require('auth0@2.17.0').ManagementClient;
  var management = new ManagementClient({
    token: auth0.accessToken,
    domain: auth0.domain
  });

  var params = { id: user.user_id, page: 0, per_page: 50, include_totals: true };
  management.getUserPermissions(params, function (err, permissions) {
    if (err) {
      // Handle error.
      console.log('err: ', err);
      callback(err);
    } else {
      var permissionsArr = map(permissions.permissions, function (permission) {
        return permission.permission_name;
      });
      context.idToken[configuration.NAMESPACE + 'user_authorization'] = {
        permissions: permissionsArr
      };
    }
    callback(null, user, context);
  });
}

I tried to flatten out the structure so the permissions are more easily accessed but whatever it is I change, it breaks something or other and stops working. If anyone knows how to do it, it would be great, thanks. Anyway, I flatten it out in my own version of getUser which is the one I make available in the Auth0Provider.

This allows me to add a permission attribute to PrivateRoute so the route won't even show if the user doesn't have the right permission.

I also added a can function in the useAuth0 hook which allows me to do:

{can('delete:xxxx') && (<Button onClick={/* ... */}>Delete</Button>)}

After all, it doesn't make sense to offer a user a 'delete' button, pop up a confirmation modal box and then, when the server does check the permissions, tell the user that they really couldn't be allowed to delete whatever it was.

Also, I don't like the idea, as shown in a blog post, of having a duplicate of the roles and permissions information available to the server via the access token, as metadata in the id_token or imported tables in code on the client side. That solution can easily get out of control.

I'm still working on it but this solution doesn't mess with the access token and provides the same information from the very same source. At least it seems so, but I am just beginning with this so I stand corrected if wrong.

@Satyam
Copy link

Satyam commented Aug 26, 2019

Also @luisrudge you say somewhere above:

you have access to all the claims that are inside your id_token

and, indeed, there is a getIdTokenClaims which is quite short of documentation so I don't have a clue on how to use it or what is good for so I don't even know if it is able to extract permissions from rbac settings in the dashboard.

@luisrudge
Copy link
Contributor

getIdTokenClaims returns the result of a decoded JWT id_token. So, everything that is inside the id_token will be there. getUser does kinda the same thing, but it filters out claims that are not related to the user (https://github.com/auth0/auth0-spa-js/blob/master/src/jwt.ts#L3-L34).

@ivarprudnikov
Copy link

Access tokens are meant to be seen only by their intended recipient

There is no real argument here. Intended recipient is a vague definition considering how thick client side apps and microservices change the landscape of product architectures.

But for the sake of an argument consider this: iat, exp and scope claims, they are all part of the client logic. First 2 allows you to track time before refreshing the token, the last allows you to make sure that requested claims (part of client flow) are the same as the ones in the token. Client needs to decode token to see that info, doesn't it?

@mmathias01
Copy link

mmathias01 commented Oct 16, 2019

@Satyam

I tried to flatten out the structure so the permissions are more easily accessed but whatever it is I change, it breaks something or other and stops working. If anyone knows how to do it, it would be great, thanks.

Here is an ES6 version that I wrote tonight thanks to your post. I had been trying to figure this out for a few days. If you put this in through the dashboard it might complain about some missing semicolons or the arrow function syntax but you can safely ignore those messages.

https://gist.github.com/mmathias01/b1257e6446de376163c05cfa272dc1a2

async (user, context, callback) => {
	const ManagementClient = require('auth0@2.17.1').ManagementClient;
	const management = new ManagementClient({
		token: auth0.accessToken,
		domain: auth0.domain
	});

	const params = { id: user.user_id, page: 0, per_page: 50, include_totals: false };

	try {
		const roles_and_permissions = {};
		const permissions = await management.getUserPermissions(params);
		permissions.reduce((rp, p) => {

			const namespace = p.resource_server_identifier;
			const _ns = rp[namespace] = rp[namespace] || {};
			_ns.permissions = _ns.permissions || [];
			_ns.roles = _ns.roles || [];
			_ns.permissions.push(p.permission_name);
			const _p = { "description": p.description };

			p.sources.map(s => {
				if (s.source_type === "ROLE") {
					if (!_ns.roles.includes(s.source_name)) {
						_ns.roles.push(s.source_name);
					}
					_p.from_role = s.source_name;
				} else {
					_p.from_role = 'direct';
				}
			});
			_ns[p.permission_name] = _p;

			return rp;
		}, roles_and_permissions);

		context.idToken.roles_and_permissions = roles_and_permissions;
		callback(null, user, context);
	} catch (err) {
		callback(err);
	}
}

What you get ends up looking like this:

{
	"email": "...",
	"picture": "...",
	"name": "...",
	"nickname": "...",
	"user_metadata": {},
	"app_metadata": {},
	"email_verified": true,
	"clientID": "...",
	"updated_at": "...",
	"user_id": "...",
	"identities": [
	  {
		"user_id": "...",
		"provider": "auth0",
		"connection": "Username-Password-Authentication",
		"isSocial": false
	  }
	],
	"created_at": "...",
	"roles_and_permissions": {
		"https://api.yourdomain.com/v2": {
		  "permissions": [ "read:something", "read:theotherthing", "write:something" ],
		  "roles": [ "Test Role" ],
		  "read:something": { "description": "Read Something","from_role": "Test Role" },
		  "read:theotherthing": { "description": "Read The Other Thing","from_role": "direct"} ,
		  "write:something": { "description": "Write Something","from_role": "Test Role" }
		},
		"https://api.anotherdomain.com/": {
		  "permissions": [ "write:subscriber" ],
		  "roles": [ "Administrator" ],
		  "write:subscriber": {"description": "Save Subscribers", "from_role": "Administrator" }
		}
	  },
	"sub": "..."
  }

The reason this can't be flatter is that you could end up with permissions name collisions between different namespaces (in this case APIs on your tenant) and that could be a disaster in certain cases. In any case if you knew for certain that this would never happen to you and really needed a flat array of permissions its quite an easy exercise to get that from this object.

Hope this helps someone, because lord knows it took me long enough to find this post.

@Stacks-88
Copy link

@mmathias01 how do you use this? :) I am updating a react SPA, and I have gotten to the point where I need to verify that a user is in a certain role prior to allowing them to see some stuff. I have added a rule, and added your code to the rule, and in the "try this script", it works like a champ. How can I leverage this information in my react app? Do I have to write my own "getUser()" as @Satyam mentioned? How do I do that?

@ivarprudnikov
Copy link

There is a simple way to get the roles as scopes:

  1. Request all possible scopes/roles you are after before initializing Auth0 client:
const auth0FromHook = await createAuth0Client({
        audience: config.audience,
        domain: config.domain,
        client_id: config.clientId,
        scope: "openid profile email admin" // ask for scopes here
})
  1. After authentication succeeds (you obtain id_token) add new method which retrieves access token with more details in it:
const getTokenSilently = async () => {
    const accessToken = await auth0Client.getTokenSilently();
    return { raw: accessToken, decoded: jwt_decode(accessToken) };
};

FYI jwt_decode is the same library used in Auth0 but you have to install it explicitly as it is hidden in the library.

  1. Check if you user has a scope or inspect all scopes:
// "scopes" is Array<String>
const hasAnyScopeAsync = async (scopes) => {
    const token = await getTokenSilently();
    const tokenScopes = (token.decoded.scope || '').split(/\W/);
    return scopes && scopes.length && scopes.some(s => tokenScopes.indexOf(s) > -1);
};

@belachkar
Copy link

belachkar commented Apr 11, 2020

I need to get the permissions, but can not, they are not included in the getIdTokenClaims response.
I'm using angular with @auth0/auth0-spa-js package.
Ex of the code:

this.auth.auth0Client$
  .subscribe(client => {
    client.getTokenSilently()
      .then(accessToken => {
        console.log({ accessToken });
        client.getIdTokenClaims()
          .then(token => {
            console.log({ token });
            this.token = token;
          }).catch(err => console.error(err));
      }).catch(err => console.error(err));
  });

When I specify the audience and the scope I don't have any response or error.
Normally when checking Enable RBAC & Add Permissions in the Access Token we must be able to get the permissions.

@stevehobbsdev
Copy link
Contributor

@belachkar The recommended way currently when using Auth0 is to use a custom rule to copy those RBAC permissions into the ID token. Auth0 doesn't do that automatically. This community post should help you get that done.

@belachkar
Copy link

@stevehobbsdev Thanks, done.

@PapaNappa
Copy link

I really would like to see that granted scopes would be exposed by the API somehow.
As pointed out in #122 (comment), OAuth says that the scopes must be returned if they differ from the requested scopes.

This information is crucial for apps to adapt the granted scopes and the user's permissions, e.g. show proper messages or change the UI (e.g. hide certain buttons etc.).
While it is true that one could go about adding the same information to the IdToken, this is not the intended OAuth way and feels hacky to me. It is duplicating information und really unnecessary.

I feel it would be quite easy to either internally parse the access token (yes it is supposed to be opaque, but an Auth0 library could know the Auth0 implementation of access tokens) or just include the scopes in the token response (as per spec) and exposing that information from the library.

@stevehobbsdev
Copy link
Contributor

@PapaNappa Are you in a situation where you are changing the scopes via some kind of custom rule and they are not being returned in the /oauth/token response? Or are you saying that they are being returned but you have no access to them?

@PapaNappa
Copy link

The latter.
When auth0-spa-js gets its token from the /token endpoint, that endpoint is also sending the scope. But auth0-spa-js does not provide this information to the caller.

And yes, in my case we have a custom rule employed that adds additional scopes based on the user's permissions (using the Authorization extension, not core RBAC yet), but afaict, this is independent of the root cause here.

@BillSchumacher
Copy link

BillSchumacher commented Aug 29, 2020

I think the disconnect is that the permissions are assigned to the API and you, like I, believed they should automatically be sent with the token. However, consider the possibility that multiple APIs share the same permission name. What you're doing with the rule is gathering the permissions from the API, not for your domain. They do not get automatically sent out when you authenticate to your domain, I haven't tested this yet but I believe they might if you authenticate to the API via the oauth/token endpoint. But this also requires sending your client secret.

 var request = require("request");
 var options = { method: 'POST',
 url: 'https://dev-snip.us.auth0.com/oauth/token',
 headers: { 'content-type': 'application/json' },
 body: 
'{"client_id":"snip","client_secret":"snip","audience":"https://example.com/api","grant_type":"client_credentials"}' };

request(options, function (error, response, body) {
   if (error) throw new Error(error);

  console.log(body);
});

@BillSchumacher
Copy link

If you have a SPA it is not recommended to put your client secret anywhere in your code as anyone can read it. Which is why the examples are for the backend. So don't go trying this in your React/Vue code.

@BillSchumacher
Copy link

BillSchumacher commented Aug 29, 2020

Here's the rule I'm using, a slight modification of one of the above.

async function(user, context, callback) {
  const namespace = 'https://example.com/api';
  const map = require('array-map');
  const ManagementClient = require('auth0@2.17.0').ManagementClient;
  const management = new ManagementClient({
    token: auth0.accessToken,
    domain: auth0.domain
  });

  const params = { id: user.user_id, page: 0, per_page: 50, include_totals: true };
  const permissions = await management.getUserPermissions(params);
  const assignedPermissions = map(permissions.permissions, function (permission) {
    return permission.permission_name;
  });

  const assignedRoles = context.authorization ? context.authorization.roles : null;
  
  if (context.idToken) {
    const idTokenClaims = context.idToken;
    idTokenClaims[`${namespace}/roles`] = assignedRoles ? assignedRoles : ["Guest"];
    idTokenClaims[`${namespace}/permissions`] = assignedPermissions;
    context.idToken = idTokenClaims;
  }

  if (context.accessToken) {
    const accessTokenClaims = context.accessToken;
    accessTokenClaims[`${namespace}/roles`] =  assignedRoles ? assignedRoles : ["Guest"];
    accessTokenClaims[`${namespace}/permissions`] = assignedPermissions;
    context.accessToken = accessTokenClaims;
  }  

  callback(null, user, context);
}

@PapaNappa
Copy link

PapaNappa commented Sep 17, 2020

you, like I, believed they should automatically be sent with the token.

The standard is mandating this behaviour.
The point is, the client asks for a set of scopes, but the user (or rules) might deny some of the scopes. So the client needs to know which scopes it actually got granted.

In https://tools.ietf.org/html/rfc6749#section-5.1, it says for a Successful Response:

The authorization server […] constructs the response by adding the following parameters:
[…]
scope
OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The scope of the access token as described by Section 3.3.

So when some of the scopes have not been granted, OAuth mandates that the scope is returned to the client.

However, consider the possibility that multiple APIs share the same permission name. What you're doing with the rule is gathering the permissions from the API, not for your domain.

Sorry, I do not get your point.
We only get tokens for a specific API/Audience, therefore name collision is not a problem.

They do not get automatically sent out when you authenticate to your domain, I haven't tested this yet but I believe they might if you authenticate to the API via the oauth/token endpoint.

Yes, the oauth/token endpoint returns the scope, but the library is just not exposing this vital information.

@stevehobbsdev Did my clarification help you? What do you think about my issue?

Some off-topic comments:

But this also requires sending your client secret.

There are flows which do not require a client secret, but still the list of scope gets returned.

If you have a SPA it is not recommended to put your client secret anywhere in your code as anyone can read it.

This is not true. Actually, the latest recommendation is to use Code Grant with PKCE for SPAs, too (if I'm not completely mistaken); and I think at least Code Grant is what the Auth0 libraries default to these days.
Yes, the client secret is in the code, but having the secret does not allow you to do more (harm) than before. And CG/PKCE adds security in some cases (I can search for some posts if you like).

@dopry
Copy link

dopry commented Feb 1, 2021

@vibronet

hey @andrejohansson - to expand a bit on the topic. Access tokens are meant to be seen only by their intended recipient, in this case the API.

makes little sense... If my client is requesting a token to access an API on a user's behalf and passing an audience and will access the API on the users behalf it is one of then intended recipients. It only makes sense that I adjust the appearance of the UI based on what the user can do with the API. In this case the client would generally be coupled to the API it is interacting with...

Again it would be really nice to have RBAC permissions somewhere more accessible that having to manually decode the tokens or add custom rules. It would greatly improve the developer experience.

@PapaNappa
Copy link

@vibronet

[…] Access tokens are meant to be seen only by their intended recipient, in this case the API.

[…] If my client is requesting a token to access an API on a user's behalf and passing an audience and will access the API on the users behalf it is one of then intended recipients.

To be fair, he is right in that clients should not decode the access token. Of course clients receive the token - but access tokens are designed (by the spec) to be opaque to the client. Basically a client should treat access tokens like session cookies - just opaque tokens that grant access to the resource.

The access token is part of the API between the resource and auth server (specifying which access the client has) - the client is only the transport. Thus the client has no business in the form or shape of the access token. Of course: it determines the access of the client, thus the client cannot - just by pure principle of security - have any role in defining the shape or form of the access token.

The id token, on the other hand, is the interface between the client and the auth server, namely giving resource owner (user) information to the client. Thus the id token has a defined format, from the perspective of the client.

It only makes sense that I adjust the appearance of the UI based on what the user can do with the API. In this case the client would generally be coupled to the API it is interacting with...

Again it would be really nice to have RBAC permissions somewhere more accessible that having to manually decode the tokens or add custom rules. It would greatly improve the developer experience.

I totally agree, that’s why I added my initial comment here as well.

@christoph-pflueger
Copy link

christoph-pflueger commented Apr 7, 2022

@stevehobbsdev

@belachkar The recommended way currently when using Auth0 is to use a custom rule to copy those RBAC permissions into the ID token. Auth0 doesn't do that automatically. This community post should help you get that done.

This approach requires the Management API which has a tremendously low rate limit (https://auth0.com/docs/troubleshoot/customer-support/operational-policies/rate-limit-policy/management-api-endpoint-rate-limits). Hence, this doesn't seem like a scalable solution whereas decoding the access token is. Any thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests