Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions firebase/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ SERVICE_ACCOUNT={"type":"service_account","project_id": "your-project-id","priva

# Set the API_KEY value to the Web API Key, which can be obtained on the project settings page in your Firebase admin console
API_KEY="abcdefg_1234567890"

GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
38 changes: 27 additions & 11 deletions firebase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ See the screen recording at `./screen_recording.gif` or Open this example on [Co

## Running locally

To run it, you need to either:
To run this example, you either need to create a Firebase project or use [the emulators](https://firebase.google.com/docs/emulator-suite):

### 1. Run against a Firebase Project
### 1. Run against a Firebase project

1. [Create a Firebase Project](https://console.firebase.google.com)
2. Enable Auth (with email) and Firestore
3. Add a Web App
4. Get the [admin-sdk](https://firebase.google.com/docs/admin/setup#initialize-sdk) and [Web API Key](https://firebase.google.com/docs/reference/rest/auth)
5. Save them to SERVICE_ACCOUNT and API_KEY in the `.env`-file
1. [Create a Firebase project](https://console.firebase.google.com).
2. Enable [Auth](https://firebase.google.com/docs/auth) (with email) and [Cloud Firestore](https://firebase.google.com/docs/firestore).
3. Add a Web App with hosting.
4. Copy `.env.example` to `.env`.
5. Set the `API_KEY` value to the [Web API Key](https://firebase.google.com/docs/reference/rest/auth), which can be obtained on the project settings page in your Firebase admin console.
6. Set `SERVICE_ACCOUNT` to the contents of your service account's private key JSON file:
- Go to: Project > Project Settings > Service Accounts.
- Click "Create Service Account" or "Generate New Private Key" to download the JSON file.

### 2. Use the Firebase emulators

Expand All @@ -34,10 +37,23 @@ When you run `npm run emulators`, an initial user is created with credentials `u
## Deploying

1. Follow the "Run against a Firebase Project" steps above if not done already
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Follow the "Run against a Firebase Project" steps above if not done already
1. Follow the "Run against a Firebase project" steps above if not done already

2. Install the Firebase CLI with `npm i -g firebase-tools`
3. Log in to the CLI with `firebase login`
4. Run `firebase use --add` and choose the Firebase project you want to deploy to
5. Deploy with `firebase deploy`
2. Log in to the CLI with `npm run firebase -- login`
3. Run `npm run firebase -- use --add` and choose the Firebase project you want to deploy to
4. Deploy with `npm run firebase -- deploy`

## Integration with Google Sign-in Provider

The "Login with Google" link will only work when running against a Firebase Project.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The "Login with Google" link will only work when running against a Firebase Project.
The "Login with Google" link will only work when running against a Firebase project.


After the steps in "Run against a Firebase Project" have been completed:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
After the steps in "Run against a Firebase Project" have been completed:
After the steps in "Run against a Firebase project" have been completed:


- In the [Firebase Console](https://console.firebase.google.com), navigate to Authentication > Sign-in method > Add new provider > Google. Make a note of the client ID and secret and add them to the .env file as GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- In the [Firebase Console](https://console.firebase.google.com), navigate to Authentication > Sign-in method > Add new provider > Google. Make a note of the client ID and secret and add them to the .env file as GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
- In the [Firebase Console](https://console.firebase.google.com), navigate to Authentication > Sign-in method > Add new provider > Google. Make a note of the client ID and secret and add them to the `.env` file as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`.

- In the [Google Cloud Credentials Console](https://console.cloud.google.com/apis/credentials), select the Web client (under OAuth 2.0 Client IDs) and the following as authorised redirects:
- `http://localhost:3000/auth/google`
- `http://localhost:5002/auth/google`
- `http://127.0.0.1:3000/auth/google`
- `http://127.0.0.1:5002/auth/google`
- `https://<projectid>.firebaseapp.com/auth/google`

## Details

Expand Down
111 changes: 108 additions & 3 deletions firebase/app/firebase-rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,114 @@ export const signInWithPassword = async (
}
) => {
const response: SignInWithPasswordResponse = await fetch(
`${restConfig!.domain}/v1/accounts:signInWithPassword?key=${
restConfig!.apiKey
}`,
`${restConfig.domain}/v1/accounts:signInWithPassword?key=${restConfig.apiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
return response.json();
};

// https://firebase.google.com/docs/reference/rest/auth#section-sign-in-email-password
interface SignInWithIdpResponse extends Response {
json(): Promise<
| RestError
| {
/**
* The unique ID identifies the IdP account.
*/
federatedId: string;
/**
* The linked provider ID (e.g. "google.com" for the Google provider).
*/
providerId: string;
/**
* The uid of the authenticated user.
*/
localId: string;
/**
* Whether the sign-in email is verified.
*/
emailVerified: boolean;
/**
* The email of the account.
*/
email: string;
/**
* The OIDC id token if available.
*/
oauthIdToken: string;
/**
* The OAuth access token if available.
*/
oauthAccessToken: string;
/**
* The OAuth 1.0 token secret if available.
*/
oauthTokenSecret: string;
/**
* The stringified JSON response containing all the IdP data corresponding to the provided OAuth credential.
*/
rawUserInfo: string;
/**
* The first name for the account.
*/
firstName: string;
/**
* The last name for the account.
*/
lastName: string;
/**
* The full name for the account.
*/
fullName: string;
/**
* The display name for the account.
*/
displayName: string;
/**
* The photo Url for the account.
*/
photoUrl: string;
/**
* A Firebase Auth ID token for the authenticated user.
*/
idToken: string;
/**
* A Firebase Auth refresh token for the authenticated user.
*/
refreshToken: string;
/**
* The number of seconds in which the ID token expires.
*/
expiresIn: string;
/**
* Whether another account with the same credential already exists. The user will need to sign in to the original account and then link the current credential to it.
*/
needConfirmation: boolean;
}
>;
}
export const signInWithIdp = async (
idToken: string,
providerId: string,
restConfig: {
apiKey: string;
domain: string;
}
) => {
const body = {
postBody: "id_token=" + idToken + "&providerId=" + providerId,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
postBody: "id_token=" + idToken + "&providerId=" + providerId,
postBody: `id_token=${idToken}&providerId=${providerId}`,

requestUri: "http://localhost",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@penx Should this stay localhost? 🤔

returnIdpCredential: true,
returnSecureToken: true,
};
const response: SignInWithIdpResponse = await fetch(
`${restConfig.domain}/v1/accounts:signInWithIdp?key=${restConfig.apiKey}`,
{
method: "POST",
headers: {
Expand Down
43 changes: 43 additions & 0 deletions firebase/app/routes/auth/google.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { LoaderArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";

import { signInWithIdp } from "~/server/auth.server";
import { commitSession, getSession } from "~/sessions";

export const loader = async ({ request }: LoaderArgs) => {
// https://developers.google.com/identity/protocols/oauth2/openid-connect
const url = new URL(request.url);
const code = url.searchParams.get("code");

const host =
request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");
if (!host) {
throw new Error("Could not determine domain URL.");
}
const protocol =
host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
const redirectUri = `${protocol}://${host}/auth/google`;

const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
code,
redirect_uri: redirectUri,
grant_type: "authorization_code",
}),
});
const token = await response.json();
const sessionCookie = await signInWithIdp(token.id_token, "google.com");
const session = await getSession(request.headers.get("cookie"));
session.set("session", sessionCookie);
return redirect("/", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
};
32 changes: 31 additions & 1 deletion firebase/app/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Link,
useActionData,
useLoaderData,
useLocation,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
useLocation,

useSubmit,
} from "@remix-run/react";
import { useCallback, useState } from "react";
Expand All @@ -27,7 +28,23 @@ export const loader = async ({ request }: LoaderArgs) => {
return redirect("/", { headers });
}
const { apiKey, domain } = getRestConfig();
return json({ apiKey, domain }, { headers });
const host =
request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");
if (!host) {
throw new Error("Could not determine domain URL.");
}
const protocol =
host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
const redirectUri = `${protocol}://${host}/auth/google`;
return json(
{
apiKey,
domain,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
redirectUri,
},
{ headers }
);
};

type ActionData = {
Expand Down Expand Up @@ -70,6 +87,8 @@ export default function Login() {
const restConfig = useLoaderData<typeof loader>();
const submit = useSubmit();

const { GOOGLE_CLIENT_ID, redirectUri } = restConfig;

const handleSubmit = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
Expand Down Expand Up @@ -113,6 +132,17 @@ export default function Login() {
Login
</button>
</form>
<p>
<a
href={`https://accounts.google.com/o/oauth2/v2/auth\
?response_type=code\
&client_id=${GOOGLE_CLIENT_ID}\
&redirect_uri=${redirectUri}\
&scope=openid%20email%20profile`}
>
Login with Google
</a>
</p>
<p>
Do you want to <Link to="/join">join</Link>?
</p>
Expand Down
5 changes: 5 additions & 0 deletions firebase/app/server/auth.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export const signInWithToken = async (idToken: string) => {
return sessionCookie;
};

export const signInWithIdp = async (token: string, providerId: string) => {
const { idToken } = await auth.signInWithIdp(token, providerId);
return signInWithToken(idToken);
};

export const signUp = async (name: string, email: string, password: string) => {
await auth.server.createUser({
email,
Expand Down
15 changes: 15 additions & 0 deletions firebase/app/server/firebase.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,22 @@ const signInWithPassword = async (email: string, password: string) => {
return signInResponse;
};

const signInWithIdp = async (token: string, providerId: string) => {
const signInResponse = await firebaseRest.signInWithIdp(
token,
providerId,
restConfig
);

if (firebaseRest.isError(signInResponse)) {
throw new Error(signInResponse.error.message);
}

return signInResponse;
};

export const auth = {
server: getServerAuth(),
signInWithPassword,
signInWithIdp,
};
1 change: 1 addition & 0 deletions firebase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"build": "remix build",
"dev": "remix dev",
"emulators": "firebase emulators:start --project remix-emulator --import=firebase-fixtures",
"firebase": "firebase",
"start": "remix-serve build",
"typecheck": "tsc"
},
Expand Down