Skip to content

Commit

Permalink
Update the auth-token event name and document how it works (#360)
Browse files Browse the repository at this point in the history
Update the auth-token event name and document how it works
  • Loading branch information
Guillaume St-Pierre authored and sslotsky committed Aug 14, 2019
1 parent 94683f9 commit 3f8e2f1
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 88 deletions.
4 changes: 1 addition & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Fixed the service card loading the free badge after rendering, which caused a jumpy UI.
- Simplied `<manifold-service-card>` (data) and `<manifold-service-card-view>` (“dumb” view)

### Fixed

- Added the ability to specify a slot on the `manifold-credentials` with a default manifold button if not set.

### Changed

- Updated Stencil to v1.2.5
- Changed the event name for the `manifold-auth-token` component from the stencil auto-generated name to `manifold-token-receive` and documented that event.

## [v0.5.3]

Expand Down
3 changes: 2 additions & 1 deletion docs/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "es5"
"trailingComma": "es5",
"proseWrap": "always"
}
70 changes: 70 additions & 0 deletions docs/docs/advanced/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Auth token provider
path: /advanced/authentication
---

# Authenticate users

To allow a user to access data locked (🔒) behind authentication, the `manifold-auth-token`
component can be used. The component will render the
[shadowcat](https://github.com/manifoldco/shadowcat) authentication iframe and attempt to log in the
currently logged in user for your platform using OAuth. See the shadowcat documentation for more
information.

```html
<manifold-auth-token></manifold-auth-token>
```

The component can be placed anywhere in the DOM tree as long as it exists within a
`<manifold-connection>` component.

## Receiving the token

The component makes no decision as to how you should save the token on your side. As such, when the
token is received from the iframe, an event is triggered that will give you the token. The token
will be stored in the connection for use in subsequent API requests, but this event gives you the
opportunity to save the token to prevent delays on the next page load as described in **Setting the
cached token**.

```js
document.addEventListener('manifold-token-receive', ({ detail: { token } }) => {
// create a cookie or localstorage value with the token
});
```

## Setting the cached token

The component can receive a token previously saved from an OAuth request to speed up all requests
made by our components. If this token is provided, the OAuth request will still happen in order to
refresh the token, but any fetch calls happening in our web components will not wait for that OAuth
request to finish.

```html
<manifold-auth-token token="new-token"></manifold-auth-token>
```

## Invalid token

If the token given to the component is invalid, endpoints will return a 401 error and the token will
be removed from the `manifold-connection`. Use the [error handling capabilities](/advanced/errors)
of our web components to detect and act on such errors.

## Authenticated requests timeout

Any requests requiring authentication - which are sent by components locked (🔒) behind
authentication - will wait on a valid token for up to 15 seconds. If this component does not inject
a token into the connection after that time, an authentication error will be thrown.

This timeout duration can be customized on the `manifold-connection` component.

```html
<manifold-connection wait-time="time-in-ms">
<!-- Application -->
</manifold-connection>
```

## Token expiration

The token's expiration is encoded in the token string that the `manifold-token-receive` gives you.
If the token is set as expired, it will automatically be refreshed with a new token using the
shadowcat OAuth iframe.
4 changes: 1 addition & 3 deletions docs/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Fixed the service card loading the free badge after rendering, which caused a jumpy UI.
- Simplied `<manifold-service-card>` (data) and `<manifold-service-card-view>` (“dumb” view)

### Fixed

- Added the ability to specify a slot on the `manifold-credentials` with a default manifold button if not set.

### Changed

- Updated Stencil to v1.2.5
- Changed the event name for the `manifold-auth-token` component from the stencil auto-generated name to `manifold-token-receive` and documented that event.

## [v0.5.3]

Expand Down
26 changes: 0 additions & 26 deletions docs/docs/data/manifold-auth-token.md

This file was deleted.

6 changes: 4 additions & 2 deletions docs/docs/data/manifold-data-resource-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Creates an unstyled, unordered list with `<a>` tags.
Navigating client-side happens via the `manifold-resourceList-click` custom event.

| Name | Details | Data |
|-------------------------------|----------------------------|-----------------------------------------------|
| ----------------------------- | -------------------------- | --------------------------------------------- |
| `manifold-resourceList-click` | User has clicked on a link | `resourceId`, `resourceName`, `resourceLabel` |

## Link format
Expand All @@ -38,7 +38,9 @@ To navigate using a traditional `<a>` tag, specify a `resource-link-format`
attribute, using `:resource` as a placeholder:

```html
<manifold-data-resource-list resource-link-format="/resource/:resource"></manifold-data-resource-list>
<manifold-data-resource-list
resource-link-format="/resource/:resource"
></manifold-data-resource-list>
```

Note that this will disable the custom event unless `preserve-event` is
Expand Down
17 changes: 0 additions & 17 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,7 @@ declare namespace LocalJSX {
}
interface ManifoldAuthToken extends JSXBase.HTMLAttributes<HTMLManifoldAuthTokenElement> {
'oauthUrl'?: string;
'onManifoldOauthTokenChange'?: (event: CustomEvent<any>) => void;
'onManifold-token-receive'?: (event: CustomEvent<any>) => void;
/**
* _(hidden)_ Passed by `<manifold-connection>`
*/
Expand Down
98 changes: 68 additions & 30 deletions src/components/manifold-auth-token/manifold-auth-token.spec.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,76 @@
import { newSpecPage } from '@stencil/core/testing';
import { ManifoldAuthToken } from './manifold-auth-token';

async function setup(token?: string) {
const page = await newSpecPage({
components: [ManifoldAuthToken],
html: '<div></div>',
});

const component = page.doc.createElement('manifold-auth-token');
component.token = token;
component.setAuthToken = jest.fn();

const root = page.root as HTMLDivElement;
root.appendChild(component);
await page.waitForChanges();

return { page, component };
}

describe('<manifold-auth-token>', () => {
describe('when the token is received', () => {
it('emits an event', async () => {
const { component, page } = await setup();

const shadowcat = component.querySelector('manifold-oauth');
expect(shadowcat).toBeDefined();
if (shadowcat) {
/* This is what we expect our manifold-auth-token component to emit */
const callback = jest.fn();
component.addEventListener('manifold-token-receive', callback);

/* This simulates the shadowcat event that our manifold-auth-token component
* is listening for
*/
const event = new CustomEvent('receiveManifoldToken', {
detail: {
token: '12345',
expiry: new Date().getTime() / 1000,
error: null,
},
});
shadowcat.dispatchEvent(event);

await page.waitForChanges();

expect(callback).toHaveBeenCalled();
}
});
});

describe('when the token is not expired', () => {
const expiry = new Date();
expiry.setDate(expiry.getDate() + 1);
const unixTime = Math.floor(expiry.getTime() / 1000);
const expirySeconds = unixTime.toString();

it('calls the set auth token on load', () => {
it('calls the set auth token on load', async () => {
const token = `test|${expirySeconds}`;
const { component } = await setup(token);

const provisionButton = new ManifoldAuthToken();
provisionButton.setAuthToken = jest.fn();

provisionButton.token = token;
provisionButton.componentWillLoad();

expect(provisionButton.setAuthToken).toHaveBeenCalledWith(token);
expect(component.setAuthToken).toHaveBeenCalledWith(token);
});

it('calls the set auth token on change', () => {
const newToken = `test-new|${expirySeconds}`;

const provisionButton = new ManifoldAuthToken();
provisionButton.setAuthToken = jest.fn();
it('calls the set auth token on change', async () => {
const token = `test|${expirySeconds}`;
const { component, page } = await setup(token);

provisionButton.tokenChange(newToken);
const newToken = `test-new|${expirySeconds}`;
component.token = newToken;
await page.waitForChanges();

expect(provisionButton.setAuthToken).toHaveBeenCalledWith(newToken);
expect(component.setAuthToken).toHaveBeenCalledWith(newToken);
});
});

Expand All @@ -37,27 +80,22 @@ describe('<manifold-auth-token>', () => {
const unixTime = Math.floor(expiry.getTime() / 1000);
const expirySeconds = unixTime.toString();

it('does not call the set auth token on load', () => {
it('does not call the set auth token on load', async () => {
const token = `test|${expirySeconds}`;
const { component } = await setup(token);

const provisionButton = new ManifoldAuthToken();
provisionButton.setAuthToken = jest.fn();

provisionButton.token = token;
provisionButton.componentWillLoad();

expect(provisionButton.setAuthToken).not.toHaveBeenCalled();
expect(component.setAuthToken).not.toHaveBeenCalled();
});

it('does not call the set auth token on change', () => {
const newToken = `test-new${expirySeconds}`;

const provisionButton = new ManifoldAuthToken();
provisionButton.setAuthToken = jest.fn();
it('does not call the set auth token on change', async () => {
const token = `test|${expirySeconds}`;
const { component, page } = await setup(token);

provisionButton.tokenChange(newToken);
const newToken = `test-new|${expirySeconds}`;
component.token = newToken;
await page.waitForChanges();

expect(provisionButton.setAuthToken).not.toHaveBeenCalledWith();
expect(component.setAuthToken).not.toHaveBeenCalledWith();
});
});
});
5 changes: 3 additions & 2 deletions src/components/manifold-auth-token/manifold-auth-token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export class ManifoldAuthToken {
/* Authorisation header token that can be used to authenticate the user in manifold */
@Prop() token?: string;
@Prop() oauthUrl?: string;
@Event() manifoldOauthTokenChange: EventEmitter;
@Event({ eventName: 'manifold-token-receive', bubbles: true })
manifoldOauthTokenChange: EventEmitter;

@Watch('token') tokenChange(newToken?: string) {
this.setExternalToken(newToken);
Expand All @@ -35,7 +36,7 @@ export class ManifoldAuthToken {
if (!payload.error && payload.expiry) {
const formattedToken = `${payload.token}|${payload.expiry}`;
this.setAuthToken(formattedToken);
this.manifoldOauthTokenChange.emit(formattedToken);
this.manifoldOauthTokenChange.emit({ token: formattedToken });
}
};

Expand Down
6 changes: 3 additions & 3 deletions stories/manifold-auth-token.stories.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { storiesOf } from '@storybook/html';
import markdown from '../docs/docs/data/manifold-auth-token.md';
import markdown from '../docs/docs/advanced/authentication.md';

function withVeryFakeExpiry(token) {
/* During the ui transition from REST calls to GraphQL and PUMA,
this component needs to be able to consume several types of tokens
to account for the demo apps that already exist and the testing of
tokens with expirations set. This fake expiry allows the resources to
become visible in testing, which means the actual token may expire
without the component being aware - if so it will return a 401 and
become visible in testing, which means the actual token may expire
without the component being aware - if so it will return a 401 and
should be changed for a fresh one. */
const expiry = new Date();
expiry.setDate(expiry.getDate() + 1);
Expand Down

1 comment on commit 3f8e2f1

@vercel
Copy link

@vercel vercel bot commented on 3f8e2f1 Aug 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.