Giter Site home page Giter Site logo

Comments (27)

vibronet avatar vibronet commented on August 15, 2024 7

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.

luisrudge avatar luisrudge commented on August 15, 2024 4

@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.

andrejohansson avatar andrejohansson commented on August 15, 2024 4

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

from auth0-spa-js.

ivarprudnikov avatar ivarprudnikov commented on August 15, 2024 4

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);
};

from auth0-spa-js.

BillSchumacher avatar BillSchumacher commented on August 15, 2024 4

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.

stevehobbsdev avatar stevehobbsdev commented on August 15, 2024 3

@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.

Satyam avatar Satyam commented on August 15, 2024 2

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.

PapaNappa avatar PapaNappa commented on August 15, 2024 2

@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.

from auth0-spa-js.

luisrudge avatar luisrudge commented on August 15, 2024 1

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

from auth0-spa-js.

andrejohansson avatar andrejohansson commented on August 15, 2024 1

@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.

luisrudge avatar luisrudge commented on August 15, 2024 1

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.

andrejohansson avatar andrejohansson commented on August 15, 2024

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.

luisrudge avatar luisrudge commented on August 15, 2024

@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.

Satyam avatar Satyam commented on August 15, 2024

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.

ivarprudnikov avatar ivarprudnikov commented on August 15, 2024

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.

mmathias01 avatar mmathias01 commented on August 15, 2024

@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('[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.

Stacks-88 avatar Stacks-88 commented on August 15, 2024

@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.

belachkar avatar belachkar commented on August 15, 2024

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.

belachkar avatar belachkar commented on August 15, 2024

@stevehobbsdev Thanks, done.

from auth0-spa-js.

PapaNappa avatar PapaNappa commented on August 15, 2024

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.

stevehobbsdev avatar stevehobbsdev commented on August 15, 2024

@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.

PapaNappa avatar PapaNappa commented on August 15, 2024

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.

BillSchumacher avatar BillSchumacher commented on August 15, 2024

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.

BillSchumacher avatar BillSchumacher commented on August 15, 2024

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.

PapaNappa avatar PapaNappa commented on August 15, 2024

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.

dopry avatar dopry commented on August 15, 2024

@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.

from auth0-spa-js.

christoph-pflueger avatar christoph-pflueger commented on August 15, 2024

@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?

from auth0-spa-js.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.