Skip to content

Commit 90ba6bf

Browse files
committed
feat(core): guard one-time token on consent request
1 parent b08c107 commit 90ba6bf

File tree

2 files changed

+79
-1
lines changed

2 files changed

+79
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { experience } from '@logto/schemas';
2+
import { type MiddlewareType } from 'koa';
3+
import { type IRouterParamContext } from 'koa-router';
4+
import { errors, type Provider } from 'oidc-provider';
5+
6+
import RequestError from '../errors/RequestError/index.js';
7+
import type Libraries from '../tenants/Libraries.js';
8+
import type Queries from '../tenants/Queries.js';
9+
import assertThat from '../utils/assert-that.js';
10+
11+
/**
12+
* Guard before allowing auto-consent
13+
*/
14+
export default function koaConsentGuard<
15+
StateT,
16+
ContextT extends IRouterParamContext,
17+
ResponseBodyT,
18+
>(
19+
provider: Provider,
20+
libraries: Libraries,
21+
query: Queries
22+
): MiddlewareType<StateT, ContextT, ResponseBodyT> {
23+
return async (ctx, next) => {
24+
const interactionDetails = await provider.interactionDetails(ctx.req, ctx.res);
25+
const {
26+
params: { token, login_hint: loginHint },
27+
session,
28+
} = interactionDetails;
29+
30+
assertThat(session, new errors.SessionNotFound('session not found'));
31+
32+
if (token && loginHint && typeof token === 'string' && typeof loginHint === 'string') {
33+
const user = await query.users.findUserById(session.accountId);
34+
35+
assertThat(user.primaryEmail, 'user.email_not_exist');
36+
37+
if (user.primaryEmail !== loginHint) {
38+
const searchParams = new URLSearchParams({
39+
account: user.primaryEmail,
40+
});
41+
ctx.redirect(`${experience.routes.switchAccount}?${searchParams.toString()}`);
42+
return;
43+
}
44+
45+
try {
46+
await libraries.oneTimeTokens.verifyOneTimeToken(token, loginHint);
47+
} catch (error: unknown) {
48+
if (error instanceof RequestError) {
49+
if (error.code === 'one_time_token.email_mismatch') {
50+
const searchParams = new URLSearchParams({
51+
account: user.primaryEmail,
52+
});
53+
ctx.redirect(`${experience.routes.switchAccount}?${searchParams.toString()}`);
54+
return;
55+
}
56+
const searchParams = new URLSearchParams({
57+
code: error.code,
58+
status: error.status.toString(),
59+
message: error.message,
60+
});
61+
ctx.redirect(`${experience.routes.error}?${searchParams.toString()}`);
62+
return;
63+
}
64+
65+
throw error;
66+
}
67+
}
68+
69+
return next();
70+
};
71+
}

packages/core/src/tenants/Tenant.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import BasicSentinel from '#src/sentinel/basic-sentinel.js';
3232

3333
import { redisCache } from '../caches/index.js';
3434
import { SubscriptionLibrary } from '../libraries/subscription.js';
35+
import koaConsentGuard from '../middleware/koa-consent-guard.js';
3536

3637
import Libraries from './Libraries.js';
3738
import Queries from './Queries.js';
@@ -195,7 +196,13 @@ export default class Tenant implements TenantContext {
195196
compose([
196197
koaExperienceSsr(libraries, queries),
197198
koaSpaSessionGuard(provider, queries),
198-
mount(`/${experience.routes.consent}`, koaAutoConsent(provider, queries)),
199+
mount(
200+
`/${experience.routes.consent}`,
201+
compose([
202+
koaConsentGuard(provider, libraries, queries),
203+
koaAutoConsent(provider, queries),
204+
])
205+
),
199206
koaSpaProxy({ mountedApps, queries }),
200207
])
201208
);

0 commit comments

Comments
 (0)