@infoxchange/make-it-so-sst-v2 3.0.1
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/.editorconfig +16 -0
- package/LICENSE +21 -0
- package/README.md +377 -0
- package/commitlint.config.ts +14 -0
- package/dist/cdk-constructs/IxApi.d.ts +12 -0
- package/dist/cdk-constructs/IxApi.d.ts.map +1 -0
- package/dist/cdk-constructs/IxApi.js +56 -0
- package/dist/cdk-constructs/IxBucket.d.ts +9 -0
- package/dist/cdk-constructs/IxBucket.d.ts.map +1 -0
- package/dist/cdk-constructs/IxBucket.js +22 -0
- package/dist/cdk-constructs/IxCertificate.d.ts +16 -0
- package/dist/cdk-constructs/IxCertificate.d.ts.map +1 -0
- package/dist/cdk-constructs/IxCertificate.js +26 -0
- package/dist/cdk-constructs/IxDnsRecord.d.ts +23 -0
- package/dist/cdk-constructs/IxDnsRecord.d.ts.map +1 -0
- package/dist/cdk-constructs/IxDnsRecord.js +43 -0
- package/dist/cdk-constructs/IxElasticache.d.ts +17 -0
- package/dist/cdk-constructs/IxElasticache.d.ts.map +1 -0
- package/dist/cdk-constructs/IxElasticache.js +70 -0
- package/dist/cdk-constructs/IxNextjsSite.d.ts +16 -0
- package/dist/cdk-constructs/IxNextjsSite.d.ts.map +1 -0
- package/dist/cdk-constructs/IxNextjsSite.js +38 -0
- package/dist/cdk-constructs/IxQuicksightWorkspace.d.ts +17 -0
- package/dist/cdk-constructs/IxQuicksightWorkspace.d.ts.map +1 -0
- package/dist/cdk-constructs/IxQuicksightWorkspace.js +29 -0
- package/dist/cdk-constructs/IxSESIdentity.d.ts +12 -0
- package/dist/cdk-constructs/IxSESIdentity.d.ts.map +1 -0
- package/dist/cdk-constructs/IxSESIdentity.js +45 -0
- package/dist/cdk-constructs/IxStaticSite.d.ts +17 -0
- package/dist/cdk-constructs/IxStaticSite.d.ts.map +1 -0
- package/dist/cdk-constructs/IxStaticSite.js +38 -0
- package/dist/cdk-constructs/IxVpcDetails.d.ts +12 -0
- package/dist/cdk-constructs/IxVpcDetails.d.ts.map +1 -0
- package/dist/cdk-constructs/IxVpcDetails.js +26 -0
- package/dist/cdk-constructs/IxWebsiteRedirect.d.ts +35 -0
- package/dist/cdk-constructs/IxWebsiteRedirect.d.ts.map +1 -0
- package/dist/cdk-constructs/IxWebsiteRedirect.js +72 -0
- package/dist/cdk-constructs/SiteOidcAuth/auth-check-handler-body.d.ts +2 -0
- package/dist/cdk-constructs/SiteOidcAuth/auth-check-handler-body.d.ts.map +1 -0
- package/dist/cdk-constructs/SiteOidcAuth/auth-check-handler-body.js +130 -0
- package/dist/cdk-constructs/SiteOidcAuth/auth-route.d.ts +2 -0
- package/dist/cdk-constructs/SiteOidcAuth/auth-route.d.ts.map +1 -0
- package/dist/cdk-constructs/SiteOidcAuth/auth-route.js +59 -0
- package/dist/cdk-constructs/SiteOidcAuth/index.d.ts +197 -0
- package/dist/cdk-constructs/SiteOidcAuth/index.d.ts.map +1 -0
- package/dist/cdk-constructs/SiteOidcAuth/index.js +188 -0
- package/dist/cdk-constructs/index.d.ts +11 -0
- package/dist/cdk-constructs/index.d.ts.map +1 -0
- package/dist/cdk-constructs/index.js +10 -0
- package/dist/deployConfig.d.ts +72 -0
- package/dist/deployConfig.d.ts.map +1 -0
- package/dist/deployConfig.js +78 -0
- package/dist/lib/auth/index.d.ts +2 -0
- package/dist/lib/auth/index.d.ts.map +1 -0
- package/dist/lib/auth/index.js +1 -0
- package/dist/lib/auth/oidc.d.ts +26 -0
- package/dist/lib/auth/oidc.d.ts.map +1 -0
- package/dist/lib/auth/oidc.js +48 -0
- package/dist/lib/proxy/fetch.d.ts +4 -0
- package/dist/lib/proxy/fetch.d.ts.map +1 -0
- package/dist/lib/proxy/fetch.js +31 -0
- package/dist/lib/proxy/index.d.ts +2 -0
- package/dist/lib/proxy/index.d.ts.map +1 -0
- package/dist/lib/proxy/index.js +1 -0
- package/dist/lib/site/support.d.ts +71 -0
- package/dist/lib/site/support.d.ts.map +1 -0
- package/dist/lib/site/support.js +262 -0
- package/dist/lib/utils/hash.d.ts +2 -0
- package/dist/lib/utils/hash.d.ts.map +1 -0
- package/dist/lib/utils/hash.js +13 -0
- package/dist/lib/utils/objects.d.ts +4 -0
- package/dist/lib/utils/objects.d.ts.map +1 -0
- package/dist/lib/utils/objects.js +7 -0
- package/eslint.config.js +11 -0
- package/package.json +66 -0
- package/src/cdk-constructs/IxApi.ts +81 -0
- package/src/cdk-constructs/IxBucket.ts +35 -0
- package/src/cdk-constructs/IxCertificate.ts +54 -0
- package/src/cdk-constructs/IxDnsRecord.ts +79 -0
- package/src/cdk-constructs/IxElasticache.ts +106 -0
- package/src/cdk-constructs/IxNextjsSite.ts +72 -0
- package/src/cdk-constructs/IxQuicksightWorkspace.ts +54 -0
- package/src/cdk-constructs/IxSESIdentity.ts +70 -0
- package/src/cdk-constructs/IxStaticSite.ts +69 -0
- package/src/cdk-constructs/IxVpcDetails.ts +38 -0
- package/src/cdk-constructs/IxWebsiteRedirect.ts +133 -0
- package/src/cdk-constructs/SiteOidcAuth/auth-check-handler-body.ts +168 -0
- package/src/cdk-constructs/SiteOidcAuth/auth-route.ts +71 -0
- package/src/cdk-constructs/SiteOidcAuth/index.ts +299 -0
- package/src/cdk-constructs/index.ts +10 -0
- package/src/deployConfig.ts +87 -0
- package/src/lib/auth/index.ts +1 -0
- package/src/lib/auth/oidc.ts +73 -0
- package/src/lib/proxy/fetch.ts +41 -0
- package/src/lib/proxy/index.ts +1 -0
- package/src/lib/site/support.ts +439 -0
- package/src/lib/utils/hash.ts +14 -0
- package/src/lib/utils/objects.ts +19 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { StaticSite } from "sst/constructs";
|
|
2
|
+
import ixDeployConfig from "../deployConfig.js";
|
|
3
|
+
import {
|
|
4
|
+
ExtendedStaticSiteProps,
|
|
5
|
+
getAliasDomain,
|
|
6
|
+
getAlternativeDomains,
|
|
7
|
+
getCustomDomains,
|
|
8
|
+
getPrimaryCustomDomain,
|
|
9
|
+
getPrimaryDomain,
|
|
10
|
+
getPrimaryOrigin,
|
|
11
|
+
processAuthProps,
|
|
12
|
+
setupCertificate,
|
|
13
|
+
setupCustomDomain,
|
|
14
|
+
setupDnsRecords,
|
|
15
|
+
setupDomainAliasRedirect,
|
|
16
|
+
} from "../lib/site/support.js";
|
|
17
|
+
|
|
18
|
+
type ConstructScope = ConstructorParameters<typeof StaticSite>[0];
|
|
19
|
+
type ConstructId = ConstructorParameters<typeof StaticSite>[1];
|
|
20
|
+
type ConstructProps = ExtendedStaticSiteProps;
|
|
21
|
+
|
|
22
|
+
export class IxStaticSite extends StaticSite {
|
|
23
|
+
// StaticSite's props are private, so we need to store them separately
|
|
24
|
+
private propsExtended: ConstructProps;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
scope: ConstructScope,
|
|
28
|
+
id: ConstructId,
|
|
29
|
+
props: ConstructProps = {},
|
|
30
|
+
) {
|
|
31
|
+
if (ixDeployConfig.isIxDeploy) {
|
|
32
|
+
props = setupCustomDomain(scope, id, props);
|
|
33
|
+
props = setupCertificate(scope, id, props);
|
|
34
|
+
props = setupDomainAliasRedirect(scope, id, props);
|
|
35
|
+
}
|
|
36
|
+
props = processAuthProps(scope, id, "StaticSite", props);
|
|
37
|
+
|
|
38
|
+
super(scope, id, props);
|
|
39
|
+
this.propsExtended = props;
|
|
40
|
+
|
|
41
|
+
if (ixDeployConfig.isIxDeploy) {
|
|
42
|
+
setupDnsRecords(this, scope, id, props);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public get customDomains(): string[] {
|
|
47
|
+
return getCustomDomains(this.propsExtended);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public get primaryCustomDomain(): string | null {
|
|
51
|
+
return getPrimaryCustomDomain(this.propsExtended);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public get aliasDomain(): string | null {
|
|
55
|
+
return getAliasDomain(this.propsExtended);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public get alternativeDomains(): string[] {
|
|
59
|
+
return getAlternativeDomains(this.propsExtended);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public get primaryDomain(): string | null {
|
|
63
|
+
return getPrimaryDomain(this, this.propsExtended);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get primaryOrigin(): string | null {
|
|
67
|
+
return getPrimaryOrigin(this, this.propsExtended);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
3
|
+
import { Vpc, IVpc } from "aws-cdk-lib/aws-ec2";
|
|
4
|
+
import ixDeployConfig from "../deployConfig.js";
|
|
5
|
+
|
|
6
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
7
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
8
|
+
|
|
9
|
+
export class IxVpcDetails extends Construct {
|
|
10
|
+
public vpc: IVpc;
|
|
11
|
+
|
|
12
|
+
constructor(scope: ConstructScope, id: ConstructId) {
|
|
13
|
+
super(scope, id);
|
|
14
|
+
this.vpc = this.getVpc(scope, id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private getVpc(scope: ConstructScope, id: ConstructId): IVpc {
|
|
18
|
+
const vpcId = StringParameter.valueForStringParameter(scope, "/vpc/id");
|
|
19
|
+
return Vpc.fromVpcAttributes(this, id + "-Vpc", {
|
|
20
|
+
vpcId,
|
|
21
|
+
availabilityZones: [
|
|
22
|
+
"ap-southeast-2a",
|
|
23
|
+
"ap-southeast-2b",
|
|
24
|
+
"ap-southeast-2c",
|
|
25
|
+
],
|
|
26
|
+
isolatedSubnetIds: IxVpcDetails.getVpcSubnetIds(scope),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static getVpcSubnetIds(scope: ConstructScope): Array<string> {
|
|
31
|
+
return [1, 2, 3].map((subnetNum) =>
|
|
32
|
+
StringParameter.valueForStringParameter(
|
|
33
|
+
scope,
|
|
34
|
+
`/vpc/subnet/private-${ixDeployConfig.workloadGroup}/${subnetNum}/id`,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Based off https://github.com/sst/v2/blob/37578037ff80638e2f0eaf0dc59a83dae52e4e45/packages/sst/src/constructs/cdk/website-redirect.ts
|
|
2
|
+
// Not quite sure why an s3 bucket is used for generating the redirect rather than a cloudfront edge lambda,
|
|
3
|
+
// maybe it's cheaper? In any case it's what SST uses and those devs have put a lot more thought into
|
|
4
|
+
// this that we have so I'm going to trust them on this one.
|
|
5
|
+
|
|
6
|
+
import { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
7
|
+
import {
|
|
8
|
+
CloudFrontWebDistribution,
|
|
9
|
+
OriginProtocolPolicy,
|
|
10
|
+
PriceClass,
|
|
11
|
+
ViewerCertificate,
|
|
12
|
+
ViewerProtocolPolicy,
|
|
13
|
+
} from "aws-cdk-lib/aws-cloudfront";
|
|
14
|
+
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
15
|
+
import {
|
|
16
|
+
BlockPublicAccess,
|
|
17
|
+
Bucket,
|
|
18
|
+
RedirectProtocol,
|
|
19
|
+
} from "aws-cdk-lib/aws-s3";
|
|
20
|
+
import { ArnFormat, RemovalPolicy, Stack, Token } from "aws-cdk-lib/core";
|
|
21
|
+
import { Construct } from "constructs";
|
|
22
|
+
import { convertToBase62Hash } from "../lib/utils/hash.js";
|
|
23
|
+
import { IxDnsRecord } from "./IxDnsRecord.js";
|
|
24
|
+
import { IxCertificate } from "./IxCertificate.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Properties to configure an HTTPS Redirect
|
|
28
|
+
*/
|
|
29
|
+
export interface WebsiteRedirectProps {
|
|
30
|
+
/**
|
|
31
|
+
* The redirect target fully qualified domain name (FQDN). An alias record
|
|
32
|
+
* will be created that points to your CloudFront distribution. Root domain
|
|
33
|
+
* or sub-domain can be supplied.
|
|
34
|
+
*/
|
|
35
|
+
readonly targetDomain: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The domain names that will redirect to `targetDomain`
|
|
39
|
+
*
|
|
40
|
+
* @default - the domain name of the hosted zone
|
|
41
|
+
*/
|
|
42
|
+
readonly recordNames: string[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The AWS Certificate Manager (ACM) certificate that will be associated with
|
|
46
|
+
* the CloudFront distribution that will be created. If provided, the certificate must be
|
|
47
|
+
* stored in us-east-1 (N. Virginia)
|
|
48
|
+
*
|
|
49
|
+
* @default - A new certificate is created in us-east-1 (N. Virginia)
|
|
50
|
+
*/
|
|
51
|
+
readonly certificate?: ICertificate;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Allows creating a domainA -> domainB redirect using CloudFront and S3.
|
|
56
|
+
* You can specify multiple domains to be redirected.
|
|
57
|
+
*/
|
|
58
|
+
export class IxWebsiteRedirect extends Construct {
|
|
59
|
+
constructor(scope: Construct, id: string, props: WebsiteRedirectProps) {
|
|
60
|
+
super(scope, id);
|
|
61
|
+
|
|
62
|
+
const domainNames = props.recordNames;
|
|
63
|
+
|
|
64
|
+
let redirectCert = props.certificate;
|
|
65
|
+
|
|
66
|
+
if (!redirectCert) {
|
|
67
|
+
const newCert = new IxCertificate(scope, id + "-IxRedirectCertificate", {
|
|
68
|
+
domainName: domainNames[0],
|
|
69
|
+
subjectAlternativeNames: domainNames.slice(1),
|
|
70
|
+
region: "us-east-1", // CloudFront will only use certificates in us-east-1
|
|
71
|
+
});
|
|
72
|
+
redirectCert = newCert.acmCertificate;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const certificateRegion = Stack.of(this).splitArn(
|
|
76
|
+
redirectCert.certificateArn,
|
|
77
|
+
ArnFormat.SLASH_RESOURCE_NAME,
|
|
78
|
+
).region;
|
|
79
|
+
if (
|
|
80
|
+
!Token.isUnresolved(certificateRegion) &&
|
|
81
|
+
certificateRegion !== "us-east-1"
|
|
82
|
+
) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`The certificate must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const redirectBucket = new Bucket(this, "RedirectBucket", {
|
|
89
|
+
websiteRedirect: {
|
|
90
|
+
hostName: props.targetDomain,
|
|
91
|
+
protocol: RedirectProtocol.HTTPS,
|
|
92
|
+
},
|
|
93
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
94
|
+
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
95
|
+
});
|
|
96
|
+
const redirectDist = new CloudFrontWebDistribution(
|
|
97
|
+
this,
|
|
98
|
+
"RedirectDistribution",
|
|
99
|
+
{
|
|
100
|
+
defaultRootObject: "",
|
|
101
|
+
originConfigs: [
|
|
102
|
+
{
|
|
103
|
+
behaviors: [{ isDefaultBehavior: true }],
|
|
104
|
+
customOriginSource: {
|
|
105
|
+
domainName: redirectBucket.bucketWebsiteDomainName,
|
|
106
|
+
originProtocolPolicy: OriginProtocolPolicy.HTTP_ONLY,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
viewerCertificate: ViewerCertificate.fromAcmCertificate(redirectCert, {
|
|
111
|
+
aliases: domainNames,
|
|
112
|
+
}),
|
|
113
|
+
comment: `Redirect to ${props.targetDomain} from ${domainNames.join(
|
|
114
|
+
", ",
|
|
115
|
+
)}`,
|
|
116
|
+
priceClass: PriceClass.PRICE_CLASS_ALL,
|
|
117
|
+
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
for (const domainName of domainNames) {
|
|
122
|
+
const domainNameLogicalId = convertToBase62Hash(domainName);
|
|
123
|
+
|
|
124
|
+
new IxDnsRecord(scope, `DnsRecord-Redirect-${domainNameLogicalId}`, {
|
|
125
|
+
type: "ALIAS",
|
|
126
|
+
name: domainName,
|
|
127
|
+
value: redirectDist.distributionDomainName,
|
|
128
|
+
aliasZoneId: CloudFrontTarget.getHostedZoneId(scope),
|
|
129
|
+
ttl: 900,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Based off: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example_cloudfront_functions_kvs_jwt_verify_section.html
|
|
2
|
+
// Note that as a CloudFront Function, this code has limitations compared to a Lambda@Edge function. For example, no
|
|
3
|
+
// external libraries can be used, and the runtime is more limited. Because SST v2's SsrSite construct uses JS v1.0
|
|
4
|
+
// runtime for CloudFront Functions, this code must also be compatible with that runtime. Also this is used in the body
|
|
5
|
+
// of a function where the variables event and request are already defined.
|
|
6
|
+
|
|
7
|
+
declare const request: AWSCloudFrontFunction.Request;
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires -- v1 runtime for CloudFront Functions do not support import statements
|
|
10
|
+
const crypto: typeof import("crypto") = require("crypto");
|
|
11
|
+
|
|
12
|
+
const jwtSecret = "__placeholder-for-jwt-secret__";
|
|
13
|
+
const authRoutePrefix = "__placeholder-for-auth-route-prefix__";
|
|
14
|
+
|
|
15
|
+
// Set to true to enable console logging
|
|
16
|
+
const loggingEnabled = false;
|
|
17
|
+
|
|
18
|
+
// Simple logger that can be enabled/disabled via the loggingEnabled variable.
|
|
19
|
+
const log: typeof console.log = function () {
|
|
20
|
+
if (!loggingEnabled) return;
|
|
21
|
+
|
|
22
|
+
// CloudFront Function runtime only prints first argument passed to console.log so add other args to the first one if given.
|
|
23
|
+
// eslint-disable-next-line prefer-rest-params -- We can't use spread or rest parameters in CloudFront Functions
|
|
24
|
+
let message = arguments[0];
|
|
25
|
+
if (arguments.length > 1) {
|
|
26
|
+
const otherArgs = [];
|
|
27
|
+
for (let i = 1; i < arguments.length; i++) {
|
|
28
|
+
// eslint-disable-next-line prefer-rest-params
|
|
29
|
+
otherArgs[i - 1] = arguments[i];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
message += " - additional args: " + JSON.stringify(otherArgs);
|
|
33
|
+
}
|
|
34
|
+
console.log(message);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
//Response when JWT is not valid.
|
|
38
|
+
const redirectResponse = {
|
|
39
|
+
statusCode: 302,
|
|
40
|
+
headers: {
|
|
41
|
+
location: { value: `${authRoutePrefix}/oidc/authorize` },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Takes a JWT token to decode and throws an error if invalid
|
|
46
|
+
function jwtDecode(token: string, key: string, noVerify?: boolean) {
|
|
47
|
+
// check segments
|
|
48
|
+
const segments = token.split(".");
|
|
49
|
+
if (segments.length !== 3) {
|
|
50
|
+
throw new Error("Not enough or too many segments");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// All segment should be base64
|
|
54
|
+
const headerSeg = segments[0];
|
|
55
|
+
const payloadSeg = segments[1];
|
|
56
|
+
const signatureSeg = segments[2];
|
|
57
|
+
|
|
58
|
+
// base64 decode and parse JSON
|
|
59
|
+
const payload = JSON.parse(_base64urlDecode(payloadSeg));
|
|
60
|
+
|
|
61
|
+
if (noVerify) {
|
|
62
|
+
return payload;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const signingMethod = "sha256";
|
|
66
|
+
const signingType = "hmac";
|
|
67
|
+
|
|
68
|
+
// Verify signature. `sign` will return base64 string.
|
|
69
|
+
const signingInput = [headerSeg, payloadSeg].join(".");
|
|
70
|
+
|
|
71
|
+
if (!_verify(signingInput, key, signingMethod, signingType, signatureSeg)) {
|
|
72
|
+
throw new Error("Signature verification failed");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Support for nbf and exp claims.
|
|
76
|
+
// According to the RFC, they should be in seconds.
|
|
77
|
+
if (payload.nbf && Date.now() < payload.nbf * 1000) {
|
|
78
|
+
throw new Error("Token not yet active");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (payload.exp && Date.now() > payload.exp * 1000) {
|
|
82
|
+
throw new Error("Token expired");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return payload;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Function to ensure a constant time comparison to prevent timing side channels.
|
|
89
|
+
function _constantTimeEquals(a: string, b: string) {
|
|
90
|
+
if (a.length != b.length) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let xor = 0;
|
|
95
|
+
for (let i = 0; i < a.length; i++) {
|
|
96
|
+
xor |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return 0 === xor;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Verifies some input matches an expected signature.
|
|
103
|
+
function _verify(
|
|
104
|
+
input: string,
|
|
105
|
+
key: string,
|
|
106
|
+
method: string,
|
|
107
|
+
type: string,
|
|
108
|
+
signature: string,
|
|
109
|
+
) {
|
|
110
|
+
if (type === "hmac") {
|
|
111
|
+
return _constantTimeEquals(signature, _sign(input, key, method));
|
|
112
|
+
} else {
|
|
113
|
+
throw new Error("Algorithm type not recognized");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Signs some input with a key and method.
|
|
118
|
+
function _sign(input: string, key: string, method: string) {
|
|
119
|
+
return crypto.createHmac(method, key).update(input).digest("base64url");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Very annoying that we have to implement this ourselves but it seems like the v1 runtime does not have atob/btoa or
|
|
123
|
+
// Buffer available.
|
|
124
|
+
function _base64urlDecode(str: string) {
|
|
125
|
+
str = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
126
|
+
while (str.length % 4) str += "=";
|
|
127
|
+
|
|
128
|
+
const chars =
|
|
129
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
130
|
+
let output = "";
|
|
131
|
+
|
|
132
|
+
let bc = 0,
|
|
133
|
+
bs = 0,
|
|
134
|
+
buffer,
|
|
135
|
+
i = 0;
|
|
136
|
+
for (; i < str.length; i++) {
|
|
137
|
+
buffer = chars.indexOf(str.charAt(i));
|
|
138
|
+
if (buffer === -1) continue;
|
|
139
|
+
|
|
140
|
+
bs = (bs << 6) | buffer;
|
|
141
|
+
bc += 6;
|
|
142
|
+
|
|
143
|
+
if (bc >= 8) {
|
|
144
|
+
bc -= 8;
|
|
145
|
+
output += String.fromCharCode((bs >> bc) & 0xff);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return output;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const jwtToken =
|
|
153
|
+
request.cookies["auth-token"] && request.cookies["auth-token"].value;
|
|
154
|
+
|
|
155
|
+
if (!jwtToken) {
|
|
156
|
+
log("Error: No JWT in the cookies");
|
|
157
|
+
// @ts-expect-error -- This code is added to a function body so we can use return here but typescript doesn't know that.
|
|
158
|
+
return redirectResponse;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
jwtDecode(jwtToken, jwtSecret);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
log(e);
|
|
164
|
+
// @ts-expect-error -- This code is added to a function body so we can use return here but typescript doesn't know that.
|
|
165
|
+
return redirectResponse;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
log("Valid JWT token");
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { AuthHandler, OidcAdapter } from "sst/node/auth";
|
|
2
|
+
import { Issuer } from "openid-client";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
|
|
5
|
+
const oidcClientId = process.env.OIDC_CLIENT_ID;
|
|
6
|
+
if (!oidcClientId) {
|
|
7
|
+
throw new Error("OIDC_CLIENT_ID not set");
|
|
8
|
+
}
|
|
9
|
+
const oidcIssuerUrl = process.env.OIDC_ISSUER_URL;
|
|
10
|
+
if (!oidcIssuerUrl) {
|
|
11
|
+
throw new Error("OIDC_ISSUER_URL not set");
|
|
12
|
+
}
|
|
13
|
+
const oidcScope = process.env.OIDC_SCOPE;
|
|
14
|
+
if (!oidcScope) {
|
|
15
|
+
throw new Error("OIDC_SCOPE not set");
|
|
16
|
+
}
|
|
17
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
18
|
+
if (!jwtSecret) {
|
|
19
|
+
throw new Error("JWT_SECRET not set");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const oidcIssuerConfigUrl = new URL(
|
|
23
|
+
`${process.env.OIDC_ISSUER_URL?.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const handler = addRequiredContext(
|
|
27
|
+
AuthHandler({
|
|
28
|
+
providers: {
|
|
29
|
+
oidc: OidcAdapter({
|
|
30
|
+
issuer: await Issuer.discover(oidcIssuerConfigUrl.href),
|
|
31
|
+
clientID: oidcClientId,
|
|
32
|
+
scope: oidcScope,
|
|
33
|
+
onSuccess: async (tokenset) => {
|
|
34
|
+
// Payload to include in the token
|
|
35
|
+
const payload = {
|
|
36
|
+
userID: tokenset.claims().sub,
|
|
37
|
+
};
|
|
38
|
+
const expiresInMs = 1000 * 60 * 60;
|
|
39
|
+
|
|
40
|
+
// Create the token
|
|
41
|
+
const token = jwt.sign(payload, jwtSecret, {
|
|
42
|
+
algorithm: "HS256",
|
|
43
|
+
expiresIn: expiresInMs / 1000,
|
|
44
|
+
});
|
|
45
|
+
const expiryDate = new Date(Date.now() + expiresInMs);
|
|
46
|
+
return {
|
|
47
|
+
statusCode: 302,
|
|
48
|
+
headers: {
|
|
49
|
+
location: "/",
|
|
50
|
+
},
|
|
51
|
+
cookies: [
|
|
52
|
+
`auth-token=${token}; HttpOnly; SameSite=None; Secure; Path=/; Expires=${expiryDate}`,
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
function addRequiredContext(
|
|
62
|
+
handler: ReturnType<typeof AuthHandler>,
|
|
63
|
+
): ReturnType<typeof AuthHandler> {
|
|
64
|
+
return async function (...args) {
|
|
65
|
+
const [event] = args;
|
|
66
|
+
// Used by AuthHandler to create callback url sent to oidc server
|
|
67
|
+
event.requestContext.domainName = event.headers["x-forwarded-host"];
|
|
68
|
+
|
|
69
|
+
return await handler(...args);
|
|
70
|
+
};
|
|
71
|
+
}
|