Skip to content

Commit

Permalink
feat: support for hosted token worker (#1208)
Browse files Browse the repository at this point in the history
<!-- By submitting a PR to this repository, you agree to the terms
within the [Auth0 Code of
Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).
Please see the [contributing
guidelines](https://github.com/auth0/.github/blob/master/CONTRIBUTING.md)
for how to create and submit a high-quality PR for this repo. -->

### Changes

When the SDK is used in combination with a strict
Content-Security-Policy (CSP), the policy must include `worker-src:
blob:` which raises a concern of `unsafe-eval`. This change allows the
SDK to be configured to load the worker code from a trusted URL,
compliant with the CSP, and allows the user to mitigate the concern.

**Todo:**
- [x] Just waiting for #1209
to land so that we can get e2e coverage of this

### References

> As defined above, special URL schemes that refer to specific pieces of
unique content, such as `data:`, `blob:` and `filesystem:` are excluded
from matching a policy of * and must be explicitly listed. Policy
authors should note that the content of such URLs is often derived from
a response body or execution in a Document context, which may be unsafe.
Especially for the
[default-src](https://www.w3.org/TR/CSP2/#default_src) and
[script-src](https://www.w3.org/TR/CSP2/#script_src) directives, policy
authors should be aware that allowing `data:` URLs is equivalent to
unsafe-inline and **allowing `blob:` or `filesystem:` URLs is equivalent
to unsafe-eval**.

https://www.w3.org/TR/CSP2/#source-list-guid-matching

### Testing

<!--
Please describe how this can be tested by reviewers. Be specific about
anything not tested and reasons why. If this library has unit and/or
integration testing, tests should be added for new functionality and
existing tests should complete without errors.
-->

- [X] This change adds unit test coverage
- [ ] This change adds integration test coverage
- [X] This change has been tested on the latest version of the
platform/language

### Checklist

- [X] I have read the [Auth0 general contribution
guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [X] I have read the [Auth0 Code of
Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [X] All code quality tools/guidelines have been run/followed

---------

Co-authored-by: Frederik Prijck <[email protected]>
  • Loading branch information
DJMcK and frederikprijck authored Dec 11, 2023
1 parent 0dda1ab commit 5ca5720
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 12 deletions.
54 changes: 45 additions & 9 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,37 +106,39 @@ There are two ways to use our SDK when you want to rely on a Content Delivery Ne

### Using our own CDN bundle

Our own CDN bundle exposes both `createAuth0Client` and `Auth0Client` on a global `auth0` variable, and can be used as shown below.
Our own CDN bundle exposes both `createAuth0Client` and `Auth0Client` on a global `auth0` variable, and can be used as shown below.

```html
<script>
const client = auth0.createAuth0Client({ ... });
// or
const client = new auth0.Auth0Client({ ... });
const client = auth0.createAuth0Client({ ... });
// or
const client = new auth0.Auth0Client({ ... });
</script>
```

### Using import maps with unpkg

If you want to use a CDN bundle together with import maps, you will need to use our ESM bundle from unpkg:

```html
<script type="importmap">
{
"imports": {
"@auth0/auth0-spa-js": "https://www.unpkg.com/@auth0/[email protected]/dist/auth0-spa-js.production.esm.js"
{
"imports": {
"@auth0/auth0-spa-js": "https://www.unpkg.com/@auth0/[email protected]/dist/auth0-spa-js.production.esm.js"
}
}
}
</script>
<script type="module">
import { createAuth0Client, Auth0Client } from '@auth0/auth0-spa-js';
const client = createAuth0Client({ ... });
// or
const client = new Auth0Client({ ... });
</script>
```

## Why is isAuthenticated returning true when there are no tokens available to call an API?

As long as the SDK has an id token, you are considered authenticated, because it knows who you are. It might be that there isn't a valid access token and you are unable to call an API, but the SDK still knows who you are because of the id token.

Authentication is about who you are (id token), not what you can do (access token). The latter is authorization, which is also why you pass the access token to the API in the Authorization header.
Expand All @@ -145,3 +147,37 @@ So even when the refresh token fails, or `getTokenSilently` returns nothing, tha

On top of that, the SDK can have multiple access tokens and multiple refresh tokens (e.g. when using multiple audience and scope combinations to call multiple API's), but only one id token.
If there are multiple access and refresh tokens, and one of the refresh tokens fails, it doesn't mean the other access tokens or refresh tokens are invalid, they might still be perfectly usable.

## The Token Worker is being blocked by my Content-Security-Policy (CSP), what should I do?

When using refresh tokens - along with the default in-memory cache - the SDK will leverage a [`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker) to globally isolate the refresh token from the rest of your application.

By default, to reduce the friction of getting started with the SDK, we ship that `Worker` code with the main SDK bundle and dynamically pass it as a string to create a new `Worker`.

Unless configured to allow for that, Content-Security-Policy (CSP) will block the loading of the dynamic `blob:`.

To allow you to keep strict Content-Security-Policy (CSP), and not have to allow `blob:` in your CSP, we also ship the `Worker` as a separate compiled JavaScript file. You can find that file in [`./dist/auth0-spa-js.worker.production.js`](./dist/auth0-spa-js.worker.production.js) or on our CDN. This allows you to either copy the worker JavaScript file to your web server's public assets folder or load it from our CDN.

For example, if I have a folder called `static` in the root of my project then I could update my build script to copy the worker file to it:

```sh
my-build-script && cp ./node_modules/@auth0/auth0-spa-js/dist/auth0-spa-js.worker.development.js ./static/
```

Now when instantiating the SDK, I can configure it to load the worker code from that location:

```ts
import { createAuth0Client, Auth0Client } from '@auth0/auth0-spa-js';

const client = createAuth0Client({
...
workerUrl: '/static/auth0-spa-js.worker.production.js'
});
// or
const client = new Auth0Client({
...
workerUrl: '/static/auth0-spa-js.worker.production.js'
});
```

In this case, the loading of the `Worker` would comply with a CSP that included `'self'`. You can follow similar steps if you'd prefer to copy the file to your own CDN instead.
13 changes: 13 additions & 0 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,19 @@ describe('Auth0', () => {
expect(auth0).toBeInstanceOf(Auth0Client);
});

it('should load token worker from provided URL when provided', async () => {
const workerUrl = '/hosted/auth0.worker.js';

await createAuth0Client({
domain: TEST_DOMAIN,
clientId: TEST_CLIENT_ID,
useRefreshTokens: true,
workerUrl,
});

expect(window.Worker).toHaveBeenCalledWith(workerUrl);
});

it('should call `utils.validateCrypto`', async () => {
const { utils } = await setup();

Expand Down
59 changes: 59 additions & 0 deletions cypress/e2e/getTokenSilently.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,64 @@ describe('getTokenSilently', () => {
);
});
});

describe('with workerUrl', () => {
const workerUrl = 'auth0-spa-js.worker.development.js';

it('loads the hosted worker file', () => {
whenReady();

cy.intercept({
method: 'GET',
url: workerUrl
}).as('workerLoaded');

cy.setSwitch('refresh-tokens', true);
cy.setSwitch('use-worker-url', true);

cy.wait('@workerLoaded').its('response.statusCode').should('eq', 200);
});

it('retrieves tokens using the hosted worker file', () => {
whenReady();

cy.setSwitch('refresh-tokens', true);
cy.setSwitch('use-worker-url', true);
cy.setSwitch('use-cache', false);

cy.intercept({
method: 'POST',
url: '**/oauth/token'
}).as('tokenApiCheck');

cy.login();
cy.getAccessTokens().should('have.length', 1);

cy.wait('@tokenApiCheck')
.its('request')
.then(request => {
cy.wrap(request)
.its('headers.referer')
.should('contain', workerUrl);
cy.wrap(request)
.its('body')
.should('contain', 'grant_type=authorization_code');
});

cy.getTokenSilently();
cy.getAccessTokens().should('have.length', 2);

cy.wait('@tokenApiCheck')
.its('request')
.then(request => {
cy.wrap(request)
.its('headers.referer')
.should('contain', workerUrl);
cy.wrap(request)
.its('body')
.should('contain', 'grant_type=refresh_token');
});
});
});
});
});
24 changes: 24 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ const getStatsPlugins = () => {
};

let bundles = [
{
input: 'src/worker/token.worker.ts',
output: {
name: EXPORT_NAME,
file: 'dist/auth0-spa-js.worker.development.js',
format: 'umd',
sourcemap: true
},
plugins: [...getPlugins(false)],
watch: {
clearScreen: false
}
},
{
input: 'src/index.ts',
output: {
Expand Down Expand Up @@ -96,6 +109,17 @@ let bundles = [

if (isProduction) {
bundles = bundles.concat(
{
input: 'src/worker/token.worker.ts',
output: [
{
name: EXPORT_NAME,
file: 'dist/auth0-spa-js.worker.production.js',
format: 'umd'
}
],
plugins: [...getPlugins(isProduction), ...getStatsPlugins()]
},
{
input: 'src/index.ts',
output: [
Expand Down
6 changes: 5 additions & 1 deletion src/Auth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,11 @@ export class Auth0Client {
this.options.useRefreshTokens &&
cacheLocation === CACHE_LOCATION_MEMORY
) {
this.worker = new TokenWorker();
if (this.options.workerUrl) {
this.worker = new Worker(this.options.workerUrl);
} else {
this.worker = new TokenWorker();
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ export interface Auth0ClientOptions extends BaseLoginOptions {
* **Note**: Using this improperly can potentially compromise the token validation.
*/
nowProvider?: () => Promise<number> | number;

/**
* If provided, the SDK will load the token worker from this URL instead of the integrated `blob`. An example of when this is useful is if you have strict
* Content-Security-Policy (CSP) and wish to avoid needing to set `worker-src: blob:`. We recommend either serving the worker, which you can find in the module
* at `<module_path>/dist/auth0-spa-js.worker.production.js`, from the same host as your application or using the Auth0 CDN
* `https://cdn.auth0.com/js/auth0-spa-js/<version>/auth0-spa-js.worker.production.js`.
*
* **Note**: The worker is only used when `useRefreshTokens: true`, `cacheLocation: 'memory'`, and the `cache` is not custom.
*/
workerUrl?: string;
}

/**
Expand Down
27 changes: 25 additions & 2 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,21 @@ <h3 class="mb-5">Other switches</h3>
>Use iframe as a fallback for refresh tokens</label
>
</div>

<div class="custom-control custom-switch mb-5">
<input
type="checkbox"
class="custom-control-input"
id="worker_url_switch"
v-model="useWorkerUrl"
/>
<label
for="worker_url_switch"
class="custom-control-label"
data-cy="switch-use-worker-url"
>Use Hosted Worker URL</label
>
</div>
</div>
<div class="col-md-6">
<div class="custom-control custom-switch mb-5">
Expand Down Expand Up @@ -453,6 +468,7 @@ <h3 class="mb-3">Client Options</h3>
useOrgAtLogin: data.useOrgAtLogin || false,
useRefreshTokensFallback: data.useRefreshTokensFallback || false,
clientOptions: '',
useWorkerUrl: data.useWorkerUrl || false,
audienceScopes: [
{
audience: data.audience || defaultAudience,
Expand Down Expand Up @@ -506,7 +522,11 @@ <h3 class="mb-3">Client Options</h3>
useRefreshTokensFallback: function () {
this.initializeClient();
this.saveForm();
}
},
useWorkerUrl: function () {
this.initializeClient();
this.saveForm();
},
},
computed: {
scopesWithSuffix: function () {
Expand All @@ -528,6 +548,7 @@ <h3 class="mb-3">Client Options</h3>
useCookiesForTransactions: _self.useCookiesForTransactions,
useFormData: _self.useFormData,
useRefreshTokensFallback: _self.useRefreshTokensFallback,
workerUrl: _self.useWorkerUrl ? '/auth0-spa-js.worker.development.js' : undefined,
authorizationParams: {
redirect_uri: window.location.origin
}
Expand Down Expand Up @@ -594,7 +615,8 @@ <h3 class="mb-3">Client Options</h3>
useCache: this.useCache,
useFormData: this.useFormData,
useOrgAtLogin: this.useOrgAtLogin,
useRefreshTokensFallback: this.useRefreshTokensFallback
useRefreshTokensFallback: this.useRefreshTokensFallback,
useWorkerUrl: this.useWorkerUrl,
})
);

Expand All @@ -614,6 +636,7 @@ <h3 class="mb-3">Client Options</h3>
this.useFormData = true;
this.useOrgAtLogin = false;
this.useRefreshTokensFallback = false;
this.useWorkerUrl = false;
this.saveForm();
},
showAuth0Info: function () {
Expand Down

0 comments on commit 5ca5720

Please sign in to comment.