0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): add metadata and support endpoint and audience variables in api guides (#4515)

refactor(console): support endpoint and audience variables in api guides
This commit is contained in:
Charles Zhao 2023-09-15 18:31:06 +08:00 committed by GitHub
parent 86113e0415
commit a449a0a21f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 60 additions and 28 deletions

View file

@ -3,6 +3,7 @@ import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components/Steps';
import Step from '@/mdx-components/Step';
import { appendPath } from '@silverhand/essentials';
<Steps>
@ -64,16 +65,22 @@ pnpm add jose
### Retrieve Logto's OIDC configurations
You will need a JWK public key set and the token issuer to verify the signature and source of the received JWS token. All the latest public Logto Authorization Configurations can be found at `https://<your-logto-domain>/oidc/.well-known/openid-configuration`.
<p>
You will need a JWK public key set and the token issuer to verify the signature and source of the received JWS token. All the latest public Logto Authorization Configurations can be found at <code>{`${appendPath(props.endpoint, '/oidc/.well-known/openid-configuration')}`}</code>.
</p>
e.g. Call `https://logto.dev/oidc/.well-known/openid-configuration`. And locate the following two fields in the response body:
<p>
e.g. Call <code>{`${appendPath(props.endpoint, '/oidc/.well-known/openid-configuration')}`}</code>. And locate the following two fields in the response body:
</p>
```ts
{
"jwks_uri": "https://logto.dev/oidc/jwks",
"issuer": "https://logto.dev/oidc"
}
```
<pre>
<code className="language-ts">
{`{
"jwks_uri": "${appendPath(props.endpoint, '/oidc/jwks')}",
"issuer": "${appendPath(props.endpoint, '/oidc')}"
}`}
</code>
</pre>
### Add auth middleware
@ -83,8 +90,9 @@ Jose's `jwtVerify` method may helps you to verify the token's JWS format, token
For <a href="https://docs.logto.io/docs/recipes/rbac/" target="_blank" rel="noopener">🔐 RBAC</a>, scope validation is also required.
</InlineNotification>
```ts
// auth-middleware.ts
<pre>
<code className="language-ts">
{`// auth-middleware.ts
import { createRemoteJWKSet, jwtVerify } from 'jose';
@ -98,12 +106,12 @@ export const verifyAuthFromRequest = async (req, res, next) => {
// The raw Bearer Token extracted from the request header
token,
// Generate a jwks using jwks_uri inquired from Logto server
createRemoteJWKSet('https://<your-logto-domain>/oidc/jwks'),
createRemoteJWKSet('${appendPath(props.endpoint, '/oidc/jwks')}'),
{
// Expected issuer of the token, should be issued by the Logto server
issuer: 'https://<your-logto-domain>/oidc',
issuer: '${appendPath(props.endpoint, '/oidc')}',
// Expected audience token, should be the resource indicator of the current API
audience: '<your request listener resource indicator>',
audience: '${props.audience}',
}
);
@ -114,8 +122,9 @@ export const verifyAuthFromRequest = async (req, res, next) => {
userId = payload.sub;
return next();
};
```
};`}
</code>
</pre>
</Step>

View file

@ -2,7 +2,8 @@ import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Express',
description: 'Protect your API on node (Express)',
description:
'Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.',
target: 'API',
});

View file

@ -28,6 +28,7 @@ export type GuideContextType = {
origin: string;
callback: string;
};
audience?: string;
};
type Props = {
@ -50,6 +51,7 @@ export const GuideContext = createContext<GuideContextType>({
postLogoutRedirectUris: [],
isCompact: false,
sampleUrls: { origin: '', callback: '' },
audience: '',
});
function Guide({ className, guideId, isEmpty, isLoading, onClose }: Props) {

View file

@ -1,29 +1,38 @@
import { type Resource } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useMemo } from 'react';
import { useContext, useMemo } from 'react';
import guides from '@/assets/docs/guides';
import Guide, { GuideContext, type GuideContextType } from '@/components/Guide';
import { AppDataContext } from '@/contexts/AppDataProvider';
import useCustomDomain from '@/hooks/use-custom-domain';
type Props = {
className?: string;
guideId: string;
apiResource?: Resource;
isCompact?: boolean;
onClose: () => void;
};
function ApiGuide({ className, guideId, isCompact, onClose }: Props) {
function ApiGuide({ className, guideId, apiResource, isCompact, onClose }: Props) {
const { tenantEndpoint } = useContext(AppDataContext);
const { applyDomain: applyCustomDomain } = useCustomDomain();
const guide = guides.find(({ id }) => id === guideId);
const memorizedContext = useMemo(
() =>
conditional(
!!guide && {
metadata: guide.metadata,
Logo: guide.Logo,
isCompact: Boolean(isCompact),
}
!!guide &&
!!apiResource && {
metadata: guide.metadata,
Logo: guide.Logo,
isCompact: Boolean(isCompact),
endpoint: applyCustomDomain(tenantEndpoint?.href ?? ''),
audience: apiResource.indicator,
}
) satisfies GuideContextType | undefined,
[guide, isCompact]
[apiResource, applyCustomDomain, guide, isCompact, tenantEndpoint?.href]
);
return memorizedContext ? (

View file

@ -1,3 +1,4 @@
import { type Resource } from '@logto/schemas';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -14,10 +15,11 @@ import ApiGuide from '../ApiGuide';
import * as styles from './index.module.scss';
type Props = {
apiResource: Resource;
onClose: () => void;
};
function GuideDrawer({ onClose }: Props) {
function GuideDrawer({ apiResource, onClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.guide' });
const guides = useApiGuideMetadata();
const [selectedGuide, setSelectedGuide] = useState<SelectedGuide>();
@ -59,6 +61,7 @@ function GuideDrawer({ onClose }: Props) {
isCompact
className={styles.guide}
guideId={selectedGuide.id}
apiResource={apiResource}
onClose={() => {
setSelectedGuide(undefined);
}}

View file

@ -1,3 +1,4 @@
import { type Resource } from '@logto/schemas';
import Modal from 'react-modal';
import ModalHeader from '@/components/Guide/ModalHeader';
@ -9,10 +10,11 @@ import * as styles from './index.module.scss';
type Props = {
guideId: string;
apiResource?: Resource;
onClose: () => void;
};
function GuideModal({ guideId, onClose }: Props) {
function GuideModal({ guideId, apiResource, onClose }: Props) {
return (
<Modal shouldCloseOnEsc isOpen className={modalStyles.fullScreen} onRequestClose={onClose}>
<div className={styles.modalContainer}>
@ -25,7 +27,12 @@ function GuideModal({ guideId, onClose }: Props) {
requestSuccessMessage="guide.request_guide_successfully"
onClose={onClose}
/>
<ApiGuide className={styles.guide} guideId={guideId} onClose={onClose} />
<ApiGuide
className={styles.guide}
guideId={guideId}
apiResource={apiResource}
onClose={onClose}
/>
</div>
</Modal>
);

View file

@ -83,6 +83,7 @@ function ApiResourceDetails() {
return (
<GuideModal
guideId={guideId}
apiResource={data}
onClose={() => {
navigate(`/api-resources/${id}`);
}}
@ -129,7 +130,7 @@ function ApiResourceDetails() {
}}
/>
<Drawer isOpen={isGuideDrawerOpen} onClose={onCloseDrawer}>
<GuideDrawer onClose={onCloseDrawer} />
<GuideDrawer apiResource={data} onClose={onCloseDrawer} />
</Drawer>
<ActionMenu
buttonProps={{ icon: <More className={styles.moreIcon} />, size: 'large' }}