Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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=""
31 changes: 21 additions & 10 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 proejct or use the emulators:
Comment thread
penx marked this conversation as resolved.
Outdated

### 1. Run against a Firebase Project
Comment thread
penx marked this conversation as resolved.
Outdated

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).
Comment thread
penx marked this conversation as resolved.
Outdated
2. Enable Auth (with email) and Firestore.
Comment thread
penx marked this conversation as resolved.
Outdated
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.
Comment thread
penx marked this conversation as resolved.
Outdated
6. Set SERVICE_ACCOUNT to the contents of your service account's private key JSON file:
Comment thread
penx marked this conversation as resolved.
Outdated
- 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,18 @@ 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 add `http://localhost:5002/auth/google`, `http://localhost:3000/auth/google` and `https://<projectid>.firebaseapp.com/auth/google` as authorised redirects.

## 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
Copy Markdown
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
Copy Markdown
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
33 changes: 33 additions & 0 deletions firebase/app/routes/auth/google.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 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: "http://localhost:3000/auth/google",
Comment thread
penx marked this conversation as resolved.
Outdated
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),
},
});
};
18 changes: 17 additions & 1 deletion firebase/app/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export const loader = async ({ request }: LoaderArgs) => {
return redirect("/", { headers });
}
const { apiKey, domain } = getRestConfig();
return json({ apiKey, domain }, { headers });
return json(
{ apiKey, domain, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID },
{ headers }
);
};

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

const { GOOGLE_CLIENT_ID } = restConfig;

const handleSubmit = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
Expand Down Expand Up @@ -113,6 +118,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=http://localhost:3000/auth/google\
Comment thread
penx marked this conversation as resolved.
Outdated
&scope=openid%20email%20profile`}
>
Login with Google
</a>
</p>
Comment thread
penx marked this conversation as resolved.
<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