Skip to content

Commit 0c6ca6f

Browse files
authored
Merge pull request #44 from zitadel/add-refresh-token-box
feat: add refresh token container and instructions for offline access
2 parents 6361b3a + 2d9f942 commit 0c6ca6f

File tree

3 files changed

+51
-18
lines changed

3 files changed

+51
-18
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,25 @@ yarn start
111111
```
112112

113113
Your application will then run on `http://localhost:3000`
114+
115+
## Enable Offline Access (Refresh Tokens)
116+
117+
To retrieve a refresh token for offline access, follow these steps:
118+
119+
1. Enable refresh tokens for your application:
120+
- Navigate to your [application settings](https://zitadel.com/docs/guides/manage/console/applications#application-settings) and enable the **Refresh Token** checkbox.
121+
122+
2. Add the `offline_access` scope:
123+
124+
```js
125+
const config: ZitadelConfig = {
126+
authority: "https://CUSTOM_DOMAIN",
127+
client_id: "YOUR_CLIENT_ID",
128+
redirect_uri: "http://localhost:3000/callback",
129+
post_logout_redirect_uri: "http://localhost:3000",
130+
response_type: 'code',
131+
scope: 'openid profile email offline_access'
132+
};
133+
```
134+
135+
3. After logging out and logging back in, you will see the refresh token container displaying the refresh token.

src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
22
import "./App.css";
33
import { createZitadelAuth, ZitadelConfig } from "@zitadel/react";
44
import { BrowserRouter, Route, Routes } from "react-router-dom";
5-
import Navbar from "./components/Navbar"
5+
import Navbar from "./components/Navbar";
66

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

3637
useEffect(() => {
3738
zitadel.userManager.getUser().then((user) => {
3839
if (user) {
3940
setAuthenticated(true);
4041
setAccessToken(user.access_token ?? null);
4142
setIdToken(user.id_token ?? null);
43+
setRefreshToken(user.refresh_token ?? null);
4244
} else {
4345
setAuthenticated(false);
4446
setAccessToken(null);
4547
setIdToken(null);
48+
setRefreshToken(null);
4649
}
4750
});
4851
}, [zitadel]);
@@ -53,7 +56,7 @@ function App() {
5356
<BrowserRouter>
5457
<Navbar />
5558
<Login authenticated={authenticated} handleLogin={login} handleLogout={logout} />
56-
<JWTContainer accessToken={accessToken} idToken={idToken} />
59+
<JWTContainer accessToken={accessToken} idToken={idToken} refreshToken={refreshToken} />
5760
<Routes>
5861
<Route
5962
path="/callback"

src/components/JWTContainer.tsx

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,28 @@ import { useState } from "react";
33
type Props = {
44
accessToken: string | null;
55
idToken: string | null;
6+
refreshToken?: string | null;
67
};
78

89
function decodeJWT(jwt: string) {
9-
// Split the JWT into its three parts: header, payload, and signature
1010
const parts = jwt.split('.');
11-
1211
if (parts.length !== 3) {
1312
console.log("Token is not in JWT format");
1413
return jwt;
1514
}
16-
17-
// The payload is the second part (index 1)
1815
const payloadBase64 = parts[1];
19-
20-
// Decode the base64-encoded payload
2116
const decodedPayload = atob(payloadBase64.replace(/_/g, '/').replace(/-/g, '+'));
22-
23-
// Parse the JSON string into an object
2417
const payloadObj = JSON.parse(decodedPayload);
25-
2618
return payloadObj;
2719
}
2820

29-
const JWTContainer = ({ accessToken, idToken }: Props) => {
21+
const JWTContainer = ({ accessToken, idToken, refreshToken }: Props) => {
3022
const decodedAccessToken = accessToken ? JSON.stringify(decodeJWT(accessToken), null, 2) : null;
3123
const decodedIdToken = idToken ? JSON.stringify(decodeJWT(idToken), null, 2) : null;
3224

3325
const [copyState, setCopyState] = useState<{ [key: string]: boolean }>({});
34-
const handleCopy = (target: "accessToken" | "idToken", text: string) => {
26+
27+
const handleCopy = (target: "accessToken" | "idToken" | "refreshToken", text: string) => {
3528
navigator.clipboard.writeText(text).then(() => {
3629
setCopyState((prev) => ({ ...prev, [target]: true }));
3730
setTimeout(() => {
@@ -48,8 +41,7 @@ const JWTContainer = ({ accessToken, idToken }: Props) => {
4841
Access Token
4942
{accessToken && (
5043
<i
51-
className={`fas fa-copy copy-btn ${copyState.accessToken ? "text-success" : "text-primary"
52-
}`}
44+
className={`fas fa-copy copy-btn ${copyState.accessToken ? "text-success" : "text-primary"}`}
5345
style={{ cursor: "pointer" }}
5446
onClick={() => handleCopy("accessToken", accessToken)}
5547
></i>
@@ -59,15 +51,31 @@ const JWTContainer = ({ accessToken, idToken }: Props) => {
5951
<pre id="access-token" className="jwt-box">{decodedAccessToken || "No token generated yet."}</pre>
6052
</div>
6153
</div>
54+
55+
{refreshToken && (
56+
<div className="card mb-3 shadow-sm">
57+
<div className="card-header fw-bold d-flex justify-content-between align-items-center">
58+
Refresh Token
59+
<i
60+
className={`fas fa-copy copy-btn ${copyState.refreshToken ? "text-success" : "text-primary"}`}
61+
style={{ cursor: "pointer" }}
62+
onClick={() => handleCopy("refreshToken", refreshToken)}
63+
></i>
64+
</div>
65+
<div className="card-body">
66+
<pre id="refresh-token" className="jwt-box">{refreshToken}</pre>
67+
</div>
68+
</div>
69+
)}
6270
</div>
71+
6372
<div className="col-md-6">
6473
<div className="card mb-3 shadow-sm">
6574
<div className="card-header fw-bold d-flex justify-content-between align-items-center">
6675
ID Token
6776
{idToken && (
6877
<i
69-
className={`fas fa-copy copy-btn ${copyState.idToken ? "text-success" : "text-primary"
70-
}`}
78+
className={`fas fa-copy copy-btn ${copyState.idToken ? "text-success" : "text-primary"}`}
7179
style={{ cursor: "pointer" }}
7280
onClick={() => handleCopy("idToken", idToken)}
7381
></i>
@@ -82,4 +90,4 @@ const JWTContainer = ({ accessToken, idToken }: Props) => {
8290
);
8391
};
8492

85-
export default JWTContainer;
93+
export default JWTContainer;

0 commit comments

Comments
 (0)