Skip to content

Commit

Permalink
feat: stronger security level for JWT verification with RSA keys
Browse files Browse the repository at this point in the history
  • Loading branch information
aversini committed Jun 23, 2024
1 parent a135015 commit dc74759
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 46 deletions.
13 changes: 13 additions & 0 deletions packages/auth-provider/src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
export const EXPIRED_SESSION =
"Oops! It looks like your session has expired. For your security, please log in again to continue.";

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;
18 changes: 16 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 { 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,16 @@ 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;
}
};
65 changes: 34 additions & 31 deletions packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { AUTH_TYPES } from "@versini/auth-common";
import { AUTH_TYPES, 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 } from "../../common/constants";
import type { AuthProviderProps, AuthState } from "../../common/types";
import { serviceCall } from "../../common/utilities";
import { serviceCall, verifyAndExtractToken } from "../../common/utilities";
import { usePrevious } from "../hooks/usePrevious";
import { AuthContext } from "./AuthContext";

Expand All @@ -16,15 +15,15 @@ export const AuthProvider = ({
accessType,
}: AuthProviderProps) => {
const [accessToken, setAccessToken, removeAccessToken] = useLocalStorage(
`@@auth@@::${clientId}::@@access@@`,
`${LOCAL_STORAGE_PREFIX}::${clientId}::@@access@@`,
"",
);
const [refreshToken, setRefreshToken, removeRefreshToken] = useLocalStorage(
`@@auth@@::${clientId}::@@refresh@@`,
`${LOCAL_STORAGE_PREFIX}::${clientId}::@@refresh@@`,
"",
);
const [idToken, setIdToken, removeIdToken] = useLocalStorage(
`@@auth@@::${clientId}::@@user@@`,
`${LOCAL_STORAGE_PREFIX}::${clientId}::@@user@@`,
"",
);
const [authState, setAuthState] = useState<AuthState>({
Expand All @@ -40,26 +39,30 @@ export const AuthProvider = ({

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: "",
});
}
(async () => {
try {
const jwt = await verifyAndExtractToken(idToken);
if (jwt && jwt.payload[JWT.USER_ID_KEY] !== "") {
setAuthState({
isAuthenticated: true,
accessToken,
refreshToken,
idToken,
logoutReason: "",
userId: jwt.payload[JWT.USER_ID_KEY] as string,
});
}
} catch (_error) {
setAuthState({
isAuthenticated: false,
accessToken: "",
refreshToken: "",
idToken: "",
logoutReason: EXPIRED_SESSION,
userId: "",
});
}
})();
} else if (previousIdToken !== idToken && idToken === "") {
setAuthState({
isAuthenticated: false,
Expand All @@ -84,8 +87,9 @@ export const AuthProvider = ({
});

try {
const { _id }: { _id: string } = jose.decodeJwt(response.data.idToken);
if (_id) {
const jwt = await verifyAndExtractToken(response.data.idToken);

if (jwt && jwt.payload[JWT.USER_ID_KEY] !== "") {
setIdToken(response.data.idToken);
response.data.accessToken && setAccessToken(response.data.accessToken);
response.data.refreshToken &&
Expand All @@ -95,8 +99,7 @@ export const AuthProvider = ({
idToken: response.data.idToken,
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken,
userId: _id,
logoutReason: "",
userId: jwt.payload[JWT.USER_ID_KEY] as string,
});
return true;
}
Expand Down

0 comments on commit dc74759

Please sign in to comment.