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

feat: support for hosted token worker #1208

Merged
merged 6 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/auth0-spa-js@2.1.2/dist/auth0-spa-js.production.esm.js"
{
"imports": {
"@auth0/auth0-spa-js": "https://www.unpkg.com/@auth0/auth0-spa-js@2.1.2/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
Loading