Skip to content
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,25 @@ yarn start
```

Your application will then run on `http://localhost:3000`

## Enable Offline Access (Refresh Tokens)

To retrieve a refresh token for offline access, follow these steps:

1. Enable refresh tokens for your application:
- Navigate to your [application settings](https://zitadel.com/docs/guides/manage/console/applications#application-settings) and enable the **Refresh Token** checkbox.

2. Add the `offline_access` scope:

```js
const config: ZitadelConfig = {
authority: "https://CUSTOM_DOMAIN",
client_id: "YOUR_CLIENT_ID",
redirect_uri: "http://localhost:3000/callback",
post_logout_redirect_uri: "http://localhost:3000",
response_type: 'code',
scope: 'openid profile email offline_access'
};
```

3. After logging out and logging back in, you will see the refresh token container displaying the refresh token.
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import "./App.css";
import { createZitadelAuth, ZitadelConfig } from "@zitadel/react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Navbar from "./components/Navbar"
import Navbar from "./components/Navbar";

import Login from "./components/Login";
import Callback from "./components/Callback";
Expand Down Expand Up @@ -32,17 +32,20 @@ function App() {
const [authenticated, setAuthenticated] = useState<boolean | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [idToken, setIdToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);

useEffect(() => {
zitadel.userManager.getUser().then((user) => {
if (user) {
setAuthenticated(true);
setAccessToken(user.access_token ?? null);
setIdToken(user.id_token ?? null);
setRefreshToken(user.refresh_token ?? null);
} else {
setAuthenticated(false);
setAccessToken(null);
setIdToken(null);
setRefreshToken(null);
}
});
}, [zitadel]);
Expand All @@ -53,7 +56,7 @@ function App() {
<BrowserRouter>
<Navbar />
<Login authenticated={authenticated} handleLogin={login} handleLogout={logout} />
<JWTContainer accessToken={accessToken} idToken={idToken} />
<JWTContainer accessToken={accessToken} idToken={idToken} refreshToken={refreshToken} />
<Routes>
<Route
path="/callback"
Expand Down
40 changes: 24 additions & 16 deletions src/components/JWTContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,28 @@ import { useState } from "react";
type Props = {
accessToken: string | null;
idToken: string | null;
refreshToken?: string | null;
};

function decodeJWT(jwt: string) {
// Split the JWT into its three parts: header, payload, and signature
const parts = jwt.split('.');

if (parts.length !== 3) {
console.log("Token is not in JWT format");
return jwt;
}

// The payload is the second part (index 1)
const payloadBase64 = parts[1];

// Decode the base64-encoded payload
const decodedPayload = atob(payloadBase64.replace(/_/g, '/').replace(/-/g, '+'));

// Parse the JSON string into an object
const payloadObj = JSON.parse(decodedPayload);

return payloadObj;
}

const JWTContainer = ({ accessToken, idToken }: Props) => {
const JWTContainer = ({ accessToken, idToken, refreshToken }: Props) => {
const decodedAccessToken = accessToken ? JSON.stringify(decodeJWT(accessToken), null, 2) : null;
const decodedIdToken = idToken ? JSON.stringify(decodeJWT(idToken), null, 2) : null;

const [copyState, setCopyState] = useState<{ [key: string]: boolean }>({});
const handleCopy = (target: "accessToken" | "idToken", text: string) => {

const handleCopy = (target: "accessToken" | "idToken" | "refreshToken", text: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopyState((prev) => ({ ...prev, [target]: true }));
setTimeout(() => {
Expand All @@ -48,8 +41,7 @@ const JWTContainer = ({ accessToken, idToken }: Props) => {
Access Token
{accessToken && (
<i
className={`fas fa-copy copy-btn ${copyState.accessToken ? "text-success" : "text-primary"
}`}
className={`fas fa-copy copy-btn ${copyState.accessToken ? "text-success" : "text-primary"}`}
style={{ cursor: "pointer" }}
onClick={() => handleCopy("accessToken", accessToken)}
></i>
Expand All @@ -59,15 +51,31 @@ const JWTContainer = ({ accessToken, idToken }: Props) => {
<pre id="access-token" className="jwt-box">{decodedAccessToken || "No token generated yet."}</pre>
</div>
</div>

{refreshToken && (
<div className="card mb-3 shadow-sm">
<div className="card-header fw-bold d-flex justify-content-between align-items-center">
Refresh Token
<i
className={`fas fa-copy copy-btn ${copyState.refreshToken ? "text-success" : "text-primary"}`}
style={{ cursor: "pointer" }}
onClick={() => handleCopy("refreshToken", refreshToken)}
></i>
</div>
<div className="card-body">
<pre id="refresh-token" className="jwt-box">{refreshToken}</pre>
</div>
</div>
)}
</div>

<div className="col-md-6">
<div className="card mb-3 shadow-sm">
<div className="card-header fw-bold d-flex justify-content-between align-items-center">
ID Token
{idToken && (
<i
className={`fas fa-copy copy-btn ${copyState.idToken ? "text-success" : "text-primary"
}`}
className={`fas fa-copy copy-btn ${copyState.idToken ? "text-success" : "text-primary"}`}
style={{ cursor: "pointer" }}
onClick={() => handleCopy("idToken", idToken)}
></i>
Expand All @@ -82,4 +90,4 @@ const JWTContainer = ({ accessToken, idToken }: Props) => {
);
};

export default JWTContainer;
export default JWTContainer;