alepha 0.19.4 → 0.19.5
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/dist/api/audits/index.d.ts +8 -8
- package/dist/api/issues/index.d.ts +810 -0
- package/dist/api/issues/index.d.ts.map +1 -0
- package/dist/api/issues/index.js +447 -0
- package/dist/api/issues/index.js.map +1 -0
- package/dist/api/keys/index.d.ts +5 -5
- package/dist/api/users/index.d.ts +6 -0
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +10 -3
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/workflows/index.d.ts +3 -3
- package/dist/captcha/index.d.ts +142 -0
- package/dist/captcha/index.d.ts.map +1 -0
- package/dist/captcha/index.js +177 -0
- package/dist/captcha/index.js.map +1 -0
- package/dist/cli/core/index.d.ts +82 -2
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +90 -6
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +84 -10
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +92 -4
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +30 -3
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +98 -21
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +2 -3
- package/dist/command/index.js.map +1 -1
- package/dist/orm/core/index.bun.js +6 -6
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +6 -6
- package/dist/orm/core/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +1 -0
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +8 -4
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +145 -2
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +364 -63
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js.map +1 -1
- package/package.json +11 -1
- package/src/api/issues/__tests__/IssueService.spec.ts +263 -0
- package/src/api/issues/controllers/AdminIssueController.ts +149 -0
- package/src/api/issues/controllers/IssueController.ts +44 -0
- package/src/api/issues/entities/issues.ts +49 -0
- package/src/api/issues/index.ts +53 -0
- package/src/api/issues/schemas/createIssueSchema.ts +13 -0
- package/src/api/issues/schemas/issueConfigAtom.ts +13 -0
- package/src/api/issues/schemas/issueQuerySchema.ts +18 -0
- package/src/api/issues/schemas/issueResourceSchema.ts +6 -0
- package/src/api/issues/schemas/myIssueQuerySchema.ts +10 -0
- package/src/api/issues/schemas/updateIssueSchema.ts +13 -0
- package/src/api/issues/services/IssueService.ts +264 -0
- package/src/api/users/primitives/$realm.ts +24 -0
- package/src/api/users/services/CredentialService.ts +6 -3
- package/src/api/users/services/RegistrationService.ts +15 -5
- package/src/api/users/services/SessionService.ts +2 -0
- package/src/captcha/__tests__/MemoryCaptchaProvider.spec.ts +74 -0
- package/src/captcha/index.ts +33 -0
- package/src/captcha/providers/CaptchaProvider.ts +17 -0
- package/src/captcha/providers/MemoryCaptchaProvider.ts +65 -0
- package/src/captcha/providers/TurnstileCaptchaProvider.ts +125 -0
- package/src/cli/core/atoms/buildOptions.ts +57 -0
- package/src/cli/core/commands/build.ts +2 -0
- package/src/cli/core/providers/ViteDevServerProvider.ts +1 -1
- package/src/cli/core/services/ViteUtils.ts +5 -2
- package/src/cli/core/tasks/BuildClientTask.ts +3 -1
- package/src/cli/core/tasks/BuildCloudflareTask.ts +4 -0
- package/src/cli/core/tasks/BuildPwaTask.ts +81 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +24 -0
- package/src/cli/platform/atoms/platformOptions.ts +19 -3
- package/src/cli/platform/hooks/PlatformHook.ts +51 -0
- package/src/cli/platform/index.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +22 -1
- package/src/cli/platform/services/PlatformOrchestrator.ts +67 -2
- package/src/cli/vendor/__tests__/VendorService.spec.ts +40 -1
- package/src/cli/vendor/commands/VendorCommand.ts +41 -38
- package/src/cli/vendor/services/VendorService.ts +108 -4
- package/src/command/__tests__/CliProvider.spec.ts +45 -0
- package/src/command/providers/CliProvider.ts +3 -4
- package/src/orm/core/services/Repository.ts +20 -6
- package/src/react/i18n/__tests__/I18nProvider.spec.ts +83 -0
- package/src/react/i18n/providers/I18nProvider.ts +12 -10
- package/src/security/primitives/$issuer.ts +3 -1
- package/src/server/auth/index.ts +7 -0
- package/src/server/auth/primitives/$auth.ts +37 -3
- package/src/server/auth/primitives/$authApple.ts +114 -4
- package/src/server/auth/primitives/$authFacebook.ts +98 -0
- package/src/server/auth/primitives/$authFranceConnect.ts +105 -0
- package/src/server/auth/primitives/$authGithub.ts +22 -16
- package/src/server/auth/primitives/$authMicrosoft.ts +88 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +197 -72
- package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -0
- package/src/server/core/__tests__/ServerRouterProvider-errorHandler.spec.ts +1 -1
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -1
|
@@ -1328,11 +1328,19 @@ var AuthPrimitive = class extends Primitive {
|
|
|
1328
1328
|
/**
|
|
1329
1329
|
* Extracts user information from the access token.
|
|
1330
1330
|
* This is used to create a user account from the access token.
|
|
1331
|
+
*
|
|
1332
|
+
* `externalProfile` carries extra profile fields that cannot be derived from the
|
|
1333
|
+
* ID token or userinfo endpoint — e.g. Apple's `user` form field that is only
|
|
1334
|
+
* delivered once, on first authorization. ID token / userinfo fields take
|
|
1335
|
+
* precedence; externalProfile only fills gaps.
|
|
1331
1336
|
*/
|
|
1332
|
-
async user(tokens) {
|
|
1337
|
+
async user(tokens, externalProfile) {
|
|
1333
1338
|
try {
|
|
1334
1339
|
if ("oauth" in this.options) {
|
|
1335
|
-
const profile =
|
|
1340
|
+
const profile = {
|
|
1341
|
+
...externalProfile,
|
|
1342
|
+
...await this.options.oauth.userinfo(tokens)
|
|
1343
|
+
};
|
|
1336
1344
|
if (this.options.oauth.account) return this.options.oauth.account({
|
|
1337
1345
|
...tokens,
|
|
1338
1346
|
user: profile
|
|
@@ -1340,7 +1348,10 @@ var AuthPrimitive = class extends Primitive {
|
|
|
1340
1348
|
return this.securityProvider.createUserFromPayload(profile);
|
|
1341
1349
|
}
|
|
1342
1350
|
if ("oidc" in this.options) {
|
|
1343
|
-
const payload =
|
|
1351
|
+
const payload = {
|
|
1352
|
+
...externalProfile,
|
|
1353
|
+
...this.getUserFromIdToken(tokens.id_token || "")
|
|
1354
|
+
};
|
|
1344
1355
|
if (this.options.oidc.account) return this.options.oidc.account({
|
|
1345
1356
|
...tokens,
|
|
1346
1357
|
user: payload
|
|
@@ -1426,6 +1437,14 @@ var ServerAuthProvider = class {
|
|
|
1426
1437
|
serverCookiesProvider = $inject(ServerCookiesProvider);
|
|
1427
1438
|
dateTimeProvider = $inject(DateTimeProvider);
|
|
1428
1439
|
serverLinksProvider = $inject(ServerLinksProvider);
|
|
1440
|
+
/**
|
|
1441
|
+
* Validates that a redirect URI is a safe relative path.
|
|
1442
|
+
* Prevents open redirect attacks by rejecting absolute URLs and protocol-relative URLs.
|
|
1443
|
+
*/
|
|
1444
|
+
validateRedirectUri(uri) {
|
|
1445
|
+
if (!uri.startsWith("/") || uri.startsWith("//")) return "/";
|
|
1446
|
+
return uri;
|
|
1447
|
+
}
|
|
1429
1448
|
get identities() {
|
|
1430
1449
|
return this.alepha.primitives($auth).filter((auth) => !auth.options.disabled);
|
|
1431
1450
|
}
|
|
@@ -1433,6 +1452,7 @@ var ServerAuthProvider = class {
|
|
|
1433
1452
|
name: "authorizationCode",
|
|
1434
1453
|
ttl: [15, "minutes"],
|
|
1435
1454
|
httpOnly: true,
|
|
1455
|
+
encrypt: true,
|
|
1436
1456
|
schema: t.object({
|
|
1437
1457
|
provider: t.text(),
|
|
1438
1458
|
realm: t.optional(t.text()),
|
|
@@ -1494,7 +1514,7 @@ var ServerAuthProvider = class {
|
|
|
1494
1514
|
const tokens = this.getTokens(cookies);
|
|
1495
1515
|
if (tokens) {
|
|
1496
1516
|
const provider = this.provider(tokens);
|
|
1497
|
-
if (!("
|
|
1517
|
+
if (!("issuer" in provider.options)) {
|
|
1498
1518
|
const user = await provider.user(tokens);
|
|
1499
1519
|
return {
|
|
1500
1520
|
api: await this.serverLinksProvider.getUserApiLinks({
|
|
@@ -1619,10 +1639,12 @@ var ServerAuthProvider = class {
|
|
|
1619
1639
|
};
|
|
1620
1640
|
if (oidc) parameters.nonce = randomState();
|
|
1621
1641
|
if (scope) parameters.scope = scope;
|
|
1642
|
+
if (oidc && oidc.responseMode) parameters.response_mode = oidc.responseMode;
|
|
1643
|
+
if (oidc && oidc.authorizationParameters) Object.assign(parameters, oidc.authorizationParameters);
|
|
1622
1644
|
this.authorizationCode.set({
|
|
1623
1645
|
state,
|
|
1624
1646
|
nonce: parameters.nonce,
|
|
1625
|
-
redirectUri: query.redirect_uri ?? "/",
|
|
1647
|
+
redirectUri: this.validateRedirectUri(query.redirect_uri ?? "/"),
|
|
1626
1648
|
loginUri,
|
|
1627
1649
|
provider: query.provider,
|
|
1628
1650
|
realm: query.realm
|
|
@@ -1638,9 +1660,11 @@ var ServerAuthProvider = class {
|
|
|
1638
1660
|
code_challenge_method: "S256"
|
|
1639
1661
|
};
|
|
1640
1662
|
if (scope) parameters.scope = scope;
|
|
1663
|
+
if (oidc && oidc.responseMode) parameters.response_mode = oidc.responseMode;
|
|
1664
|
+
if (oidc && oidc.authorizationParameters) Object.assign(parameters, oidc.authorizationParameters);
|
|
1641
1665
|
this.authorizationCode.set({
|
|
1642
1666
|
codeVerifier,
|
|
1643
|
-
redirectUri: query.redirect_uri ?? "/",
|
|
1667
|
+
redirectUri: this.validateRedirectUri(query.redirect_uri ?? "/"),
|
|
1644
1668
|
loginUri,
|
|
1645
1669
|
provider: query.provider,
|
|
1646
1670
|
realm: query.realm
|
|
@@ -1649,55 +1673,103 @@ var ServerAuthProvider = class {
|
|
|
1649
1673
|
}
|
|
1650
1674
|
});
|
|
1651
1675
|
/**
|
|
1676
|
+
* Extracts provider-specific extra profile fields delivered via the
|
|
1677
|
+
* authorization callback form body rather than the ID token or userinfo
|
|
1678
|
+
* endpoint. Currently handles Apple Sign In's `user` field, which is sent
|
|
1679
|
+
* only on the user's first authorization and contains their name.
|
|
1680
|
+
*/
|
|
1681
|
+
async extractFormPostProfile(req) {
|
|
1682
|
+
try {
|
|
1683
|
+
const userField = (await req.formData()).get("user");
|
|
1684
|
+
if (typeof userField !== "string") return;
|
|
1685
|
+
const parsed = JSON.parse(userField);
|
|
1686
|
+
const profile = {};
|
|
1687
|
+
if (parsed.name?.firstName) profile.given_name = parsed.name.firstName;
|
|
1688
|
+
if (parsed.name?.lastName) profile.family_name = parsed.name.lastName;
|
|
1689
|
+
if (parsed.name?.firstName || parsed.name?.lastName) profile.name = [parsed.name?.firstName, parsed.name?.lastName].filter(Boolean).join(" ");
|
|
1690
|
+
if (parsed.email) profile.email = parsed.email;
|
|
1691
|
+
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
this.log.warn("Failed to parse form_post profile from callback body", e);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Shared callback logic for both GET and POST OAuth2/OIDC callbacks.
|
|
1699
|
+
* For form_post response mode (e.g. Apple Sign In), the raw Request object
|
|
1700
|
+
* is passed so openid-client can read the authorization code from the POST body.
|
|
1701
|
+
*/
|
|
1702
|
+
async handleCallback(url, reply, cookies, raw) {
|
|
1703
|
+
const authorizationCode = this.authorizationCode.get({ cookies });
|
|
1704
|
+
if (!authorizationCode) throw new BadRequestError("Missing code verifier");
|
|
1705
|
+
const provider = this.provider(authorizationCode);
|
|
1706
|
+
const oauth = await provider.getOAuth();
|
|
1707
|
+
if (!oauth) throw new SecurityError(`Auth provider '${provider.name}' does not support OAuth2`);
|
|
1708
|
+
const redirectUri = authorizationCode.redirectUri ?? "/";
|
|
1709
|
+
const loginUri = authorizationCode.loginUri;
|
|
1710
|
+
let currentUrl = url;
|
|
1711
|
+
let externalProfile;
|
|
1712
|
+
if (raw?.web?.req && raw.web.req.method === "POST") {
|
|
1713
|
+
const cloned = raw.web.req.clone();
|
|
1714
|
+
currentUrl = raw.web.req;
|
|
1715
|
+
externalProfile = await this.extractFormPostProfile(cloned);
|
|
1716
|
+
}
|
|
1717
|
+
const externalTokens = await authorizationCodeGrant(oauth, currentUrl, {
|
|
1718
|
+
pkceCodeVerifier: authorizationCode.codeVerifier,
|
|
1719
|
+
expectedState: authorizationCode.state,
|
|
1720
|
+
expectedNonce: authorizationCode.nonce
|
|
1721
|
+
}).then((tokens) => ({
|
|
1722
|
+
issued_at: this.dateTimeProvider.now().unix(),
|
|
1723
|
+
provider: provider.name,
|
|
1724
|
+
...tokens
|
|
1725
|
+
})).catch((e) => {
|
|
1726
|
+
this.log.error("Failed to get access token", e);
|
|
1727
|
+
throw new SecurityError("Failed to get access token", { cause: e });
|
|
1728
|
+
});
|
|
1729
|
+
this.authorizationCode.del({ cookies });
|
|
1730
|
+
const issuer = provider.issuer;
|
|
1731
|
+
if (!issuer) {
|
|
1732
|
+
this.setTokens(externalTokens, cookies);
|
|
1733
|
+
reply.redirect(redirectUri, 302);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
let user;
|
|
1737
|
+
try {
|
|
1738
|
+
user = await provider.user(externalTokens, externalProfile);
|
|
1739
|
+
} catch (e) {
|
|
1740
|
+
this.log.warn("OAuth2 account linking failed", e);
|
|
1741
|
+
const errorUrl = new URL(loginUri || redirectUri, url.origin);
|
|
1742
|
+
errorUrl.searchParams.set("error", e instanceof BadRequestError ? e.message : "Authentication failed");
|
|
1743
|
+
reply.redirect(errorUrl.pathname + errorUrl.search, 302);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const tokens = await issuer.createToken(user);
|
|
1747
|
+
this.setTokens({
|
|
1748
|
+
...tokens,
|
|
1749
|
+
issued_at: this.dateTimeProvider.now().unix(),
|
|
1750
|
+
provider: provider.name
|
|
1751
|
+
}, cookies);
|
|
1752
|
+
reply.redirect(redirectUri, 302);
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1652
1755
|
* Callback for OAuth2/OIDC providers.
|
|
1653
1756
|
* It handles the authorization code flow and retrieves the access token.
|
|
1654
1757
|
*/
|
|
1655
1758
|
callback = $route({
|
|
1656
1759
|
path: alephaServerAuthRoutes.callback,
|
|
1657
1760
|
handler: async ({ url, reply, cookies }) => {
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
issued_at: this.dateTimeProvider.now().unix(),
|
|
1671
|
-
provider: provider.name,
|
|
1672
|
-
...tokens
|
|
1673
|
-
})).catch((e) => {
|
|
1674
|
-
this.log.error("Failed to get access token", e);
|
|
1675
|
-
throw new SecurityError("Failed to get access token", { cause: e });
|
|
1676
|
-
});
|
|
1677
|
-
this.authorizationCode.del({ cookies });
|
|
1678
|
-
const issuer = provider.issuer;
|
|
1679
|
-
if (!issuer) {
|
|
1680
|
-
this.setTokens(externalTokens, cookies);
|
|
1681
|
-
reply.redirect(redirectUri, 302);
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
let user;
|
|
1685
|
-
try {
|
|
1686
|
-
user = await provider.user(externalTokens);
|
|
1687
|
-
} catch (e) {
|
|
1688
|
-
this.log.warn("OAuth2 account linking failed", e);
|
|
1689
|
-
const errorUrl = new URL(loginUri || redirectUri, url.origin);
|
|
1690
|
-
errorUrl.searchParams.set("error", e instanceof BadRequestError ? e.message : "Authentication failed");
|
|
1691
|
-
reply.redirect(errorUrl.pathname + errorUrl.search, 302);
|
|
1692
|
-
return;
|
|
1693
|
-
}
|
|
1694
|
-
const tokens = await issuer.createToken(user);
|
|
1695
|
-
this.setTokens({
|
|
1696
|
-
...tokens,
|
|
1697
|
-
issued_at: this.dateTimeProvider.now().unix(),
|
|
1698
|
-
provider: provider.name
|
|
1699
|
-
}, cookies);
|
|
1700
|
-
reply.redirect(redirectUri, 302);
|
|
1761
|
+
await this.handleCallback(url, reply, cookies);
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
/**
|
|
1765
|
+
* POST callback for OAuth2/OIDC providers using form_post response mode.
|
|
1766
|
+
* Apple Sign In sends the authorization code via POST body instead of URL query parameters.
|
|
1767
|
+
*/
|
|
1768
|
+
callbackPost = $route({
|
|
1769
|
+
path: alephaServerAuthRoutes.callback,
|
|
1770
|
+
method: "POST",
|
|
1771
|
+
handler: async ({ url, reply, cookies, raw }) => {
|
|
1772
|
+
await this.handleCallback(url, reply, cookies, raw);
|
|
1701
1773
|
}
|
|
1702
1774
|
});
|
|
1703
1775
|
/**
|
|
@@ -1708,7 +1780,7 @@ var ServerAuthProvider = class {
|
|
|
1708
1780
|
method: "POST",
|
|
1709
1781
|
schema: { query: t.object({ post_logout_redirect_uri: t.optional(t.text()) }) },
|
|
1710
1782
|
handler: async ({ query, reply, cookies }) => {
|
|
1711
|
-
const redirect = query.post_logout_redirect_uri ?? "/";
|
|
1783
|
+
const redirect = this.validateRedirectUri(query.post_logout_redirect_uri ?? "/");
|
|
1712
1784
|
const tokens = this.getTokens(cookies);
|
|
1713
1785
|
if (!tokens) {
|
|
1714
1786
|
reply.redirect(redirect, 302);
|
|
@@ -1812,7 +1884,7 @@ var ServerAuthProvider = class {
|
|
|
1812
1884
|
}
|
|
1813
1885
|
extractAccessToken(tokens) {
|
|
1814
1886
|
const idp = this.provider(tokens.provider);
|
|
1815
|
-
if ("oidc" in idp.options && !("
|
|
1887
|
+
if ("oidc" in idp.options && !("issuer" in idp.options) && idp.options.oidc?.useIdToken) return tokens.id_token;
|
|
1816
1888
|
return tokens.access_token;
|
|
1817
1889
|
}
|
|
1818
1890
|
async refreshTokens(tokens) {
|
|
@@ -1851,6 +1923,84 @@ const authenticationProviderSchema = t.object({
|
|
|
1851
1923
|
], { description: "Type of the authentication provider." })
|
|
1852
1924
|
}, { title: "AuthenticationProvider" });
|
|
1853
1925
|
//#endregion
|
|
1926
|
+
//#region ../../src/server/auth/primitives/$authApple.ts
|
|
1927
|
+
/**
|
|
1928
|
+
* Already configured Apple authentication primitive.
|
|
1929
|
+
*
|
|
1930
|
+
* Uses OpenID Connect (OIDC) to authenticate users via their Apple accounts.
|
|
1931
|
+
* Upon successful authentication, it links the Apple account to a user session.
|
|
1932
|
+
*
|
|
1933
|
+
* Apple-specific behavior:
|
|
1934
|
+
* - `response_mode=form_post` (required by Apple when requesting `email`/`name`).
|
|
1935
|
+
* - Scope: `name email` (Apple does not support the standard `profile` scope).
|
|
1936
|
+
* - The user's name is only provided on the first authorization, as a `user`
|
|
1937
|
+
* form field on the POST callback. The framework extracts it and injects
|
|
1938
|
+
* `given_name` / `family_name` / `name` into the profile before linking.
|
|
1939
|
+
* Subsequent logins only return `sub` and `email` in the ID token.
|
|
1940
|
+
* - `email_verified` and `is_private_email` are normalized from Apple's
|
|
1941
|
+
* string ("true"/"false") representation to booleans.
|
|
1942
|
+
*
|
|
1943
|
+
* Client secret:
|
|
1944
|
+
* Apple requires the client secret to be a signed ES256 JWT generated from
|
|
1945
|
+
* your Apple private key, team ID, and key ID. This JWT is valid for up to 6
|
|
1946
|
+
* months; you must rotate it before expiration. Generate it out of band and
|
|
1947
|
+
* set it via `APPLE_CLIENT_SECRET`.
|
|
1948
|
+
*
|
|
1949
|
+
* See: https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
|
|
1950
|
+
*
|
|
1951
|
+
* Environment Variables:
|
|
1952
|
+
* - `APPLE_CLIENT_ID`: The Service ID obtained from the Apple Developer Console.
|
|
1953
|
+
* - `APPLE_CLIENT_SECRET`: The signed ES256 JWT client secret generated from your
|
|
1954
|
+
* Apple private key.
|
|
1955
|
+
*/
|
|
1956
|
+
const $authApple = (realm, options = {}) => {
|
|
1957
|
+
const { alepha } = $context();
|
|
1958
|
+
const env = alepha.parseEnv(t.object({
|
|
1959
|
+
APPLE_CLIENT_ID: t.optional(t.text({ description: "The Service ID obtained from the Apple Developer Console." })),
|
|
1960
|
+
APPLE_CLIENT_SECRET: t.optional(t.text({ description: "The signed ES256 JWT client secret generated from your Apple private key." }))
|
|
1961
|
+
}));
|
|
1962
|
+
const disabled = !env.APPLE_CLIENT_ID || !env.APPLE_CLIENT_SECRET;
|
|
1963
|
+
const name = "apple";
|
|
1964
|
+
const userAccount = options.account ?? (realm.link ? realm.link(name) : void 0);
|
|
1965
|
+
if (!userAccount) throw new AlephaError("Authentication requires a link function in the realm primitive.");
|
|
1966
|
+
const account = async (opts) => {
|
|
1967
|
+
return userAccount(normalizeApplePayload(opts));
|
|
1968
|
+
};
|
|
1969
|
+
return $auth({
|
|
1970
|
+
issuer: realm,
|
|
1971
|
+
name,
|
|
1972
|
+
oidc: {
|
|
1973
|
+
issuer: "https://appleid.apple.com",
|
|
1974
|
+
clientId: env.APPLE_CLIENT_ID,
|
|
1975
|
+
clientSecret: env.APPLE_CLIENT_SECRET,
|
|
1976
|
+
scope: "name email",
|
|
1977
|
+
responseMode: "form_post",
|
|
1978
|
+
...options,
|
|
1979
|
+
account
|
|
1980
|
+
},
|
|
1981
|
+
disabled
|
|
1982
|
+
});
|
|
1983
|
+
};
|
|
1984
|
+
/**
|
|
1985
|
+
* Normalize Apple-specific profile quirks before handing off to the
|
|
1986
|
+
* user-provided link function.
|
|
1987
|
+
*
|
|
1988
|
+
* Why: Apple's ID token non-conformities — `email_verified` and
|
|
1989
|
+
* `is_private_email` are delivered as the strings "true"/"false" rather than
|
|
1990
|
+
* booleans. Normalize so downstream code can rely on standard OIDC shapes.
|
|
1991
|
+
*/
|
|
1992
|
+
const normalizeApplePayload = (opts) => {
|
|
1993
|
+
const user = { ...opts.user };
|
|
1994
|
+
for (const key of ["email_verified", "is_private_email"]) {
|
|
1995
|
+
const raw = user[key];
|
|
1996
|
+
if (typeof raw === "string") user[key] = raw === "true";
|
|
1997
|
+
}
|
|
1998
|
+
return {
|
|
1999
|
+
...opts,
|
|
2000
|
+
user
|
|
2001
|
+
};
|
|
2002
|
+
};
|
|
2003
|
+
//#endregion
|
|
1854
2004
|
//#region ../../src/server/auth/primitives/$authCredentials.ts
|
|
1855
2005
|
/**
|
|
1856
2006
|
* Already configured Credentials authentication primitive.
|
|
@@ -1868,6 +2018,108 @@ const $authCredentials = (realm, options = {}) => {
|
|
|
1868
2018
|
});
|
|
1869
2019
|
};
|
|
1870
2020
|
//#endregion
|
|
2021
|
+
//#region ../../src/server/auth/primitives/$authFacebook.ts
|
|
2022
|
+
/**
|
|
2023
|
+
* Already configured Facebook authentication primitive.
|
|
2024
|
+
*
|
|
2025
|
+
* Uses OAuth2 to authenticate users via their Facebook accounts.
|
|
2026
|
+
* Upon successful authentication, it links the Facebook account to a user session.
|
|
2027
|
+
*
|
|
2028
|
+
* Environment Variables:
|
|
2029
|
+
* - `FACEBOOK_CLIENT_ID`: The App ID obtained from the Meta Developer Console.
|
|
2030
|
+
* - `FACEBOOK_CLIENT_SECRET`: The App Secret obtained from the Meta Developer Console.
|
|
2031
|
+
*/
|
|
2032
|
+
const $authFacebook = (realm, options = {}) => {
|
|
2033
|
+
const { alepha } = $context();
|
|
2034
|
+
const env = alepha.parseEnv(t.object({
|
|
2035
|
+
FACEBOOK_CLIENT_ID: t.optional(t.text({ description: "The App ID obtained from the Meta Developer Console." })),
|
|
2036
|
+
FACEBOOK_CLIENT_SECRET: t.optional(t.text({ description: "The App Secret obtained from the Meta Developer Console." }))
|
|
2037
|
+
}));
|
|
2038
|
+
const disabled = !env.FACEBOOK_CLIENT_ID || !env.FACEBOOK_CLIENT_SECRET;
|
|
2039
|
+
const name = "facebook";
|
|
2040
|
+
const account = options.account ?? (realm.link ? realm.link(name) : void 0);
|
|
2041
|
+
if (!account) throw new AlephaError("Authentication requires a link function in the realm primitive.");
|
|
2042
|
+
return $auth({
|
|
2043
|
+
issuer: realm,
|
|
2044
|
+
name,
|
|
2045
|
+
oauth: {
|
|
2046
|
+
clientId: env.FACEBOOK_CLIENT_ID,
|
|
2047
|
+
clientSecret: env.FACEBOOK_CLIENT_SECRET,
|
|
2048
|
+
authorization: "https://www.facebook.com/v25.0/dialog/oauth",
|
|
2049
|
+
token: "https://graph.facebook.com/v25.0/oauth/access_token",
|
|
2050
|
+
scope: "email",
|
|
2051
|
+
userinfo: async (tokens) => {
|
|
2052
|
+
const res = await fetch("https://graph.facebook.com/v25.0/me?fields=id,name,email,picture.width(200).height(200)", { headers: { Authorization: `Bearer ${tokens.access_token}` } }).then((res) => res.json());
|
|
2053
|
+
const user = { sub: res.id };
|
|
2054
|
+
if (res.email) user.email = res.email;
|
|
2055
|
+
if (res.name) user.name = res.name.trim();
|
|
2056
|
+
if (res.picture?.data?.url) user.picture = res.picture.data.url;
|
|
2057
|
+
return user;
|
|
2058
|
+
},
|
|
2059
|
+
...options,
|
|
2060
|
+
account
|
|
2061
|
+
},
|
|
2062
|
+
disabled
|
|
2063
|
+
});
|
|
2064
|
+
};
|
|
2065
|
+
//#endregion
|
|
2066
|
+
//#region ../../src/server/auth/primitives/$authFranceConnect.ts
|
|
2067
|
+
/**
|
|
2068
|
+
* Creates an authentication provider primitive for France Connect.
|
|
2069
|
+
*
|
|
2070
|
+
* Uses OpenID Connect (OIDC) to authenticate users via France Connect,
|
|
2071
|
+
* the French government's identity federation system. It provides verified
|
|
2072
|
+
* identity data (name, email, birthdate) sourced directly from government
|
|
2073
|
+
* databases.
|
|
2074
|
+
*
|
|
2075
|
+
* **France Connect-specific behaviour**:
|
|
2076
|
+
* - Scopes use individual claim names (`given_name`, `family_name`) rather
|
|
2077
|
+
* than the standard grouped `profile` scope.
|
|
2078
|
+
* - The `acr_values=eidas1` authorization parameter is mandatory and is
|
|
2079
|
+
* included automatically.
|
|
2080
|
+
* - Logout is mandatory in France Connect integrations. Store the `id_token`
|
|
2081
|
+
* returned at login and pass it to the logout endpoint when the session ends.
|
|
2082
|
+
*
|
|
2083
|
+
* **Environment Variables** (obtain from partenaires.franceconnect.gouv.fr):
|
|
2084
|
+
* - `FRANCECONNECT_CLIENT_ID` — OAuth 2.0 client ID for your France Connect service provider.
|
|
2085
|
+
* - `FRANCECONNECT_CLIENT_SECRET` — OAuth 2.0 client secret for your France Connect service provider.
|
|
2086
|
+
*
|
|
2087
|
+
* @example
|
|
2088
|
+
* ```ts
|
|
2089
|
+
* class AuthProviders {
|
|
2090
|
+
* franceconnect = $authFranceConnect(this.userRealm);
|
|
2091
|
+
* }
|
|
2092
|
+
* ```
|
|
2093
|
+
*/
|
|
2094
|
+
const $authFranceConnect = (realm, options = {}) => {
|
|
2095
|
+
const { alepha } = $context();
|
|
2096
|
+
const env = alepha.parseEnv(t.object({
|
|
2097
|
+
FRANCECONNECT_CLIENT_ID: t.optional(t.text({ description: "The OAuth 2.0 client ID for your France Connect service provider, obtained from partenaires.franceconnect.gouv.fr." })),
|
|
2098
|
+
FRANCECONNECT_CLIENT_SECRET: t.optional(t.text({ description: "The OAuth 2.0 client secret for your France Connect service provider, obtained from partenaires.franceconnect.gouv.fr." }))
|
|
2099
|
+
}));
|
|
2100
|
+
const disabled = !env.FRANCECONNECT_CLIENT_ID || !env.FRANCECONNECT_CLIENT_SECRET;
|
|
2101
|
+
const name = "franceconnect";
|
|
2102
|
+
const account = options.account ?? (realm.link ? realm.link(name) : void 0);
|
|
2103
|
+
if (!account) throw new AlephaError("Authentication requires a link function in the realm primitive.");
|
|
2104
|
+
return $auth({
|
|
2105
|
+
issuer: realm,
|
|
2106
|
+
name,
|
|
2107
|
+
oidc: {
|
|
2108
|
+
issuer: "https://oidc.franceconnect.gouv.fr/api/v2",
|
|
2109
|
+
clientId: env.FRANCECONNECT_CLIENT_ID,
|
|
2110
|
+
clientSecret: env.FRANCECONNECT_CLIENT_SECRET,
|
|
2111
|
+
scope: "openid given_name family_name email",
|
|
2112
|
+
...options,
|
|
2113
|
+
authorizationParameters: {
|
|
2114
|
+
acr_values: "eidas1",
|
|
2115
|
+
...options.authorizationParameters
|
|
2116
|
+
},
|
|
2117
|
+
account
|
|
2118
|
+
},
|
|
2119
|
+
disabled
|
|
2120
|
+
});
|
|
2121
|
+
};
|
|
2122
|
+
//#endregion
|
|
1871
2123
|
//#region ../../src/server/auth/primitives/$authGithub.ts
|
|
1872
2124
|
/**
|
|
1873
2125
|
* Already configured GitHub authentication primitive.
|
|
@@ -1900,23 +2152,20 @@ const $authGithub = (realm, options = {}) => {
|
|
|
1900
2152
|
scope: "read:user user:email",
|
|
1901
2153
|
userinfo: async (tokens) => {
|
|
1902
2154
|
const BASE_URL = "https://api.github.com";
|
|
1903
|
-
const
|
|
2155
|
+
const headers = {
|
|
1904
2156
|
Authorization: `Bearer ${tokens.access_token}`,
|
|
1905
2157
|
"User-Agent": "Alepha"
|
|
1906
|
-
}
|
|
2158
|
+
};
|
|
2159
|
+
const res = await fetch(`${BASE_URL}/user`, { headers }).then((res) => res.json());
|
|
1907
2160
|
const user = { sub: res.id.toString() };
|
|
1908
2161
|
if (res.email) user.email = res.email;
|
|
1909
2162
|
if (res.name) user.name = res.name.trim();
|
|
1910
2163
|
if (res.avatar_url) user.picture = res.avatar_url;
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
if (res.ok) {
|
|
1917
|
-
const emails = await res.json();
|
|
1918
|
-
user.email = (emails.find((e) => e.primary) ?? emails[0]).email;
|
|
1919
|
-
}
|
|
2164
|
+
const emailsRes = await fetch(`${BASE_URL}/user/emails`, { headers });
|
|
2165
|
+
if (emailsRes.ok) {
|
|
2166
|
+
const emails = await emailsRes.json();
|
|
2167
|
+
if (!user.email) user.email = (emails.find((e) => e.primary) ?? emails[0])?.email;
|
|
2168
|
+
if (user.email) user.email_verified = emails.find((e) => e.email === user.email)?.verified ?? false;
|
|
1920
2169
|
}
|
|
1921
2170
|
return user;
|
|
1922
2171
|
},
|
|
@@ -1962,6 +2211,55 @@ const $authGoogle = (realm, options = {}) => {
|
|
|
1962
2211
|
});
|
|
1963
2212
|
};
|
|
1964
2213
|
//#endregion
|
|
2214
|
+
//#region ../../src/server/auth/primitives/$authMicrosoft.ts
|
|
2215
|
+
/**
|
|
2216
|
+
* Already configured Microsoft Entra ID (Azure AD) authentication primitive.
|
|
2217
|
+
*
|
|
2218
|
+
* Uses OpenID Connect (OIDC) to authenticate users via their Microsoft accounts.
|
|
2219
|
+
* Supports personal Microsoft accounts, work/school (Azure AD) accounts, and
|
|
2220
|
+
* multi-tenant applications.
|
|
2221
|
+
*
|
|
2222
|
+
* The tenant ID defaults to `"common"`, which allows all Microsoft account types
|
|
2223
|
+
* (personal, work, school). To restrict to a specific Azure AD tenant, set
|
|
2224
|
+
* `MICROSOFT_TENANT_ID` to your tenant's GUID or domain.
|
|
2225
|
+
*
|
|
2226
|
+
* **Note on multi-tenant issuer validation**: Microsoft's OIDC discovery document
|
|
2227
|
+
* for the `common` endpoint returns `{tenantid}` as a literal placeholder in the
|
|
2228
|
+
* `issuer` field. This is expected behavior for multi-tenant endpoints. The
|
|
2229
|
+
* openid-client library handles this during token validation automatically.
|
|
2230
|
+
*
|
|
2231
|
+
* Environment Variables:
|
|
2232
|
+
* - `MICROSOFT_CLIENT_ID`: The application (client) ID from the Azure Portal.
|
|
2233
|
+
* - `MICROSOFT_CLIENT_SECRET`: The client secret value from the Azure Portal.
|
|
2234
|
+
* - `MICROSOFT_TENANT_ID`: (Optional) Azure AD tenant ID or `"common"` for
|
|
2235
|
+
* multi-tenant. Defaults to `"common"`.
|
|
2236
|
+
*/
|
|
2237
|
+
const $authMicrosoft = (realm, options = {}) => {
|
|
2238
|
+
const { alepha } = $context();
|
|
2239
|
+
const env = alepha.parseEnv(t.object({
|
|
2240
|
+
MICROSOFT_CLIENT_ID: t.optional(t.text({ description: "The application (client) ID obtained from the Azure Portal." })),
|
|
2241
|
+
MICROSOFT_CLIENT_SECRET: t.optional(t.text({ description: "The client secret value obtained from the Azure Portal." })),
|
|
2242
|
+
MICROSOFT_TENANT_ID: t.optional(t.text({ description: "The Azure AD tenant ID or 'common' for multi-tenant. Defaults to 'common'." }))
|
|
2243
|
+
}));
|
|
2244
|
+
const disabled = !env.MICROSOFT_CLIENT_ID || !env.MICROSOFT_CLIENT_SECRET;
|
|
2245
|
+
const tenantId = env.MICROSOFT_TENANT_ID ?? "common";
|
|
2246
|
+
const name = "microsoft";
|
|
2247
|
+
const account = options.account ?? (realm.link ? realm.link(name) : void 0);
|
|
2248
|
+
if (!account) throw new AlephaError("Authentication requires a link function in the realm primitive.");
|
|
2249
|
+
return $auth({
|
|
2250
|
+
issuer: realm,
|
|
2251
|
+
name,
|
|
2252
|
+
oidc: {
|
|
2253
|
+
issuer: `https://login.microsoftonline.com/${tenantId}/v2.0`,
|
|
2254
|
+
clientId: env.MICROSOFT_CLIENT_ID,
|
|
2255
|
+
clientSecret: env.MICROSOFT_CLIENT_SECRET,
|
|
2256
|
+
...options,
|
|
2257
|
+
account
|
|
2258
|
+
},
|
|
2259
|
+
disabled
|
|
2260
|
+
});
|
|
2261
|
+
};
|
|
2262
|
+
//#endregion
|
|
1965
2263
|
//#region ../../src/server/auth/index.ts
|
|
1966
2264
|
/**
|
|
1967
2265
|
* OAuth2/OIDC authentication with social login providers.
|
|
@@ -1972,6 +2270,9 @@ const $authGoogle = (realm, options = {}) => {
|
|
|
1972
2270
|
* - Google OAuth integration
|
|
1973
2271
|
* - GitHub OAuth integration
|
|
1974
2272
|
* - Apple OAuth integration
|
|
2273
|
+
* - Facebook OAuth integration
|
|
2274
|
+
* - Microsoft Entra ID (Azure AD) integration
|
|
2275
|
+
* - France Connect integration
|
|
1975
2276
|
* - Cookie-based, SSR-friendly authentication
|
|
1976
2277
|
* - Token management and refresh
|
|
1977
2278
|
*
|
|
@@ -1983,6 +2284,6 @@ const AlephaServerAuth = $module({
|
|
|
1983
2284
|
services: [AlephaServerCookies, ServerAuthProvider]
|
|
1984
2285
|
});
|
|
1985
2286
|
//#endregion
|
|
1986
|
-
export { $auth, $authCredentials, $authGithub, $authGoogle, AlephaServerAuth, AuthPrimitive, ServerAuthProvider, alephaServerAuthRoutes, authenticationProviderSchema, tokenResponseSchema, tokensSchema, userinfoResponseSchema };
|
|
2287
|
+
export { $auth, $authApple, $authCredentials, $authFacebook, $authFranceConnect, $authGithub, $authGoogle, $authMicrosoft, AlephaServerAuth, AuthPrimitive, ServerAuthProvider, alephaServerAuthRoutes, authenticationProviderSchema, tokenResponseSchema, tokensSchema, userinfoResponseSchema };
|
|
1987
2288
|
|
|
1988
2289
|
//# sourceMappingURL=index.js.map
|