@sentropic/auth-hono 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -1
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +2 -0
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/oauth/authorize-handler.d.ts +13 -0
- package/dist/oauth/authorize-handler.d.ts.map +1 -0
- package/dist/oauth/authorize-handler.js +143 -0
- package/dist/oauth/authorize-handler.js.map +1 -0
- package/dist/oauth/consent-decision-handler.d.ts +11 -0
- package/dist/oauth/consent-decision-handler.d.ts.map +1 -0
- package/dist/oauth/consent-decision-handler.js +58 -0
- package/dist/oauth/consent-decision-handler.js.map +1 -0
- package/dist/oauth/crypto-utils.d.ts +3 -0
- package/dist/oauth/crypto-utils.d.ts.map +1 -0
- package/dist/oauth/crypto-utils.js +13 -0
- package/dist/oauth/crypto-utils.js.map +1 -0
- package/dist/oauth/dpop.d.ts +18 -0
- package/dist/oauth/dpop.d.ts.map +1 -0
- package/dist/oauth/dpop.js +54 -0
- package/dist/oauth/dpop.js.map +1 -0
- package/dist/oauth/http-utils.d.ts +6 -0
- package/dist/oauth/http-utils.d.ts.map +1 -0
- package/dist/oauth/http-utils.js +27 -0
- package/dist/oauth/http-utils.js.map +1 -0
- package/dist/oauth/introspect-handler.d.ts +8 -0
- package/dist/oauth/introspect-handler.d.ts.map +1 -0
- package/dist/oauth/introspect-handler.js +63 -0
- package/dist/oauth/introspect-handler.js.map +1 -0
- package/dist/oauth/jwks-service.d.ts +25 -0
- package/dist/oauth/jwks-service.d.ts.map +1 -0
- package/dist/oauth/jwks-service.js +61 -0
- package/dist/oauth/jwks-service.js.map +1 -0
- package/dist/oauth/revoke-handler.d.ts +8 -0
- package/dist/oauth/revoke-handler.d.ts.map +1 -0
- package/dist/oauth/revoke-handler.js +55 -0
- package/dist/oauth/revoke-handler.js.map +1 -0
- package/dist/oauth/router.d.ts +8 -0
- package/dist/oauth/router.d.ts.map +1 -0
- package/dist/oauth/router.js +30 -0
- package/dist/oauth/router.js.map +1 -0
- package/dist/oauth/session-resolver.d.ts +9 -0
- package/dist/oauth/session-resolver.d.ts.map +1 -0
- package/dist/oauth/session-resolver.js +28 -0
- package/dist/oauth/session-resolver.js.map +1 -0
- package/dist/oauth/state-codec.d.ts +25 -0
- package/dist/oauth/state-codec.d.ts.map +1 -0
- package/dist/oauth/state-codec.js +60 -0
- package/dist/oauth/state-codec.js.map +1 -0
- package/dist/oauth/state-store-types.d.ts +86 -0
- package/dist/oauth/state-store-types.d.ts.map +1 -0
- package/dist/oauth/state-store-types.js +2 -0
- package/dist/oauth/state-store-types.js.map +1 -0
- package/dist/oauth/token-handler.d.ts +11 -0
- package/dist/oauth/token-handler.d.ts.map +1 -0
- package/dist/oauth/token-handler.js +176 -0
- package/dist/oauth/token-handler.js.map +1 -0
- package/dist/oauth/userinfo-handler.d.ts +9 -0
- package/dist/oauth/userinfo-handler.d.ts.map +1 -0
- package/dist/oauth/userinfo-handler.js +93 -0
- package/dist/oauth/userinfo-handler.js.map +1 -0
- package/dist/oauth/wellknown-handler.d.ts +9 -0
- package/dist/oauth/wellknown-handler.d.ts.map +1 -0
- package/dist/oauth/wellknown-handler.js +37 -0
- package/dist/oauth/wellknown-handler.js.map +1 -0
- package/dist/ports.d.ts +4 -0
- package/dist/ports.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/contracts.ts +2 -0
- package/src/index.ts +15 -0
- package/src/oauth/authorize-handler.ts +201 -0
- package/src/oauth/consent-decision-handler.ts +93 -0
- package/src/oauth/crypto-utils.ts +14 -0
- package/src/oauth/dpop.ts +93 -0
- package/src/oauth/http-utils.ts +58 -0
- package/src/oauth/introspect-handler.ts +88 -0
- package/src/oauth/jwks-service.ts +103 -0
- package/src/oauth/revoke-handler.ts +70 -0
- package/src/oauth/router.ts +42 -0
- package/src/oauth/session-resolver.ts +48 -0
- package/src/oauth/state-codec.ts +98 -0
- package/src/oauth/state-store-types.ts +94 -0
- package/src/oauth/token-handler.ts +252 -0
- package/src/oauth/userinfo-handler.ts +129 -0
- package/src/oauth/wellknown-handler.ts +52 -0
- package/src/ports.ts +16 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { JWTPayload } from 'jose';
|
|
3
|
+
|
|
4
|
+
import type { AuthHonoPorts } from '../ports.js';
|
|
5
|
+
import { OAuthDpopProofError, verifyOAuthDpopProof } from './dpop.js';
|
|
6
|
+
import { oauthJsonError } from './http-utils.js';
|
|
7
|
+
import { createJwksService } from './jwks-service.js';
|
|
8
|
+
import type { TokenMeta } from './state-store-types.js';
|
|
9
|
+
|
|
10
|
+
export interface OAuthUserInfoHandlerOptions {
|
|
11
|
+
dpopIatSkewSeconds?: number;
|
|
12
|
+
issuer: string;
|
|
13
|
+
ports: AuthHonoPorts;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const createOAuthUserInfoHandler =
|
|
17
|
+
(options: OAuthUserInfoHandlerOptions) =>
|
|
18
|
+
async (c: Context): Promise<Response> => {
|
|
19
|
+
const authorization = parseAccessToken(c.req.header('authorization'));
|
|
20
|
+
if (!authorization) return unauthorized(c, 'Access token is required.');
|
|
21
|
+
|
|
22
|
+
const payload = await verifyAccessToken(c, options, authorization.token);
|
|
23
|
+
if (payload instanceof Response) return payload;
|
|
24
|
+
|
|
25
|
+
const meta = await resolveActiveTokenMeta(c, options.ports, payload);
|
|
26
|
+
if (meta instanceof Response) return meta;
|
|
27
|
+
if (meta.dpopJkt) {
|
|
28
|
+
const dpop = await verifyBoundDpop(c, options, authorization, meta);
|
|
29
|
+
if (dpop instanceof Response) return dpop;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const scopes = meta.scope.split(/\s+/).filter(Boolean);
|
|
33
|
+
if (scopes.some((scope) => !['openid', 'profile', 'email'].includes(scope))) {
|
|
34
|
+
return unauthorized(c, 'Access token contains unsupported scopes.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const user = await options.ports.users.findById(meta.userId);
|
|
38
|
+
if (!user) return unauthorized(c, 'Access token user is invalid.');
|
|
39
|
+
|
|
40
|
+
return c.json({
|
|
41
|
+
sub: user.id,
|
|
42
|
+
...(scopes.includes('profile') ? { name: user.displayName } : {}),
|
|
43
|
+
...(scopes.includes('email') ? { email: user.email, email_verified: user.emailVerified } : {}),
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const verifyAccessToken = async (
|
|
48
|
+
c: Context,
|
|
49
|
+
options: OAuthUserInfoHandlerOptions,
|
|
50
|
+
token: string
|
|
51
|
+
): Promise<JWTPayload | Response> => {
|
|
52
|
+
try {
|
|
53
|
+
const jwks = createJwksService({ clock: options.ports.clock, jwksPort: options.ports.jwks });
|
|
54
|
+
const result = await jwks.verifyJwt(token, {
|
|
55
|
+
audience: `${trimTrailingSlash(options.issuer)}/api/v1/auth/oauth/userinfo`,
|
|
56
|
+
currentDate: options.ports.clock.now(),
|
|
57
|
+
issuer: trimTrailingSlash(options.issuer),
|
|
58
|
+
});
|
|
59
|
+
return result.payload;
|
|
60
|
+
} catch {
|
|
61
|
+
return unauthorized(c, 'Access token is invalid.');
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const resolveActiveTokenMeta = async (
|
|
66
|
+
c: Context,
|
|
67
|
+
ports: AuthHonoPorts,
|
|
68
|
+
payload: JWTPayload
|
|
69
|
+
): Promise<TokenMeta | Response> => {
|
|
70
|
+
const jti = payload.jti;
|
|
71
|
+
if (!jti) return unauthorized(c, 'Access token jti is missing.');
|
|
72
|
+
|
|
73
|
+
const meta = await ports.oauthStateStore.findTokenMeta(jti);
|
|
74
|
+
if (
|
|
75
|
+
!meta ||
|
|
76
|
+
meta.tokenType !== 'access_token' ||
|
|
77
|
+
meta.expiresAt <= ports.clock.now() ||
|
|
78
|
+
(await ports.oauthStateStore.isTokenRevoked(jti))
|
|
79
|
+
) {
|
|
80
|
+
return unauthorized(c, 'Access token is inactive.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return meta;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const verifyBoundDpop = async (
|
|
87
|
+
c: Context,
|
|
88
|
+
options: OAuthUserInfoHandlerOptions,
|
|
89
|
+
authorization: { scheme: 'Bearer' | 'DPoP'; token: string },
|
|
90
|
+
meta: TokenMeta
|
|
91
|
+
): Promise<null | Response> => {
|
|
92
|
+
const proof = c.req.header('dpop');
|
|
93
|
+
if (authorization.scheme !== 'DPoP' || !proof) {
|
|
94
|
+
return unauthorized(c, 'DPoP proof is required for this access token.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const verified = await verifyOAuthDpopProof({
|
|
99
|
+
accessToken: authorization.token,
|
|
100
|
+
htm: c.req.method,
|
|
101
|
+
htu: c.req.url,
|
|
102
|
+
iatSkewSeconds: options.dpopIatSkewSeconds,
|
|
103
|
+
ports: options.ports,
|
|
104
|
+
proof,
|
|
105
|
+
});
|
|
106
|
+
if (verified.jkt !== meta.dpopJkt) {
|
|
107
|
+
return unauthorized(c, 'DPoP proof key does not match the access token.');
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof OAuthDpopProofError) {
|
|
112
|
+
return unauthorized(c, error.message);
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const parseAccessToken = (
|
|
119
|
+
authorization: string | undefined
|
|
120
|
+
): { scheme: 'Bearer' | 'DPoP'; token: string } | null => {
|
|
121
|
+
const [scheme, token, extra] = authorization?.split(/\s+/) ?? [];
|
|
122
|
+
if (extra || !token || (scheme !== 'Bearer' && scheme !== 'DPoP')) return null;
|
|
123
|
+
return { scheme, token };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const unauthorized = (c: Context, message: string): Response =>
|
|
127
|
+
oauthJsonError(c, 401, 'invalid_token', message);
|
|
128
|
+
|
|
129
|
+
const trimTrailingSlash = (value: string): string => value.replace(/\/+$/u, '');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
|
|
3
|
+
import type { AuthHonoPorts } from '../ports.js';
|
|
4
|
+
import { createJwksService } from './jwks-service.js';
|
|
5
|
+
|
|
6
|
+
export interface CreateWellKnownRouterOptions {
|
|
7
|
+
issuer: string;
|
|
8
|
+
oauthPathPrefix?: string;
|
|
9
|
+
ports: AuthHonoPorts;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const createWellKnownRouter = (options: CreateWellKnownRouterOptions): Hono => {
|
|
13
|
+
const router = new Hono();
|
|
14
|
+
const issuer = trimTrailingSlash(options.issuer);
|
|
15
|
+
const oauthPrefix = normalizePathPrefix(options.oauthPathPrefix ?? '/api/v1/auth/oauth');
|
|
16
|
+
|
|
17
|
+
router.get('/openid-configuration', (c) =>
|
|
18
|
+
c.json({
|
|
19
|
+
authorization_endpoint: `${issuer}${oauthPrefix}/authorize`,
|
|
20
|
+
claims_supported: ['sub', 'aud', 'iss', 'exp', 'iat', 'nonce', 'auth_time', 'acr', 'email', 'email_verified', 'name'],
|
|
21
|
+
code_challenge_methods_supported: ['S256'],
|
|
22
|
+
dpop_signing_alg_values_supported: ['EdDSA'],
|
|
23
|
+
grant_types_supported: ['authorization_code'],
|
|
24
|
+
id_token_signing_alg_values_supported: ['EdDSA'],
|
|
25
|
+
introspection_endpoint: `${issuer}${oauthPrefix}/introspect`,
|
|
26
|
+
issuer,
|
|
27
|
+
jwks_uri: `${issuer}/.well-known/jwks.json`,
|
|
28
|
+
response_types_supported: ['code'],
|
|
29
|
+
revocation_endpoint: `${issuer}${oauthPrefix}/revoke`,
|
|
30
|
+
scopes_supported: ['openid', 'profile', 'email'],
|
|
31
|
+
subject_types_supported: ['public'],
|
|
32
|
+
token_endpoint: `${issuer}${oauthPrefix}/token`,
|
|
33
|
+
token_endpoint_auth_methods_supported: ['client_secret_basic', 'none'],
|
|
34
|
+
userinfo_endpoint: `${issuer}${oauthPrefix}/userinfo`,
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
router.get('/jwks.json', async (c) => {
|
|
39
|
+
const jwks = createJwksService({ clock: options.ports.clock, jwksPort: options.ports.jwks });
|
|
40
|
+
c.header('Cache-Control', 'public, max-age=300');
|
|
41
|
+
return c.json(await jwks.getPublicJwks());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return router;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const trimTrailingSlash = (value: string): string => value.replace(/\/+$/u, '');
|
|
48
|
+
|
|
49
|
+
const normalizePathPrefix = (value: string): string => {
|
|
50
|
+
const trimmed = value.replace(/^\/+|\/+$/gu, '');
|
|
51
|
+
return trimmed ? `/${trimmed}` : '';
|
|
52
|
+
};
|
package/src/ports.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import type { JwksPort, OauthStateStorePort } from './oauth/state-store-types.js';
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
AuthCodePayload,
|
|
5
|
+
DpopProofRecord,
|
|
6
|
+
JwksKeyRecord,
|
|
7
|
+
JwksPort,
|
|
8
|
+
JwksPublicJwk,
|
|
9
|
+
OauthClientRecord,
|
|
10
|
+
OauthStateStorePort,
|
|
11
|
+
OauthTokenType,
|
|
12
|
+
TokenMeta,
|
|
13
|
+
} from './oauth/state-store-types.js';
|
|
14
|
+
|
|
1
15
|
export type AuthHonoAccountStatus =
|
|
2
16
|
| 'active'
|
|
3
17
|
| 'pending_admin_approval'
|
|
@@ -286,4 +300,6 @@ export interface AuthHonoPorts {
|
|
|
286
300
|
clock: AuthHonoClockPort;
|
|
287
301
|
random: AuthHonoRandomPort;
|
|
288
302
|
accountPolicy: AuthHonoAccountPolicyPort;
|
|
303
|
+
oauthStateStore: OauthStateStorePort;
|
|
304
|
+
jwks: JwksPort;
|
|
289
305
|
}
|