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

Auth Persistence #9

Merged
merged 6 commits into from
Nov 13, 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
Binary file added example-dimo-auth/dimo-login-button-sdk-1.0.0.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion example-dimo-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"@types/node": "^16.18.114",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"dimo-login-button-sdk": "file:../sdk",
"dimo-login-button-sdk": "file:dimo-login-button-sdk-1.0.0.tgz",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/auth/embedAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { handleMessageForEmbed } from "../utils/eventHandler";
export const embedAuth = (
onSuccess: (authData: { token: string }) => void,
onError: (error: Error) => void,
setAuthenticated: React.Dispatch<React.SetStateAction<boolean>>,
dimoLogin: string,
clientId?: string,
redirectUri?: string,
apiKey?: string,
permissionTemplateId?: string,
vehicles?: string[]
) => {
// Embed logic TBD

const cleanup = handleMessageForEmbed(
dimoLogin,
onSuccess,
onError,
setAuthenticated,
clientId,
redirectUri,
apiKey,
Expand Down
4 changes: 3 additions & 1 deletion sdk/src/auth/popupAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { handleMessageForPopup } from "../utils/eventHandler";
export const popupAuth = (
onSuccess: (authData: { token: string }) => void,
onError: (error: Error) => void,
setAuthenticated: React.Dispatch<React.SetStateAction<boolean>>,
dimoLogin: string,
clientId?: string,
redirectUri?: string,
apiKey?: string,
permissionTemplateId?: string,
vehicles?: string[]
vehicles?: string[],
) => {
try {
const popup = window.open(
Expand All @@ -26,6 +27,7 @@ export const popupAuth = (
dimoLogin,
onSuccess,
onError,
setAuthenticated,
popup,
clientId,
redirectUri,
Expand Down
26 changes: 19 additions & 7 deletions sdk/src/components/LoginWithDimo.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// src/components/LoginWithDimo.tsx
import React from "react";
import React, { useEffect, useState } from "react";

import { popupAuth } from "../auth/popupAuth";
import { embedAuth } from "../auth/embedAuth";
import { redirectAuth } from "../auth/redirectAuth";
import "../styles/LoginWithDimo.css";
import { getJWTFromCookies } from "../storage/storageManager";
import { isTokenExpired } from "../token/tokenManager";

interface LoginWithDimoProps {
mode: "popup" | "embed" | "redirect"; // The mode of login
Expand All @@ -29,20 +31,29 @@ const LoginWithDimo: React.FC<LoginWithDimoProps> = ({
vehicles,
environment,
}) => {
const dimoLogin = environment == "development" ? "https://login.dev.dimo.org" : "https://login.dimo.org";
const [authenticated, setAuthenticated] = useState(false);

useEffect(()=>{
const jwt = getJWTFromCookies()
if ( jwt && !isTokenExpired(jwt) ) {
setAuthenticated(true);
}
},[authenticated])

const dimoLogin = environment == "development" ? "https://login.dev.dimo.org" : "https://login.dimo.org"; //TODO: Pull from ENV
const handleButtonClick = () => {
switch (mode) {
case "popup":
popupAuth(
onSuccess,
onError,
setAuthenticated,
dimoLogin,
clientId,
redirectUri,
apiKey,
permissionTemplateId,
vehicles
vehicles,
);
break;
case "redirect":
Expand All @@ -54,7 +65,7 @@ const LoginWithDimo: React.FC<LoginWithDimoProps> = ({
redirectUri,
apiKey,
permissionTemplateId,
vehicles
vehicles,
);
break;
default:
Expand All @@ -68,6 +79,7 @@ const LoginWithDimo: React.FC<LoginWithDimoProps> = ({
embedAuth(
onSuccess,
onError,
setAuthenticated,
dimoLogin,
clientId,
redirectUri,
Expand All @@ -78,7 +90,7 @@ const LoginWithDimo: React.FC<LoginWithDimoProps> = ({
}
};

// Renders iframe for embed mode
// Renders iframe for embed mode
const renderEmbedIframe = () => (
<iframe
id="dimo-iframe"
Expand All @@ -98,12 +110,12 @@ const LoginWithDimo: React.FC<LoginWithDimoProps> = ({
{mode === "embed" ? (
renderEmbedIframe()
) : (
<button className="custom-button" onClick={handleButtonClick}>
<button className="custom-button" disabled={authenticated} onClick={handleButtonClick}>
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 13.0041H11.077C13.4042 13.0041 15.2272 11.2785 15.2272 9.07558C15.2272 6.87266 13.3665 4.99755 11.0787 4.99755H5.4982C4.97342 4.99755 4.54296 5.42387 4.54296 5.9436V11.8169H0V5.75847C0 2.85918 2.3821 0.5 5.30955 0.5H11.3102C16.1019 0.5 20 4.34704 20 9.07388C20 13.8007 16.1825 17.5 11.3102 17.5H0V13.0024V13.0041Z" fill="black"/>
</svg>

<span className="button-label">Continue With DIMO</span>
<span className="button-label">{ authenticated ? "Connected with DIMO" : "Continue With DIMO"}</span>
</button>
)}
</div>
Expand Down
21 changes: 21 additions & 0 deletions sdk/src/storage/storageManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @file storageManager.ts
* @description This module is responsible for managing JWT tokens.
*
* It handles:
* Storing tokens (cookies)
* Retrieving tokens from cookies

*/

export const storeJWTInCookies = (jwt: string): void => {
const expirationDate = new Date();
expirationDate.setFullYear(expirationDate.getFullYear() + 10); // Set expiration to 10 years in the future

document.cookie = `dimo_auth_token=${jwt}; expires=${expirationDate.toUTCString()}; path=/`;
};

export const getJWTFromCookies = (): string | null => {
const cookie = document.cookie.split('; ').find(row => row.startsWith(`dimo_auth_token=`));
return cookie ? cookie.split('=')[1] : null;
};
6 changes: 5 additions & 1 deletion sdk/src/styles/LoginWithDimo.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
transition: background-color 0.3s ease, color 0.3s ease, transform 0.1s ease;
}

.custom-button:hover {
.custom-button:disabled {
cursor: not-allowed;
}

.custom-button:not(:disabled):hover {
background-color: #f7f7f7; /* Adjusted for a lighter, subtle hover effect */
transform: scale(1.02); /* Slightly enlarges the button for a modern feel */
}
Expand Down
41 changes: 35 additions & 6 deletions sdk/src/token/tokenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,40 @@
* @description This module is responsible for managing JWT tokens.
*
* It handles:
* - @todo: Storing tokens (e.g., in localStorage or sessionStorage)
* - @todo: Retrieving tokens for authenticated API requests
* - Decoding JWT Tokens
* - Checking for Expiry
*
* Future Considerations:
* - Token expiration and automatic refresh handling
* - Validation utilities for checking token integrity

*/
*/
function base64UrlDecode(base64Url: string) {
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const decodedData = atob(base64);
return decodedData;
}

// Function to decode JWT
function decodeJWT(token: string) {
try {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format");
}

// Decode the payload (second part of the JWT)
const payload = base64UrlDecode(parts[1]);
return JSON.parse(payload);
} catch (e) {
throw new Error("Error decoding JWT: " + e);
}
}

export const isTokenExpired = (token: string) => {
try {
const decoded = decodeJWT(token);
const currentTime = Date.now() / 1000; // current time in seconds
return decoded.exp < currentTime;
} catch (error) {
console.error("Failed to decode JWT:", error);
return true; // If decoding fails, assume it's expired
}
};
8 changes: 8 additions & 0 deletions sdk/src/utils/eventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { storeJWTInCookies } from "../storage/storageManager";

/**
* @file eventHandler.ts
* @description Handles message passing between the parent window and a child window (popup or iframe)
Expand All @@ -20,6 +22,7 @@ export const handleMessageForPopup = (
expectedOrigin: string,
onSuccess: (authData: { token: string }) => void,
onError: (error: Error) => void,
setAuthenticated: React.Dispatch<React.SetStateAction<boolean>>,
popup?: Window | null,
clientId?: string,
redirectUri?: string,
Expand Down Expand Up @@ -50,6 +53,8 @@ export const handleMessageForPopup = (


if (authType === 'popup' && token) {
storeJWTInCookies(token);
setAuthenticated(true);
onSuccess({ token });

// Close the popup after success
Expand All @@ -73,6 +78,7 @@ export const handleMessageForEmbed = (
expectedOrigin: string,
onSuccess: (authData: { token: string }) => void,
onError: (error: Error) => void,
setAuthenticated: React.Dispatch<React.SetStateAction<boolean>>,
clientId?: string,
redirectUri?: string,
apiKey?: string,
Expand Down Expand Up @@ -109,6 +115,8 @@ export const handleMessageForEmbed = (
}

if (authType === 'embed' && token) {
storeJWTInCookies(token);
setAuthenticated(true);
onSuccess({ token });
}
};
Expand Down