mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): add m2m implementation guide (#4343)
* feat(console): add m2m implementation guide add m2m implementation guide * fix(console): address m2m guide comment issues address m2m guide comment issues
This commit is contained in:
parent
c4a9246071
commit
d5835e4e93
8 changed files with 221 additions and 7 deletions
|
@ -1 +1,114 @@
|
|||
## Replace this with actual guide
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import ApplicationCredentials from '@/mdx-components-v2/ApplicationCredentials';
|
||||
import EnableAdminAccess from './components/EnableAdminAccess';
|
||||
import EnableAdminAccessSrc from './assets/enable-admin-access.png';
|
||||
import AppIdentifierSrc from './assets/api-identifier.png';
|
||||
|
||||
<Steps>
|
||||
<Step title="Intro">
|
||||
Machine-to-machine (M2M) is a common practice to authenticate if you have an app that needs to directly talks to resources. E.g., an API service that updates users' custom data in Logto, a statistic service that pulls daily orders, etc.
|
||||
|
||||
Usually, an M2M app doesn't need user interactions, i.e., it has no UI.
|
||||
|
||||
</Step>
|
||||
<Step title="Locate the app ID and app secret">
|
||||
Get your App ID and App Secret.
|
||||
|
||||
<ApplicationCredentials />
|
||||
|
||||
</Step>
|
||||
<Step title="Enable admin access" subtitle="(optional)">
|
||||
|
||||
### Accessing Logto Management API
|
||||
|
||||
If you want to use this m2m app for accessing Logto [Management API](https://docs.logto.io/docs/references/core/#management-api), you will also need to turn on "admin access" for you application.
|
||||
|
||||
<EnableAdminAccess />
|
||||
|
||||
</Step>
|
||||
<Step title="Locate the API Resource">
|
||||
|
||||
### Find the API identifier
|
||||
|
||||
In the API Resource tab, find the API identifier that the app needs to access. If you haven't added the API Resource in Logto or don't know what API Resource is, see [API Resource](https://docs.logto.io/docs/references/resources).
|
||||
|
||||
<img alt="API identifier" src={AppIdentifierSrc} width="600px" style={{ paddingBottom: '12px' }} />
|
||||
|
||||
</Step>
|
||||
<Step title="Compose and send request">
|
||||
|
||||
### Compose them into a request (all mandatory):
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Use Token Endpoint <code>{`${props.endpoint}/oidc/token`}</code> as the request endpoint, and
|
||||
use POST as the method.
|
||||
</li>
|
||||
<li>
|
||||
Set header <code>Content-Type: application/x-www-form-urlencoded</code>
|
||||
</li>
|
||||
<li>
|
||||
Use{' '}
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization#basic_authentication">
|
||||
Basic authentication
|
||||
</a>
|
||||
, where username is the App ID, and password is the App Secret.
|
||||
</li>
|
||||
<li>Carry the body data</li>
|
||||
</ul>
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"grant_type": "client_credentials",
|
||||
"resource": "https://shopping.api", // Replace with your API identifier
|
||||
"scope": "scope_1 scope_2" // Replace with your desired scope(s) if you're using RBAC
|
||||
}
|
||||
```
|
||||
|
||||
If you are using cURL:
|
||||
|
||||
<pre>
|
||||
<code className="language-bash">
|
||||
{`curl --location
|
||||
--request POST '${props.endpoint}/oidc/token'
|
||||
--header 'Authorization: Basic eW91ci1hcHAtaWQ6eW91ci1hcHAtc2VjcmV0'
|
||||
--header 'Content-Type: application/x-www-form-urlencoded'
|
||||
--data-urlencode 'grant_type=client_credentials'
|
||||
--data-urlencode 'resource=https://shopping.api'
|
||||
--data-urlencode 'scope=scope_1 scope_2'
|
||||
`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
### Token response
|
||||
|
||||
A successful response body would be like:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"access_token": "eyJhbG...2g", // Use this token for accessing the resource
|
||||
"expires_in": 3600, // Token expiration in seconds
|
||||
"token_type": "Bearer" // Auth type for your request when using the Access Token
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step title="Access resource using Access Token">
|
||||
|
||||
You may notice the token response has a `token_type` field, which it's fixed to `Bearer`. Thus you should put the Access Token in the Authorization field of HTTP headers with the Bearer format (`Bearer YOUR_TOKEN`).
|
||||
|
||||
For example, if you have requested an Access Token with the resource `https://api.logto.io`, to get all applications in Logto:
|
||||
|
||||
<pre>
|
||||
<code className="language-bash">
|
||||
{`curl --location
|
||||
--request GET '${props.endpoint}/api/applications'
|
||||
--header 'Authorization: Bearer eyJhbG...2g' # Access Token
|
||||
`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
|
@ -0,0 +1,41 @@
|
|||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import Switch from '@/ds-components/Switch';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { GuideContext } from '@/pages/Applications/components/GuideV2';
|
||||
|
||||
export default function EnableAdminAccess() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
app: { id: appId, isAdmin },
|
||||
} = useContext(GuideContext);
|
||||
const [value, setValue] = useState(isAdmin);
|
||||
const api = useApi();
|
||||
|
||||
const onSubmit = async (value: boolean) => {
|
||||
setValue(value);
|
||||
try {
|
||||
await api.patch(`api/applications/${appId}`, {
|
||||
json: {
|
||||
isAdmin: value,
|
||||
},
|
||||
});
|
||||
toast.success(t('general.saved'));
|
||||
} catch {
|
||||
setValue(!value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormField title="application_details.enable_admin_access">
|
||||
<Switch
|
||||
label={t('application_details.enable_admin_access_label')}
|
||||
checked={value}
|
||||
onChange={async ({ currentTarget: { checked } }) => onSubmit(checked)}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.textField {
|
||||
@include _.form-text-field;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: _.unit(6) 0;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { useContext } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import { GuideContext } from '@/pages/Applications/components/GuideV2';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
function ApplicationCredentials() {
|
||||
const {
|
||||
app: { id, secret },
|
||||
} = useContext(GuideContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<FormField
|
||||
title="application_details.application_id"
|
||||
tip={(closeTipHandler) => (
|
||||
<Trans
|
||||
components={{
|
||||
a: (
|
||||
<TextLink
|
||||
href="https://openid.net/specs/openid-connect-core-1_0.html"
|
||||
target="_blank"
|
||||
onClick={closeTipHandler}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{t('application_details.application_id_tip')}
|
||||
</Trans>
|
||||
)}
|
||||
>
|
||||
<CopyToClipboard value={id} variant="border" className={styles.textField} />
|
||||
</FormField>
|
||||
<FormField title="application_details.application_secret">
|
||||
<CopyToClipboard
|
||||
hasVisibilityToggle
|
||||
value={secret}
|
||||
variant="border"
|
||||
className={styles.textField}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationCredentials;
|
|
@ -1,4 +1,4 @@
|
|||
import type { Application } from '@logto/schemas';
|
||||
import type { ApplicationResponse } from '@logto/schemas';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
@ -7,7 +7,7 @@ import GuideV2 from '../GuideV2';
|
|||
|
||||
type Props = {
|
||||
guideId: string;
|
||||
app?: Application;
|
||||
app?: ApplicationResponse;
|
||||
onClose: (id: string) => void;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DomainStatus, type Application } from '@logto/schemas';
|
||||
import { DomainStatus, type ApplicationResponse } from '@logto/schemas';
|
||||
import { MDXProvider } from '@mdx-js/react';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import {
|
||||
|
@ -27,7 +27,7 @@ import * as styles from './index.module.scss';
|
|||
type GuideContextType = {
|
||||
metadata: Readonly<GuideMetadata>;
|
||||
Logo?: LazyExoticComponent<ComponentType>;
|
||||
app: Application;
|
||||
app: ApplicationResponse;
|
||||
endpoint: string;
|
||||
alternativeEndpoint?: string;
|
||||
redirectUris: string[];
|
||||
|
@ -45,7 +45,7 @@ export const GuideContext = createContext<GuideContextType>({
|
|||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, no-restricted-syntax
|
||||
metadata: {} as GuideMetadata,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, no-restricted-syntax
|
||||
app: {} as Application,
|
||||
app: {} as ApplicationResponse,
|
||||
endpoint: '',
|
||||
redirectUris: [],
|
||||
postLogoutRedirectUris: [],
|
||||
|
@ -55,7 +55,7 @@ export const GuideContext = createContext<GuideContextType>({
|
|||
|
||||
type Props = {
|
||||
guideId: string;
|
||||
app?: Application;
|
||||
app?: ApplicationResponse;
|
||||
isCompact?: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue