sst 2.1.16 → 2.1.17

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.
@@ -0,0 +1,79 @@
1
+ import { Construct } from "constructs";
2
+ import { Api, ApiProps } from "../Api.js";
3
+ import { FunctionDefinition } from "../Function.js";
4
+ import { SSTConstruct } from "../Construct.js";
5
+ import { FunctionBindingProps } from "../util/functionBinding.js";
6
+ export interface AuthProps {
7
+ /**
8
+ * The function that will handle authentication
9
+ */
10
+ authenticator: FunctionDefinition;
11
+ customDomain?: ApiProps["customDomain"];
12
+ cdk?: {
13
+ /**
14
+ * Allows you to override default id for this construct.
15
+ */
16
+ id?: string;
17
+ };
18
+ }
19
+ export interface ApiAttachmentProps {
20
+ /**
21
+ * The API to attach auth routes to
22
+ *
23
+ * @example
24
+ * ```js
25
+ * const api = new Api(stack, "Api", {});
26
+ * const auth = new Auth(stack, "Auth", {
27
+ * authenticator: "functions/authenticator.handler"
28
+ * })
29
+ * auth.attach(stack, {
30
+ * api
31
+ * })
32
+ * ```
33
+ */
34
+ api: Api;
35
+ /**
36
+ * Optionally specify the prefix to mount authentication routes
37
+ *
38
+ * @default "/auth"
39
+ *
40
+ * @example
41
+ * ```js
42
+ * const api = new Api(stack, "Api", {});
43
+ * const auth = new Auth(stack, "Auth", {
44
+ * authenticator: "functions/authenticator.handler"
45
+ * })
46
+ * auth.attach(stack, {
47
+ * api,
48
+ * prefix: "/custom/prefix"
49
+ * })
50
+ * ```
51
+ */
52
+ prefix?: string;
53
+ }
54
+ /**
55
+ * SST Auth is a lightweight authentication solution for your applications. With a simple set of configuration you can deploy a function attached to your API that can handle various authentication flows. *
56
+ * @example
57
+ * ```
58
+ * import { Auth } from "@serverless-stack/resources"
59
+ *
60
+ * new Auth(stack, "auth", {
61
+ * authenticator: "functions/authenticator.handler"
62
+ * })
63
+ */
64
+ export declare class Auth extends Construct implements SSTConstruct {
65
+ readonly id: string;
66
+ private readonly authenticator;
67
+ private api;
68
+ private publicKey;
69
+ private privateKey;
70
+ constructor(scope: Construct, id: string, props: AuthProps);
71
+ get url(): string;
72
+ /** @internal */
73
+ getConstructMetadata(): {
74
+ type: "Auth";
75
+ data: {};
76
+ };
77
+ /** @internal */
78
+ getFunctionBinding(): FunctionBindingProps;
79
+ }
@@ -0,0 +1,116 @@
1
+ import { Effect, Policy, PolicyStatement } from "aws-cdk-lib/aws-iam";
2
+ import { Construct } from "constructs";
3
+ import { Api } from "../Api.js";
4
+ import { Stack } from "../Stack.js";
5
+ import { Secret } from "../Secret.js";
6
+ import { getParameterPath, } from "../util/functionBinding.js";
7
+ import { CustomResource } from "aws-cdk-lib";
8
+ /**
9
+ * SST Auth is a lightweight authentication solution for your applications. With a simple set of configuration you can deploy a function attached to your API that can handle various authentication flows. *
10
+ * @example
11
+ * ```
12
+ * import { Auth } from "@serverless-stack/resources"
13
+ *
14
+ * new Auth(stack, "auth", {
15
+ * authenticator: "functions/authenticator.handler"
16
+ * })
17
+ */
18
+ export class Auth extends Construct {
19
+ id;
20
+ authenticator;
21
+ api;
22
+ publicKey;
23
+ privateKey;
24
+ constructor(scope, id, props) {
25
+ super(scope, props.cdk?.id || id);
26
+ if (!props.authenticator ||
27
+ "defaults" in props ||
28
+ "login" in props ||
29
+ "triggers" in props ||
30
+ "identityPoolFederation" in props ||
31
+ "cdk" in props) {
32
+ throw new Error(`It looks like you may be passing in Cognito props to the Auth construct. The Auth construct was renamed to Cognito in version 1.10.0`);
33
+ }
34
+ this.id = id;
35
+ const stack = Stack.of(scope);
36
+ this.authenticator = props.authenticator;
37
+ this.api = new Api(this, id + "-api", {
38
+ routes: {
39
+ "ANY /{step}": {
40
+ function: this.authenticator,
41
+ },
42
+ "ANY /": {
43
+ function: this.authenticator,
44
+ },
45
+ },
46
+ customDomain: props.customDomain,
47
+ });
48
+ this.publicKey = new Secret(this, id + "PublicKey");
49
+ this.privateKey = new Secret(this, id + "PrivateKey");
50
+ const fn = this.api.getFunction("ANY /{step}");
51
+ fn.bind([this.publicKey, this.privateKey]);
52
+ const app = this.node.root;
53
+ fn.addEnvironment("AUTH_ID", id);
54
+ fn.attachPermissions([
55
+ new PolicyStatement({
56
+ actions: ["ssm:GetParameters"],
57
+ effect: Effect.ALLOW,
58
+ resources: [
59
+ `arn:${Stack.of(this).partition}:ssm:${app.region}:${app.account}:parameter${getParameterPath(this.publicKey, "value")}`,
60
+ `arn:${Stack.of(this).partition}:ssm:${app.region}:${app.account}:parameter${getParameterPath(this.privateKey, "value")}`,
61
+ ],
62
+ }),
63
+ ]);
64
+ const policy = new Policy(this, "CloudFrontInvalidatorPolicy", {
65
+ statements: [
66
+ new PolicyStatement({
67
+ actions: [
68
+ "ssm:GetParameter",
69
+ "ssm:PutParameter",
70
+ "ssm:DeleteParameter",
71
+ ],
72
+ resources: [
73
+ `arn:${stack.partition}:ssm:${stack.region}:${stack.account}:parameter/*`,
74
+ ],
75
+ }),
76
+ ],
77
+ });
78
+ stack.customResourceHandler.role?.attachInlinePolicy(policy);
79
+ const resource = new CustomResource(this, "StackMetadata", {
80
+ serviceToken: stack.customResourceHandler.functionArn,
81
+ resourceType: "Custom::AuthKeys",
82
+ properties: {
83
+ publicPath: getParameterPath(this.publicKey, "value"),
84
+ privatePath: getParameterPath(this.privateKey, "value"),
85
+ },
86
+ });
87
+ resource.node.addDependency(policy);
88
+ }
89
+ get url() {
90
+ return this.api.customDomainUrl || this.api.url;
91
+ }
92
+ /** @internal */
93
+ getConstructMetadata() {
94
+ return {
95
+ type: "Auth",
96
+ data: {},
97
+ };
98
+ }
99
+ /** @internal */
100
+ getFunctionBinding() {
101
+ return {
102
+ clientPackage: "future/auth",
103
+ variables: {
104
+ publicKey: {
105
+ type: "secret_reference",
106
+ secret: this.publicKey,
107
+ },
108
+ url: {
109
+ type: "plain",
110
+ value: this.url,
111
+ },
112
+ },
113
+ permissions: {},
114
+ };
115
+ }
116
+ }
@@ -0,0 +1 @@
1
+ export * from "./Auth.js";
@@ -0,0 +1 @@
1
+ export * from "./Auth.js";
@@ -2,7 +2,7 @@ import { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Context as L
2
2
  export interface Handlers {
3
3
  api: {
4
4
  event: APIGatewayProxyEventV2;
5
- response: APIGatewayProxyStructuredResultV2;
5
+ response: APIGatewayProxyStructuredResultV2 | void;
6
6
  };
7
7
  sqs: {
8
8
  event: SQSEvent;
@@ -2,6 +2,7 @@
2
2
  /// <reference types="node/url.js" />
3
3
  /// <reference types=".pnpm/@types+node@17.0.45/node_modules/@types/node/url.js" />
4
4
  import { Handler } from "../../context/handler.js";
5
+ import { APIGatewayProxyStructuredResultV2 } from "aws-lambda";
5
6
  export interface ApiResources {
6
7
  }
7
8
  export interface AppSyncApiResources {
@@ -23,19 +24,41 @@ export declare const WebSocketApi: WebSocketApiResources;
23
24
  * })
24
25
  * ```
25
26
  */
26
- export declare function ApiHandler(cb: Parameters<typeof Handler<"api">>[1]): (event: import("aws-lambda").APIGatewayProxyEventV2, context: import("aws-lambda").Context) => Promise<import("aws-lambda").APIGatewayProxyStructuredResultV2>;
27
- export declare const useCookies: () => any;
28
- export declare function useCookie(name: string): any;
27
+ export declare function ApiHandler(cb: Parameters<typeof Handler<"api">>[1]): (event: import("aws-lambda").APIGatewayProxyEventV2, context: import("aws-lambda").Context) => Promise<APIGatewayProxyStructuredResultV2>;
28
+ export declare const useCookies: () => {
29
+ [k: string]: string;
30
+ };
31
+ export declare function useCookie(name: string): string | undefined;
29
32
  export declare const useBody: () => string | undefined;
30
33
  export declare const useJsonBody: () => any;
31
34
  export declare const useFormData: () => import("url").URLSearchParams | undefined;
32
35
  export declare const usePath: () => string[];
36
+ interface CookieOptions {
37
+ expires?: Date;
38
+ maxAge?: number;
39
+ domain?: string;
40
+ path?: string;
41
+ secure?: boolean;
42
+ httpOnly?: boolean;
43
+ sameSite?: "Strict" | "Lax" | "None";
44
+ }
45
+ export declare const useResponse: () => {
46
+ cookies(values: Record<string, string>, options: CookieOptions): any;
47
+ cookie(input: {
48
+ key: string;
49
+ value: string;
50
+ } & CookieOptions): any;
51
+ status(code: number): any;
52
+ header(key: string, value: string): any;
53
+ serialize(input: APIGatewayProxyStructuredResultV2): APIGatewayProxyStructuredResultV2;
54
+ };
33
55
  export declare function useDomainName(): string;
34
56
  export declare function useMethod(): string;
35
57
  export declare function useHeaders(): import("aws-lambda").APIGatewayProxyEventHeaders;
36
58
  export declare function useHeader(key: string): string | undefined;
37
59
  export declare function useFormValue(name: string): string | null | undefined;
38
60
  export declare function useQueryParams(): import("aws-lambda").APIGatewayProxyEventQueryStringParameters;
39
- export declare function useQueryParam(name: string): string | undefined;
61
+ export declare function useQueryParam<T = string>(name: string): T | undefined;
40
62
  export declare function usePathParams(): import("aws-lambda").APIGatewayProxyEventPathParameters;
41
63
  export declare function usePathParam(name: string): string | undefined;
64
+ export {};
package/node/api/index.js CHANGED
@@ -19,12 +19,16 @@ Object.assign(WebSocketApi, getVariables("WebSocketApi"));
19
19
  * ```
20
20
  */
21
21
  export function ApiHandler(cb) {
22
- return Handler("api", cb);
22
+ return Handler("api", async (evt, ctx) => {
23
+ const result = await cb(evt, ctx);
24
+ const serialized = useResponse().serialize(result || {});
25
+ return serialized;
26
+ });
23
27
  }
24
28
  export const useCookies = /* @__PURE__ */ Context.memo(() => {
25
29
  const evt = useEvent("api");
26
30
  const cookies = evt.cookies || [];
27
- return Object.fromEntries(cookies.map((c) => c.split("=")));
31
+ return Object.fromEntries(cookies.map((c) => c.split("=")).map(([k, v]) => [k, decodeURIComponent(v)]));
28
32
  });
29
33
  export function useCookie(name) {
30
34
  const cookies = useCookies();
@@ -56,6 +60,64 @@ export const usePath = /* @__PURE__ */ Context.memo(() => {
56
60
  const evt = useEvent("api");
57
61
  return evt.rawPath.split("/").filter(Boolean);
58
62
  });
63
+ export const useResponse = /* @__PURE__ */ Context.memo(() => {
64
+ const response = {
65
+ headers: {},
66
+ cookies: [],
67
+ };
68
+ const result = {
69
+ cookies(values, options) {
70
+ for (const [key, value] of Object.entries(values)) {
71
+ result.cookie({
72
+ key,
73
+ value,
74
+ ...options,
75
+ });
76
+ }
77
+ return result;
78
+ },
79
+ cookie(input) {
80
+ const value = encodeURIComponent(input.value);
81
+ const parts = [input.key + "=" + value];
82
+ if (input.domain)
83
+ parts.push("Domain=" + input.domain);
84
+ if (input.path)
85
+ parts.push("Path=" + input.path);
86
+ if (input.expires)
87
+ parts.push("Expires=" + input.expires.toUTCString());
88
+ if (input.maxAge)
89
+ parts.push("Max-Age=" + input.maxAge);
90
+ if (input.httpOnly)
91
+ parts.push("HttpOnly");
92
+ if (input.secure)
93
+ parts.push("Secure");
94
+ if (input.sameSite)
95
+ parts.push("SameSite=" + input.sameSite);
96
+ response.cookies.push(parts.join("; "));
97
+ return result;
98
+ },
99
+ status(code) {
100
+ response.statusCode = code;
101
+ return result;
102
+ },
103
+ header(key, value) {
104
+ response.headers[key] = value;
105
+ return result;
106
+ },
107
+ serialize(input) {
108
+ return {
109
+ ...response,
110
+ ...input,
111
+ cookies: [...(input.cookies || []), ...response.cookies],
112
+ headers: {
113
+ ...response.headers,
114
+ ...input.headers,
115
+ },
116
+ };
117
+ },
118
+ };
119
+ return result;
120
+ });
59
121
  export function useDomainName() {
60
122
  const evt = useEvent("api");
61
123
  return evt.requestContext.domainName;
@@ -8,3 +8,6 @@ export * from "./adapter/github.js";
8
8
  export * from "./adapter/oidc.js";
9
9
  export * from "./adapter/oauth.js";
10
10
  export * from "./adapter/link.js";
11
+ export interface AuthResources {
12
+ }
13
+ export declare const Auth: AuthResources;
@@ -8,3 +8,6 @@ export * from "./adapter/github.js";
8
8
  export * from "./adapter/oidc.js";
9
9
  export * from "./adapter/oauth.js";
10
10
  export * from "./adapter/link.js";
11
+ import { createProxy, getVariables } from "../util/index.js";
12
+ export const Auth = createProxy("Auth");
13
+ Object.assign(Auth, await getVariables("Auth"));
@@ -0,0 +1,10 @@
1
+ import { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from "aws-lambda";
2
+ export type Adapter<T = any> = (evt: APIGatewayProxyEventV2) => Promise<{
3
+ type: "step";
4
+ properties: APIGatewayProxyStructuredResultV2;
5
+ } | {
6
+ type: "success";
7
+ properties: T;
8
+ } | {
9
+ type: "error";
10
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { OauthBasicConfig } from "./oauth.js";
2
+ export declare const GithubAdapter: (config: OauthBasicConfig) => () => Promise<{
3
+ type: "success";
4
+ properties: {
5
+ tokenset: import("openid-client").TokenSet;
6
+ client: import("openid-client").BaseClient;
7
+ };
8
+ } | {
9
+ type: "step";
10
+ properties: {
11
+ statusCode: number;
12
+ headers: {
13
+ location: string;
14
+ };
15
+ };
16
+ }>;
@@ -0,0 +1,22 @@
1
+ import { Issuer } from "openid-client";
2
+ import { OauthAdapter } from "./oauth.js";
3
+ import { OidcAdapter } from "./oidc.js";
4
+ const issuer = new Issuer({
5
+ issuer: "https://github.com",
6
+ authorization_endpoint: "https://github.com/login/oauth/authorize",
7
+ token_endpoint: "https://github.com/login/oauth/access_token",
8
+ });
9
+ export const GithubAdapter =
10
+ /* @__PURE__ */
11
+ (config) => {
12
+ if (config.clientSecret) {
13
+ return OauthAdapter({
14
+ issuer,
15
+ ...config,
16
+ });
17
+ }
18
+ return OidcAdapter({
19
+ issuer,
20
+ ...config,
21
+ });
22
+ };
@@ -0,0 +1,20 @@
1
+ import { OidcBasicConfig } from "./oidc.js";
2
+ type GoogleConfig = OidcBasicConfig & {
3
+ mode: "oidc";
4
+ };
5
+ export declare function GoogleAdapter(config: GoogleConfig): () => Promise<{
6
+ type: "success";
7
+ properties: {
8
+ tokenset: import("openid-client").TokenSet;
9
+ client: import("openid-client").BaseClient;
10
+ };
11
+ } | {
12
+ type: "step";
13
+ properties: {
14
+ statusCode: number;
15
+ headers: {
16
+ location: string;
17
+ };
18
+ };
19
+ }>;
20
+ export {};
@@ -0,0 +1,10 @@
1
+ import { Issuer } from "openid-client";
2
+ import { OidcAdapter } from "./oidc.js";
3
+ const issuer = await Issuer.discover("https://accounts.google.com");
4
+ export function GoogleAdapter(config) {
5
+ return OidcAdapter({
6
+ issuer,
7
+ scope: "openid email profile",
8
+ ...config,
9
+ });
10
+ }
@@ -0,0 +1,11 @@
1
+ import { APIGatewayProxyStructuredResultV2 } from "aws-lambda";
2
+ export declare function LinkAdapter(config: {
3
+ onLink: (link: string, claims: Record<string, any>) => Promise<APIGatewayProxyStructuredResultV2>;
4
+ onError: () => Promise<APIGatewayProxyStructuredResultV2>;
5
+ }): () => Promise<{
6
+ type: "step";
7
+ properties: APIGatewayProxyStructuredResultV2;
8
+ } | {
9
+ type: "success";
10
+ properties: any;
11
+ }>;
@@ -0,0 +1,48 @@
1
+ import { createSigner, createVerifier } from "fast-jwt";
2
+ import { Config } from "../../../config/index.js";
3
+ import { useDomainName, usePathParam, useQueryParam, useQueryParams, } from "../../../api/index.js";
4
+ export function LinkAdapter(config) {
5
+ // @ts-expect-error
6
+ const key = Config[process.env.AUTH_ID + "PrivateKey"];
7
+ const signer = createSigner({
8
+ expiresIn: 1000 * 60 * 10,
9
+ key,
10
+ algorithm: "RS512",
11
+ });
12
+ return async function () {
13
+ const callback = "https://" + useDomainName() + "/callback";
14
+ const step = usePathParam("step");
15
+ if (step === "authorize") {
16
+ const url = new URL(callback);
17
+ const claims = useQueryParams();
18
+ url.searchParams.append("token", signer(claims));
19
+ return {
20
+ type: "step",
21
+ properties: await config.onLink(url.toString(), claims),
22
+ };
23
+ }
24
+ if (step === "callback") {
25
+ const token = useQueryParam("token");
26
+ if (!token)
27
+ throw new Error("Missing token parameter");
28
+ try {
29
+ const verifier = createVerifier({
30
+ algorithms: ["RS512"],
31
+ key,
32
+ });
33
+ const jwt = verifier(token);
34
+ return {
35
+ type: "success",
36
+ properties: jwt,
37
+ };
38
+ }
39
+ catch {
40
+ return {
41
+ type: "step",
42
+ properties: await config.onError(),
43
+ };
44
+ }
45
+ }
46
+ throw new Error("Invalid auth request");
47
+ };
48
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseClient, Issuer, TokenSet } from "openid-client";
2
+ export interface OauthBasicConfig {
3
+ /**
4
+ * The clientID provided by the third party oauth service
5
+ */
6
+ clientID: string;
7
+ /**
8
+ * The clientSecret provided by the third party oauth service
9
+ */
10
+ clientSecret: string;
11
+ /**
12
+ * Various scopes requested for the access token
13
+ */
14
+ scope: string;
15
+ prompt?: string;
16
+ }
17
+ export interface OauthConfig extends OauthBasicConfig {
18
+ issuer: Issuer;
19
+ }
20
+ export declare const OauthAdapter: (config: OauthConfig) => () => Promise<{
21
+ type: "success";
22
+ properties: {
23
+ tokenset: TokenSet;
24
+ client: BaseClient;
25
+ };
26
+ } | {
27
+ type: "step";
28
+ properties: {
29
+ statusCode: number;
30
+ headers: {
31
+ location: string;
32
+ };
33
+ };
34
+ }>;
@@ -0,0 +1,66 @@
1
+ import { generators } from "openid-client";
2
+ import { useCookie, useDomainName, usePathParam, useQueryParams, useResponse, } from "../../../api/index.js";
3
+ export const OauthAdapter =
4
+ /* @__PURE__ */
5
+ (config) => {
6
+ return async function () {
7
+ const step = usePathParam("step");
8
+ const callback = "https://" + useDomainName() + "/callback";
9
+ const client = new config.issuer.Client({
10
+ client_id: config.clientID,
11
+ client_secret: config.clientSecret,
12
+ redirect_uris: [callback],
13
+ response_types: ["code"],
14
+ });
15
+ if (step === "authorize") {
16
+ const code_verifier = generators.codeVerifier();
17
+ const state = generators.state();
18
+ const code_challenge = generators.codeChallenge(code_verifier);
19
+ const url = client.authorizationUrl({
20
+ scope: config.scope,
21
+ code_challenge: code_challenge,
22
+ code_challenge_method: "S256",
23
+ state,
24
+ prompt: config.prompt,
25
+ });
26
+ useResponse().cookies({
27
+ auth_code_verifier: code_verifier,
28
+ auth_state: state,
29
+ }, {
30
+ httpOnly: true,
31
+ secure: true,
32
+ maxAge: 60 * 10,
33
+ sameSite: "None",
34
+ });
35
+ return {
36
+ type: "step",
37
+ properties: {
38
+ statusCode: 302,
39
+ headers: {
40
+ location: url,
41
+ },
42
+ },
43
+ };
44
+ }
45
+ if (step === "callback") {
46
+ const params = useQueryParams();
47
+ const code_verifier = useCookie("auth_code_verifier");
48
+ const state = useCookie("auth_state");
49
+ const tokenset = await client[config.issuer.metadata.userinfo_endpoint
50
+ ? "callback"
51
+ : "oauthCallback"](callback, params, {
52
+ code_verifier,
53
+ state,
54
+ });
55
+ const x = {
56
+ type: "success",
57
+ properties: {
58
+ tokenset,
59
+ client,
60
+ },
61
+ };
62
+ return x;
63
+ }
64
+ throw new Error("Invalid auth request");
65
+ };
66
+ };
@@ -0,0 +1,26 @@
1
+ import { Issuer } from "openid-client";
2
+ export interface OidcBasicConfig {
3
+ /**
4
+ * The clientID provided by the third party oauth service
5
+ */
6
+ clientID: string;
7
+ }
8
+ export interface OidcConfig extends OidcBasicConfig {
9
+ issuer: Issuer;
10
+ scope: string;
11
+ }
12
+ export declare const OidcAdapter: (config: OidcConfig) => () => Promise<{
13
+ type: "success";
14
+ properties: {
15
+ tokenset: import("openid-client").TokenSet;
16
+ client: import("openid-client").BaseClient;
17
+ };
18
+ } | {
19
+ type: "step";
20
+ properties: {
21
+ statusCode: number;
22
+ headers: {
23
+ location: string;
24
+ };
25
+ };
26
+ }>;
@@ -0,0 +1,62 @@
1
+ import { generators } from "openid-client";
2
+ import { useCookie, useDomainName, useFormData, usePathParam, useResponse, } from "../../../api/index.js";
3
+ export const OidcAdapter = /* @__PURE__ */ (config) => {
4
+ return async function () {
5
+ const step = usePathParam("step");
6
+ const callback = "https://" + useDomainName() + "/callback";
7
+ const client = new config.issuer.Client({
8
+ client_id: config.clientID,
9
+ redirect_uris: [callback],
10
+ response_types: ["id_token"],
11
+ });
12
+ if (step === "authorize") {
13
+ const nonce = generators.nonce();
14
+ const state = generators.state();
15
+ const url = client.authorizationUrl({
16
+ scope: config.scope,
17
+ response_mode: "form_post",
18
+ nonce,
19
+ state,
20
+ });
21
+ useResponse().cookies({
22
+ auth_nonce: nonce,
23
+ auth_state: state,
24
+ }, {
25
+ httpOnly: true,
26
+ secure: true,
27
+ maxAge: 60 * 10,
28
+ sameSite: "None",
29
+ });
30
+ return {
31
+ type: "step",
32
+ properties: {
33
+ statusCode: 302,
34
+ headers: {
35
+ location: url,
36
+ },
37
+ },
38
+ };
39
+ }
40
+ if (step === "callback") {
41
+ const form = useFormData();
42
+ if (!form)
43
+ throw new Error("Missing body");
44
+ const params = Object.fromEntries(form.entries());
45
+ const nonce = useCookie("auth_nonce");
46
+ const state = useCookie("auth_state");
47
+ const tokenset = await client.callback(callback, params, {
48
+ nonce,
49
+ state,
50
+ });
51
+ const x = {
52
+ type: "success",
53
+ properties: {
54
+ tokenset,
55
+ client,
56
+ },
57
+ };
58
+ return x;
59
+ }
60
+ throw new Error("Invalid auth request");
61
+ };
62
+ };
@@ -0,0 +1,18 @@
1
+ import { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from "aws-lambda";
2
+ import { Adapter } from "./adapter/adapter.js";
3
+ import { SignerOptions } from "fast-jwt";
4
+ import { SessionValue } from "./session.js";
5
+ export declare function AuthHandler<Providers extends Record<string, Adapter<any>>, Result = {
6
+ [key in keyof Providers]: {
7
+ provider: key;
8
+ } & Extract<Awaited<ReturnType<Providers[key]>>, {
9
+ type: "success";
10
+ }>["properties"];
11
+ }[keyof Providers]>(input: {
12
+ providers: Providers;
13
+ clients: () => Promise<Record<string, string>>;
14
+ onAuthorize?: (event: APIGatewayProxyEventV2) => Promise<void | keyof Providers>;
15
+ onSuccess: (input: Result) => Promise<SessionCreateInput>;
16
+ onError: () => Promise<APIGatewayProxyStructuredResultV2>;
17
+ }): (event: APIGatewayProxyEventV2, context: import("aws-lambda").Context) => Promise<APIGatewayProxyStructuredResultV2>;
18
+ export type SessionCreateInput = SessionValue & Partial<SignerOptions>;
@@ -0,0 +1,189 @@
1
+ import { createSigner, createVerifier } from "fast-jwt";
2
+ import { ApiHandler, useCookie, useCookies, useFormValue, usePathParam, useQueryParam, useQueryParams, useResponse, } from "../../api/index.js";
3
+ import { Config } from "../../config/index.js";
4
+ export function AuthHandler(input) {
5
+ return ApiHandler(async (evt) => {
6
+ const step = usePathParam("step");
7
+ if (!step) {
8
+ return {
9
+ statusCode: 200,
10
+ headers: {
11
+ "Content-Type": "text/html",
12
+ },
13
+ body: `
14
+ <html>
15
+ <body>
16
+ ${Object.keys(input.providers).map((name) => {
17
+ return `<a href="/authorize?provider=${name}&response_type=code&client_id=local&redirect_uri=http://localhost:300">${name}</a>`;
18
+ })}
19
+ </body>
20
+ </html>
21
+ `,
22
+ };
23
+ }
24
+ if (step === "token") {
25
+ if (useFormValue("grant_type") !== "authorization_code") {
26
+ return {
27
+ statusCode: 400,
28
+ body: "Invalid grant_type",
29
+ };
30
+ }
31
+ const code = useFormValue("code");
32
+ if (!code) {
33
+ return {
34
+ statusCode: 400,
35
+ body: "Missing code",
36
+ };
37
+ }
38
+ // @ts-expect-error
39
+ const pub = Config[process.env.AUTH_ID + "PublicKey"];
40
+ const verified = createVerifier({
41
+ algorithms: ["RS512"],
42
+ key: pub,
43
+ })(code);
44
+ if (verified.redirect_uri !== useFormValue("redirect_uri")) {
45
+ return {
46
+ statusCode: 400,
47
+ body: "redirect_uri mismatch",
48
+ };
49
+ }
50
+ if (verified.client_id !== useFormValue("client_id")) {
51
+ return {
52
+ statusCode: 400,
53
+ body: "client_id mismatch",
54
+ };
55
+ }
56
+ return {
57
+ statusCode: 200,
58
+ headers: {
59
+ "content-type": "application/json",
60
+ },
61
+ body: JSON.stringify({
62
+ access_token: verified.token,
63
+ }),
64
+ };
65
+ }
66
+ let provider = useCookie("provider");
67
+ if (step === "authorize") {
68
+ provider = useQueryParam("provider");
69
+ if (input.onAuthorize) {
70
+ const result = await input.onAuthorize(evt);
71
+ if (result)
72
+ provider = result;
73
+ }
74
+ if (!provider) {
75
+ return {
76
+ statusCode: 400,
77
+ body: "Missing provider",
78
+ };
79
+ }
80
+ const { response_type, client_id, redirect_uri, state } = useQueryParams();
81
+ if (!provider) {
82
+ return {
83
+ statusCode: 400,
84
+ body: "Missing provider",
85
+ };
86
+ }
87
+ if (!response_type) {
88
+ return {
89
+ statusCode: 400,
90
+ body: "Missing response_type",
91
+ };
92
+ }
93
+ if (!client_id) {
94
+ return {
95
+ statusCode: 400,
96
+ body: "Missing client_id",
97
+ };
98
+ }
99
+ if (!redirect_uri) {
100
+ return {
101
+ statusCode: 400,
102
+ body: "Missing redirect_uri",
103
+ };
104
+ }
105
+ useResponse().cookies({
106
+ provider: provider,
107
+ response_type: response_type,
108
+ client_id: client_id,
109
+ redirect_uri: redirect_uri,
110
+ state: state || "",
111
+ }, {
112
+ maxAge: 60 * 15,
113
+ secure: true,
114
+ sameSite: "None",
115
+ httpOnly: true,
116
+ });
117
+ }
118
+ if (!provider || !input.providers[provider]) {
119
+ return {
120
+ statusCode: 400,
121
+ body: `Was not able to find provider "${String(provider)}"`,
122
+ headers: {
123
+ "Content-Type": "text/html",
124
+ },
125
+ };
126
+ }
127
+ const adapter = input.providers[provider];
128
+ const result = await adapter(evt);
129
+ if (result.type === "step") {
130
+ return result.properties;
131
+ }
132
+ if (result.type === "success") {
133
+ const { type, properties, ...rest } = await input.onSuccess({
134
+ provider,
135
+ ...result.properties,
136
+ });
137
+ // @ts-expect-error
138
+ const priv = Config[process.env.AUTH_ID + "PrivateKey"];
139
+ const signer = createSigner({
140
+ ...rest,
141
+ key: priv,
142
+ algorithm: "RS512",
143
+ });
144
+ const token = signer({
145
+ type,
146
+ properties,
147
+ });
148
+ const { client_id, response_type, redirect_uri, state } = {
149
+ ...useCookies(),
150
+ ...useQueryParams(),
151
+ };
152
+ if (response_type === "token") {
153
+ return {
154
+ statusCode: 302,
155
+ headers: {
156
+ Location: `${redirect_uri}#access_token=${token}&state=${state || ""}`,
157
+ },
158
+ };
159
+ }
160
+ if (response_type === "code") {
161
+ // This allows the code to be reused within a 30 second window
162
+ // The code should be single use but we're making this tradeoff to remain stateless
163
+ // In the future can store this in a dynamo table to ensure single use
164
+ const code = createSigner({
165
+ expiresIn: 1000 * 60 * 5,
166
+ key: priv,
167
+ algorithm: "RS512",
168
+ })({
169
+ client_id,
170
+ redirect_uri,
171
+ token: token,
172
+ });
173
+ return {
174
+ statusCode: 302,
175
+ headers: {
176
+ Location: `${redirect_uri}?code=${code}&state=${state || ""}`,
177
+ },
178
+ };
179
+ }
180
+ return {
181
+ statusCode: 400,
182
+ body: `Unsupported response_type: ${response_type}`,
183
+ };
184
+ }
185
+ if (result.type === "error") {
186
+ return input.onError();
187
+ }
188
+ });
189
+ }
@@ -0,0 +1,10 @@
1
+ export interface AuthResources {
2
+ }
3
+ export declare const Auth: AuthResources;
4
+ export * from "./adapter/oidc.js";
5
+ export * from "./adapter/google.js";
6
+ export * from "./adapter/link.js";
7
+ export * from "./adapter/github.js";
8
+ export * from "./adapter/oauth.js";
9
+ export * from "./session.js";
10
+ export * from "./handler.js";
@@ -0,0 +1,10 @@
1
+ import { createProxy, getVariables } from "../../util/index.js";
2
+ export const Auth = createProxy("Auth");
3
+ Object.assign(Auth, getVariables("Auth"));
4
+ export * from "./adapter/oidc.js";
5
+ export * from "./adapter/google.js";
6
+ export * from "./adapter/link.js";
7
+ export * from "./adapter/github.js";
8
+ export * from "./adapter/oauth.js";
9
+ export * from "./session.js";
10
+ export * from "./handler.js";
@@ -0,0 +1,46 @@
1
+ import { SignerOptions } from "fast-jwt";
2
+ export interface SessionTypes {
3
+ public: {};
4
+ }
5
+ export type SessionValue = {
6
+ [type in keyof SessionTypes]: {
7
+ type: type;
8
+ properties: SessionTypes[type];
9
+ };
10
+ }[keyof SessionTypes];
11
+ export declare function useSession<T = SessionValue>(): T;
12
+ /**
13
+ * Creates a new session token with provided information
14
+ *
15
+ * @example
16
+ * ```js
17
+ * Session.create({
18
+ * type: "user",
19
+ * properties: {
20
+ * userID: "123"
21
+ * }
22
+ * })
23
+ * ```
24
+ */
25
+ declare function create<T extends keyof SessionTypes>(input: {
26
+ type: T;
27
+ properties: SessionTypes[T];
28
+ options?: Partial<SignerOptions>;
29
+ }): string;
30
+ /**
31
+ * Verifies a session token and returns the session data
32
+ *
33
+ * @example
34
+ * ```js
35
+ * Session.verify()
36
+ * ```
37
+ */
38
+ declare function verify<T = SessionValue>(token: string): T | {
39
+ type: string;
40
+ properties: {};
41
+ };
42
+ export declare const Session: {
43
+ create: typeof create;
44
+ verify: typeof verify;
45
+ };
46
+ export {};
@@ -0,0 +1,99 @@
1
+ import { createSigner, createVerifier } from "fast-jwt";
2
+ import { Context } from "../../../context/context.js";
3
+ import { useCookie, useHeader } from "../../api/index.js";
4
+ import { Auth } from "../../auth/index.js";
5
+ import { Config } from "../../config/index.js";
6
+ const SessionMemo = /* @__PURE__ */ Context.memo(() => {
7
+ let token = "";
8
+ const header = useHeader("authorization");
9
+ if (header)
10
+ token = header.substring(7);
11
+ const cookie = useCookie("sst_auth_token");
12
+ if (cookie)
13
+ token = cookie;
14
+ if (token) {
15
+ try {
16
+ const jwt = createVerifier({
17
+ algorithms: ["RS512"],
18
+ key: getPublicKey(),
19
+ })(token);
20
+ return jwt;
21
+ }
22
+ catch {
23
+ return {
24
+ type: "public",
25
+ properties: {},
26
+ };
27
+ }
28
+ }
29
+ return {
30
+ type: "public",
31
+ properties: {},
32
+ };
33
+ });
34
+ // This is a crazy TS hack to prevent the types from being evaluated too soon
35
+ export function useSession() {
36
+ const ctx = SessionMemo();
37
+ return ctx;
38
+ }
39
+ function getPublicKey() {
40
+ const [first] = Object.values(Auth);
41
+ if (!first)
42
+ throw new Error("No auth provider found. Did you forget to add one?");
43
+ return first.publicKey;
44
+ }
45
+ /**
46
+ * Creates a new session token with provided information
47
+ *
48
+ * @example
49
+ * ```js
50
+ * Session.create({
51
+ * type: "user",
52
+ * properties: {
53
+ * userID: "123"
54
+ * }
55
+ * })
56
+ * ```
57
+ */
58
+ function create(input) {
59
+ // @ts-expect-error
60
+ const key = Config[process.env.AUTH_ID + "PrivateKey"];
61
+ const signer = createSigner({
62
+ ...input.options,
63
+ key,
64
+ algorithm: "RS512",
65
+ });
66
+ const token = signer({
67
+ type: input.type,
68
+ properties: input.properties,
69
+ });
70
+ return token;
71
+ }
72
+ /**
73
+ * Verifies a session token and returns the session data
74
+ *
75
+ * @example
76
+ * ```js
77
+ * Session.verify()
78
+ * ```
79
+ */
80
+ function verify(token) {
81
+ if (token) {
82
+ try {
83
+ const jwt = createVerifier({
84
+ algorithms: ["RS512"],
85
+ key: getPublicKey(),
86
+ })(token);
87
+ return jwt;
88
+ }
89
+ catch { }
90
+ }
91
+ return {
92
+ type: "public",
93
+ properties: {},
94
+ };
95
+ }
96
+ export const Session = {
97
+ create,
98
+ verify,
99
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sst",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "bin": {
5
5
  "sst": "cli/sst.js"
6
6
  },
@@ -14,8 +14,10 @@
14
14
  },
15
15
  "exports": {
16
16
  "./constructs/deprecated": "./constructs/deprecated/index.js",
17
+ "./constructs/future": "./constructs/future/index.js",
17
18
  "./constructs": "./constructs/index.js",
18
19
  "./context": "./context/index.js",
20
+ "./node/future/*": "./node/future/*/index.js",
19
21
  "./node/*": "./node/*/index.js",
20
22
  ".": "./index.js",
21
23
  "./*": "./*"
@@ -113,6 +115,7 @@
113
115
  "@types/ws": "^8.5.3",
114
116
  "@types/yargs": "^17.0.13",
115
117
  "tsx": "^3.12.1",
118
+ "typescript": "^4.9.5",
116
119
  "vitest": "^0.28.3"
117
120
  },
118
121
  "peerDependencies": {