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

Implement Firebase Token Rotation #32

Merged
merged 10 commits into from
Mar 29, 2024
Merged
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
2 changes: 1 addition & 1 deletion backend/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "0.3.0"
VERSION = "0.3.1"
4 changes: 2 additions & 2 deletions frontend/gatsby-browser.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "antd/dist/reset.css";
import "./src/styles/global.css";

import AuthProvider from "./src/hooks/provider";
import Provider from "./src/hooks/provider";

export const wrapRootElement = AuthProvider;
export const wrapRootElement = Provider;
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ai-in-hand-platform-ui",
"version": "0.3.0",
"version": "0.3.1",
"private": true,
"description": "AI in Hand Platform - Build and Test Your AI Workforce",
"author": "AI in Hand Team",
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {SetEmail} from "../store/actions/usersActions";

const Login = () => {
const dispatch = useDispatch();
const signInVerifyUrl = `${window.location.origin}/sign-in-verify`;
const [loading, setLoading] = useState(false);
function handleRegister(data: {email: string}) {
const auth = getAuth();
sendSignInLinkToEmail (auth, data.email, {handleCodeInApp: true, url: 'https://platform.ainhand.com/sign-in-verify'}).then((res) => {
sendSignInLinkToEmail (auth, data.email, {handleCodeInApp: true, url: signInVerifyUrl}).then((res) => {
dispatch(SetEmail(data.email))
message.success("Please check your email for your login link");
}).catch((error) => {
Expand Down
23 changes: 17 additions & 6 deletions frontend/src/components/register.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {useEffect} from 'react';
import { message } from "antd";
import {getAuth, signInWithEmailLink} from "firebase/auth";
import { navigate} from "gatsby";
import {useDispatch, useSelector} from "react-redux";
Expand All @@ -11,13 +12,23 @@ const LogInVerify = () => {
function handleLogin(email: string) {
const auth = getAuth();
// @ts-ignore
signInWithEmailLink (auth, email, location.href).then((res) => {
signInWithEmailLink(auth, email, location.href)
.then((res) => {
// @ts-ignore
dispatch(SignIn({token: res.user.accessToken, user: {email: res.user.email, uid: res.user.uid}}))
navigate('/')
}).catch((error) => {
console.log(error.message)
});
const expiresIn = Date.now() + (60 * 60 - 1) * 1000; // 1 hour from now, minus 1 second
dispatch(
SignIn({
token: res.user.accessToken,
expiresIn,
user: { email: res.user.email, uid: res.user.uid },
})
);
navigate('/');
})
.catch((error) => {
console.log(error.message);
message.error("Error logging in. Please try again.");
});
}
useEffect(() => {
handleLogin(email);
Expand Down
71 changes: 62 additions & 9 deletions frontend/src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
ISkill,
IStatus,
} from "./types";
import {store} from "../store";
import { message } from "antd";
import { getAuth, getIdToken } from "firebase/auth";
import { store } from "../store";
import { RefreshToken } from "../store/actions/usersActions";

export const getServerUrl = () => {
return process.env.GATSBY_API_URL || "/v1/api";
Expand Down Expand Up @@ -63,15 +66,60 @@ export function getLocalStorage(name: string, stringify: boolean = true): any {
}
}

export function checkAndRefreshToken() {
return new Promise((resolve) => {
const state = store.getState();
const { expiresIn } = state.user;

if (Date.now() >= expiresIn) {
const auth = getAuth();
const user = auth.currentUser;
if (user) {
user.getIdToken(true).then((res) => {
const expiresIn = Date.now() + (60 * 60 - 1) * 1000; // 1 hour from now, minus 1 second
store.dispatch(
RefreshToken({
token: res,
expiresIn,
})
);
resolve(true);
}).catch(error => {
console.error("Error refreshing token:", error);
message.error("Error refreshing token. Please login again.");
resolve(false);
});
} else {
resolve(false);
}
} else {
resolve(true); // Token is still valid
}
});
}

export function fetchJSON(
url: string | URL,
payload: any = {},
onSuccess: (data: any) => void,
onError: (error: IStatus) => void
) {
// @ts-ignore
const accessToken = store.getState().user.accessToken;
return fetch(url, {method: payload.method, headers: {...payload.headers, "Authorization": `Bearer ${accessToken}`}})
checkAndRefreshToken().then((canProceed) => {
if (!canProceed) {
console.error("Cannot proceed, token invalid or not refreshed.");
onError({
status: false,
message: "Authentication error: please login again.",
});
return;
}

const accessToken = store.getState().user.accessToken;
fetch(url, {
method: payload.method,
headers: { ...payload.headers, Authorization: `Bearer ${accessToken}` },
body: payload.body,
})
.then(function (response) {
if (response.status !== 200) {
console.log(
Expand All @@ -83,14 +131,11 @@ export function fetchJSON(
});
onError({
status: false,
message:
"Connection error " + response.status + " " + response.statusText,
message: "Connection error " + response.status + " " + response.statusText,
});
return;
}
return response.json().then(function (data) {
onSuccess(data);
});
response.json().then(onSuccess);
})
.catch(function (err) {
console.log("Fetch Error :-S", err);
Expand All @@ -99,7 +144,15 @@ export function fetchJSON(
message: `There was an error connecting to server. (${err}) `,
});
});
}).catch((error) => {
console.error("Error in checkAndRefreshToken:", error);
onError({
status: false,
message: "Error in token refresh process.",
});
});
}

export const capitalize = (s: string) => {
if (typeof s !== "string") return "";
return s.charAt(0).toUpperCase() + s.slice(1);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/sign-in/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Login from "../../components/login";

const LoginForm = ({ data }: any) => {
return (
<Layout meta={data.site.siteMetadata} title="Sign-In" link={'sing-in'}>
<Layout meta={data.site.siteMetadata} title="Sign-In" link={'sign-in'}>
<Login/>
</Layout>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/store/actions/usersActions/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export const SIGN_IN = "SIGN_IN";
export const SIGN_UP = "SIGN_UP";
export const SET_EMAIL = "SET_EMAIL";
export const RESET_STATE = "RESET_STATE";
export const REFRESH_TOKEN = "REFRESH_TOKEN";
10 changes: 8 additions & 2 deletions frontend/src/store/actions/usersActions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as usersActions from "./constants";


export function SignIn(data: {token: string, user: any}) {
export function SignIn(data: {token: string, expiresIn: number, user: any}) {
return {
type: usersActions.SIGN_IN,
payload: data,
};
}
export function SignUp(data: {token: string, user: any}) {
export function SignUp(data: {token: string, expiresIn: number, user: any}) {
return {
type: usersActions.SIGN_UP,
payload: data,
Expand All @@ -24,3 +24,9 @@ export function ResetState() {
type: usersActions.RESET_STATE,
};
}
export function RefreshToken(data: {token: string, expiresIn: number, user: any}) {
return {
type: usersActions.REFRESH_TOKEN,
payload: data,
};
}
3 changes: 3 additions & 0 deletions frontend/src/store/reducers/userReducer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as usersActions from "../../actions/usersActions/constants";
const initialState = {
loggedIn: false,
accessToken: null,
expiresIn: null,
user: {},
email: ""
};
Expand All @@ -11,10 +12,12 @@ export default (state = initialState, action: any) => {
switch (action.type) {
case usersActions.SIGN_IN:
case usersActions.SIGN_UP:
case usersActions.REFRESH_TOKEN:
return {
...state,
loggedIn: true,
accessToken: action.payload.token,
expiresIn: action.payload.expiresIn,
user: action.payload?.user ?? {},
};
case usersActions.SET_EMAIL:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ai-in-hand",
"version": "0.3.0",
"version": "0.3.1",
"description": "Root package.json for Heroku deployment",
"private": true,
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "ai-in-hand"
version = "0.3.0"
version = "0.3.1"
description = "A FastAPI app to manage swarm agency configurations."
authors = [
"AI in Hand <[email protected]>"
Expand Down
Loading