-
-
Notifications
You must be signed in to change notification settings - Fork 422
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(console): add traditional web guide - express js demo
- Loading branch information
1 parent
8e2eda8
commit 60c9ceb
Showing
5 changed files
with
775 additions
and
3 deletions.
There are no files selected for viewing
386 changes: 386 additions & 0 deletions
386
packages/console/src/assets/docs/tutorial/integrate-sdk/express.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,386 @@ | ||
import UriInputField from '@mdx/components/UriInputField'; | ||
import Step from '@mdx/components/Step'; | ||
import Tabs from '@mdx/components/Tabs'; | ||
import TabItem from '@mdx/components/TabItem'; | ||
import Alert from '@/components/Alert'; | ||
|
||
<Step | ||
title="Add Logto SDK as a dependency" | ||
subtitle="Please select your favorite package manager" | ||
index={0} | ||
activeIndex={props.activeStepIndex} | ||
onButtonClick={() => props.onNext(1)} | ||
> | ||
|
||
The express demo app will need 4 dependencies: | ||
|
||
1. **@logto/js**: Logto's core SDK for JavaScript. | ||
2. **node-fetch**: Minimal code for a `window.fetch` compatible API on Node.js runtime. | ||
3. **express-session**: A session middleware, we'll use the session to store user tokens. | ||
4. **js-base64**: Yet another Base64 transcoder. | ||
|
||
<Tabs> | ||
<TabItem value="npm" label="npm"> | ||
|
||
```bash | ||
npm i @logto/js node-fetch@v2 express-session js-base64 | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="yarn" label="Yarn"> | ||
|
||
```bash | ||
yarn add @logto/js node-fetch@v2 express-session js-base64 | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="pnpm" label="pnpm"> | ||
|
||
```bash | ||
pnpm add @logto/js node-fetch@v2 express-session js-base64 | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
</Step> | ||
|
||
<Step | ||
title="Use session" | ||
subtitle="1 step" | ||
index={1} | ||
activeIndex={props.activeStepIndex} | ||
onButtonClick={() => props.onNext(2)} | ||
> | ||
|
||
When users are signed in, they will get a set of tokens (Access Token, ID Token, Refresh Token) and interaction data, and the session is an excellent place to store them. | ||
|
||
We have installed [express-session](https://github.com/expressjs/session) in the previous step, so now let's simply add the following code to set it up: | ||
|
||
```js | ||
// app.js | ||
|
||
const session = require('express-session'); | ||
|
||
app.use( | ||
session({ | ||
secret: 'keyboard cat', // Change to your own secret key | ||
cookie: { maxAge: 86400 }, | ||
}) | ||
); | ||
``` | ||
|
||
</Step> | ||
|
||
<Step | ||
title="Prepare for authentication" | ||
subtitle="2 steps" | ||
index={2} | ||
activeIndex={props.activeStepIndex} | ||
onButtonClick={() => props.onNext(3)} | ||
> | ||
|
||
<Alert> | ||
In the following steps, we assume your app is running on <code>http://localhost:3000</code>. | ||
</Alert> | ||
|
||
### Configure Redirect URI | ||
|
||
First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`. | ||
|
||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="Redirect URI" /> | ||
|
||
Go back to your IDE/editor, we need to implement the following authenticate functions. | ||
|
||
### Implement authenticate functions | ||
|
||
1. `getSignInUrl`: builds and returns a complete URL of the Logto Authorization Server to which users will be redirected. | ||
2. `handleSignIn`: parses the callback URL after the authentication process completes, gets the code query parameter, and then fetches tokens (an access token, the refresh token, and an ID token) to complete the sign in process. | ||
3. `refreshTokens`: exchanges a new access token using the refresh token. | ||
|
||
<pre> | ||
<code className={"language-js"}> | ||
{`// logto.js | ||
const { | ||
withReservedScopes, | ||
fetchOidcConfig, | ||
discoveryPath, | ||
createRequester, | ||
generateSignInUri, | ||
verifyAndParseCodeFromCallbackUri, | ||
fetchTokenByAuthorizationCode, | ||
fetchTokenByRefreshToken, | ||
} = require('@logto/js'); | ||
const fetch = require('node-fetch'); | ||
const { randomFillSync, createHash } = require('crypto'); | ||
const { fromUint8Array } = require('js-base64'); | ||
const config = { | ||
endpoint: '${props.endpoint}', | ||
appId: '${props.appId}', | ||
redirectUri: 'http://localhost:3000/callback', // Configured in the previous step | ||
scopes: withReservedScopes().split(' '), | ||
}; | ||
const requester = createRequester(fetch); | ||
const generateRandomString = (length = 64) => { | ||
return fromUint8Array(randomFillSync(new Uint8Array(length)), true); | ||
}; | ||
const generateCodeChallenge = async (codeVerifier) => { | ||
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier); | ||
const hash = createHash('sha256'); | ||
hash.update(encodedCodeVerifier); | ||
const codeChallenge = hash.digest(); | ||
return fromUint8Array(codeChallenge, true); | ||
}; | ||
const getOidcConfig = async () => { | ||
return fetchOidcConfig(new URL(discoveryPath, config.endpoint).toString(), requester); | ||
}; | ||
exports.getSignInUrl = async () => { | ||
const { authorizationEndpoint } = await getOidcConfig(); | ||
const codeVerifier = generateRandomString(); | ||
const codeChallenge = await generateCodeChallenge(codeVerifier); | ||
const state = generateRandomString(); | ||
const { redirectUri, scopes, appId: clientId } = config; | ||
const signInUri = generateSignInUri({ | ||
authorizationEndpoint, | ||
clientId, | ||
redirectUri: redirectUri, | ||
codeChallenge, | ||
state, | ||
scopes, | ||
}); | ||
return { redirectUri, codeVerifier, state, signInUri }; | ||
}; | ||
exports.handleSignIn = async (signInSession, callbackUri) => { | ||
const { redirectUri, state, codeVerifier } = signInSession; | ||
const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state); | ||
const { appId: clientId } = config; | ||
const { tokenEndpoint } = await getOidcConfig(); | ||
const codeTokenResponse = await fetchTokenByAuthorizationCode( | ||
{ | ||
clientId, | ||
tokenEndpoint, | ||
redirectUri, | ||
codeVerifier, | ||
code, | ||
}, | ||
requester | ||
); | ||
return codeTokenResponse; | ||
}; | ||
exports.refreshTokens = async (refreshToken) => { | ||
const { appId: clientId, scopes } = config; | ||
const { tokenEndpoint } = await getOidcConfig(); | ||
const tokenResponse = await fetchTokenByRefreshToken( | ||
{ | ||
clientId, | ||
tokenEndpoint, | ||
refreshToken, | ||
scopes, | ||
}, | ||
requester | ||
); | ||
return tokenResponse; | ||
};`} | ||
</code> | ||
</pre> | ||
|
||
</Step> | ||
|
||
<Step | ||
title="Sign in" | ||
subtitle="2 steps" | ||
index={3} | ||
activeIndex={props.activeStepIndex} | ||
onButtonClick={() => props.onNext(4)} | ||
> | ||
|
||
### Create a route in Express to sign in: | ||
|
||
```js | ||
const { getSignInUrl } = require('./logto'); | ||
|
||
app.get('/sign-in', async (req, res) => { | ||
const { redirectUri, codeVerifier, state, signInUri } = await getSignInUrl(); | ||
req.session.signIn = { codeVerifier, state, redirectUri }; | ||
res.redirect(signInUri); | ||
}); | ||
``` | ||
|
||
### Create a route to handle callback: | ||
|
||
```js | ||
app.get('/callback', async (req, res) => { | ||
if (!req.session.signIn) { | ||
res.send('Bad request.'); | ||
return; | ||
} | ||
|
||
const response = await handleSignIn( | ||
req.session.signIn, | ||
`${req.protocol}://${req.get('host')}${req.originalUrl}` | ||
); | ||
req.session.tokens = { | ||
...response, | ||
expiresAt: response.expiresIn + Date.now(), | ||
idToken: decodeIdToken(response.idToken), | ||
}; | ||
req.session.signIn = null; | ||
|
||
res.redirect('/'); | ||
}); | ||
``` | ||
|
||
</Step> | ||
|
||
<Step | ||
title="Sign out" | ||
subtitle="1 step" | ||
index={4} | ||
activeIndex={props.activeStepIndex} | ||
onButtonClick={() => props.onNext(5)} | ||
> | ||
|
||
TODO: link to the "session & cookies" chapter in users reference. | ||
|
||
You can clear tokens in session to sign out a user from this application. And check this link to read more about "sign out". | ||
|
||
```js | ||
app.get('/sign-out', (req, res) => { | ||
req.session.tokens = null; | ||
res.send('Sign out successfully'); | ||
}); | ||
``` | ||
|
||
</Step> | ||
|
||
<Step | ||
title="Access protected resource" | ||
subtitle="3 steps" | ||
index={5} | ||
activeIndex={props.activeStepIndex} | ||
onButtonClick={() => props.onNext(6)} | ||
> | ||
|
||
### Middleware | ||
|
||
Create a middleware named `withAuth` to attach an `auth` object to `req`, and verify if a user is signed in. | ||
|
||
```js | ||
// auth.js | ||
|
||
const { decodeIdToken } = require('@logto/js'); | ||
const { refreshTokens } = require('./logto'); | ||
|
||
const withAuth = | ||
({ requireAuth } = { requireAuth: true }) => | ||
async (req, res, next) => { | ||
if (requireAuth && !req.session.tokens) { | ||
res.redirect('/sign-in'); | ||
return; | ||
} | ||
|
||
if (req.session.tokens) { | ||
if (req.session.tokens.expiresAt >= Date.now()) { | ||
// Access token expired, refresh for new tokens | ||
try { | ||
const response = await refreshTokens(req.session.tokens.refreshToken); | ||
req.session.tokens = { | ||
...response, | ||
expiresAt: response.expiresIn + Date.now(), | ||
idToken: decodeIdToken(response.idToken), | ||
}; | ||
} catch { | ||
// Exchange failed, redirect to sign in | ||
res.redirect('/sign-in'); | ||
return; | ||
} | ||
} | ||
|
||
req.auth = req.session.tokens.idToken.sub; | ||
} | ||
|
||
next(); | ||
}; | ||
|
||
module.exports = withAuth; | ||
``` | ||
|
||
### Implement index page | ||
|
||
In this page, we will show a sign-in link for guests, and a go-to-profile link for users that already signed in: | ||
|
||
```js | ||
// routes/index.js | ||
|
||
router.get('/', withAuth({ requireAuth: false }), function (req, res, next) { | ||
res.render('index', { auth: Boolean(req.auth) }); | ||
}); | ||
``` | ||
|
||
```pug | ||
// views/index.jade | ||
extends layout | ||
block content | ||
h1 Hello logto | ||
if auth | ||
p: a(href="/user") Go to profile | ||
else | ||
p: a(href="/sign-in") Click here to sign in | ||
``` | ||
|
||
### Implement user page | ||
|
||
In the user page, we will fetch the protected resource `userId` (`subject`): | ||
|
||
```js | ||
// routes/user.js | ||
|
||
app.get('/user', withAuth(), (req, res, next) => { | ||
res.render('user', { userId: req.auth }); | ||
}); | ||
``` | ||
|
||
```pug | ||
// views/index.jade | ||
extends layout | ||
block content | ||
h1 Hello logto | ||
p userId: #{userId} | ||
``` | ||
|
||
</Step> | ||
|
||
<Step | ||
title="Further readings" | ||
subtitle="4 articles" | ||
index={6} | ||
activeIndex={props.activeStepIndex} | ||
buttonText="admin_console.general.done" | ||
buttonType="primary" | ||
onButtonClick={props.onComplete} | ||
> | ||
|
||
- [Customize sign-in experience](https://docs.logto.io/docs/recipes/customize-sie) | ||
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/enable-passcode-sign-in) | ||
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in) | ||
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api) | ||
|
||
</Step> |
Oops, something went wrong.