@authcore/core 0.7.0 → 0.12.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/LICENSE +21 -21
- package/README.md +141 -125
- package/dist/auth.d.ts +140 -12
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +265 -7
- package/dist/auth.js.map +1 -1
- package/dist/features/emailVerification.d.ts +6 -1
- package/dist/features/emailVerification.d.ts.map +1 -1
- package/dist/features/emailVerification.js +7 -10
- package/dist/features/emailVerification.js.map +1 -1
- package/dist/features/invitation.d.ts +7 -1
- package/dist/features/invitation.d.ts.map +1 -1
- package/dist/features/invitation.js +7 -9
- package/dist/features/invitation.js.map +1 -1
- package/dist/features/magicLink.d.ts +56 -0
- package/dist/features/magicLink.d.ts.map +1 -0
- package/dist/features/magicLink.js +88 -0
- package/dist/features/magicLink.js.map +1 -0
- package/dist/features/oauth.d.ts +39 -0
- package/dist/features/oauth.d.ts.map +1 -0
- package/dist/features/oauth.js +161 -0
- package/dist/features/oauth.js.map +1 -0
- package/dist/features/passwordReset.d.ts +6 -1
- package/dist/features/passwordReset.d.ts.map +1 -1
- package/dist/features/passwordReset.js +7 -10
- package/dist/features/passwordReset.js.map +1 -1
- package/dist/features/refresh.d.ts +41 -0
- package/dist/features/refresh.d.ts.map +1 -0
- package/dist/features/refresh.js +58 -0
- package/dist/features/refresh.js.map +1 -0
- package/dist/features/templates.d.ts +46 -0
- package/dist/features/templates.d.ts.map +1 -0
- package/dist/features/templates.js +67 -0
- package/dist/features/templates.js.map +1 -0
- package/dist/features/twoFactor.d.ts +72 -0
- package/dist/features/twoFactor.d.ts.map +1 -0
- package/dist/features/twoFactor.js +119 -0
- package/dist/features/twoFactor.js.map +1 -0
- package/dist/index.d.ts +21 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/oauth/apple.d.ts +80 -0
- package/dist/oauth/apple.d.ts.map +1 -0
- package/dist/oauth/apple.js +148 -0
- package/dist/oauth/apple.js.map +1 -0
- package/dist/oauth/discord.d.ts +32 -0
- package/dist/oauth/discord.d.ts.map +1 -0
- package/dist/oauth/discord.js +86 -0
- package/dist/oauth/discord.js.map +1 -0
- package/dist/oauth/github.d.ts +35 -0
- package/dist/oauth/github.d.ts.map +1 -0
- package/dist/oauth/github.js +114 -0
- package/dist/oauth/github.js.map +1 -0
- package/dist/oauth/google.d.ts +21 -0
- package/dist/oauth/google.d.ts.map +1 -0
- package/dist/oauth/google.js +76 -0
- package/dist/oauth/google.js.map +1 -0
- package/dist/oauth/microsoft.d.ts +40 -0
- package/dist/oauth/microsoft.d.ts.map +1 -0
- package/dist/oauth/microsoft.js +126 -0
- package/dist/oauth/microsoft.js.map +1 -0
- package/dist/utils/token.d.ts +37 -0
- package/dist/utils/token.d.ts.map +1 -1
- package/dist/utils/token.js +53 -0
- package/dist/utils/token.js.map +1 -1
- package/dist/utils/totp.d.ts +59 -0
- package/dist/utils/totp.d.ts.map +1 -0
- package/dist/utils/totp.js +176 -0
- package/dist/utils/totp.js.map +1 -0
- package/dist/utils/validation.d.ts +18 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +8 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +2 -2
- package/dist/adapters/database.interface.d.ts +0 -42
- package/dist/adapters/database.interface.d.ts.map +0 -1
- package/dist/adapters/database.interface.js +0 -2
- package/dist/adapters/database.interface.js.map +0 -1
- package/dist/adapters/email.interface.d.ts +0 -31
- package/dist/adapters/email.interface.d.ts.map +0 -1
- package/dist/adapters/email.interface.js +0 -2
- package/dist/adapters/email.interface.js.map +0 -1
- package/dist/types.d.ts +0 -76
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DatabaseAdapter, User } from '@authcore/types';
|
|
2
|
+
/**
|
|
3
|
+
* Begin 2FA enrollment for a user.
|
|
4
|
+
*
|
|
5
|
+
* Generates a fresh TOTP secret + a fresh set of recovery codes. The secret is
|
|
6
|
+
* stored on the user immediately so a `setup → enable` sequence on the same
|
|
7
|
+
* device works, but `twoFactorEnabled` stays `false` until `enableTwoFactor`
|
|
8
|
+
* verifies the first code from the authenticator app.
|
|
9
|
+
*
|
|
10
|
+
* Recovery codes are SHA-256 hashed before storage (same pattern as every
|
|
11
|
+
* other AuthCore token). The raw codes are returned ONCE — the caller MUST
|
|
12
|
+
* show them to the user, who copies them down.
|
|
13
|
+
*/
|
|
14
|
+
export declare function setupTwoFactor(params: {
|
|
15
|
+
userId: string;
|
|
16
|
+
email: string;
|
|
17
|
+
issuer: string;
|
|
18
|
+
db: DatabaseAdapter;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
secret: string;
|
|
21
|
+
otpauthUrl: string;
|
|
22
|
+
recoveryCodes: string[];
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Enable 2FA for a user. Requires a valid TOTP code matching the secret stored
|
|
26
|
+
* during {@link setupTwoFactor}. Idempotent against the already-enabled state.
|
|
27
|
+
*
|
|
28
|
+
* @throws Error('TWO_FACTOR_NOT_SET_UP') if setup hasn't run.
|
|
29
|
+
* @throws Error('INVALID_TWO_FACTOR_CODE') if the code doesn't match.
|
|
30
|
+
*/
|
|
31
|
+
export declare function enableTwoFactor(params: {
|
|
32
|
+
userId: string;
|
|
33
|
+
code: string;
|
|
34
|
+
db: DatabaseAdapter;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Disable 2FA for a user. Clears the secret + all recovery codes + the enabled flag.
|
|
38
|
+
*
|
|
39
|
+
* Callers are responsible for additional confirmation (e.g. password re-entry) —
|
|
40
|
+
* this function trusts that the caller has already authorized the action.
|
|
41
|
+
*/
|
|
42
|
+
export declare function disableTwoFactor(params: {
|
|
43
|
+
userId: string;
|
|
44
|
+
db: DatabaseAdapter;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Verify a TOTP code as the second factor in a login challenge.
|
|
48
|
+
* Returns the user record on success.
|
|
49
|
+
*
|
|
50
|
+
* @throws Error('USER_NOT_FOUND') if no user with that id.
|
|
51
|
+
* @throws Error('TWO_FACTOR_NOT_ENABLED') if the user has not enrolled.
|
|
52
|
+
* @throws Error('INVALID_TWO_FACTOR_CODE') on mismatch.
|
|
53
|
+
*/
|
|
54
|
+
export declare function verifyTwoFactor(params: {
|
|
55
|
+
userId: string;
|
|
56
|
+
code: string;
|
|
57
|
+
db: DatabaseAdapter;
|
|
58
|
+
}): Promise<User>;
|
|
59
|
+
/**
|
|
60
|
+
* Use a single-use recovery code instead of a TOTP code. The matching token
|
|
61
|
+
* row is deleted before this function returns so a replay fails.
|
|
62
|
+
*
|
|
63
|
+
* @throws Error('USER_NOT_FOUND') if the user does not exist.
|
|
64
|
+
* @throws Error('TWO_FACTOR_NOT_ENABLED') if 2FA is not enrolled for the user.
|
|
65
|
+
* @throws Error('INVALID_RECOVERY_CODE') if the code is unknown.
|
|
66
|
+
*/
|
|
67
|
+
export declare function useRecoveryCode(params: {
|
|
68
|
+
userId: string;
|
|
69
|
+
rawCode: string;
|
|
70
|
+
db: DatabaseAdapter;
|
|
71
|
+
}): Promise<User>;
|
|
72
|
+
//# sourceMappingURL=twoFactor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twoFactor.d.ts","sourceRoot":"","sources":["../../src/features/twoFactor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAS5D;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA0B3E;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhB;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBhB"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { generateTotpSecret, verifyTotpCode, buildOtpauthUrl, generateRecoveryCodes, } from '../utils/totp.js';
|
|
2
|
+
import { hashToken } from '../utils/token.js';
|
|
3
|
+
/**
|
|
4
|
+
* Begin 2FA enrollment for a user.
|
|
5
|
+
*
|
|
6
|
+
* Generates a fresh TOTP secret + a fresh set of recovery codes. The secret is
|
|
7
|
+
* stored on the user immediately so a `setup → enable` sequence on the same
|
|
8
|
+
* device works, but `twoFactorEnabled` stays `false` until `enableTwoFactor`
|
|
9
|
+
* verifies the first code from the authenticator app.
|
|
10
|
+
*
|
|
11
|
+
* Recovery codes are SHA-256 hashed before storage (same pattern as every
|
|
12
|
+
* other AuthCore token). The raw codes are returned ONCE — the caller MUST
|
|
13
|
+
* show them to the user, who copies them down.
|
|
14
|
+
*/
|
|
15
|
+
export async function setupTwoFactor(params) {
|
|
16
|
+
const { userId, email, issuer, db } = params;
|
|
17
|
+
const secret = generateTotpSecret();
|
|
18
|
+
const recoveryCodes = generateRecoveryCodes(10);
|
|
19
|
+
// Persist the secret on the user (still disabled). If the user re-runs setup,
|
|
20
|
+
// this overwrites the previous secret — they need to re-scan their authenticator.
|
|
21
|
+
await db.updateUser(userId, { twoFactorSecret: secret });
|
|
22
|
+
// Wipe any prior recovery codes (idempotent re-enrollment).
|
|
23
|
+
await db.deleteTokensByUserAndType(userId, 'RECOVERY_CODE');
|
|
24
|
+
// Persist hashed recovery codes — no expiry, single-use.
|
|
25
|
+
const farFuture = new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000);
|
|
26
|
+
for (const raw of recoveryCodes) {
|
|
27
|
+
await db.createToken({
|
|
28
|
+
userId,
|
|
29
|
+
type: 'RECOVERY_CODE',
|
|
30
|
+
token: hashToken(raw),
|
|
31
|
+
expiresAt: farFuture,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const otpauthUrl = buildOtpauthUrl({ secret, accountName: email, issuer });
|
|
35
|
+
return { secret, otpauthUrl, recoveryCodes };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Enable 2FA for a user. Requires a valid TOTP code matching the secret stored
|
|
39
|
+
* during {@link setupTwoFactor}. Idempotent against the already-enabled state.
|
|
40
|
+
*
|
|
41
|
+
* @throws Error('TWO_FACTOR_NOT_SET_UP') if setup hasn't run.
|
|
42
|
+
* @throws Error('INVALID_TWO_FACTOR_CODE') if the code doesn't match.
|
|
43
|
+
*/
|
|
44
|
+
export async function enableTwoFactor(params) {
|
|
45
|
+
const { userId, code, db } = params;
|
|
46
|
+
const user = await db.findUserById(userId);
|
|
47
|
+
if (!user)
|
|
48
|
+
throw new Error('USER_NOT_FOUND');
|
|
49
|
+
if (!user.twoFactorSecret)
|
|
50
|
+
throw new Error('TWO_FACTOR_NOT_SET_UP');
|
|
51
|
+
if (!verifyTotpCode(user.twoFactorSecret, code)) {
|
|
52
|
+
throw new Error('INVALID_TWO_FACTOR_CODE');
|
|
53
|
+
}
|
|
54
|
+
await db.updateUser(userId, { twoFactorEnabled: true });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Disable 2FA for a user. Clears the secret + all recovery codes + the enabled flag.
|
|
58
|
+
*
|
|
59
|
+
* Callers are responsible for additional confirmation (e.g. password re-entry) —
|
|
60
|
+
* this function trusts that the caller has already authorized the action.
|
|
61
|
+
*/
|
|
62
|
+
export async function disableTwoFactor(params) {
|
|
63
|
+
const { userId, db } = params;
|
|
64
|
+
await db.updateUser(userId, { twoFactorEnabled: false, twoFactorSecret: null });
|
|
65
|
+
await db.deleteTokensByUserAndType(userId, 'RECOVERY_CODE');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Verify a TOTP code as the second factor in a login challenge.
|
|
69
|
+
* Returns the user record on success.
|
|
70
|
+
*
|
|
71
|
+
* @throws Error('USER_NOT_FOUND') if no user with that id.
|
|
72
|
+
* @throws Error('TWO_FACTOR_NOT_ENABLED') if the user has not enrolled.
|
|
73
|
+
* @throws Error('INVALID_TWO_FACTOR_CODE') on mismatch.
|
|
74
|
+
*/
|
|
75
|
+
export async function verifyTwoFactor(params) {
|
|
76
|
+
const { userId, code, db } = params;
|
|
77
|
+
const user = await db.findUserById(userId);
|
|
78
|
+
if (!user)
|
|
79
|
+
throw new Error('USER_NOT_FOUND');
|
|
80
|
+
if (!user.twoFactorEnabled || !user.twoFactorSecret) {
|
|
81
|
+
throw new Error('TWO_FACTOR_NOT_ENABLED');
|
|
82
|
+
}
|
|
83
|
+
if (!verifyTotpCode(user.twoFactorSecret, code)) {
|
|
84
|
+
throw new Error('INVALID_TWO_FACTOR_CODE');
|
|
85
|
+
}
|
|
86
|
+
return user;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Use a single-use recovery code instead of a TOTP code. The matching token
|
|
90
|
+
* row is deleted before this function returns so a replay fails.
|
|
91
|
+
*
|
|
92
|
+
* @throws Error('USER_NOT_FOUND') if the user does not exist.
|
|
93
|
+
* @throws Error('TWO_FACTOR_NOT_ENABLED') if 2FA is not enrolled for the user.
|
|
94
|
+
* @throws Error('INVALID_RECOVERY_CODE') if the code is unknown.
|
|
95
|
+
*/
|
|
96
|
+
export async function useRecoveryCode(params) {
|
|
97
|
+
const { userId, rawCode, db } = params;
|
|
98
|
+
const user = await db.findUserById(userId);
|
|
99
|
+
if (!user)
|
|
100
|
+
throw new Error('USER_NOT_FOUND');
|
|
101
|
+
if (!user.twoFactorEnabled)
|
|
102
|
+
throw new Error('TWO_FACTOR_NOT_ENABLED');
|
|
103
|
+
const tokenRecord = await db.findToken(rawCode, 'RECOVERY_CODE');
|
|
104
|
+
// The token must (1) exist, (2) belong to this user, (3) be non-expired.
|
|
105
|
+
// Recovery codes are stored with a far-future expiry so the second check is
|
|
106
|
+
// belt-and-suspenders — but verifying user ownership prevents an attacker
|
|
107
|
+
// who knows one user's recovery code from authenticating as someone else.
|
|
108
|
+
if (!tokenRecord || tokenRecord.userId !== userId) {
|
|
109
|
+
throw new Error('INVALID_RECOVERY_CODE');
|
|
110
|
+
}
|
|
111
|
+
if (tokenRecord.expiresAt < new Date()) {
|
|
112
|
+
await db.deleteToken(tokenRecord.id);
|
|
113
|
+
throw new Error('INVALID_RECOVERY_CODE');
|
|
114
|
+
}
|
|
115
|
+
// Single-use: delete first so a concurrent replay fails.
|
|
116
|
+
await db.deleteToken(tokenRecord.id);
|
|
117
|
+
return user;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=twoFactor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twoFactor.js","sourceRoot":"","sources":["../../src/features/twoFactor.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
export { hashPassword, verifyPassword } from './utils/password.js';
|
|
2
|
-
export { generateOpaqueToken, hashToken, safeCompareTokens, signJwt, verifyJwt, } from './utils/token.js';
|
|
3
|
-
export type { JwtPayload } from './utils/token.js';
|
|
4
|
-
export {
|
|
5
|
-
export
|
|
2
|
+
export { generateOpaqueToken, generateCsrfToken, generatePkceVerifier, pkceChallenge, hashToken, safeCompareTokens, signJwt, verifyJwt, signTwoFactorChallenge, verifyTwoFactorChallenge, } from './utils/token.js';
|
|
3
|
+
export type { JwtPayload, TwoFactorChallengePayload } from './utils/token.js';
|
|
4
|
+
export { base32Encode, base32Decode, generateTotpSecret, generateTotpCode, verifyTotpCode, buildOtpauthUrl, generateRecoveryCodes, } from './utils/totp.js';
|
|
5
|
+
export { registerSchema, loginSchema, forgotPasswordSchema, resetPasswordSchema, verifyEmailSchema, inviteSchema, acceptInvitationSchema, sendMagicLinkSchema, consumeMagicLinkSchema, } from './utils/validation.js';
|
|
6
|
+
export type { RegisterInput, LoginInput, ForgotPasswordInput, ResetPasswordInput, VerifyEmailInput, InviteInput, AcceptInvitationInput, SendMagicLinkInput, ConsumeMagicLinkInput, } from './utils/validation.js';
|
|
6
7
|
export { createEmailVerification, verifyEmail } from './features/emailVerification.js';
|
|
7
8
|
export { createPasswordReset, resetPassword } from './features/passwordReset.js';
|
|
8
9
|
export { createInvitation, acceptInvitation } from './features/invitation.js';
|
|
10
|
+
export { issueRefreshToken, rotateRefreshToken, revokeRefreshToken, revokeAllRefreshTokensForUser, } from './features/refresh.js';
|
|
11
|
+
export { defaultVerifyEmailTemplate, defaultResetPasswordTemplate, defaultInvitationTemplate, defaultMagicLinkTemplate, } from './features/templates.js';
|
|
12
|
+
export { sendMagicLink, consumeMagicLink, MAGIC_LINK_NO_PASSWORD_SENTINEL, } from './features/magicLink.js';
|
|
13
|
+
export { setupTwoFactor, enableTwoFactor, disableTwoFactor, verifyTwoFactor, useRecoveryCode, } from './features/twoFactor.js';
|
|
14
|
+
export { startOAuth, completeOAuth } from './features/oauth.js';
|
|
15
|
+
export { createGoogleProvider } from './oauth/google.js';
|
|
16
|
+
export type { GoogleProviderConfig } from './oauth/google.js';
|
|
17
|
+
export { createGithubProvider } from './oauth/github.js';
|
|
18
|
+
export type { GithubProviderConfig } from './oauth/github.js';
|
|
19
|
+
export { createMicrosoftProvider } from './oauth/microsoft.js';
|
|
20
|
+
export type { MicrosoftProviderConfig } from './oauth/microsoft.js';
|
|
21
|
+
export { createDiscordProvider } from './oauth/discord.js';
|
|
22
|
+
export type { DiscordProviderConfig } from './oauth/discord.js';
|
|
23
|
+
export { createAppleProvider, generateAppleClientSecret } from './oauth/apple.js';
|
|
24
|
+
export type { AppleProviderConfig } from './oauth/apple.js';
|
|
9
25
|
export { createAuth, AuthError } from './auth.js';
|
|
10
|
-
export type { AuthCore } from './auth.js';
|
|
26
|
+
export type { AuthCore, SessionResult, TwoFactorChallengeResult, LoginResult, } from './auth.js';
|
|
11
27
|
export * from '@authcore/types';
|
|
12
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,SAAS,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,SAAS,EACT,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,UAAU,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAC7E,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,qBAAqB,GACtB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,cAAc,EACd,WAAW,EACX,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,aAAa,EACb,UAAU,EACV,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,uBAAuB,CAAA;AAG9B,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAA;AACtF,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC7E,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,6BAA6B,GAC9B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,+BAA+B,GAChC,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AACjF,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAG3D,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACjD,YAAY,EACV,QAAQ,EACR,aAAa,EACb,wBAAwB,EACxB,WAAW,GACZ,MAAM,WAAW,CAAA;AAElB,cAAc,iBAAiB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
// Utilities
|
|
2
2
|
export { hashPassword, verifyPassword } from './utils/password.js';
|
|
3
|
-
export { generateOpaqueToken, hashToken, safeCompareTokens, signJwt, verifyJwt, } from './utils/token.js';
|
|
4
|
-
export {
|
|
3
|
+
export { generateOpaqueToken, generateCsrfToken, generatePkceVerifier, pkceChallenge, hashToken, safeCompareTokens, signJwt, verifyJwt, signTwoFactorChallenge, verifyTwoFactorChallenge, } from './utils/token.js';
|
|
4
|
+
export { base32Encode, base32Decode, generateTotpSecret, generateTotpCode, verifyTotpCode, buildOtpauthUrl, generateRecoveryCodes, } from './utils/totp.js';
|
|
5
|
+
export { registerSchema, loginSchema, forgotPasswordSchema, resetPasswordSchema, verifyEmailSchema, inviteSchema, acceptInvitationSchema, sendMagicLinkSchema, consumeMagicLinkSchema, } from './utils/validation.js';
|
|
5
6
|
// Features
|
|
6
7
|
export { createEmailVerification, verifyEmail } from './features/emailVerification.js';
|
|
7
8
|
export { createPasswordReset, resetPassword } from './features/passwordReset.js';
|
|
8
9
|
export { createInvitation, acceptInvitation } from './features/invitation.js';
|
|
10
|
+
export { issueRefreshToken, rotateRefreshToken, revokeRefreshToken, revokeAllRefreshTokensForUser, } from './features/refresh.js';
|
|
11
|
+
export { defaultVerifyEmailTemplate, defaultResetPasswordTemplate, defaultInvitationTemplate, defaultMagicLinkTemplate, } from './features/templates.js';
|
|
12
|
+
export { sendMagicLink, consumeMagicLink, MAGIC_LINK_NO_PASSWORD_SENTINEL, } from './features/magicLink.js';
|
|
13
|
+
export { setupTwoFactor, enableTwoFactor, disableTwoFactor, verifyTwoFactor, useRecoveryCode, } from './features/twoFactor.js';
|
|
14
|
+
export { startOAuth, completeOAuth } from './features/oauth.js';
|
|
15
|
+
export { createGoogleProvider } from './oauth/google.js';
|
|
16
|
+
export { createGithubProvider } from './oauth/github.js';
|
|
17
|
+
export { createMicrosoftProvider } from './oauth/microsoft.js';
|
|
18
|
+
export { createDiscordProvider } from './oauth/discord.js';
|
|
19
|
+
export { createAppleProvider, generateAppleClientSecret } from './oauth/apple.js';
|
|
9
20
|
// Auth factory
|
|
10
21
|
export { createAuth, AuthError } from './auth.js';
|
|
11
22
|
export * from '@authcore/types';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY;AACZ,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,SAAS,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY;AACZ,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,SAAS,EACT,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,qBAAqB,GACtB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,cAAc,EACd,WAAW,EACX,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,uBAAuB,CAAA;AAa9B,WAAW;AACX,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAA;AACtF,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC7E,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,6BAA6B,GAC9B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,+BAA+B,GAChC,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAE9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAGjF,eAAe;AACf,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAQjD,cAAc,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { OAuthProvider } from '@authcore/types';
|
|
2
|
+
export interface AppleProviderConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Apple Services ID (the OAuth client id), e.g. `com.example.myapp.service`.
|
|
5
|
+
* NOT the iOS app's Bundle ID — Sign in with Apple uses a separate Services ID
|
|
6
|
+
* registered in Apple Developer → Identifiers.
|
|
7
|
+
*/
|
|
8
|
+
clientId: string;
|
|
9
|
+
/** Apple Developer Team ID (10-char string, e.g. `ABC1234DEF`). */
|
|
10
|
+
teamId: string;
|
|
11
|
+
/** Sign in with Apple Key ID (10-char string from the .p8 download). */
|
|
12
|
+
keyId: string;
|
|
13
|
+
/**
|
|
14
|
+
* The .p8 private key downloaded from Apple Developer → Keys. Pass the full
|
|
15
|
+
* PEM string including `-----BEGIN PRIVATE KEY-----` / `-----END PRIVATE KEY-----`.
|
|
16
|
+
* Apple delivers the key once at creation time; store it as a secret env var.
|
|
17
|
+
*/
|
|
18
|
+
privateKey: string;
|
|
19
|
+
/**
|
|
20
|
+
* Defaults to `['name', 'email']`. Apple only returns `email` in the id_token —
|
|
21
|
+
* `name` is delivered exactly once, on the first sign-in, via a posted form
|
|
22
|
+
* field. AuthCore uses query response_mode and reads identity from the
|
|
23
|
+
* id_token, so the `name` scope is effectively no-op here.
|
|
24
|
+
*/
|
|
25
|
+
scopes?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Client secret JWT TTL in seconds. Defaults to 600 (10 minutes). Apple's
|
|
28
|
+
* maximum is 6 months (15777000s); shorter is safer — we mint a fresh JWT
|
|
29
|
+
* on every exchange anyway.
|
|
30
|
+
*/
|
|
31
|
+
clientSecretTtlSeconds?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a Sign in with Apple OAuth 2.0 provider.
|
|
35
|
+
*
|
|
36
|
+
* Apple's protocol differs from other providers in one important way: the
|
|
37
|
+
* `client_secret` sent to the token endpoint is **not** a static string. It's
|
|
38
|
+
* an ES256-signed JWT minted on each exchange, signed with a `.p8` private
|
|
39
|
+
* key from Apple Developer. AuthCore handles that for you.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { createAppleProvider } from '@authcore/core'
|
|
43
|
+
* const apple = createAppleProvider({
|
|
44
|
+
* clientId: 'com.example.myapp.service', // Apple Services ID
|
|
45
|
+
* teamId: 'ABC1234DEF', // Apple Team ID
|
|
46
|
+
* keyId: 'XYZ9876ABC', // Key ID from the .p8 file
|
|
47
|
+
* privateKey: process.env.APPLE_PRIVATE_KEY!, // contents of the .p8 (PEM)
|
|
48
|
+
* })
|
|
49
|
+
* createAuth({ ..., oauth: { apple } })
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* **Apple-specific notes:**
|
|
53
|
+
* - Uses `response_mode=query` so the existing AuthCore callback route (GET)
|
|
54
|
+
* handles the response. (Apple's default `form_post` mode would require
|
|
55
|
+
* urlencoded body parsing on the callback; query mode works equivalently
|
|
56
|
+
* for the data we need.)
|
|
57
|
+
* - Apple delivers the user's name **only on first sign-in** via a posted
|
|
58
|
+
* `user` form field that AuthCore does not consume in query mode. The
|
|
59
|
+
* sign-in still works — AuthCore creates the user with `name` unset; the
|
|
60
|
+
* user can edit their profile later. The email is always present.
|
|
61
|
+
* - Apple's `email_verified` claim may be the string `"true"` or a boolean.
|
|
62
|
+
* AuthCore normalizes both to `true`.
|
|
63
|
+
* - Some Apple users sign in with private relay emails (`*@privaterelay.appleid.com`).
|
|
64
|
+
* These are valid forwarding addresses; treat them as real emails.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createAppleProvider(config: AppleProviderConfig): OAuthProvider;
|
|
67
|
+
/**
|
|
68
|
+
* Generate the client secret JWT Apple requires on the token endpoint.
|
|
69
|
+
* Signed with ES256 using the developer's .p8 private key.
|
|
70
|
+
*
|
|
71
|
+
* Claims per https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
|
|
72
|
+
*/
|
|
73
|
+
export declare function generateAppleClientSecret(params: {
|
|
74
|
+
teamId: string;
|
|
75
|
+
keyId: string;
|
|
76
|
+
clientId: string;
|
|
77
|
+
privateKey: string;
|
|
78
|
+
ttlSeconds?: number;
|
|
79
|
+
}): string;
|
|
80
|
+
//# sourceMappingURL=apple.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple.d.ts","sourceRoot":"","sources":["../../src/oauth/apple.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAChC;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAmF9E;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,GAAG,MAAM,CAyBT"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createSign } from 'node:crypto';
|
|
2
|
+
const DEFAULT_SCOPES = ['name', 'email'];
|
|
3
|
+
const APPLE_AUD = 'https://appleid.apple.com';
|
|
4
|
+
/**
|
|
5
|
+
* Create a Sign in with Apple OAuth 2.0 provider.
|
|
6
|
+
*
|
|
7
|
+
* Apple's protocol differs from other providers in one important way: the
|
|
8
|
+
* `client_secret` sent to the token endpoint is **not** a static string. It's
|
|
9
|
+
* an ES256-signed JWT minted on each exchange, signed with a `.p8` private
|
|
10
|
+
* key from Apple Developer. AuthCore handles that for you.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createAppleProvider } from '@authcore/core'
|
|
14
|
+
* const apple = createAppleProvider({
|
|
15
|
+
* clientId: 'com.example.myapp.service', // Apple Services ID
|
|
16
|
+
* teamId: 'ABC1234DEF', // Apple Team ID
|
|
17
|
+
* keyId: 'XYZ9876ABC', // Key ID from the .p8 file
|
|
18
|
+
* privateKey: process.env.APPLE_PRIVATE_KEY!, // contents of the .p8 (PEM)
|
|
19
|
+
* })
|
|
20
|
+
* createAuth({ ..., oauth: { apple } })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* **Apple-specific notes:**
|
|
24
|
+
* - Uses `response_mode=query` so the existing AuthCore callback route (GET)
|
|
25
|
+
* handles the response. (Apple's default `form_post` mode would require
|
|
26
|
+
* urlencoded body parsing on the callback; query mode works equivalently
|
|
27
|
+
* for the data we need.)
|
|
28
|
+
* - Apple delivers the user's name **only on first sign-in** via a posted
|
|
29
|
+
* `user` form field that AuthCore does not consume in query mode. The
|
|
30
|
+
* sign-in still works — AuthCore creates the user with `name` unset; the
|
|
31
|
+
* user can edit their profile later. The email is always present.
|
|
32
|
+
* - Apple's `email_verified` claim may be the string `"true"` or a boolean.
|
|
33
|
+
* AuthCore normalizes both to `true`.
|
|
34
|
+
* - Some Apple users sign in with private relay emails (`*@privaterelay.appleid.com`).
|
|
35
|
+
* These are valid forwarding addresses; treat them as real emails.
|
|
36
|
+
*/
|
|
37
|
+
export function createAppleProvider(config) {
|
|
38
|
+
const scopes = config.scopes ?? DEFAULT_SCOPES;
|
|
39
|
+
const clientSecretTtl = config.clientSecretTtlSeconds ?? 600;
|
|
40
|
+
return {
|
|
41
|
+
id: 'apple',
|
|
42
|
+
scopes,
|
|
43
|
+
authorize: ({ state, codeChallenge, redirectUri }) => {
|
|
44
|
+
const url = new URL('https://appleid.apple.com/auth/authorize');
|
|
45
|
+
url.searchParams.set('client_id', config.clientId);
|
|
46
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
47
|
+
url.searchParams.set('response_type', 'code');
|
|
48
|
+
// Apple defaults to form_post; we use query to land on the existing
|
|
49
|
+
// GET callback route. Apple supports both.
|
|
50
|
+
url.searchParams.set('response_mode', 'query');
|
|
51
|
+
url.searchParams.set('scope', scopes.join(' '));
|
|
52
|
+
url.searchParams.set('state', state);
|
|
53
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
54
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
55
|
+
return url.toString();
|
|
56
|
+
},
|
|
57
|
+
exchangeCode: async ({ code, codeVerifier, redirectUri }) => {
|
|
58
|
+
const clientSecret = generateAppleClientSecret({
|
|
59
|
+
teamId: config.teamId,
|
|
60
|
+
keyId: config.keyId,
|
|
61
|
+
clientId: config.clientId,
|
|
62
|
+
privateKey: config.privateKey,
|
|
63
|
+
ttlSeconds: clientSecretTtl,
|
|
64
|
+
});
|
|
65
|
+
const res = await fetch('https://appleid.apple.com/auth/token', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
68
|
+
body: new URLSearchParams({
|
|
69
|
+
code,
|
|
70
|
+
client_id: config.clientId,
|
|
71
|
+
client_secret: clientSecret,
|
|
72
|
+
redirect_uri: redirectUri,
|
|
73
|
+
grant_type: 'authorization_code',
|
|
74
|
+
code_verifier: codeVerifier,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
throw new Error(`Apple token exchange failed (${res.status}): ${await res.text()}`);
|
|
79
|
+
}
|
|
80
|
+
const body = (await res.json());
|
|
81
|
+
return {
|
|
82
|
+
accessToken: body.access_token,
|
|
83
|
+
...(body.refresh_token !== undefined ? { refreshToken: body.refresh_token } : {}),
|
|
84
|
+
...(body.expires_in !== undefined ? { expiresIn: body.expires_in } : {}),
|
|
85
|
+
...(body.id_token !== undefined ? { idToken: body.id_token } : {}),
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
getUserInfo: async (_accessToken, idToken) => {
|
|
89
|
+
if (!idToken) {
|
|
90
|
+
throw new Error('Apple OAuth: id_token missing from token response');
|
|
91
|
+
}
|
|
92
|
+
const claims = decodeIdTokenClaims(idToken);
|
|
93
|
+
if (!claims || typeof claims['sub'] !== 'string' || typeof claims['email'] !== 'string') {
|
|
94
|
+
throw new Error('Apple OAuth: id_token missing required claims (sub, email)');
|
|
95
|
+
}
|
|
96
|
+
// Apple sends "true" / "false" as STRINGS in the id_token; some versions
|
|
97
|
+
// return booleans. Normalize.
|
|
98
|
+
const rawVerified = claims['email_verified'];
|
|
99
|
+
const emailVerified = rawVerified === true || rawVerified === 'true' ? true : false;
|
|
100
|
+
return {
|
|
101
|
+
id: claims['sub'],
|
|
102
|
+
email: claims['email'],
|
|
103
|
+
emailVerified,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Generate the client secret JWT Apple requires on the token endpoint.
|
|
110
|
+
* Signed with ES256 using the developer's .p8 private key.
|
|
111
|
+
*
|
|
112
|
+
* Claims per https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
|
|
113
|
+
*/
|
|
114
|
+
export function generateAppleClientSecret(params) {
|
|
115
|
+
const { teamId, keyId, clientId, privateKey, ttlSeconds = 600 } = params;
|
|
116
|
+
const now = Math.floor(Date.now() / 1000);
|
|
117
|
+
const header = { alg: 'ES256', kid: keyId, typ: 'JWT' };
|
|
118
|
+
const payload = {
|
|
119
|
+
iss: teamId,
|
|
120
|
+
iat: now,
|
|
121
|
+
exp: now + ttlSeconds,
|
|
122
|
+
aud: APPLE_AUD,
|
|
123
|
+
sub: clientId,
|
|
124
|
+
};
|
|
125
|
+
const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
126
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
127
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
128
|
+
// Node's signer outputs DER-encoded ECDSA signatures; JWS requires raw R||S.
|
|
129
|
+
// We use `dsaEncoding: 'ieee-p1363'` (Node 16.4+) which produces the raw form.
|
|
130
|
+
const signature = createSign('SHA256')
|
|
131
|
+
.update(signingInput)
|
|
132
|
+
.sign({ key: privateKey, dsaEncoding: 'ieee-p1363' });
|
|
133
|
+
const sigB64 = Buffer.from(signature).toString('base64url');
|
|
134
|
+
return `${signingInput}.${sigB64}`;
|
|
135
|
+
}
|
|
136
|
+
function decodeIdTokenClaims(idToken) {
|
|
137
|
+
const parts = idToken.split('.');
|
|
138
|
+
if (parts.length < 2)
|
|
139
|
+
return null;
|
|
140
|
+
try {
|
|
141
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
142
|
+
return JSON.parse(payload);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=apple.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple.js","sourceRoot":"","sources":["../../src/oauth/apple.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { OAuthProvider } from '@authcore/types';
|
|
2
|
+
export interface DiscordProviderConfig {
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
/**
|
|
6
|
+
* Defaults to `['identify', 'email']`. `identify` returns the basic profile;
|
|
7
|
+
* `email` is required to get the user's email + verification status.
|
|
8
|
+
*/
|
|
9
|
+
scopes?: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a Discord OAuth 2.0 provider.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createDiscordProvider } from '@authcore/core'
|
|
16
|
+
* const discord = createDiscordProvider({
|
|
17
|
+
* clientId: process.env.DISCORD_CLIENT_ID!,
|
|
18
|
+
* clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
|
19
|
+
* })
|
|
20
|
+
* createAuth({ ..., oauth: { discord } })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Notes:
|
|
24
|
+
* - Discord exposes `verified` on the user object — AuthCore threads that
|
|
25
|
+
* straight through to `emailVerified`. Unverified Discord users will hit
|
|
26
|
+
* the standard AuthCore "EMAIL_NOT_VERIFIED_BY_PROVIDER" gate when linking
|
|
27
|
+
* to an existing local account.
|
|
28
|
+
* - User avatar URLs are constructed from the user's `id` + `avatar` hash
|
|
29
|
+
* (https://discord.com/developers/docs/reference#image-formatting).
|
|
30
|
+
*/
|
|
31
|
+
export declare function createDiscordProvider(config: DiscordProviderConfig): OAuthProvider;
|
|
32
|
+
//# sourceMappingURL=discord.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../../src/oauth/discord.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAID;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,aAAa,CAgFlF"}
|