@c9up/transit 0.1.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 -0
- package/README.md +191 -0
- package/dist/Oauth1Driver.d.ts +68 -0
- package/dist/Oauth1Driver.d.ts.map +1 -0
- package/dist/Oauth1Driver.js +148 -0
- package/dist/Oauth1Driver.js.map +1 -0
- package/dist/Oauth2Driver.d.ts +89 -0
- package/dist/Oauth2Driver.d.ts.map +1 -0
- package/dist/Oauth2Driver.js +196 -0
- package/dist/Oauth2Driver.js.map +1 -0
- package/dist/TransitManager.d.ts +59 -0
- package/dist/TransitManager.d.ts.map +1 -0
- package/dist/TransitManager.js +100 -0
- package/dist/TransitManager.js.map +1 -0
- package/dist/TransitProvider.d.ts +26 -0
- package/dist/TransitProvider.d.ts.map +1 -0
- package/dist/TransitProvider.js +32 -0
- package/dist/TransitProvider.js.map +1 -0
- package/dist/apple.d.ts +45 -0
- package/dist/apple.d.ts.map +1 -0
- package/dist/apple.js +80 -0
- package/dist/apple.js.map +1 -0
- package/dist/ber.d.ts +50 -0
- package/dist/ber.d.ts.map +1 -0
- package/dist/ber.js +156 -0
- package/dist/ber.js.map +1 -0
- package/dist/c14n.d.ts +38 -0
- package/dist/c14n.d.ts.map +1 -0
- package/dist/c14n.js +129 -0
- package/dist/c14n.js.map +1 -0
- package/dist/config.d.ts +115 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +146 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +18 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +33 -0
- package/dist/configure.js.map +1 -0
- package/dist/drivers/AppleDriver.d.ts +36 -0
- package/dist/drivers/AppleDriver.d.ts.map +1 -0
- package/dist/drivers/AppleDriver.js +58 -0
- package/dist/drivers/AppleDriver.js.map +1 -0
- package/dist/drivers/DiscordDriver.d.ts +15 -0
- package/dist/drivers/DiscordDriver.d.ts.map +1 -0
- package/dist/drivers/DiscordDriver.js +45 -0
- package/dist/drivers/DiscordDriver.js.map +1 -0
- package/dist/drivers/FacebookDriver.d.ts +15 -0
- package/dist/drivers/FacebookDriver.d.ts.map +1 -0
- package/dist/drivers/FacebookDriver.js +47 -0
- package/dist/drivers/FacebookDriver.js.map +1 -0
- package/dist/drivers/GitHubDriver.d.ts +33 -0
- package/dist/drivers/GitHubDriver.d.ts.map +1 -0
- package/dist/drivers/GitHubDriver.js +72 -0
- package/dist/drivers/GitHubDriver.js.map +1 -0
- package/dist/drivers/GoogleDriver.d.ts +16 -0
- package/dist/drivers/GoogleDriver.d.ts.map +1 -0
- package/dist/drivers/GoogleDriver.js +31 -0
- package/dist/drivers/GoogleDriver.js.map +1 -0
- package/dist/drivers/LdapDirectory.d.ts +51 -0
- package/dist/drivers/LdapDirectory.d.ts.map +1 -0
- package/dist/drivers/LdapDirectory.js +127 -0
- package/dist/drivers/LdapDirectory.js.map +1 -0
- package/dist/drivers/LinkedInDriver.d.ts +29 -0
- package/dist/drivers/LinkedInDriver.d.ts.map +1 -0
- package/dist/drivers/LinkedInDriver.js +97 -0
- package/dist/drivers/LinkedInDriver.js.map +1 -0
- package/dist/drivers/LinkedInOpenidConnectDriver.d.ts +17 -0
- package/dist/drivers/LinkedInOpenidConnectDriver.d.ts.map +1 -0
- package/dist/drivers/LinkedInOpenidConnectDriver.js +27 -0
- package/dist/drivers/LinkedInOpenidConnectDriver.js.map +1 -0
- package/dist/drivers/OidcDriver.d.ts +64 -0
- package/dist/drivers/OidcDriver.d.ts.map +1 -0
- package/dist/drivers/OidcDriver.js +259 -0
- package/dist/drivers/OidcDriver.js.map +1 -0
- package/dist/drivers/SamlDriver.d.ts +66 -0
- package/dist/drivers/SamlDriver.d.ts.map +1 -0
- package/dist/drivers/SamlDriver.js +228 -0
- package/dist/drivers/SamlDriver.js.map +1 -0
- package/dist/drivers/SpotifyDriver.d.ts +16 -0
- package/dist/drivers/SpotifyDriver.d.ts.map +1 -0
- package/dist/drivers/SpotifyDriver.js +34 -0
- package/dist/drivers/SpotifyDriver.js.map +1 -0
- package/dist/drivers/TwitterDriver.d.ts +21 -0
- package/dist/drivers/TwitterDriver.d.ts.map +1 -0
- package/dist/drivers/TwitterDriver.js +46 -0
- package/dist/drivers/TwitterDriver.js.map +1 -0
- package/dist/drivers/TwitterXDriver.d.ts +22 -0
- package/dist/drivers/TwitterXDriver.d.ts.map +1 -0
- package/dist/drivers/TwitterXDriver.js +47 -0
- package/dist/drivers/TwitterXDriver.js.map +1 -0
- package/dist/dsig.d.ts +51 -0
- package/dist/dsig.d.ts.map +1 -0
- package/dist/dsig.js +273 -0
- package/dist/dsig.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +135 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +167 -0
- package/dist/jwt.js.map +1 -0
- package/dist/ldap.d.ts +82 -0
- package/dist/ldap.d.ts.map +1 -0
- package/dist/ldap.js +271 -0
- package/dist/ldap.js.map +1 -0
- package/dist/nodeEnv.d.ts +17 -0
- package/dist/nodeEnv.d.ts.map +1 -0
- package/dist/nodeEnv.js +33 -0
- package/dist/nodeEnv.js.map +1 -0
- package/dist/oauth1.d.ts +68 -0
- package/dist/oauth1.d.ts.map +1 -0
- package/dist/oauth1.js +106 -0
- package/dist/oauth1.js.map +1 -0
- package/dist/oidc.d.ts +61 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +151 -0
- package/dist/oidc.js.map +1 -0
- package/dist/quasar.d.ts +14 -0
- package/dist/quasar.d.ts.map +1 -0
- package/dist/quasar.js +42 -0
- package/dist/quasar.js.map +1 -0
- package/dist/replay.d.ts +70 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +82 -0
- package/dist/replay.js.map +1 -0
- package/dist/saml.d.ts +68 -0
- package/dist/saml.d.ts.map +1 -0
- package/dist/saml.js +181 -0
- package/dist/saml.js.map +1 -0
- package/dist/services/main.d.ts +22 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +39 -0
- package/dist/services/main.js.map +1 -0
- package/dist/testing/FakeTransit.d.ts +82 -0
- package/dist/testing/FakeTransit.d.ts.map +1 -0
- package/dist/testing/FakeTransit.js +160 -0
- package/dist/testing/FakeTransit.js.map +1 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +27 -0
- package/dist/types.js.map +1 -0
- package/dist/xml.d.ts +72 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +353 -0
- package/dist/xml.js.map +1 -0
- package/package.json +73 -0
- package/src/Oauth1Driver.ts +216 -0
- package/src/Oauth2Driver.ts +271 -0
- package/src/TransitManager.ts +145 -0
- package/src/TransitProvider.ts +51 -0
- package/src/apple.ts +101 -0
- package/src/ber.ts +175 -0
- package/src/c14n.ts +187 -0
- package/src/config.ts +160 -0
- package/src/configure.ts +47 -0
- package/src/drivers/AppleDriver.ts +69 -0
- package/src/drivers/DiscordDriver.ts +50 -0
- package/src/drivers/FacebookDriver.ts +53 -0
- package/src/drivers/GitHubDriver.ts +91 -0
- package/src/drivers/GoogleDriver.ts +38 -0
- package/src/drivers/LdapDirectory.ts +185 -0
- package/src/drivers/LinkedInDriver.ts +105 -0
- package/src/drivers/LinkedInOpenidConnectDriver.ts +33 -0
- package/src/drivers/OidcDriver.ts +379 -0
- package/src/drivers/SamlDriver.ts +326 -0
- package/src/drivers/SpotifyDriver.ts +38 -0
- package/src/drivers/TwitterDriver.ts +55 -0
- package/src/drivers/TwitterXDriver.ts +53 -0
- package/src/dsig.ts +405 -0
- package/src/index.ts +85 -0
- package/src/jwt.ts +263 -0
- package/src/ldap.ts +374 -0
- package/src/nodeEnv.ts +31 -0
- package/src/oauth1.ts +144 -0
- package/src/oidc.ts +208 -0
- package/src/quasar.ts +64 -0
- package/src/replay.ts +136 -0
- package/src/saml.ts +309 -0
- package/src/services/main.ts +47 -0
- package/src/testing/FakeTransit.ts +233 -0
- package/src/types.ts +150 -0
- package/src/xml.ts +438 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream configure @c9up/transit` — wire federated sign-in in one command.
|
|
3
|
+
*
|
|
4
|
+
* The provider alone is not enough: it reads `config/transit.ts`, and a package
|
|
5
|
+
* registered without one falls back to a default that is rarely the one an
|
|
6
|
+
* application wants. Writing both together is what makes `ream add` mean
|
|
7
|
+
* installed AND working.
|
|
8
|
+
*/
|
|
9
|
+
export async function configure(codemods) {
|
|
10
|
+
// The config below reads these, so they are declared here. Writing the file
|
|
11
|
+
// without them leaves an application whose config asks the environment for
|
|
12
|
+
// something nothing ever put there.
|
|
13
|
+
await codemods.addEnvVars({
|
|
14
|
+
GOOGLE_CLIENT_ID: "",
|
|
15
|
+
GOOGLE_CLIENT_SECRET: "",
|
|
16
|
+
});
|
|
17
|
+
await codemods.addProvider("@c9up/transit/provider");
|
|
18
|
+
await codemods.writeFile("config/transit.ts", `import { defineConfig, socials } from '@c9up/transit'
|
|
19
|
+
import env from '#start/env'
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
// The key is yours: it is what \`transit.begin(name)\` asks for.
|
|
23
|
+
google: socials.google({
|
|
24
|
+
clientId: env.get('GOOGLE_CLIENT_ID', ''),
|
|
25
|
+
clientSecret: env.get('GOOGLE_CLIENT_SECRET', ''),
|
|
26
|
+
callbackUrl: 'http://localhost:3333/auth/google/callback',
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
// An OpenID Connect provider needs only its issuer:
|
|
30
|
+
// work: oidc({ issuer: env.get('OIDC_ISSUER'), clientId, clientSecret, callbackUrl }),
|
|
31
|
+
})`);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=configure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configure.js","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAYH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAkB;IACjD,4EAA4E;IAC5E,2EAA2E;IAC3E,oCAAoC;IACpC,MAAM,QAAQ,CAAC,UAAU,CAAC;QACzB,gBAAgB,EAAE,EAAE;QACpB,oBAAoB,EAAE,EAAE;KACxB,CAAC,CAAC;IAEH,MAAM,QAAQ,CAAC,WAAW,CAAC,wBAAwB,CAAC,CAAC;IACrD,MAAM,QAAQ,CAAC,SAAS,CACvB,mBAAmB,EACnB;;;;;;;;;;;;;GAaC,CACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign in with Apple.
|
|
3
|
+
*
|
|
4
|
+
* Apple speaks OpenID Connect, so most of this is the generic driver. Three
|
|
5
|
+
* things differ, and each is a place implementations get caught:
|
|
6
|
+
*
|
|
7
|
+
* - There is no client secret. Apple takes a short-lived JWT signed with the
|
|
8
|
+
* `.p8` private key from the developer account.
|
|
9
|
+
* - The callback arrives as a **POST**, not a redirect with a query string,
|
|
10
|
+
* because asking for a name or an address forces `response_mode=form_post`.
|
|
11
|
+
* - The name is sent **once**, in that first POST, and never again.
|
|
12
|
+
*
|
|
13
|
+
* Apple publishes no userinfo endpoint: everything there is comes from the
|
|
14
|
+
* id_token.
|
|
15
|
+
*/
|
|
16
|
+
import { type AppleKey } from "../apple.js";
|
|
17
|
+
import type { TransitUser } from "../types.js";
|
|
18
|
+
import { OidcDriver } from "./OidcDriver.js";
|
|
19
|
+
export interface AppleConfig extends AppleKey {
|
|
20
|
+
callbackUrl: string;
|
|
21
|
+
/** Default `["name", "email"]`. */
|
|
22
|
+
scopes?: string[];
|
|
23
|
+
/** Extra parameters on the authorize URL. */
|
|
24
|
+
authorizeParams?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
export declare class AppleDriver extends OidcDriver {
|
|
27
|
+
constructor(config: AppleConfig);
|
|
28
|
+
/**
|
|
29
|
+
* Apple sends `email_verified` and `is_private_email` as the strings
|
|
30
|
+
* `"true"` / `"false"`. Read as a boolean, a verified address reads as
|
|
31
|
+
* unverified — and an application that gates account linking on that would
|
|
32
|
+
* refuse every Apple sign-in.
|
|
33
|
+
*/
|
|
34
|
+
protected mapUser(raw: Record<string, unknown>): TransitUser;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=AppleDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppleDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/AppleDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,QAAQ,EAAqB,MAAM,aAAa,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,6CAA6C;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,qBAAa,WAAY,SAAQ,UAAU;gBAC9B,MAAM,EAAE,WAAW;IAmB/B;;;;;OAKG;cACgB,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW;CAUrE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign in with Apple.
|
|
3
|
+
*
|
|
4
|
+
* Apple speaks OpenID Connect, so most of this is the generic driver. Three
|
|
5
|
+
* things differ, and each is a place implementations get caught:
|
|
6
|
+
*
|
|
7
|
+
* - There is no client secret. Apple takes a short-lived JWT signed with the
|
|
8
|
+
* `.p8` private key from the developer account.
|
|
9
|
+
* - The callback arrives as a **POST**, not a redirect with a query string,
|
|
10
|
+
* because asking for a name or an address forces `response_mode=form_post`.
|
|
11
|
+
* - The name is sent **once**, in that first POST, and never again.
|
|
12
|
+
*
|
|
13
|
+
* Apple publishes no userinfo endpoint: everything there is comes from the
|
|
14
|
+
* id_token.
|
|
15
|
+
*/
|
|
16
|
+
import { appleClientSecret } from "../apple.js";
|
|
17
|
+
import { OidcDriver } from "./OidcDriver.js";
|
|
18
|
+
export class AppleDriver extends OidcDriver {
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super({
|
|
21
|
+
issuer: "https://appleid.apple.com",
|
|
22
|
+
clientId: config.clientId,
|
|
23
|
+
clientSecret: () => appleClientSecret(config),
|
|
24
|
+
callbackUrl: config.callbackUrl,
|
|
25
|
+
scopes: config.scopes ?? ["name", "email"],
|
|
26
|
+
authorizeParams: {
|
|
27
|
+
// Apple refuses to send a name or an address back on a redirect,
|
|
28
|
+
// so the callback route has to accept a POST.
|
|
29
|
+
response_mode: "form_post",
|
|
30
|
+
...config.authorizeParams,
|
|
31
|
+
},
|
|
32
|
+
// Apple has no userinfo endpoint; asking would be one failed request
|
|
33
|
+
// per sign-in.
|
|
34
|
+
userinfo: false,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Apple sends `email_verified` and `is_private_email` as the strings
|
|
39
|
+
* `"true"` / `"false"`. Read as a boolean, a verified address reads as
|
|
40
|
+
* unverified — and an application that gates account linking on that would
|
|
41
|
+
* refuse every Apple sign-in.
|
|
42
|
+
*/
|
|
43
|
+
mapUser(raw) {
|
|
44
|
+
const user = super.mapUser(raw);
|
|
45
|
+
if (user.email === "")
|
|
46
|
+
return user;
|
|
47
|
+
return {
|
|
48
|
+
...user,
|
|
49
|
+
emailVerificationState: appleFlag(raw.email_verified)
|
|
50
|
+
? "verified"
|
|
51
|
+
: "unverified",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function appleFlag(value) {
|
|
56
|
+
return value === true || value === "true";
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=AppleDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppleDriver.js","sourceRoot":"","sources":["../../src/drivers/AppleDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAiB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE/D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAU7C,MAAM,OAAO,WAAY,SAAQ,UAAU;IAC1C,YAAY,MAAmB;QAC9B,KAAK,CAAC;YACL,MAAM,EAAE,2BAA2B;YACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,YAAY,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC;YAC7C,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC;YAC1C,eAAe,EAAE;gBAChB,iEAAiE;gBACjE,8CAA8C;gBAC9C,aAAa,EAAE,WAAW;gBAC1B,GAAG,MAAM,CAAC,eAAe;aACzB;YACD,qEAAqE;YACrE,eAAe;YACf,QAAQ,EAAE,KAAK;SACf,CAAC,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACgB,OAAO,CAAC,GAA4B;QACtD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,IAAI,CAAC,KAAK,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QACnC,OAAO;YACN,GAAG,IAAI;YACP,sBAAsB,EAAE,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC;gBACpD,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,YAAY;SACf,CAAC;IACH,CAAC;CACD;AAED,SAAS,SAAS,CAAC,KAAc;IAChC,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord OAuth2 driver.
|
|
3
|
+
*/
|
|
4
|
+
import { Oauth2Driver } from "../Oauth2Driver.js";
|
|
5
|
+
import type { TransitUser } from "../types.js";
|
|
6
|
+
export declare class DiscordDriver extends Oauth2Driver {
|
|
7
|
+
protected readonly provider = "Discord";
|
|
8
|
+
protected readonly authorizeUrl = "https://discord.com/oauth2/authorize";
|
|
9
|
+
protected readonly accessTokenUrl = "https://discord.com/api/oauth2/token";
|
|
10
|
+
protected readonly userInfoUrl = "https://discord.com/api/users/@me";
|
|
11
|
+
/** `email` is what makes the address present at all; `identify` is the rest. */
|
|
12
|
+
protected readonly defaultScopes: readonly ["identify", "email"];
|
|
13
|
+
protected mapUser(raw: Record<string, unknown>): TransitUser;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=DiscordDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiscordDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/DiscordDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,qBAAa,aAAc,SAAQ,YAAY;IAC9C,SAAS,CAAC,QAAQ,CAAC,QAAQ,aAAa;IACxC,SAAS,CAAC,QAAQ,CAAC,YAAY,0CAA0C;IACzE,SAAS,CAAC,QAAQ,CAAC,cAAc,0CAA0C;IAC3E,SAAS,CAAC,QAAQ,CAAC,WAAW,uCAAuC;IACrE,gFAAgF;IAChF,SAAS,CAAC,QAAQ,CAAC,aAAa,iCAAkC;IAElE,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW;CAkB5D"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord OAuth2 driver.
|
|
3
|
+
*/
|
|
4
|
+
import { Oauth2Driver } from "../Oauth2Driver.js";
|
|
5
|
+
export class DiscordDriver extends Oauth2Driver {
|
|
6
|
+
provider = "Discord";
|
|
7
|
+
authorizeUrl = "https://discord.com/oauth2/authorize";
|
|
8
|
+
accessTokenUrl = "https://discord.com/api/oauth2/token";
|
|
9
|
+
userInfoUrl = "https://discord.com/api/users/@me";
|
|
10
|
+
/** `email` is what makes the address present at all; `identify` is the rest. */
|
|
11
|
+
defaultScopes = ["identify", "email"];
|
|
12
|
+
mapUser(raw) {
|
|
13
|
+
const id = String(raw.id ?? "");
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
email: String(raw.email ?? ""),
|
|
17
|
+
name: String(raw.global_name ?? raw.username ?? ""),
|
|
18
|
+
nickName: typeof raw.username === "string" ? raw.username : undefined,
|
|
19
|
+
avatarUrl: avatarUrl(id, raw),
|
|
20
|
+
// Discord reports it only when the `email` scope was granted.
|
|
21
|
+
emailVerificationState: "verified" in raw
|
|
22
|
+
? raw.verified === true
|
|
23
|
+
? "verified"
|
|
24
|
+
: "unverified"
|
|
25
|
+
: "unsupported",
|
|
26
|
+
raw,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Discord serves avatars from a CDN path built from the id and the hash, and
|
|
32
|
+
* animated ones only as `.gif`. An account with no avatar has a default one
|
|
33
|
+
* derived from its id.
|
|
34
|
+
*/
|
|
35
|
+
function avatarUrl(id, raw) {
|
|
36
|
+
const hash = raw.avatar;
|
|
37
|
+
if (typeof hash !== "string" || hash === "") {
|
|
38
|
+
// Post-migration accounts (no discriminator) index the defaults by id.
|
|
39
|
+
const index = (BigInt(id || "0") >> 22n) % 6n;
|
|
40
|
+
return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
|
|
41
|
+
}
|
|
42
|
+
const extension = hash.startsWith("a_") ? "gif" : "png";
|
|
43
|
+
return `https://cdn.discordapp.com/avatars/${id}/${hash}.${extension}`;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=DiscordDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiscordDriver.js","sourceRoot":"","sources":["../../src/drivers/DiscordDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,MAAM,OAAO,aAAc,SAAQ,YAAY;IAC3B,QAAQ,GAAG,SAAS,CAAC;IACrB,YAAY,GAAG,sCAAsC,CAAC;IACtD,cAAc,GAAG,sCAAsC,CAAC;IACxD,WAAW,GAAG,mCAAmC,CAAC;IACrE,gFAAgF;IAC7D,aAAa,GAAG,CAAC,UAAU,EAAE,OAAO,CAAU,CAAC;IAExD,OAAO,CAAC,GAA4B;QAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QAChC,OAAO;YACN,EAAE;YACF,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YAC9B,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC;YACnD,QAAQ,EAAE,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;YACrE,SAAS,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,CAAC;YAC7B,8DAA8D;YAC9D,sBAAsB,EACrB,UAAU,IAAI,GAAG;gBAChB,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,IAAI;oBACtB,CAAC,CAAC,UAAU;oBACZ,CAAC,CAAC,YAAY;gBACf,CAAC,CAAC,aAAa;YACjB,GAAG;SACH,CAAC;IACH,CAAC;CACD;AAED;;;;GAIG;AACH,SAAS,SAAS,CAAC,EAAU,EAAE,GAA4B;IAC1D,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC;IACxB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAC7C,uEAAuE;QACvE,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;QAC9C,OAAO,4CAA4C,KAAK,MAAM,CAAC;IAChE,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IACxD,OAAO,sCAAsC,EAAE,IAAI,IAAI,IAAI,SAAS,EAAE,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facebook OAuth2 driver.
|
|
3
|
+
*/
|
|
4
|
+
import { Oauth2Driver } from "../Oauth2Driver.js";
|
|
5
|
+
import type { TransitUser } from "../types.js";
|
|
6
|
+
export declare class FacebookDriver extends Oauth2Driver {
|
|
7
|
+
protected readonly provider = "Facebook";
|
|
8
|
+
protected readonly authorizeUrl = "https://www.facebook.com/v21.0/dialog/oauth";
|
|
9
|
+
protected readonly accessTokenUrl = "https://graph.facebook.com/v21.0/oauth/access_token";
|
|
10
|
+
protected readonly userInfoUrl = "https://graph.facebook.com/v21.0/me";
|
|
11
|
+
protected readonly defaultScopes: readonly ["email"];
|
|
12
|
+
protected userInfoParams(): Record<string, string>;
|
|
13
|
+
protected mapUser(raw: Record<string, unknown>): TransitUser;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=FacebookDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FacebookDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/FacebookDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAY/C,qBAAa,cAAe,SAAQ,YAAY;IAC/C,SAAS,CAAC,QAAQ,CAAC,QAAQ,cAAc;IACzC,SAAS,CAAC,QAAQ,CAAC,YAAY,iDACgB;IAC/C,SAAS,CAAC,QAAQ,CAAC,cAAc,yDACsB;IACvD,SAAS,CAAC,QAAQ,CAAC,WAAW,yCAAyC;IACvE,SAAS,CAAC,QAAQ,CAAC,aAAa,qBAAsB;cAEnC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI3D,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW;CAY5D"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facebook OAuth2 driver.
|
|
3
|
+
*/
|
|
4
|
+
import { Oauth2Driver } from "../Oauth2Driver.js";
|
|
5
|
+
/** The Graph API returns nothing but the id unless the fields are asked for. */
|
|
6
|
+
const USER_FIELDS = [
|
|
7
|
+
"name",
|
|
8
|
+
"first_name",
|
|
9
|
+
"last_name",
|
|
10
|
+
"link",
|
|
11
|
+
"email",
|
|
12
|
+
"picture.width(400).height(400)",
|
|
13
|
+
];
|
|
14
|
+
export class FacebookDriver extends Oauth2Driver {
|
|
15
|
+
provider = "Facebook";
|
|
16
|
+
authorizeUrl = "https://www.facebook.com/v21.0/dialog/oauth";
|
|
17
|
+
accessTokenUrl = "https://graph.facebook.com/v21.0/oauth/access_token";
|
|
18
|
+
userInfoUrl = "https://graph.facebook.com/v21.0/me";
|
|
19
|
+
defaultScopes = ["email"];
|
|
20
|
+
userInfoParams() {
|
|
21
|
+
return { fields: USER_FIELDS.join(",") };
|
|
22
|
+
}
|
|
23
|
+
mapUser(raw) {
|
|
24
|
+
return {
|
|
25
|
+
id: String(raw.id ?? ""),
|
|
26
|
+
// The address is absent unless the user granted the `email` scope.
|
|
27
|
+
email: String(raw.email ?? ""),
|
|
28
|
+
name: String(raw.name ?? ""),
|
|
29
|
+
nickName: typeof raw.name === "string" ? raw.name : undefined,
|
|
30
|
+
avatarUrl: pictureUrl(raw),
|
|
31
|
+
emailVerificationState: "unsupported",
|
|
32
|
+
raw,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The picture arrives nested as `picture.data.url`. */
|
|
37
|
+
function pictureUrl(raw) {
|
|
38
|
+
const picture = raw.picture;
|
|
39
|
+
if (typeof picture !== "object" || picture === null)
|
|
40
|
+
return undefined;
|
|
41
|
+
const data = Reflect.get(picture, "data");
|
|
42
|
+
if (typeof data !== "object" || data === null)
|
|
43
|
+
return undefined;
|
|
44
|
+
const url = Reflect.get(data, "url");
|
|
45
|
+
return typeof url === "string" && url !== "" ? url : undefined;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=FacebookDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FacebookDriver.js","sourceRoot":"","sources":["../../src/drivers/FacebookDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,gFAAgF;AAChF,MAAM,WAAW,GAAG;IACnB,MAAM;IACN,YAAY;IACZ,WAAW;IACX,MAAM;IACN,OAAO;IACP,gCAAgC;CACvB,CAAC;AAEX,MAAM,OAAO,cAAe,SAAQ,YAAY;IAC5B,QAAQ,GAAG,UAAU,CAAC;IACtB,YAAY,GAC9B,6CAA6C,CAAC;IAC5B,cAAc,GAChC,qDAAqD,CAAC;IACpC,WAAW,GAAG,qCAAqC,CAAC;IACpD,aAAa,GAAG,CAAC,OAAO,CAAU,CAAC;IAEnC,cAAc;QAChC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1C,CAAC;IAES,OAAO,CAAC,GAA4B;QAC7C,OAAO;YACN,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;YACxB,mEAAmE;YACnE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YAC9B,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;YAC5B,QAAQ,EAAE,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YAC7D,SAAS,EAAE,UAAU,CAAC,GAAG,CAAC;YAC1B,sBAAsB,EAAE,aAAa;YACrC,GAAG;SACH,CAAC;IACH,CAAC;CACD;AAED,wDAAwD;AACxD,SAAS,UAAU,CAAC,GAA4B;IAC/C,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC5B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAChE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth2 driver.
|
|
3
|
+
*/
|
|
4
|
+
import { Oauth2Driver } from "../Oauth2Driver.js";
|
|
5
|
+
import type { TransitUser } from "../types.js";
|
|
6
|
+
/** One entry of `/user/emails`. */
|
|
7
|
+
interface GitHubEmail {
|
|
8
|
+
email: string;
|
|
9
|
+
primary: boolean;
|
|
10
|
+
verified: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class GitHubDriver extends Oauth2Driver {
|
|
13
|
+
protected readonly provider = "GitHub";
|
|
14
|
+
protected readonly authorizeUrl = "https://github.com/login/oauth/authorize";
|
|
15
|
+
protected readonly accessTokenUrl = "https://github.com/login/oauth/access_token";
|
|
16
|
+
protected readonly userInfoUrl = "https://api.github.com/user";
|
|
17
|
+
protected readonly defaultScopes: readonly ["read:user", "user:email"];
|
|
18
|
+
/**
|
|
19
|
+
* An account whose address is private returns `email: null` from `/user`,
|
|
20
|
+
* which is most accounts. The address then lives behind `/user/emails`,
|
|
21
|
+
* which also says whether GitHub verified it.
|
|
22
|
+
*/
|
|
23
|
+
protected fetchUser(accessToken: string): Promise<TransitUser>;
|
|
24
|
+
/**
|
|
25
|
+
* The verified primary address, or the best available one. Returns
|
|
26
|
+
* `undefined` when the token was not granted `user:email` — a sign-in
|
|
27
|
+
* without an address is still a sign-in.
|
|
28
|
+
*/
|
|
29
|
+
fetchEmail(accessToken: string): Promise<GitHubEmail | undefined>;
|
|
30
|
+
protected mapUser(raw: Record<string, unknown>): TransitUser;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=GitHubDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHubDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/GitHubDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAO,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,mCAAmC;AACnC,UAAU,WAAW;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,YAAa,SAAQ,YAAY;IAC7C,SAAS,CAAC,QAAQ,CAAC,QAAQ,YAAY;IACvC,SAAS,CAAC,QAAQ,CAAC,YAAY,8CAA8C;IAC7E,SAAS,CAAC,QAAQ,CAAC,cAAc,iDACc;IAC/C,SAAS,CAAC,QAAQ,CAAC,WAAW,iCAAiC;IAC/D,SAAS,CAAC,QAAQ,CAAC,aAAa,uCAAwC;IAExE;;;;OAIG;cACsB,SAAS,CACjC,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC;IAcvB;;;;OAIG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAkBvE,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW;CAc5D"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth2 driver.
|
|
3
|
+
*/
|
|
4
|
+
import { Oauth2Driver, str } from "../Oauth2Driver.js";
|
|
5
|
+
const EMAILS_URL = "https://api.github.com/user/emails";
|
|
6
|
+
export class GitHubDriver extends Oauth2Driver {
|
|
7
|
+
provider = "GitHub";
|
|
8
|
+
authorizeUrl = "https://github.com/login/oauth/authorize";
|
|
9
|
+
accessTokenUrl = "https://github.com/login/oauth/access_token";
|
|
10
|
+
userInfoUrl = "https://api.github.com/user";
|
|
11
|
+
defaultScopes = ["read:user", "user:email"];
|
|
12
|
+
/**
|
|
13
|
+
* An account whose address is private returns `email: null` from `/user`,
|
|
14
|
+
* which is most accounts. The address then lives behind `/user/emails`,
|
|
15
|
+
* which also says whether GitHub verified it.
|
|
16
|
+
*/
|
|
17
|
+
async fetchUser(accessToken) {
|
|
18
|
+
const raw = await this.get(this.userInfoUrl, accessToken);
|
|
19
|
+
const user = this.mapUser(raw);
|
|
20
|
+
if (user.email !== "")
|
|
21
|
+
return user;
|
|
22
|
+
const found = await this.fetchEmail(accessToken);
|
|
23
|
+
if (found === undefined)
|
|
24
|
+
return user;
|
|
25
|
+
return {
|
|
26
|
+
...user,
|
|
27
|
+
email: found.email,
|
|
28
|
+
emailVerificationState: found.verified ? "verified" : "unverified",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The verified primary address, or the best available one. Returns
|
|
33
|
+
* `undefined` when the token was not granted `user:email` — a sign-in
|
|
34
|
+
* without an address is still a sign-in.
|
|
35
|
+
*/
|
|
36
|
+
async fetchEmail(accessToken) {
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = await this.getList(EMAILS_URL, accessToken);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const emails = entries.filter(isGitHubEmail);
|
|
45
|
+
// Primary first, then verified: an unverified primary is still the
|
|
46
|
+
// address the account is known by.
|
|
47
|
+
return (emails.find((entry) => entry.primary && entry.verified) ??
|
|
48
|
+
emails.find((entry) => entry.verified) ??
|
|
49
|
+
emails.find((entry) => entry.primary) ??
|
|
50
|
+
emails[0]);
|
|
51
|
+
}
|
|
52
|
+
mapUser(raw) {
|
|
53
|
+
return {
|
|
54
|
+
id: String(raw.id ?? ""),
|
|
55
|
+
email: String(raw.email ?? ""),
|
|
56
|
+
// An account with no display name still has a login.
|
|
57
|
+
name: String(raw.name ?? raw.login ?? ""),
|
|
58
|
+
nickName: str(raw, "login"),
|
|
59
|
+
avatarUrl: str(raw, "avatar_url"),
|
|
60
|
+
// `/user` says nothing about the address; `fetchUser` refines this
|
|
61
|
+
// when it has to read `/user/emails`.
|
|
62
|
+
emailVerificationState: "unsupported",
|
|
63
|
+
raw,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isGitHubEmail(value) {
|
|
68
|
+
return (typeof value === "object" &&
|
|
69
|
+
value !== null &&
|
|
70
|
+
typeof Reflect.get(value, "email") === "string");
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=GitHubDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHubDriver.js","sourceRoot":"","sources":["../../src/drivers/GitHubDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGvD,MAAM,UAAU,GAAG,oCAAoC,CAAC;AASxD,MAAM,OAAO,YAAa,SAAQ,YAAY;IAC1B,QAAQ,GAAG,QAAQ,CAAC;IACpB,YAAY,GAAG,0CAA0C,CAAC;IAC1D,cAAc,GAChC,6CAA6C,CAAC;IAC5B,WAAW,GAAG,6BAA6B,CAAC;IAC5C,aAAa,GAAG,CAAC,WAAW,EAAE,YAAY,CAAU,CAAC;IAExE;;;;OAIG;IACgB,KAAK,CAAC,SAAS,CACjC,WAAmB;QAEnB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;QAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,IAAI,CAAC,KAAK,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAEnC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACrC,OAAO;YACN,GAAG,IAAI;YACP,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,sBAAsB,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY;SAClE,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,UAAU,CAAC,WAAmB;QACnC,IAAI,OAAkB,CAAC;QACvB,IAAI,CAAC;YACJ,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QAC7C,mEAAmE;QACnE,mCAAmC;QACnC,OAAO,CACN,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,QAAQ,CAAC;YACvD,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;YACtC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;YACrC,MAAM,CAAC,CAAC,CAAC,CACT,CAAC;IACH,CAAC;IAES,OAAO,CAAC,GAA4B;QAC7C,OAAO;YACN,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;YACxB,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YAC9B,qDAAqD;YACrD,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YACzC,QAAQ,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC;YAC3B,SAAS,EAAE,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC;YACjC,mEAAmE;YACnE,sCAAsC;YACtC,sBAAsB,EAAE,aAAa;YACrC,GAAG;SACH,CAAC;IACH,CAAC;CACD;AAED,SAAS,aAAa,CAAC,KAAc;IACpC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,QAAQ,CAC/C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 driver.
|
|
3
|
+
*/
|
|
4
|
+
import { Oauth2Driver } from "../Oauth2Driver.js";
|
|
5
|
+
import type { TransitUser } from "../types.js";
|
|
6
|
+
export declare class GoogleDriver extends Oauth2Driver {
|
|
7
|
+
protected readonly provider = "Google";
|
|
8
|
+
protected readonly authorizeUrl = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
9
|
+
protected readonly accessTokenUrl = "https://oauth2.googleapis.com/token";
|
|
10
|
+
protected readonly userInfoUrl = "https://www.googleapis.com/oauth2/v3/userinfo";
|
|
11
|
+
protected readonly defaultScopes: readonly ["openid", "email", "profile"];
|
|
12
|
+
/** Asking for offline access is what makes Google issue a refresh token. */
|
|
13
|
+
protected authorizeParams(): Record<string, string>;
|
|
14
|
+
protected mapUser(raw: Record<string, unknown>): TransitUser;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=GoogleDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GoogleDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/GoogleDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAO,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,qBAAa,YAAa,SAAQ,YAAY;IAC7C,SAAS,CAAC,QAAQ,CAAC,QAAQ,YAAY;IACvC,SAAS,CAAC,QAAQ,CAAC,YAAY,kDACiB;IAChD,SAAS,CAAC,QAAQ,CAAC,cAAc,yCAAyC;IAC1E,SAAS,CAAC,QAAQ,CAAC,WAAW,mDACmB;IACjD,SAAS,CAAC,QAAQ,CAAC,aAAa,0CAA2C;IAE3E,4EAA4E;cACzD,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI5D,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW;CAgB5D"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 driver.
|
|
3
|
+
*/
|
|
4
|
+
import { Oauth2Driver, str } from "../Oauth2Driver.js";
|
|
5
|
+
export class GoogleDriver extends Oauth2Driver {
|
|
6
|
+
provider = "Google";
|
|
7
|
+
authorizeUrl = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8
|
+
accessTokenUrl = "https://oauth2.googleapis.com/token";
|
|
9
|
+
userInfoUrl = "https://www.googleapis.com/oauth2/v3/userinfo";
|
|
10
|
+
defaultScopes = ["openid", "email", "profile"];
|
|
11
|
+
/** Asking for offline access is what makes Google issue a refresh token. */
|
|
12
|
+
authorizeParams() {
|
|
13
|
+
return { access_type: "offline" };
|
|
14
|
+
}
|
|
15
|
+
mapUser(raw) {
|
|
16
|
+
return {
|
|
17
|
+
// The OpenID Connect endpoint calls the subject `sub`; the older one
|
|
18
|
+
// called it `id`, and both are the same value.
|
|
19
|
+
id: String(raw.sub ?? raw.id ?? ""),
|
|
20
|
+
email: String(raw.email ?? ""),
|
|
21
|
+
name: String(raw.name ?? ""),
|
|
22
|
+
nickName: str(raw, "given_name") ?? str(raw, "name"),
|
|
23
|
+
avatarUrl: str(raw, "picture"),
|
|
24
|
+
emailVerificationState: raw.email_verified === true || raw.verified_email === true
|
|
25
|
+
? "verified"
|
|
26
|
+
: "unverified",
|
|
27
|
+
raw,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=GoogleDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GoogleDriver.js","sourceRoot":"","sources":["../../src/drivers/GoogleDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGvD,MAAM,OAAO,YAAa,SAAQ,YAAY;IAC1B,QAAQ,GAAG,QAAQ,CAAC;IACpB,YAAY,GAC9B,8CAA8C,CAAC;IAC7B,cAAc,GAAG,qCAAqC,CAAC;IACvD,WAAW,GAC7B,+CAA+C,CAAC;IAC9B,aAAa,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAU,CAAC;IAE3E,4EAA4E;IACzD,eAAe;QACjC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;IACnC,CAAC;IAES,OAAO,CAAC,GAA4B;QAC7C,OAAO;YACN,qEAAqE;YACrE,+CAA+C;YAC/C,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;YACnC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YAC9B,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;YAC5B,QAAQ,EAAE,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC;YACpD,SAAS,EAAE,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC;YAC9B,sBAAsB,EACrB,GAAG,CAAC,cAAc,KAAK,IAAI,IAAI,GAAG,CAAC,cAAc,KAAK,IAAI;gBACzD,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,YAAY;YAChB,GAAG;SACH,CAAC;IACH,CAAC;CACD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An LDAP or Active Directory directory.
|
|
3
|
+
*
|
|
4
|
+
* Nothing is redirected: the application already holds the credentials, and the
|
|
5
|
+
* directory is the authority that says whether they are right. Verifying them
|
|
6
|
+
* means binding AS that person — a bind that succeeds is the answer, and there
|
|
7
|
+
* is no other one.
|
|
8
|
+
*
|
|
9
|
+
* The flow is two connections on purpose. The first finds the person's DN,
|
|
10
|
+
* bound as whatever the application is allowed to search with. The second binds
|
|
11
|
+
* as the person, and is closed immediately: a connection carries the identity
|
|
12
|
+
* of whatever last bound on it, so nothing else may run on that one.
|
|
13
|
+
*/
|
|
14
|
+
import { type LdapFilter } from "../ldap.js";
|
|
15
|
+
import type { DirectoryDriver, TransitUser } from "../types.js";
|
|
16
|
+
export interface LdapConfig {
|
|
17
|
+
/** `ldaps://directory.acme.test:636`. */
|
|
18
|
+
url: string;
|
|
19
|
+
/** Where to search from — `dc=acme,dc=test`. */
|
|
20
|
+
baseDn: string;
|
|
21
|
+
/**
|
|
22
|
+
* The attribute a person types. `uid` on OpenLDAP,
|
|
23
|
+
* `sAMAccountName` or `userPrincipalName` on Active Directory.
|
|
24
|
+
*/
|
|
25
|
+
loginAttribute: string;
|
|
26
|
+
/** The account the search runs as. Omitted means an anonymous search. */
|
|
27
|
+
bindDn?: string;
|
|
28
|
+
bindPassword?: string;
|
|
29
|
+
/** Extra conditions on the search, ANDed with the login attribute. */
|
|
30
|
+
filter?: LdapFilter;
|
|
31
|
+
/** Attributes to read. Defaults to the ones the mapping below uses. */
|
|
32
|
+
attributes?: string[];
|
|
33
|
+
/** The attribute names this directory uses, when they are not the usual ones. */
|
|
34
|
+
claims?: {
|
|
35
|
+
email?: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
nickName?: string;
|
|
38
|
+
};
|
|
39
|
+
/** Allow a plaintext connection — see the note on `LdapOptions`. */
|
|
40
|
+
allowInsecure?: boolean;
|
|
41
|
+
/** Refuse a server certificate that does not check out. Default true. */
|
|
42
|
+
rejectUnauthorized?: boolean;
|
|
43
|
+
/** How long to wait for the directory, in milliseconds. Default 10 000. */
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
}
|
|
46
|
+
export declare class LdapDirectory implements DirectoryDriver {
|
|
47
|
+
#private;
|
|
48
|
+
constructor(config: LdapConfig);
|
|
49
|
+
authenticate(username: string, password: string): Promise<TransitUser>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=LdapDirectory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LdapDirectory.d.ts","sourceRoot":"","sources":["../../src/drivers/LdapDirectory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAIN,KAAK,UAAU,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,UAAU;IAC1B,yCAAyC;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,iFAAiF;IACjF,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD,qBAAa,aAAc,YAAW,eAAe;;gBAGxC,MAAM,EAAE,UAAU;IAIxB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAgH5E"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An LDAP or Active Directory directory.
|
|
3
|
+
*
|
|
4
|
+
* Nothing is redirected: the application already holds the credentials, and the
|
|
5
|
+
* directory is the authority that says whether they are right. Verifying them
|
|
6
|
+
* means binding AS that person — a bind that succeeds is the answer, and there
|
|
7
|
+
* is no other one.
|
|
8
|
+
*
|
|
9
|
+
* The flow is two connections on purpose. The first finds the person's DN,
|
|
10
|
+
* bound as whatever the application is allowed to search with. The second binds
|
|
11
|
+
* as the person, and is closed immediately: a connection carries the identity
|
|
12
|
+
* of whatever last bound on it, so nothing else may run on that one.
|
|
13
|
+
*/
|
|
14
|
+
import { LdapConnection, LdapError, } from "../ldap.js";
|
|
15
|
+
const EMAIL_ATTRS = ["mail", "userPrincipalName", "email"];
|
|
16
|
+
const NAME_ATTRS = ["displayName", "cn", "name"];
|
|
17
|
+
const NICK_ATTRS = ["uid", "sAMAccountName", "givenName"];
|
|
18
|
+
export class LdapDirectory {
|
|
19
|
+
#config;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.#config = config;
|
|
22
|
+
}
|
|
23
|
+
async authenticate(username, password) {
|
|
24
|
+
// Refused here as well as in the bind: a form that submits an empty
|
|
25
|
+
// password must not reach the directory, which would answer success to
|
|
26
|
+
// an anonymous bind.
|
|
27
|
+
if (password === "") {
|
|
28
|
+
throw new LdapError("a password is required — an empty one is an anonymous bind, which the directory would accept");
|
|
29
|
+
}
|
|
30
|
+
if (username === "") {
|
|
31
|
+
throw new LdapError("a username is required");
|
|
32
|
+
}
|
|
33
|
+
const entry = await this.#find(username);
|
|
34
|
+
await this.#bindAs(entry.dn, password);
|
|
35
|
+
return this.#mapUser(entry);
|
|
36
|
+
}
|
|
37
|
+
/** Find the person, bound as whatever may search. */
|
|
38
|
+
async #find(username) {
|
|
39
|
+
const connection = this.#open();
|
|
40
|
+
await connection.connect();
|
|
41
|
+
try {
|
|
42
|
+
if (this.#config.bindDn) {
|
|
43
|
+
await connection.bind(this.#config.bindDn, this.#config.bindPassword ?? "");
|
|
44
|
+
}
|
|
45
|
+
// The username is a VALUE in a structure, never text in a filter, so
|
|
46
|
+
// a login of `*)(uid=admin` is a login containing those characters.
|
|
47
|
+
const login = {
|
|
48
|
+
equals: [this.#config.loginAttribute, username],
|
|
49
|
+
};
|
|
50
|
+
const filter = this.#config.filter
|
|
51
|
+
? { and: [this.#config.filter, login] }
|
|
52
|
+
: login;
|
|
53
|
+
const entries = await connection.search({
|
|
54
|
+
base: this.#config.baseDn,
|
|
55
|
+
filter,
|
|
56
|
+
attributes: this.#config.attributes ?? [
|
|
57
|
+
...EMAIL_ATTRS,
|
|
58
|
+
...NAME_ATTRS,
|
|
59
|
+
...NICK_ATTRS,
|
|
60
|
+
],
|
|
61
|
+
// Two, so an ambiguous login is seen rather than silently
|
|
62
|
+
// resolved to whichever the directory returned first.
|
|
63
|
+
sizeLimit: 2,
|
|
64
|
+
});
|
|
65
|
+
if (entries.length === 0) {
|
|
66
|
+
// The same message as a wrong password: which of the two it was
|
|
67
|
+
// is not something a sign-in form should disclose.
|
|
68
|
+
throw new LdapError("the directory refused the bind: invalid credentials", 49);
|
|
69
|
+
}
|
|
70
|
+
if (entries.length > 1) {
|
|
71
|
+
throw new LdapError(`'${username}' matches ${entries.length} entries, and this signs in one person`);
|
|
72
|
+
}
|
|
73
|
+
return entries[0];
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await connection.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Bind as the person. A bind that succeeds IS the verification. */
|
|
80
|
+
async #bindAs(dn, password) {
|
|
81
|
+
const connection = this.#open();
|
|
82
|
+
await connection.connect();
|
|
83
|
+
try {
|
|
84
|
+
await connection.bind(dn, password);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
// The connection now carries this person's identity; it is closed
|
|
88
|
+
// rather than reused.
|
|
89
|
+
await connection.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
#open() {
|
|
93
|
+
return new LdapConnection({
|
|
94
|
+
url: this.#config.url,
|
|
95
|
+
allowInsecure: this.#config.allowInsecure,
|
|
96
|
+
rejectUnauthorized: this.#config.rejectUnauthorized,
|
|
97
|
+
timeoutMs: this.#config.timeoutMs,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
#mapUser(entry) {
|
|
101
|
+
const claims = this.#config.claims ?? {};
|
|
102
|
+
const email = pick(entry, claims.email, EMAIL_ATTRS);
|
|
103
|
+
const name = pick(entry, claims.name, NAME_ATTRS);
|
|
104
|
+
const nickName = pick(entry, claims.nickName, NICK_ATTRS);
|
|
105
|
+
return {
|
|
106
|
+
// The DN is what the directory calls this entry, and the only value
|
|
107
|
+
// guaranteed to be unique and stable.
|
|
108
|
+
id: entry.dn,
|
|
109
|
+
email: email ?? "",
|
|
110
|
+
name: name ?? nickName ?? entry.dn,
|
|
111
|
+
nickName,
|
|
112
|
+
// A directory that holds an address has vouched for it: that is what
|
|
113
|
+
// a directory is.
|
|
114
|
+
emailVerificationState: email === undefined ? "unsupported" : "verified",
|
|
115
|
+
raw: { dn: entry.dn, attributes: entry.attributes },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function pick(entry, configured, candidates) {
|
|
120
|
+
for (const name of configured ? [configured] : candidates) {
|
|
121
|
+
const value = entry.attributes[name]?.[0];
|
|
122
|
+
if (value !== undefined && value !== "")
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=LdapDirectory.js.map
|