Skip to content

Commit

Permalink
feat: stronger security level for JWT verification with RSA keys (#21)
Browse files Browse the repository at this point in the history
* feat: stronger security level for JWT verification with RSA keys

* Update bundlesize.config.js

* more refactor
  • Loading branch information
aversini authored Jun 24, 2024
1 parent a135015 commit 0750b75
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 127 deletions.
2 changes: 1 addition & 1 deletion examples/implicit-flow/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const App: React.FC = () => {
variant="danger"
disabled={!isAuthenticated}
>
Logout (valid)
Logout
</Button>
</FlexgridItem>
</Flexgrid>
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-provider/bundlesize.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default {
*/
{
path: "dist/index.js",
limit: "4 kb",
limit: "9 kb",
},
],
};
6 changes: 2 additions & 4 deletions packages/auth-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"files": ["dist"],
"scripts": {
"build:check": "tsc",
"build:js": "vite build",
Expand Down Expand Up @@ -45,7 +43,7 @@
},
"dependencies": {
"@versini/auth-common": "workspace:../auth-common",
"@versini/ui-hooks": "3.0.0",
"@versini/ui-hooks": "4.0.0",
"jose": "5.4.1",
"uuid": "10.0.0"
}
Expand Down
14 changes: 14 additions & 0 deletions packages/auth-provider/src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
export const EXPIRED_SESSION =
"Oops! It looks like your session has expired. For your security, please log in again to continue.";
export const LOGOUT_SESSION = "Your session has been successfully terminated.";

export const AUTH_CONTEXT_ERROR =
"You forgot to wrap your component in <AuthProvider>.";

export const API_ENDPOINT = {
dev: "https://auth.gizmette.local.com:3003",
prod: "https://mylogin.gizmette.com",
};

export const LOCAL_STORAGE_PREFIX = "@@auth@@";

export const JWT_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsF6i3Jd9fY/3COqCw/m7
w5PKyTYLGAI2I6SIIdpe6i6DOCbEkmDz7LdVsBqwNtVi8gvWYIj+8ol6rU3qu1v5
i1Jd45GSK4kzkVdgCmQZbM5ak0KI99q5wsrAIzUd+LRJ2HRvWtr5IYdsIiXaQjle
aMwPFOIcJH+rKfFgNcHLcaS5syp7zU1ANwZ+trgR+DifBr8TLVkBynmNeTyhDm2+
l0haqjMk0UoNPPE8iYBWUHQJJE1Dqstj65d6Eh5g64Pao25y4cmYJbKjiblIGEkE
sjqybA9mARAqh9k/eiIopecWSiffNQTwVQVd2I9ZH3BalhEXHlqFgrjz51kFqg81
awIDAQAB
-----END PUBLIC KEY-----`;
21 changes: 8 additions & 13 deletions packages/auth-provider/src/common/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,23 @@ export type ServiceCallProps = {
params: any;
};

export type AuthState = {
isAuthenticated: boolean;
idToken: string;
logoutReason: string;
userId: string;
accessToken?: string;
refreshToken?: string;
};

export type AuthProviderProps = {
children: React.ReactNode;
sessionExpiration?: string;
clientId: string;
accessType?: string;
};

export type AuthContextProps = {
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
export type AuthState = {
isAuthenticated: boolean;
idToken?: string;
accessToken?: string;
refreshToken?: string;
idToken?: string;
logoutReason?: string;
userId?: string;
};

export type AuthContextProps = {
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
} & AuthState;
58 changes: 56 additions & 2 deletions packages/auth-provider/src/common/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { HEADERS } from "@versini/auth-common";
import { AUTH_TYPES, HEADERS, JWT } from "@versini/auth-common";
import * as jose from "jose";
import { v4 as uuidv4 } from "uuid";

import { API_ENDPOINT } from "./constants";
import { API_ENDPOINT, JWT_PUBLIC_KEY } from "./constants";
import type { ServiceCallProps } from "./types";

export const isProd = process.env.NODE_ENV === "production";
Expand Down Expand Up @@ -43,3 +44,56 @@ export const serviceCall = async ({ params = {} }: ServiceCallProps) => {
return { status: 500, data: [] };
}
};

export const verifyAndExtractToken = async (token: string) => {
try {
const alg = JWT.ALG;
const spki = JWT_PUBLIC_KEY;
const publicKey = await jose.importSPKI(spki, alg);
return await jose.jwtVerify(token, publicKey, {
issuer: JWT.ISSUER,
});
} catch (_error) {
return undefined;
}
};

export const authenticateUser = async ({
username,
password,
clientId,
sessionExpiration,
}: {
username: string;
password: string;
clientId: string;
sessionExpiration?: string;
}) => {
try {
const response = await serviceCall({
params: {
type: AUTH_TYPES.ID_TOKEN,
username,
password,
sessionExpiration,
clientId,
},
});
const jwt = await verifyAndExtractToken(response.data.idToken);
if (jwt && jwt.payload[JWT.USER_ID_KEY] !== "") {
return {
idToken: response.data.idToken,
userId: jwt.payload[JWT.USER_ID_KEY] as string,
status: true,
};
} else {
return {
status: false,
};
}
} catch (_error) {
return {
status: false,
};
}
};
146 changes: 62 additions & 84 deletions packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,114 +1,92 @@
import { AUTH_TYPES } from "@versini/auth-common";
import { JWT } from "@versini/auth-common";
import { useLocalStorage } from "@versini/ui-hooks";
import * as jose from "jose";
import { useEffect, useState } from "react";

import { EXPIRED_SESSION } from "../../common/constants";
import {
EXPIRED_SESSION,
LOCAL_STORAGE_PREFIX,
LOGOUT_SESSION,
} from "../../common/constants";
import type { AuthProviderProps, AuthState } from "../../common/types";
import { serviceCall } from "../../common/utilities";
import {
authenticateUser,
verifyAndExtractToken,
} from "../../common/utilities";
import { usePrevious } from "../hooks/usePrevious";
import { AuthContext } from "./AuthContext";

export const AuthProvider = ({
children,
sessionExpiration,
clientId,
accessType,
}: AuthProviderProps) => {
const [accessToken, setAccessToken, removeAccessToken] = useLocalStorage(
`@@auth@@::${clientId}::@@access@@`,
"",
);
const [refreshToken, setRefreshToken, removeRefreshToken] = useLocalStorage(
`@@auth@@::${clientId}::@@refresh@@`,
"",
);
const [idToken, setIdToken, removeIdToken] = useLocalStorage(
`@@auth@@::${clientId}::@@user@@`,
"",
);
const [idToken, setIdToken, , removeIdToken] = useLocalStorage({
key: `${LOCAL_STORAGE_PREFIX}::${clientId}::@@user@@`,
});

const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: !!idToken,
accessToken,
refreshToken,
idToken,
isAuthenticated: Boolean(idToken),
logoutReason: "",
userId: "",
});

const previousIdToken = usePrevious(idToken) || "";

/**
* This effect is responsible to set the authentication state based on the
* idToken stored in the local storage. It is used when the page is being
* refreshed.
*/
useEffect(() => {
if (previousIdToken !== idToken && idToken !== "") {
try {
const { _id }: { _id: string } = jose.decodeJwt(idToken);
setAuthState({
isAuthenticated: true,
accessToken,
refreshToken,
idToken,
logoutReason: "",
userId: _id || "",
});
} catch (_error) {
setAuthState({
isAuthenticated: false,
accessToken: "",
refreshToken: "",
idToken: "",
logoutReason: EXPIRED_SESSION,
userId: "",
});
}
} else if (previousIdToken !== idToken && idToken === "") {
setAuthState({
isAuthenticated: false,
accessToken: "",
refreshToken: "",
idToken: "",
logoutReason: EXPIRED_SESSION,
userId: "",
});
if (previousIdToken !== idToken && idToken !== null) {
(async () => {
try {
const jwt = await verifyAndExtractToken(idToken);
if (jwt && jwt.payload[JWT.USER_ID_KEY] !== "") {
setAuthState({
isAuthenticated: true,
logoutReason: "",
userId: jwt.payload[JWT.USER_ID_KEY] as string,
});
}
} catch (_error) {
setAuthState({
isAuthenticated: false,
logoutReason: EXPIRED_SESSION,
userId: "",
});
}
})();
}
}, [accessToken, refreshToken, idToken, previousIdToken]);
}, [idToken, previousIdToken]);

const login = async (username: string, password: string) => {
const response = await serviceCall({
params: {
type: accessType || AUTH_TYPES.ID_TOKEN,
username,
password,
sessionExpiration,
clientId,
},
const login = async (
username: string,
password: string,
): Promise<boolean> => {
const response = await authenticateUser({
username,
password,
clientId,
sessionExpiration,
});

try {
const { _id }: { _id: string } = jose.decodeJwt(response.data.idToken);
if (_id) {
setIdToken(response.data.idToken);
response.data.accessToken && setAccessToken(response.data.accessToken);
response.data.refreshToken &&
setRefreshToken(response.data.refreshToken);
setAuthState({
isAuthenticated: true,
idToken: response.data.idToken,
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken,
userId: _id,
logoutReason: "",
});
return true;
}
return false;
} catch (_error) {
return false;
if (response.status) {
setIdToken(response.idToken);
setAuthState({
isAuthenticated: true,
userId: response.userId,
});
return true;
}
return false;
};

const logout = () => {
removeAccessToken();
removeRefreshToken();
setAuthState({
isAuthenticated: false,
logoutReason: LOGOUT_SESSION,
userId: "",
});
removeIdToken();
};

Expand Down
Loading

0 comments on commit 0750b75

Please sign in to comment.