Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set up ng-dev auth and service #720

Closed
wants to merge 8 commits into from
4,078 changes: 470 additions & 3,608 deletions .github/local-actions/changelog/main.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion apps/functions/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ ts_library(
],
deps = [
"//apps/functions/githubWebhook",
"@npm//firebase-functions",
"//apps/functions/ng-dev",
"@npm//firebase-admin",
],
)

Expand Down
3 changes: 0 additions & 3 deletions apps/functions/githubWebhook/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import {handlePullRequestEvent} from './pull-request.js';
import {handleStatusEvent} from './status.js';
import {LabelEvent, StatusEvent} from '@octokit/webhooks-types';
import {handleLabelEvent} from './label.js';

admin.initializeApp({...functions.firebaseConfig()});

/**
* Firebase function to handle all incoming webhooks from Github. This function checks the incoming
* webhook to ensure it is a valid request from Github, and then delegates processing of a payload
Expand Down
6 changes: 5 additions & 1 deletion apps/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export {githubWebhook} from './githubWebhook/index.js';
export * from './githubWebhook/index.js';
export * from './ng-dev/index.js';
import * as admin from 'firebase-admin';

admin.initializeApp();
29 changes: 29 additions & 0 deletions apps/functions/ng-dev/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
load("//tools:defaults.bzl", "ts_library")

package(default_visibility = ["//visibility:private"])

ts_library(
name = "ng-dev",
srcs = [
"index.ts",
],
visibility = [
"//apps/functions:__pkg__",
],
deps = [
":lib",
],
josephperrott marked this conversation as resolved.
Show resolved Hide resolved
)

ts_library(
name = "lib",
srcs = [
"ng-dev-token.ts",
],
deps = [
"//apps/shared/models:server",
"@npm//@octokit/webhooks-types",
"@npm//firebase-admin",
"@npm//firebase-functions",
],
)
1 change: 1 addition & 0 deletions apps/functions/ng-dev/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {ngDevRevokeToken, ngDevTokenRequest, ngDevTokenValidate} from './ng-dev-token.js';
55 changes: 55 additions & 0 deletions apps/functions/ng-dev/ng-dev-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import {CallableContext} from 'firebase-functions/lib/providers/https';

/**
* Request a short lived ng-dev token. If granted, we rely on session cookies as this token. The token
* is to be used for all requests to the ng-dev service.
*/
export const ngDevTokenRequest = functions.https.onCall(
async ({idToken}: {idToken: string}, context: CallableContext) => {
if (!context.auth) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError(
'unauthenticated',
'Requesting an ng-dev token requires authentication',
);
}
const {auth_time} = await admin.auth().verifyIdToken(idToken, /* checkRevoked */ true);

// Only allow creation if the user signed in in the last minute. We use one minute, as this
// token should be immediately requested upon login, rather than using a long lived session.
if (new Date().getTime() / 1000 - auth_time > 1 * 60) {
throw new functions.https.HttpsError(
'permission-denied',
'ng-dev tokens must be requested within one minute of verifying login',
);
}

return admin.auth().createSessionCookie(idToken, {expiresIn: /* 20 Hours in ms */ 72000000});
},
);

/**
* Validate the provided token is still valid.
*/
export const ngDevTokenValidate = functions.https.onCall(validateToken);

/**
* Revokes the all tokens for the user of the provided token.
*/
export const ngDevRevokeToken = functions.https.onCall(async ({token}: {token: string}) => {
await admin
.auth()
.verifySessionCookie(token)
.then((decodedToken: admin.auth.DecodedIdToken) => {
return admin.auth().revokeRefreshTokens(decodedToken.uid);
});
});

/**
* Verify a ng-dev token is still valid.
*/
async function validateToken({token}: {token: string}) {
return !!(token && (await admin.auth().verifySessionCookie(token, /* checkRevoked */ true)));
}
2 changes: 1 addition & 1 deletion apps/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"engines": {
"node": "16"
},
"main": "functions_compiled/index.js",
"main": "functions_compiled/index.mjs",
"type": "module",
"private": true
}
3,606 changes: 240 additions & 3,366 deletions github-actions/commit-message-based-labels/main.js

Large diffs are not rendered by default.

3,606 changes: 240 additions & 3,366 deletions github-actions/feature-request/main.js

Large diffs are not rendered by default.

3,606 changes: 240 additions & 3,366 deletions github-actions/lock-closed/main.js

Large diffs are not rendered by default.

3,606 changes: 240 additions & 3,366 deletions github-actions/org-file-sync/main.js

Large diffs are not rendered by default.

3,606 changes: 240 additions & 3,366 deletions github-actions/post-approval-changes/main.js

Large diffs are not rendered by default.

3,625 changes: 242 additions & 3,383 deletions github-actions/slash-commands/main.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ng-dev/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ts_library(
"//ng-dev:__subpackages__",
],
deps = [
"//ng-dev/auth",
"//ng-dev/caretaker",
"//ng-dev/ci",
"//ng-dev/commit-message",
Expand Down
16 changes: 16 additions & 0 deletions ng-dev/auth/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "auth",
srcs = [
"cli.ts",
],
visibility = ["//ng-dev:__subpackages__"],
deps = [
"//ng-dev/auth/login",
"//ng-dev/auth/logout",
"//ng-dev/auth/shared",
"@npm//@types/yargs",
"@npm//firebase",
],
)
16 changes: 16 additions & 0 deletions ng-dev/auth/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Argv} from 'yargs';
import {LoginModule} from './login/cli.js';
import {LogoutModule} from './logout/cli.js';

/** CLI command module. */
export function buildAuthParser(yargs: Argv) {
return yargs.command(LoginModule).command(LogoutModule);
}
18 changes: 18 additions & 0 deletions ng-dev/auth/login/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "login",
srcs = [
"cli.ts",
],
visibility = ["//ng-dev/auth:__pkg__"],
deps = [
"//ng-dev/auth/shared",
"//ng-dev/utils",
"//ng-dev/utils:ng-dev-service",
"@npm//@types/node",
"@npm//@types/yargs",
"@npm//firebase",
"@npm//yargs",
],
)
40 changes: 40 additions & 0 deletions ng-dev/auth/login/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Argv, CommandModule} from 'yargs';
import {bold, Log} from '../../utils/logging.js';
import {loginToFirebase} from '../shared/firebase.js';
import {requestNgDevToken, getCurrentUser} from '../shared/ng-dev-token.js';
import {requiresNgDevService} from '../../utils/ng-dev-service.js';

export interface Options {}

/** Builds the command. */
function builder(yargs: Argv) {
return requiresNgDevService(yargs) as Argv;
}

/** Handles the command. */
async function handler() {
/** The currently logged in user email, if a user is logged in. */
const email = await getCurrentUser();
if (email) {
Log.info(`Already logged in as ${bold(email)}`);
return;
}

if (await loginToFirebase()) {
await requestNgDevToken();

const expireTimestamp = new Date(Date.now() + 1000 * 60 * 60 * 20).toISOString();
Log.info(`Logged in as ${bold(await getCurrentUser())}`);
Log.info(`Credential will expire in ~20 hours (${expireTimestamp})`);
josephperrott marked this conversation as resolved.
Show resolved Hide resolved
} else {
Log.error('Login failed');
}
}

/** yargs command module for logging into the ng-dev service. */
export const LoginModule: CommandModule<{}, Options> = {
handler,
builder,
command: 'login',
describe: 'Log into the ng-dev service',
};
16 changes: 16 additions & 0 deletions ng-dev/auth/logout/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "logout",
srcs = [
"cli.ts",
],
visibility = ["//ng-dev/auth:__pkg__"],
deps = [
"//ng-dev/auth/shared",
"//ng-dev/utils",
"//ng-dev/utils:ng-dev-service",
"@npm//@types/yargs",
"@npm//yargs",
],
)
31 changes: 31 additions & 0 deletions ng-dev/auth/logout/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Argv, CommandModule} from 'yargs';
import {bold, Log} from '../../utils/logging.js';
import {invokeServerFunction, getCurrentUser} from '../shared/ng-dev-token.js';
import {requiresNgDevService} from '../../utils/ng-dev-service.js';

export interface Options {}

/** Builds the command. */
function builder(yargs: Argv) {
return requiresNgDevService(yargs);
}

/** Handles the command. */
async function handler() {
/** The currently logged in user email, if a user is logged in. */
const email = await getCurrentUser();
if (email) {
await invokeServerFunction<{}, void>('ngDevRevokeToken');
Log.info(`Successfully logged out, ${bold(email)}.`);
return;
}
Log.info('No user currently logged in.');
}

/** yargs command module for logging out of the ng-dev service. */
export const LogoutModule: CommandModule<{}, Options> = {
handler,
builder,
command: 'logout',
describe: 'Log out of the ng-dev service',
};
19 changes: 19 additions & 0 deletions ng-dev/auth/shared/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "shared",
srcs = glob(["*.ts"]),
visibility = [
"//ng-dev/auth:__subpackages__",
"//ng-dev/utils:__pkg__",
],
deps = [
"//ng-dev/utils",
"@npm//@openid/appauth",
"@npm//@types/node",
"@npm//@types/opener",
"@npm//firebase",
"@npm//node-fetch",
"@npm//opener",
],
)
46 changes: 46 additions & 0 deletions ng-dev/auth/shared/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
GoogleAuthProvider,
getAuth,
signInWithCredential,
linkWithCredential,
GithubAuthProvider,
} from 'firebase/auth';
import {Log} from '../../utils/logging.js';
import {
deviceCodeOAuthDance,
authorizationCodeOAuthDance,
GithubOAuthDanceConfig,
GoogleOAuthDanceConfig,
} from './oauth.js';

export async function loginToFirebase() {
/** The type of OAuth dance to do based on whether a session display is available. */
const oAuthDance = process.env.DISPLAY ? authorizationCodeOAuthDance : deviceCodeOAuthDance;
try {
/** The id and access tokens for Google from the oauth login. */
const {idToken, accessToken} = await oAuthDance(GoogleOAuthDanceConfig);
/** The credential generated by the GoogleAuthProvider from the OAuth tokens. */
const googleCredential = GoogleAuthProvider.credential(idToken, accessToken);
/** The newly signed in user. */
const {user} = await signInWithCredential(getAuth(), googleCredential);

// If the user already has a github account linked to their account, the login is complete.
if (user.providerData.find((provider) => provider.providerId === 'github.com')) {
return true;
}

/** The access token for Github from the oauth login. */
const {accessToken: githubAccessToken} = await oAuthDance(GithubOAuthDanceConfig);

// Link the Github account to the account for the currently logged in user.
await linkWithCredential(user, GithubAuthProvider.credential(githubAccessToken));
return true;
} catch (e) {
if (e instanceof Error) {
Log.error(`${e.name}: ${e.message}`);
} else {
Log.error(e);
}
return false;
}
}