my own indieAuth provider!
indiko.dunkirk.sh/docs
indieauth
oauth2-server
1<!doctype html>
2<html lang="en">
3
4<head>
5 <meta charset="UTF-8" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>oauth test client • indiko</title>
8 <link rel="icon" href="../../public/favicon.svg" type="image/svg+xml" />
9 <link rel="preconnect" href="https://fonts.googleapis.com">
10 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11 <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
12 <style>
13 :root {
14 --mahogany: #26242b;
15 --lavender: #d9d0de;
16 --old-rose: #bc8da0;
17 --rosewood: #a04668;
18 --berry-crush: #ab4967;
19 }
20
21 * {
22 margin: 0;
23 padding: 0;
24 box-sizing: border-box;
25 }
26
27 body {
28 font-family: "Space Grotesk", sans-serif;
29 background: var(--mahogany);
30 color: var(--lavender);
31 min-height: 100vh;
32 padding: 2.5rem 1.25rem;
33 }
34
35 .container {
36 max-width: 56.25rem;
37 margin: 0 auto;
38 }
39
40 h1 {
41 font-size: 2rem;
42 font-weight: 700;
43 background: linear-gradient(135deg, var(--old-rose), var(--berry-crush), var(--rosewood));
44 -webkit-background-clip: text;
45 -webkit-text-fill-color: transparent;
46 background-clip: text;
47 letter-spacing: -0.125rem;
48 margin-bottom: 0.5rem;
49 }
50
51 .subtitle {
52 color: var(--old-rose);
53 margin-bottom: 2rem;
54 font-size: 1rem;
55 font-weight: 300;
56 }
57
58 .section {
59 background: rgba(188, 141, 160, 0.05);
60 border: 1px solid var(--old-rose);
61 padding: 2rem;
62 margin-bottom: 1.5rem;
63 }
64
65 .section h2 {
66 font-size: 1.25rem;
67 font-weight: 600;
68 color: var(--lavender);
69 margin-bottom: 1.5rem;
70 }
71
72 label {
73 display: block;
74 color: var(--old-rose);
75 font-size: 0.875rem;
76 font-weight: 500;
77 margin-bottom: 0.5rem;
78 text-transform: uppercase;
79 letter-spacing: 0.05rem;
80 }
81
82 input[type="text"],
83 input[type="url"] {
84 width: 100%;
85 padding: 0.875rem 1rem;
86 background: rgba(12, 23, 19, 0.6);
87 border: 2px solid var(--rosewood);
88 border-radius: 0;
89 color: var(--lavender);
90 font-size: 1rem;
91 font-family: "Space Grotesk", sans-serif;
92 margin-bottom: 1.5rem;
93 transition: border-color 0.2s;
94 }
95
96 input:focus {
97 outline: none;
98 border-color: var(--berry-crush);
99 background: rgba(12, 23, 19, 0.8);
100 }
101
102 .checkbox-group {
103 margin-bottom: 1.5rem;
104 }
105
106 .checkbox-group label {
107 display: flex;
108 align-items: center;
109 gap: 0.5rem;
110 text-transform: none;
111 font-weight: 400;
112 margin-bottom: 0.75rem;
113 cursor: pointer;
114 }
115
116 input[type="checkbox"] {
117 width: 1.25rem;
118 height: 1.25rem;
119 cursor: pointer;
120 }
121
122 button {
123 position: relative;
124 padding: 1rem 2rem;
125 background: var(--berry-crush);
126 color: var(--lavender);
127 border: 4px solid var(--mahogany);
128 border-radius: 0;
129 font-size: 1rem;
130 font-weight: 700;
131 cursor: pointer;
132 font-family: "Space Grotesk", sans-serif;
133 transition: all 0.15s ease;
134 text-transform: uppercase;
135 letter-spacing: 0.1rem;
136 box-shadow: 6px 6px 0 var(--mahogany);
137 width: 100%;
138 }
139
140 button::before {
141 content: '';
142 position: absolute;
143 top: -4px;
144 left: -4px;
145 right: -4px;
146 bottom: -4px;
147 background: transparent;
148 border: 4px solid var(--rosewood);
149 pointer-events: none;
150 transition: all 0.15s ease;
151 }
152
153 button:hover:not(:disabled) {
154 transform: translate(3px, 3px);
155 box-shadow: 3px 3px 0 var(--mahogany);
156 }
157
158 button:hover:not(:disabled)::before {
159 top: -7px;
160 left: -7px;
161 right: -7px;
162 bottom: -7px;
163 }
164
165 button:active:not(:disabled) {
166 transform: translate(6px, 6px);
167 box-shadow: 0 0 0 var(--mahogany);
168 }
169
170 button:disabled {
171 opacity: 0.5;
172 cursor: not-allowed;
173 }
174
175 .result {
176 background: rgba(12, 23, 19, 0.6);
177 border: 2px solid var(--rosewood);
178 padding: 1.5rem;
179 margin-top: 1.5rem;
180 font-family: monospace;
181 font-size: 0.875rem;
182 white-space: pre-wrap;
183 word-break: break-all;
184 display: none;
185 }
186
187 .result.show {
188 display: block;
189 }
190
191 .result.success {
192 border-color: #81c784;
193 background: rgba(139, 195, 74, 0.1);
194 }
195
196 .result.error {
197 border-color: var(--rosewood);
198 background: rgba(160, 70, 104, 0.1);
199 }
200
201 .info-box {
202 background: rgba(188, 141, 160, 0.1);
203 border-left: 3px solid var(--berry-crush);
204 padding: 1rem;
205 margin-bottom: 1.5rem;
206 font-size: 0.875rem;
207 color: var(--old-rose);
208 }
209
210 .info-box strong {
211 color: var(--lavender);
212 }
213
214 code {
215 background: rgba(12, 23, 19, 0.8);
216 padding: 0.125rem 0.375rem;
217 font-family: monospace;
218 color: var(--berry-crush);
219 }
220
221 a {
222 color: var(--berry-crush);
223 text-decoration: none;
224 }
225
226 a:hover {
227 text-decoration: underline;
228 }
229
230 /* JSON syntax highlighting */
231 .json-key {
232 color: var(--berry-crush);
233 }
234
235 .json-string {
236 color: #a5d6a7;
237 }
238
239 .json-number {
240 color: #81c784;
241 }
242
243 .json-boolean {
244 color: var(--old-rose);
245 }
246
247 .json-null {
248 color: #9e9e9e;
249 }
250 </style>
251</head>
252
253<body>
254 <div class="container">
255 <h1>oauth test client</h1>
256 <p class="subtitle">test your indiko indieauth/oauth 2.0 server</p>
257
258 <div class="section">
259 <h2>step 1: configure</h2>
260
261 <div class="info-box">
262 <strong>How this works:</strong><br>
263 This page simulates an OAuth client (like your blog or app). It will redirect you to indiko for authentication,
264 show you a consent screen, then exchange the authorization code for your user profile.
265 </div>
266
267 <label for="clientId">client id (your app's URL)</label>
268 <input type="url" id="clientId" value="" placeholder="https://example.com" />
269
270 <label for="redirectUri">redirect uri (callback URL)</label>
271 <input type="url" id="redirectUri" value="" placeholder="https://example.com/callback" />
272
273 <div class="checkbox-group">
274 <label>scopes to request:</label>
275 <label>
276 <input type="checkbox" name="scope" value="profile" checked />
277 <span>profile (name, photo, URL)</span>
278 </label>
279 <label>
280 <input type="checkbox" name="scope" value="email" />
281 <span>email</span>
282 </label>
283 </div>
284
285 <button type="button" id="startBtn">start oauth flow</button>
286 </div>
287
288 <div class="section" id="callbackSection" style="display: none;">
289 <h2>step 2: callback handler</h2>
290
291 <div class="info-box">
292 You've been redirected back with an authorization code. Click below to exchange it for user data.
293 </div>
294
295 <div id="callbackInfo"></div>
296
297 <button type="button" id="exchangeBtn">exchange code for profile</button>
298 </div>
299
300 <div class="section" id="resultSection" style="display: none;">
301 <h2>step 3: result</h2>
302 <div id="result" class="result"></div>
303 </div>
304
305 <div class="section">
306 <h2>development notes</h2>
307 <ul style="list-style: none; line-height: 1.8;">
308 <li>• This page handles the OAuth callback at the current URL</li>
309 <li>• Set <code>redirect_uri</code> to the current page URL (it will be auto-filled)</li>
310 <li>• <code>client_id</code> should be a valid URL (can be any URL, it auto-registers)</li>
311 <li>• Authorization codes expire in 60 seconds</li>
312 <li>• Codes are single-use only</li>
313 <li>• PKCE (S256) is required and handled automatically</li>
314 </ul>
315 </div>
316
317 <div style="text-align: center; margin-top: 2rem;">
318 <a href="/">← back to dashboard</a>
319 </div>
320 </div>
321
322 <script type="module" src="../client/oauth-test.ts"></script>
323</body>
324
325</html>