0
Fork 0
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:
simeng-li 2023-08-17 10:46:14 +08:00 committed by GitHub
parent c4a9246071
commit d5835e4e93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 221 additions and 7 deletions

View file

@ -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

View file

@ -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>
);
}

View file

@ -0,0 +1,9 @@
@use '@/scss/underscore' as _;
.textField {
@include _.form-text-field;
}
.container {
padding: _.unit(6) 0;
}

View file

@ -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;

View file

@ -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;
};

View file

@ -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;
};