Comments (27)
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.
from auth0-spa-js.
@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.
from auth0-spa-js.
@luisrudge forgive me if I'm naive, but how else am I going to get the claims then?
from auth0-spa-js.
There is a simple way to get the roles as scopes:
- 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
})
- 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.
- 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);
};
from auth0-spa-js.
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('[email protected]').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);
}
from auth0-spa-js.
@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.
from auth0-spa-js.
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('[email protected]').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.
from auth0-spa-js.
[…] 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.
from auth0-spa-js.
Let's discuss this in the beta community post: https://community.auth0.com/t/access-rbac-permissions/25629
from auth0-spa-js.
@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>
);
};
from auth0-spa-js.
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).
from auth0-spa-js.
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?
from auth0-spa-js.
@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
from auth0-spa-js.
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.
from auth0-spa-js.
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?
from auth0-spa-js.
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('[email protected]').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.
from auth0-spa-js.
@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?
from auth0-spa-js.
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.
from auth0-spa-js.
@stevehobbsdev Thanks, done.
from auth0-spa-js.
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.
from auth0-spa-js.
@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?
from auth0-spa-js.
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.
from auth0-spa-js.
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);
});
from auth0-spa-js.
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.
from auth0-spa-js.
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).
from auth0-spa-js.
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.
from auth0-spa-js.
@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?
from auth0-spa-js.
Related Issues (20)
- "Misconfiguration in the system or a service outage" error in Auth0 HOT 3
- prompt in authorizationParams is not getting override. It is coming none even if it is set to other value like login. HOT 7
- Sophos Cyber Security Endpoint detects ransomware in the package HOT 3
- Total Cookie Protection breaks auth HOT 2
- Better error messaging when out of local storage quota? HOT 4
- Angular SSR window.crypto HOT 1
- User without Organization Membership caused infitine login redirect loop HOT 2
- NullInjectorError HOT 1
- Use '.well-known/openid-configuration' to configure Auth0Provider (Auth0Client) calls HOT 6
- Clarify limits for checkSession method HOT 7
- React Quickstart causing error upon clicking the login button HOT 2
- organization option of Auth0Client.getTokenSilently function is unexpectedly reuse previous value HOT 4
- Make getTokenSilently Generic HOT 2
- Support `getAccessTokenSilently` for multiple audiences in parallel HOT 4
- auth0-spa-js for multipage app? HOT 3
- Suport for multi tab sessions HOT 5
- Add configuration option for custom transaction storage
- LoginWithPopup doesn't work in Microsoft Word add-in on Mac HOT 3
- Extra redirect on silent login after upgrade to v2 HOT 9
- Implicit return in auth0-spa-js/src/cache /cache-manager.ts when TS "noImplicitReturns" is set to true HOT 4
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from auth0-spa-js.