Add authentication to your traditional web application
This guide assumes you have created an Application of type "Traditional web" in Admin Console.
Your app may run on the server-side instead of the browser, using frameworks like Django, Laravel, etc. That is called a Traditional web app. If you cannot find a suitable SDK in this page, you probably need to integrate manually.
This article guides you on how to finish it step by step. And we take Express in Node.js as an example.
This article is not just for Express, and if you are using other frameworks or even other languages, you can replace @logto/js
with other language's core SDK and then adjust some of the steps.
Get source code
You can go to GitHub to get the final code for this guide.
Start an Express project
With express-generator
, you can quickly start an Express project.
mkdir express-logto
cd express-logto
npx express-generator
Install dependencies
The demo app will need 4 dependencies:
- @logto/js: Logto's core SDK for JavaScript.
- node-fetch: Minimal code for a
window.fetch
compatible API on Node.js runtime. - express-session: A session middleware, we'll use the session to store user tokens.
- js-base64: Yet another Base64 transcoder.
- npm
- Yarn
- pnpm
npm i @logto/js node-fetch@v2 express-session js-base64
yarn add @logto/js node-fetch@v2 express-session js-base64
pnpm add @logto/js node-fetch@v2 express-session js-base64
Use session
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 in the previous step, so now let's simply add the following code to set it up:
// app.js
const session = require('express-session');
app.use(
session({
secret: 'keyboard cat', // Change to your own secret key
cookie: { maxAge: 86400 },
})
);
Implement functions to authenticate users
We assume the application is running on http://localhost:3000
in the following code snippets.
In this step, we need to implement the following authenticate functions:
getSignInUrl
: builds and returns a complete URL of the Logto Authorization Server to which users will be redirected.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.refreshTokens
: exchanges a new access token using the refresh token.
You can find and copy "App Secret" from application details page in Admin Console:
// logto.js
const {
withReservedScopes,
fetchOidcConfig,
discoveryPath,
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: 'https://logto.dev',
appId: 'foo',
appSecret: '<your-app-secret-copied-from-console>',
redirectUri: 'http://localhost:3000/callback', // You may need to replace it with your app's production address
scopes: withReservedScopes().split(' '),
};
const requester = (input, init) => {
const { appId, appSecret } = config;
return fetch(input, {
...init,
headers: {
...init?.headers,
authorization: `Basic ${Buffer.from(`${appId}:${appSecret}`, 'utf8').toString('base64')}`,
},
});
};
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;
};
Sign in
Before we dive into the details, here's a quick overview of the end-user experience. The sign-in process can be simplified as follows:
- Your app invokes the sign-in method.
- The user is redirected to the Logto sign-in page. For native apps, the system browser is opened.
- The user signs in and is redirected back to your app (configured as the redirect URI).
Regarding redirect-based sign-in
- This authentication process follows the OpenID Connect (OIDC) protocol, and Logto enforces strict security measures to protect user sign-in.
- If you have multiple apps, you can use the same identity provider (Logto). Once the user signs in to one app, Logto will automatically complete the sign-in process when the user accesses another app.
To learn more about the rationale and benefits of redirect-based sign-in, see Logto sign-in experience explained.
Configure Redirect URI
Let's switch to the Application details page of Logto Console. Add a Redirect URI http://localhost:3000/callback
and click "Save changes".
Implement sign in route
Before calling getSignInUrl()
, make sure you have correctly configured Redirect URI
in Admin Console.
Create a route in Express to sign in:
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);
});
and a route to handle callback:
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('/');
});
Sign out
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".
app.get('/sign-out', (req, res) => {
req.session.tokens = null;
res.send('Sign out successful');
});
Access protected resource
Create a middleware named withAuth
to attach an auth
object to req
, and verify if a user is signed in.
// 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;
create index
page, show a sign-in link for guests, and a go-to-profile link for users that already signed in:
router.get('/', withAuth({ requireAuth: false }), function (req, res, next) {
res.render('index', { auth: Boolean(req.auth) });
});
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
create user
page, to display userId
(subject
):
app.get('/user', withAuth(), (req, res, next) => {
res.render('user', { userId: req.auth });
});
extends layout
block content
h1 Hello logto
p userId: #{userId}
Further readings
End-user flows: authentication flows, account flows, and organization flows Configure connectors Protect your APIAuthenticate users in GPT actions: Build a personal agenda assistant