Skip to content

Commit

Permalink
Add a refresh token example
Browse files Browse the repository at this point in the history
  • Loading branch information
sandhose committed Dec 10, 2024
1 parent dcecf82 commit bea0ffd
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 1 deletion.
9 changes: 9 additions & 0 deletions src/pages/client-implementation-guide/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
AuthParametersForm,
DisplayAuthorizationUrl,
CodeExchangeForm,
CurrentAccessToken,
RefreshTokenForm,
} from "./interactive";

export const components = { a: Link };
Expand Down Expand Up @@ -88,6 +90,13 @@ Once the authorization is complete, you'll get a `code` back, which you can past

<CodeExchangeForm client:load />

## Refresh token [MSC2956]

The access token (<code><CurrentAccessToken client:load /></code>) is only valid for a short period of time.
It must be refreshed before it expires.

<RefreshTokenForm client:load />

# Implementation examples

- Hydrogen - [https://github.com/sandhose/hydrogen-web/blob/sandhose/oidc-login/src/matrix/net/OidcApi.ts](https://github.com/sandhose/hydrogen-web/blob/sandhose/oidc-login/src/matrix/net/OidcApi.ts)
Expand Down
121 changes: 120 additions & 1 deletion src/pages/client-implementation-guide/interactive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ const codeVerifier = persistentAtom<string>(
const codeChallenge = computed(codeVerifier, (codeVerifier) =>
task(() => computeCodeChallenge(codeVerifier)),
);
const code = persistentAtom("code");
const code = persistentAtom<string | null>("code", null, {
encode: JSON.stringify,
decode: JSON.parse,
});
const accessToken = persistentAtom<string | null>("access-token", null, {
encode: JSON.stringify,
decode: JSON.parse,
});
const refreshToken = persistentAtom("refresh-token");

const computeCodeChallenge = async (codeVerifier: string): Promise<string> => {
// Hash the verifier
Expand Down Expand Up @@ -437,6 +445,14 @@ export const CodeExchangeForm = () => {

const data = await res.json();
setResponse(data);

if ("access_token" in data && typeof data.access_token === "string") {
accessToken.set(data.access_token);
}

if ("refresh_token" in data && typeof data.refresh_token === "string") {
refreshToken.set(data.refresh_token);
}
},
},
$queryClient,
Expand Down Expand Up @@ -506,6 +522,108 @@ export const CodeExchangeForm = () => {
);
};

export const RefreshTokenForm = () => {
const $refreshToken = useStore(refreshToken) || "";
const $queryClient = useStore(queryClient);
const $clientId = useStore(clientId) || "";
const $serverMetadata = useStore(serverMetadata);

const [response, setResponse] = useState<null | object>(null);

const mutation = useMutation(
{
mutationFn: async (): Promise<void> => {
const params = {
grant_type: "refresh_token",
refresh_token: $refreshToken,
client_id: $clientId,
} satisfies Record<string, string>;

const body = new URLSearchParams(params).toString();

const res = await fetch($serverMetadata.token_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body,
});

const data = await res.json();
setResponse(data);

if ("access_token" in data && typeof data.access_token === "string") {
accessToken.set(data.access_token);
}

if ("refresh_token" in data && typeof data.refresh_token === "string") {
refreshToken.set(data.refresh_token);
}
},
},
$queryClient,
);

const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutation.mutate();
};

return (
<div className={cx(styles["form-wrapper"])}>
<Form.Root onSubmit={onSubmit} className={cx(styles.form)}>
<Form.Field name="endpoint">
<Form.Label>Token endpoint</Form.Label>
<Form.TextControl
type="url"
readOnly
value={$serverMetadata.token_endpoint}
/>
</Form.Field>

<Form.Field name="grant-type">
<Form.Label>Grant type</Form.Label>
<Form.TextControl type="text" readOnly value="refresh_token" />
</Form.Field>

<Form.Field name="client-id">
<Form.Label>Client ID</Form.Label>
<Form.TextControl type="text" required readOnly value={$clientId} />
{!$clientId && (
<Form.ErrorMessage>
Client must be registered to get one
</Form.ErrorMessage>
)}
</Form.Field>

<Form.Field name="refresh-token">
<Form.Label>Refresh token</Form.Label>
<Form.TextControl
type="text"
required
readOnly
value={$refreshToken}
/>
{!$refreshToken && (
<Form.ErrorMessage>No refresh token available</Form.ErrorMessage>
)}
</Form.Field>

<Form.Submit
size="sm"
kind="secondary"
disabled={!$refreshToken || !$clientId}
>
Refresh token
</Form.Submit>

{response && <DataViewer data={response} />}
</Form.Root>
</div>
);
};

export const CurrentCsApiRoot = (): string => useStore(csApi);
export const AuthIssuerEndpoint = (): string => {
const $csApi = useStore(csApi);
Expand All @@ -526,6 +644,7 @@ export const OidcDiscoveryDocument = (): string => {

export const CurrentState = (): string => useStore(state);
export const CurrentClientId = (): string => `${useStore(clientId)}`;
export const CurrentAccessToken = (): string => `${useStore(accessToken)}`;

export const DisplayAuthorizationUrl: React.FC = () => {
const $state = useStore(state);
Expand Down

0 comments on commit bea0ffd

Please sign in to comment.