diff --git a/apps/account-functions/BUILD.bazel b/apps/account-functions/BUILD.bazel new file mode 100644 index 000000000..b2be783b6 --- /dev/null +++ b/apps/account-functions/BUILD.bazel @@ -0,0 +1,30 @@ +load("//tools:defaults.bzl", "esbuild", "ts_library") + +package(default_visibility = ["//visibility:private"]) + +ts_library( + name = "accounts", + srcs = [ + "before-create.ts", + "before-sign-in.ts", + "index.ts", + ], + visibility = [ + "//apps/functions:__pkg__", + ], + deps = [ + "@npm//gcip-cloud-functions", + ], +) + +esbuild( + name = "accounts_compiled", + entry_points = [ + "index.ts", + ], + format = "esm", + visibility = ["//apps:__pkg__"], + deps = [ + ":accounts", + ], +) diff --git a/apps/account-functions/before-create.ts b/apps/account-functions/before-create.ts new file mode 100644 index 000000000..a4b55614d --- /dev/null +++ b/apps/account-functions/before-create.ts @@ -0,0 +1,10 @@ +import {Auth, https, UserRecord} from 'gcip-cloud-functions'; + +/** Validate accounts before their creation using google cloud before create syncronous function. */ +export const beforeCreate = new Auth().functions().beforeCreateHandler((user: UserRecord) => { + if (user.email && user.email.indexOf('@google.com') === -1) { + throw new https.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`); + } + + return {}; +}); diff --git a/apps/account-functions/before-sign-in.ts b/apps/account-functions/before-sign-in.ts new file mode 100644 index 000000000..efb4d8132 --- /dev/null +++ b/apps/account-functions/before-sign-in.ts @@ -0,0 +1,34 @@ +import { + Auth, + https, + UserRecord, + UserEventUpdateRequest, + AuthEventContext, +} from 'gcip-cloud-functions'; + +/** Validate accounts before sign in using google cloud before sigin in syncronous function. */ + +export const beforeSignIn = new Auth() + .functions() + .beforeSignInHandler( + async (user: UserRecord, context: AuthEventContext): Promise => { + /** The UserEventUpdate to save based on the signin results. */ + const event: UserEventUpdateRequest = {}; + + // If a user is able to reach this without a login credential being present, throw an auth error. + // Note: This should not be possible, but it doesn't hurt to have this check in place. + if (context.credential === undefined) { + throw new https.HttpsError( + 'unauthenticated', + `Cannot sign in as '${user.email}' without credential.`, + ); + } + + // When users sign in with github, we save the access token as a claim on the user object. + if (context.credential.providerId === 'github.com') { + event.customClaims = {...event.customClaims, githubToken: context.credential.accessToken}; + } + + return event; + }, + ); diff --git a/apps/account-functions/index.ts b/apps/account-functions/index.ts new file mode 100644 index 000000000..48d2a83ea --- /dev/null +++ b/apps/account-functions/index.ts @@ -0,0 +1,2 @@ +export {beforeCreate} from './before-create'; +export {beforeSignIn} from './before-sign-in'; diff --git a/package.json b/package.json index 82b3628d3..5ee8be914 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "firebase-functions": "^3.19.0", "firebase-tools": "^10.5.0", "font-color-contrast": "^11.1.0", + "gcip-cloud-functions": "0.0.1", "git-raw-commits": "^2.0.10", "glob": "7.2.0", "husky": "^7.0.1", diff --git a/yarn.lock b/yarn.lock index 39d8dea70..27201b534 100644 --- a/yarn.lock +++ b/yarn.lock @@ -466,6 +466,7 @@ __metadata: firebase-functions: ^3.19.0 firebase-tools: ^10.5.0 font-color-contrast: ^11.1.0 + gcip-cloud-functions: 0.0.1 git-raw-commits: ^2.0.10 glob: 7.2.0 husky: ^7.0.1 @@ -3727,7 +3728,7 @@ __metadata: languageName: node linkType: hard -"@types/cors@npm:^2.8.12, @types/cors@npm:^2.8.5": +"@types/cors@npm:^2.8.1, @types/cors@npm:^2.8.12, @types/cors@npm:^2.8.5": version: 2.8.12 resolution: "@types/cors@npm:2.8.12" checksum: 8c45f112c7d1d2d831b4b266f2e6ed33a1887a35dcbfe2a18b28370751fababb7cd045e745ef84a523c33a25932678097bf79afaa367c6cb3fa0daa7a6438257 @@ -3814,7 +3815,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.17.13": +"@types/express@npm:*, @types/express@npm:^4.11.1, @types/express@npm:^4.17.13": version: 4.17.13 resolution: "@types/express@npm:4.17.13" dependencies: @@ -4038,6 +4039,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^8.0.53": + version: 8.10.66 + resolution: "@types/node@npm:8.10.66" + checksum: c52039de862654a139abdc6a51de532a69dd80516ac35a959c3b3a2831ecbaaf065b0df5f9db943f5e28b544ebb9a891730d52b52f7a169b86a82bc060210000 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -6329,7 +6337,7 @@ __metadata: languageName: node linkType: hard -"cors@npm:^2.8.5, cors@npm:~2.8.5": +"cors@npm:^2.8.4, cors@npm:^2.8.5, cors@npm:~2.8.5": version: 2.8.5 resolution: "cors@npm:2.8.5" dependencies: @@ -7789,7 +7797,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.16.4, express@npm:^4.17.1, express@npm:^4.17.3": +"express@npm:^4.16.2, express@npm:^4.16.4, express@npm:^4.17.1, express@npm:^4.17.3": version: 4.17.3 resolution: "express@npm:4.17.3" dependencies: @@ -8500,6 +8508,21 @@ __metadata: languageName: node linkType: hard +"gcip-cloud-functions@npm:0.0.1": + version: 0.0.1 + resolution: "gcip-cloud-functions@npm:0.0.1" + dependencies: + "@types/cors": ^2.8.1 + "@types/express": ^4.11.1 + "@types/node": ^8.0.53 + cors: ^2.8.4 + express: ^4.16.2 + jsonwebtoken: 8.5.1 + node-forge: ^0.10.0 + checksum: 3cd07dbafa51ba0ba7d9f8efc50c25f4c08c85c3750fb156af590be209e47efd9d541e892fcfeba12432465d134984b14bc177369eb0c6b6a3587c9c9f851622 + languageName: node + linkType: hard + "gcp-metadata@npm:^4.2.0": version: 4.3.1 resolution: "gcp-metadata@npm:4.3.1" @@ -10258,7 +10281,7 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:^8.5.1": +"jsonwebtoken@npm:8.5.1, jsonwebtoken@npm:^8.5.1": version: 8.5.1 resolution: "jsonwebtoken@npm:8.5.1" dependencies: @@ -11725,6 +11748,13 @@ __metadata: languageName: node linkType: hard +"node-forge@npm:^0.10.0": + version: 0.10.0 + resolution: "node-forge@npm:0.10.0" + checksum: 5aa6dc9922e424a20ef101d2f517418e2bc9cfc0255dd22e0701c0fad1568445f510ee67f6f3fcdf085812c4ca1b847b8ba45683b34776828e41f5c1794e42e1 + languageName: node + linkType: hard + "node-forge@npm:^1, node-forge@npm:^1.0.0": version: 1.3.1 resolution: "node-forge@npm:1.3.1"