Rest Authentication
All network requests are run through the getRequestInit optionally defined in your RestEndpoint.
Cookie Auth (credentials)
Here's an example using simple cookie auth by sending fetch credentials:
import { RestEndpoint } from '@data-client/rest'; export default class AuthdEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { async getRequestInit(body: any): Promise<RequestInit> { return { ...(await super.getRequestInit(body)), credentials: 'same-origin', }; } }
import { MyResource } from './MyResource'; MyResource.get({ id: 1 });
GET /my/1
Content-Type: application/json
Cookie: session=abc;
{
"id": "1",
"title": "this post"
}
See Django Integration for an example that also includes CSRF protection.
Access Tokens or JWT
- static member
- function singleton
- async function
import { RestEndpoint } from '@data-client/rest'; import { login } from './login'; export default class AuthdEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { declare static accessToken?: string; getHeaders(headers: HeadersInit) { // TypeScript doesn't infer properly const EP = this.constructor as typeof AuthdEndpoint; if (!EP.accessToken) return headers; return { ...headers, 'Access-Token': EP.accessToken, }; } } export const handleLogin = async e => { const { accessToken } = await login(new FormData(e.target)); AuthdEndpoint.accessToken = accessToken; };
import { MyResource } from './MyResource'; MyResource.get({ id: 1 });
GET /my/1
Content-Type: application/json
Access-Token: mytoken
{
"id": "1",
"title": "this post"
}
import { RestEndpoint } from '@data-client/rest'; import { getAuthToken, setAuthToken, login } from './login'; export default class AuthdEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { async getHeaders(headers: HeadersInit) { return { ...headers, 'Access-Token': await getAuthToken(), }; } } export const handleLogin = async e => { const { accessToken } = await login(new FormData(e.target)); setAuthToken(accessToken); };
import { MyResource } from './MyResource'; MyResource.get({ id: 1 });
GET /my/1
Content-Type: application/json
Access-Token: mytoken
{
"id": "1",
"title": "this post"
}
import { RestEndpoint } from '@data-client/rest'; import { getAuthToken, setAuthToken, login } from './login'; export default class AuthdEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { getHeaders(headers: HeadersInit) { return { ...headers, 'Access-Token': getAuthToken(), }; } } export const handleLogin = async e => { const { accessToken } = await login(new FormData(e.target)); setAuthToken(accessToken); };
import { MyResource } from './MyResource'; MyResource.get({ id: 1 });
GET /my/1
Content-Type: application/json
Access-Token: mytoken
{
"id": "1",
"title": "this post"
}
Auth Headers from React Context
Using React Context for state that is not displayed (like auth tokens) is not recommended. This will result in unnecessary re-renders and application complexity.
- Resource
- RestEndpoint
We can transform any Resource into one that uses hooks to create endpoints by using hookifyResource
import { resource, hookifyResource } from '@data-client/rest';
// Post defined here
export const PostResource = hookifyResource(
resource({ path: '/posts/:id', schema: Post }),
function useInit(): RequestInit {
const accessToken = useAuthContext();
return {
headers: {
'Access-Token': accessToken,
},
};
},
);
Then we can get the endpoints as hooks in our React Components
import { useSuspense } from '@data-client/react';
import { PostResource } from 'resources/Post';
function PostDetail({ id }) {
const post = useSuspense(PostResource.useGet(), { id });
return <div>{post.title}</div>;
}
Using this means all endpoint calls must only occur during a function render.
function CreatePost() {
const controller = useController();
const createPost = PostResource.useCreate();
return (
<form
onSubmit={e => controller.fetch(createPost, new FormData(e.target))}
>
{/* ... */}
</form>
);
}
We will first provide an easy way of using the context to alter the fetch headers.
import { RestEndpoint } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
declare accessToken?: string;
getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
'Access-Token': this.accessToken,
};
}
}
Next we will extend to generate a new endpoint with this context injected.
function useEndpoint(endpoint: RestEndpoint) {
const accessToken = useAuthContext();
return useMemo(
() => endpoint.extend({ accessToken }),
[endpoint, accessToken],
);
}
Using this means all endpoint calls must only occur during a function render.
function CreatePost() {
const controller = useController();
const createPost = useEndpoint(PostResource.create);
return (
<form
onSubmit={e =>
controller.fetch(createPost, {}, new FormData(e.target))
}
>
{/* ... */}
</form>
);
}
Code organization
If much of your Resources
share a similar auth mechanism, you might
try extending from a base class that defines such common customizations.
401 Logout Handling
In case a users authorization expires, the server will typically responsd to indicate as such. The standard way of doing this is with a 401. LogoutManager can be used to easily trigger any de-authorization cleanup.