mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
Merge pull request #4335 from logto-io/gao-log-6880-mdx-add-python-sdk-integration-guide
feat(console): python guide
This commit is contained in:
commit
0e11740f55
5 changed files with 238 additions and 6 deletions
|
@ -1,5 +1,4 @@
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||||
import FormField from '@/ds-components/FormField';
|
import FormField from '@/ds-components/FormField';
|
||||||
|
@ -11,7 +10,6 @@ export default function ClientBasics() {
|
||||||
const {
|
const {
|
||||||
app: { id, secret },
|
app: { id, secret },
|
||||||
} = useContext(GuideContext);
|
} = useContext(GuideContext);
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.basic}>
|
<div className={styles.basic}>
|
||||||
|
|
|
@ -1 +1,222 @@
|
||||||
## Replace this with actual guide
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
|
import Tabs from '@mdx/components/Tabs';
|
||||||
|
import TabItem from '@mdx/components/TabItem';
|
||||||
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
import { buildIdGenerator } from '@logto/shared/universal';
|
||||||
|
import Steps from '@/mdx-components-v2/Steps';
|
||||||
|
import Step from '@/mdx-components-v2/Step';
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step title="Get started">
|
||||||
|
|
||||||
|
This tutorial will show you how to integrate Logto into your Python web application.
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>The example uses Flask, but the concepts are the same for other frameworks.</li>
|
||||||
|
<li>This tutorial assumes your website is hosted on <code>{props.sampleUrls.origin}</code>.</li>
|
||||||
|
<li>Logto SDK leverages coroutines, remember to use <code>await</code> when calling async functions.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install logto # or `poetry add logto` or whatever you use
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Init LogtoClient">
|
||||||
|
|
||||||
|
Insert the following code into your python file:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-python">
|
||||||
|
{`from logto import LogtoClient, LogtoConfig
|
||||||
|
|
||||||
|
client = LogtoClient(
|
||||||
|
LogtoConfig(
|
||||||
|
endpoint= "${props.endpoint}",${props.alternativeEndpoint ? ' // or "${props.alternativeEndpoint}"' : ''}
|
||||||
|
appId="${props.app.id}",
|
||||||
|
appSecret="${props.app.secret}",
|
||||||
|
)
|
||||||
|
)`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
Also replace the default memory storage with a persistent storage, for example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from logto import LogtoClient, LogtoConfig, Storage
|
||||||
|
from flask import session
|
||||||
|
|
||||||
|
class SessionStorage(Storage):
|
||||||
|
def get(self, key: str) -> str | None:
|
||||||
|
return session.get(key, None)
|
||||||
|
|
||||||
|
def set(self, key: str, value: str | None) -> None:
|
||||||
|
session[key] = value
|
||||||
|
|
||||||
|
def delete(self, key: str) -> None:
|
||||||
|
session.pop(key, None)
|
||||||
|
|
||||||
|
client = LogtoClient(
|
||||||
|
LogtoConfig(...),
|
||||||
|
storage=SessionStorage(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Implement the sign-in route">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
First, let’s enter your redirect URI. E.g. <code>{props.sampleUrls.callback}</code>. This is where Logto will redirect users after they sign in.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UriInputField name="redirectUris" />
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-python">
|
||||||
|
{`@app.route("/sign-in")
|
||||||
|
async def sign_in():
|
||||||
|
# Get the sign-in URL and redirect the user to it
|
||||||
|
return redirect(await client.signIn(
|
||||||
|
redirectUri="${props.redirectUris[0] || props.sampleUrls.callback}",
|
||||||
|
))`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
If you want to show the sign-up page on the first screen, you can set `interactionMode` to `signUp`:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-python">
|
||||||
|
{`@app.route("/sign-in")
|
||||||
|
async def sign_in():
|
||||||
|
# Get the sign-in URL and redirect the user to it
|
||||||
|
return redirect(await client.signIn(
|
||||||
|
redirectUri="${props.redirectUris[0] || props.sampleUrls.callback}",
|
||||||
|
interactionMode="signUp", # Show the sign-up page on the first screen
|
||||||
|
))`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
Now, whenever your users visit `/sign-in`, it will start a new sign-in attempt and redirect the user to the Logto sign-in page.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Implement the callback route">
|
||||||
|
|
||||||
|
After the user signs in, Logto will redirect the user to the callback URL you set in the Logto Console. In this example, we use `/callback` as the callback URL:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-python">
|
||||||
|
{`@app.route("/callback")
|
||||||
|
async def callback():
|
||||||
|
try:
|
||||||
|
await client.handleSignInCallback(request.url) # Handle a lot of stuff
|
||||||
|
return redirect("/") # Redirect the user to the home page after a successful sign-in
|
||||||
|
except Exception as e:
|
||||||
|
# Change this to your error handling logic
|
||||||
|
return "Error: " + str(e)
|
||||||
|
`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Implement the home page">
|
||||||
|
|
||||||
|
Here we implement a simple home page for demonstration:
|
||||||
|
|
||||||
|
- If the user is not signed in, show a sign-in button;
|
||||||
|
- If the user is signed in, show some basic information about the user.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.route("/")
|
||||||
|
async def home():
|
||||||
|
if client.isAuthenticated() is False:
|
||||||
|
return "Not authenticated <a href='/sign-in'>Sign in</a>"
|
||||||
|
|
||||||
|
return (
|
||||||
|
# Get local ID token claims
|
||||||
|
client.getIdTokenClaims().model_dump_json(exclude_unset=True)
|
||||||
|
+ "<br>"
|
||||||
|
# Fetch user info from Logto userinfo endpoint
|
||||||
|
(await client.fetchUserInfo()).model_dump_json(exclude_unset=True)
|
||||||
|
+ "<br><a href='/sign-out'>Sign out</a>"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Our data models are based on [pydantic](https://docs.pydantic.dev/), so you can use `model_dump_json` to dump the data model to JSON.
|
||||||
|
|
||||||
|
Adding `exclude_unset=True` will exclude unset fields from the JSON output, which makes the output more precise.
|
||||||
|
|
||||||
|
For example, if we didn't request the `email` scope when signing in, and the `email` field will be excluded from the JSON output. However, if we requested the `email` scope, but the user doesn't have an email address, the `email` field will be included in the JSON output with a `null` value.
|
||||||
|
|
||||||
|
To learn more about scopes and claims, see [Scopes and claims](https://github.com/logto-io/python/blob/master/docs/tutorial.md#scopes-and-claims).
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Implement the sign-out route">
|
||||||
|
|
||||||
|
To clean up the Python session and Logto session, a sign-out route can be implemented as follows:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-python">
|
||||||
|
{`@app.route("/sign-out")
|
||||||
|
async def sign_out():
|
||||||
|
return redirect(
|
||||||
|
# Redirect the user to the home page after a successful sign-out
|
||||||
|
await client.signOut(postLogoutRedirectUri="${props.sampleUrls.origin}")
|
||||||
|
)
|
||||||
|
`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
`postLogoutRedirectUri` is optional, and if not provided, the user will be redirected to a Logto default page after a successful sign-out (without redirecting back to your application).
|
||||||
|
|
||||||
|
> The name `postLogoutRedirectUri` is from the [OpenID Connect RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) specification. Although Logto uses "sign-out" instead of "logout", the concept is the same.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Checkpoint: Test your application">
|
||||||
|
|
||||||
|
Now, you can test your application:
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Visit <code>{props.sampleUrls.origin}</code>, and you should see a "Not authenticated" message with a "Sign in" button.</li>
|
||||||
|
<li>Click the "Sign in" button, and you should be redirected to the Logto sign-in page.</li>
|
||||||
|
<li>After you sign in, you should be redirected back to <code>{props.sampleUrls.origin}</code>, and you should see your user info and a "Sign out" button.</li>
|
||||||
|
<li>Click the "Sign out" button, and you should be redirected back to <code>{props.sampleUrls.origin}</code>, and you should see a "Not authenticated" message with a "Sign in" button.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Protect your routes">
|
||||||
|
|
||||||
|
Now, you have a working sign-in flow, but your routes are still unprotected. Per the framework you use, you can create a decorator such as `@authenticated` to protect your routes. For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def authenticated(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
if client.isAuthenticated() is False:
|
||||||
|
return redirect("/sign-in") # Or directly call `client.signIn`
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you can use the decorator to protect your routes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.route("/protected")
|
||||||
|
@authenticated
|
||||||
|
async def protected():
|
||||||
|
return "Protected page"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also create a middleware to achieve the same goal.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
|
@ -4,9 +4,12 @@ import { type GuideMetadata } from '../types';
|
||||||
|
|
||||||
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||||
name: 'Python',
|
name: 'Python',
|
||||||
description:
|
description: 'Integrate Logto into your Python web app, such as Django and Flask.',
|
||||||
'Python is a programming language that lets you work quickly and integrate systems more effectively.',
|
|
||||||
target: ApplicationType.Traditional,
|
target: ApplicationType.Traditional,
|
||||||
|
sample: {
|
||||||
|
repo: 'python',
|
||||||
|
path: 'samples',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
margin: _.unit(4) 0;
|
margin: _.unit(4) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
section ul > li {
|
section ul > li,
|
||||||
|
section ol > li {
|
||||||
|
font: var(--font-body-2);
|
||||||
margin-block: _.unit(2);
|
margin-block: _.unit(2);
|
||||||
padding-inline-start: _.unit(1);
|
padding-inline-start: _.unit(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,10 @@ type GuideContextType = {
|
||||||
redirectUris: string[];
|
redirectUris: string[];
|
||||||
postLogoutRedirectUris: string[];
|
postLogoutRedirectUris: string[];
|
||||||
isCompact: boolean;
|
isCompact: boolean;
|
||||||
|
sampleUrls: {
|
||||||
|
origin: string;
|
||||||
|
callback: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
@ -68,6 +72,10 @@ function GuideV2({ guideId, app, isCompact, onClose }: Props) {
|
||||||
redirectUris: app.oidcClientMetadata.redirectUris,
|
redirectUris: app.oidcClientMetadata.redirectUris,
|
||||||
postLogoutRedirectUris: app.oidcClientMetadata.postLogoutRedirectUris,
|
postLogoutRedirectUris: app.oidcClientMetadata.postLogoutRedirectUris,
|
||||||
isCompact: Boolean(isCompact),
|
isCompact: Boolean(isCompact),
|
||||||
|
sampleUrls: {
|
||||||
|
origin: 'http://localhost:3001/',
|
||||||
|
callback: 'http://localhost:3001/callback',
|
||||||
|
},
|
||||||
}) satisfies GuideContextType,
|
}) satisfies GuideContextType,
|
||||||
[guide, app, tenantEndpoint, isCustomDomainActive, isCompact]
|
[guide, app, tenantEndpoint, isCustomDomainActive, isCompact]
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue