Skip to content

Commit

Permalink
add gcloud and email authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
j-mendez committed Jun 27, 2021
1 parent e062c17 commit 361231e
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 32 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ jobs:
with:
deno-version: "1.10.3"
- name: Run test
run: FIREBASE_PROJECT_ID=$FIREBASE_PROJECT_ID FIREBASE_TOKEN=$FIREBASE_TOKEN deno test --allow-net=firestore.googleapis.com --allow-env --allow-read -A
run:
FIREBASE_PROJECT_ID=$FIREBASE_PROJECT_ID FIREBASE_TOKEN=$FIREBASE_TOKEN FIREBASE_PROJECT_KEY=$FIREBASE_PROJECT_KEY
FIREBASE_AUTH_EMAIL=$FIREBASE_AUTH_EMAIL FIREBASE_AUTH_PASSWORD=$FIREBASE_AUTH_PASSWORD
deno test --unstable --allow-run --allow-net=firestore.googleapis.com,identitytoolkit.googleapis.com --allow-env --allow-read --allow-write -A
env:
FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
FIREBASE_AUTH_EMAIL: ${{ secrets.FIREBASE_AUTH_EMAIL }}
FIREBASE_AUTH_PASSWORD: ${{ secrets.FIREBASE_AUTH_PASSWORD }}
FIREBASE_PROJECT_KEY: ${{ secrets.FIREBASE_PROJECT_KEY }}
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.vscode
.DS_Store
.env
GoogleService-Info.plist
GoogleService-Info.plist

# self token
firebase_auth_token.json
71 changes: 63 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,89 @@
# dfirestore

a deno Firebase Firestore client #WIP do not use in production
a deno Firebase Firestore REST client

## Usage

To get started with the package you can either setup the application with your tokens or authenticate with one of the helpers.
After you authenticate you can freely use the REST client and your access tokens will rotate before they expire.

### Configuration

All configuration settings are optional if you are passing the options per request.

```typescript
import {
setDatabase,
setToken,
setProjectID,
firestore,
setTokenFromServiceAccount,
setTokenFromEmailPassword,
} from "https://deno.land/dfirestore/mod.ts";

// set firebase db
setDatabase("(default)");
/*
* CONFIGURATION: Add authentication token for all request. Use one of the `setToken` methods below
*/

// set firebase project
// If GoogleService-Info.plist and gcloud installed on machine run to get service token
setTokenFromServiceAccount();
// If Email and Password secret shared. optional params if using env variables
setTokenFromEmailPassword(
{
email: "someemail@something.com",
password: "something",
},
true // background refresh token before expiration
);
// Manually set authentication
setToken("FIREBASE_AUTHORIZATION_TOKEN");

// set db
setDatabase("(default)");
// set project
setProjectID("myprojectid");
```

// set authorization
setToken("FIREBASE_AUTHORIZATION_TOKEN");
### Main

Use the REST client below via the following methods to perform CRUD operations.

await firestore.getDocument("document/id");
```typescript
import { firestore } from "https://deno.land/dfirestore/mod.ts";

await firestore.getDocument({
collection: "mycollection",
id: "collection id",
});
await firestore.getDocumentList("document");
await firestore.deleteDocument("document/id");
await firestore.updateDocument("document/id");
```

## ENV variables

If you have a .env file the below env variables will be picked up. If you not you need to make sure you pass in the required params.

### Project

```
FIREBASE_DATABASE=
FIREBASE_PROJECT_ID=
```

### IAM admin auth - via email, password

```
FIREBASE_PROJECT_KEY=
FIREBASE_AUTH_EMAIL=
FIREBASE_AUTH_PASSWORD=
```

### Explicite authenticated user

```
FIREBASE_TOKEN=
```

### CI

CI=false
10 changes: 5 additions & 5 deletions client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ const client = {

requestHeaders.set("Content-Type", "application/json");

if (authorization ?? config?.token) {
requestHeaders.set(
"Authorization",
`Bearer ${authorization ?? config.token}`
);
const token =
typeof authorization !== "undefined" ? authorization : config.token;

if (token) {
requestHeaders.set("Authorization", `Bearer ${token}`);
}

const req = await fetch(
Expand Down
140 changes: 137 additions & 3 deletions config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
const FIREBASE_TOKEN = "FIREBASE_TOKEN";
const FIREBASE_DATABASE = "FIREBASE_DATABASE";
const FIREBASE_PROJECT_ID = "FIREBASE_PROJECT_ID";
const FIREBASE_PROJECT_KEY = "FIREBASE_PROJECT_KEY";

const projectID = Deno.env.get(FIREBASE_PROJECT_ID) ?? "";
export const projectID = Deno.env.get(FIREBASE_PROJECT_ID) ?? "";
export const projectkey = Deno.env.get(FIREBASE_PROJECT_KEY) ?? "";

let backgroundRefetchStarted = false;

const config = {
firebaseDb: Deno.env.get("FIREBASE_DATABASE") ?? "(default)",
token: Deno.env.get(FIREBASE_TOKEN),
host: `https://firestore.googleapis.com/v1/projects/${projectID}`,
get token() {
return this.storedToken?.id_token ?? Deno.env.get(FIREBASE_TOKEN);
},
get storedToken() {
try {
const file = Deno.readTextFileSync("./firebase_auth_token.json");

return file ? JSON.parse(file) : null;
} catch (e) {
console.error(e);
}
},
};

const setProjectID = (id: string) => {
Expand All @@ -16,10 +31,129 @@ const setProjectID = (id: string) => {

const setToken = (token: string) => {
Deno.env.set(FIREBASE_TOKEN, token);
Deno.writeTextFileSync(
"./firebase_auth_token.json",
JSON.stringify({ id_token: token })
);
};

const setDatabase = (db: string) => {
Deno.env.set(FIREBASE_DATABASE, db);
};

export { config, setToken, setDatabase, setProjectID };
/*
* Login with your IAM account to establish user.
* Required GoogleService-Info.plist
*/
const setTokenFromServiceAccount = async () => {
const p = Deno.run({
cmd: ["gcloud", "auth", "application-default", "print-access-token"],
stdout: "piped",
stderr: "piped",
stdin: "piped",
});

const output = new TextDecoder().decode(await p.output());

await p.close();

const token = String(output).replace(/\\n/gm, "\n").replace("\n", "");

setToken(token);

return token;
};

/*
* Login with your email and password to establish iam user
*/
const setTokenFromEmailPassword = async (
params?: {
email?: string;
password?: string;
refreshToken?: string;
},
refresh: boolean = false
) => {
const { email, refreshToken, password } = params ?? {};

let baseUrl = "";
let body = {};

if (typeof refreshToken !== "undefined") {
baseUrl = "securetoken.googleapis.com/v1/token";
body = {
refresh_token: refreshToken,
grant_type: "refresh_token",
};
} else {
baseUrl = "identitytoolkit.googleapis.com/v1/accounts:signInWithPassword";
body = {
email: email ?? Deno.env.get("FIREBASE_AUTH_EMAIL"),
password: password ?? Deno.env.get("FIREBASE_AUTH_PASSWORD"),
returnSecureToken: true,
};
}

const firebase = await fetch(`https://${baseUrl}?key=${projectkey}`, {
headers: {
contentType: "application/json",
},
method: "POST",
body: JSON.stringify(body),
});

const json = await firebase.json();

const token = json?.idToken;

token && setToken(token);

if (refresh) {
setRefetchBeforeExp({
expiresIn: json.expiresIn,
refreshToken: json.refreshToken,
});
}

return token;
};

type Token = {
expiresIn: number;
refreshToken: string;
};

// TODO: GET PID ACCESS TO VAR FOR HARD STOP
const setRefetchBeforeExp = ({ expiresIn, refreshToken }: Token) => {
const expMS = (expiresIn / 60) * 60000;

if (!backgroundRefetchStarted) {
Deno.run({
cmd: [
"deno",
"run",
"--allow-read",
"--allow-env",
"--unstable",
"--allow-net",
"--allow-run",
"--allow-write",
"./refresh.ts",
String(expMS),
refreshToken,
],
});
}
backgroundRefetchStarted = true;
};

export {
config,
setToken,
setDatabase,
setProjectID,
setRefetchBeforeExp,
setTokenFromServiceAccount,
setTokenFromEmailPassword,
};
56 changes: 54 additions & 2 deletions firestore.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { assertEquals } from "./test_deps.ts";
import { firestore } from "./firestore.ts";
import { setToken } from "./config.ts";
import {
config,
setToken,
setTokenFromServiceAccount,
setTokenFromEmailPassword,
} from "./config.ts";

const body = {
collection: "users",
id: "L0xO1Yri80WlrFSw6KxqccHhKhv2",
};

const t = await setTokenFromEmailPassword();

Deno.test({
name: "firestore should run",
fn: async () => {
const d = await firestore.getDocument(body);
const d = await firestore.getDocument({ ...body });
assertEquals(d.fields.firstname.stringValue, "Jeff");
},
sanitizeResources: false,
sanitizeOps: false,
});

Deno.test({
Expand All @@ -30,4 +39,47 @@ Deno.test({
},
});
},
sanitizeResources: false,
sanitizeOps: false,
});

Deno.test({
name: "firestore should invalidate admin account and error",
fn: async () => {
setToken("");
const d = await firestore.getDocument({ ...body, authorization: false });
assertEquals(d, {
error: {
code: 403,
message: "Missing or insufficient permissions.",
status: "PERMISSION_DENIED",
},
});
},
});

Deno.test({
name: "firestore should pass in token and fetch",
fn: async () => {
setToken("");
const d = await firestore.getDocument({ ...body, authorization: t });
assertEquals(d.fields.firstname.stringValue, "Jeff");
},
});

Deno.test({
name: "firestore should get token from service account",
fn: async () => {
if (Boolean(Deno.env.get("CI")) === true) {
const d = await setTokenFromEmailPassword();
const v = d.slice(0, 4);
assertEquals(v, "eyJh");
} else {
const d = await setTokenFromServiceAccount();
const v = d.slice(0, 4);
assertEquals(v, "ya29");
}
},
sanitizeResources: false,
sanitizeOps: false,
});
Loading

0 comments on commit 361231e

Please sign in to comment.