my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
at main 37 kB view raw
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>documentation • indiko</title> 8 <meta name="description" content="IndieAuth/OAuth 2.0 server documentation and interactive API testing" /> 9 <link rel="icon" href="../../public/favicon.svg" type="image/svg+xml" /> 10 11 <!-- Open Graph / Facebook --> 12 <meta property="og:type" content="website" /> 13 <meta property="og:title" content="Documentation • Indiko" /> 14 <meta property="og:description" content="IndieAuth/OAuth 2.0 server documentation and interactive API testing" /> 15 16 <!-- Twitter --> 17 <meta name="twitter:card" content="summary" /> 18 <meta name="twitter:title" content="Documentation • Indiko" /> 19 <meta name="twitter:description" content="IndieAuth/OAuth 2.0 server documentation and interactive API testing" /> 20 <link rel="preconnect" href="https://fonts.googleapis.com"> 21 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 22 <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet"> 23 <style> 24 :root { 25 --mahogany: #26242b; 26 --lavender: #d9d0de; 27 --old-rose: #bc8da0; 28 --rosewood: #a04668; 29 --berry-crush: #ab4967; 30 } 31 32 * { 33 margin: 0; 34 padding: 0; 35 box-sizing: border-box; 36 } 37 38 body { 39 font-family: "Space Grotesk", sans-serif; 40 background: var(--mahogany); 41 color: var(--lavender); 42 min-height: 100vh; 43 padding: 2.5rem 1.25rem; 44 } 45 46 .container { 47 max-width: 56.25rem; 48 margin: 0 auto; 49 } 50 51 header { 52 margin-bottom: 3rem; 53 } 54 55 h1 { 56 font-size: 2.5rem; 57 font-weight: 700; 58 background: linear-gradient(135deg, var(--old-rose), var(--berry-crush), var(--rosewood)); 59 -webkit-background-clip: text; 60 -webkit-text-fill-color: transparent; 61 background-clip: text; 62 letter-spacing: -0.125rem; 63 margin-bottom: 0.5rem; 64 } 65 66 .subtitle { 67 color: var(--old-rose); 68 margin-bottom: 2rem; 69 font-size: 1.125rem; 70 font-weight: 300; 71 } 72 73 h2 { 74 font-size: 1.75rem; 75 font-weight: 600; 76 color: var(--lavender); 77 margin-top: 0; 78 margin-bottom: 1rem; 79 letter-spacing: -0.05rem; 80 } 81 82 h3 { 83 font-size: 1.25rem; 84 font-weight: 600; 85 color: var(--lavender); 86 margin-top: 0; 87 margin-bottom: 1rem; 88 } 89 90 p { 91 line-height: 1.8; 92 margin-bottom: 1rem; 93 color: var(--lavender); 94 } 95 96 .section { 97 background: rgba(188, 141, 160, 0.05); 98 border: 1px solid var(--old-rose); 99 padding: 2rem; 100 margin-bottom: 2rem; 101 } 102 103 .info-box { 104 background: rgba(188, 141, 160, 0.1); 105 border-left: 4px solid var(--berry-crush); 106 padding: 1.25rem; 107 margin: 1.5rem 0; 108 font-size: 0.9375rem; 109 color: var(--old-rose); 110 line-height: 1.8; 111 } 112 113 .info-box strong { 114 color: var(--lavender); 115 display: block; 116 margin-bottom: 0.5rem; 117 } 118 119 code { 120 background: rgba(12, 23, 19, 0.8); 121 padding: 0.25rem 0.5rem; 122 font-family: monospace; 123 color: var(--berry-crush); 124 font-size: 0.875rem; 125 border-radius: 2px; 126 } 127 128 pre { 129 background: rgba(12, 23, 19, 0.8); 130 border: 1px solid var(--rosewood); 131 padding: 1.5rem; 132 margin: 1.5rem 0; 133 overflow-x: auto; 134 line-height: 1.6; 135 } 136 137 pre code { 138 background: none; 139 padding: 0; 140 font-size: 0.875rem; 141 color: inherit; 142 } 143 144 /* Override Prism theme to match our colors */ 145 pre[class*="language-"] { 146 background: rgba(12, 23, 19, 0.8); 147 border: 1px solid var(--rosewood); 148 } 149 150 /* HTTP syntax highlighting */ 151 .http-method { 152 color: var(--berry-crush); 153 font-weight: 700; 154 } 155 156 .http-url { 157 color: #a5d6a7; 158 } 159 160 .http-header { 161 color: var(--old-rose); 162 } 163 164 .http-param { 165 color: #81c784; 166 } 167 168 /* JSON syntax highlighting */ 169 .json-key { 170 color: var(--berry-crush); 171 } 172 173 .json-string { 174 color: #a5d6a7; 175 } 176 177 .json-number { 178 color: #81c784; 179 } 180 181 .json-boolean { 182 color: var(--old-rose); 183 } 184 185 /* HTML/CSS syntax highlighting */ 186 .html-tag { 187 color: var(--berry-crush); 188 } 189 190 .html-attr { 191 color: var(--old-rose); 192 } 193 194 .html-string { 195 color: #a5d6a7; 196 } 197 198 .html-comment { 199 color: #7a7a7a; 200 font-style: italic; 201 } 202 203 .css-selector { 204 color: var(--berry-crush); 205 } 206 207 .css-property { 208 color: var(--old-rose); 209 } 210 211 .css-value { 212 color: #a5d6a7; 213 } 214 215 .css-unit { 216 color: #81c784; 217 } 218 219 .token.property, 220 .token.tag, 221 .token.boolean, 222 .token.number, 223 .token.constant, 224 .token.symbol { 225 color: #81c784; 226 } 227 228 .token.selector, 229 .token.attr-name, 230 .token.string, 231 .token.char, 232 .token.builtin { 233 color: #a5d6a7; 234 } 235 236 .token.punctuation { 237 color: var(--lavender); 238 } 239 240 .token.operator, 241 .token.entity, 242 .token.url, 243 .language-css .token.string, 244 .style .token.string { 245 color: var(--old-rose); 246 } 247 248 ul, 249 ol { 250 margin-left: 1.5rem; 251 margin-bottom: 1rem; 252 line-height: 1.8; 253 color: var(--lavender); 254 } 255 256 li { 257 margin-bottom: 0.5rem; 258 } 259 260 a { 261 color: var(--berry-crush); 262 text-decoration: none; 263 font-weight: 500; 264 } 265 266 a:hover { 267 text-decoration: underline; 268 } 269 270 table { 271 width: 100%; 272 border-collapse: collapse; 273 margin: 1.5rem 0; 274 } 275 276 th { 277 background: rgba(188, 141, 160, 0.2); 278 padding: 0.75rem; 279 text-align: left; 280 color: var(--lavender); 281 font-weight: 600; 282 border: 1px solid var(--old-rose); 283 } 284 285 td { 286 padding: 0.75rem; 287 border: 1px solid var(--old-rose); 288 color: var(--lavender); 289 } 290 291 tr:nth-child(even) { 292 background: rgba(188, 141, 160, 0.05); 293 } 294 295 .toc { 296 background: rgba(188, 141, 160, 0.05); 297 border: 1px solid var(--old-rose); 298 padding: 1.5rem; 299 margin-bottom: 2rem; 300 } 301 302 .toc h3 { 303 margin-top: 0; 304 margin-bottom: 1rem; 305 font-size: 1rem; 306 color: var(--old-rose); 307 text-transform: uppercase; 308 letter-spacing: 0.05rem; 309 } 310 311 .toc ul { 312 list-style: none; 313 margin: 0; 314 padding: 0; 315 } 316 317 .toc li { 318 margin-bottom: 0.5rem; 319 } 320 321 .toc a { 322 color: var(--lavender); 323 text-decoration: none; 324 transition: color 0.2s; 325 } 326 327 .toc a:hover { 328 color: var(--berry-crush); 329 text-decoration: underline; 330 } 331 332 .copy-btn { 333 display: block; 334 width: auto; 335 padding: 0.75rem 1.5rem; 336 font-size: 0.875rem; 337 margin: 2rem auto; 338 } 339 340 .back-link { 341 text-align: center; 342 margin-top: 3rem; 343 padding-top: 2rem; 344 border-top: 1px solid var(--old-rose); 345 } 346 347 /* OAuth Tester Styles */ 348 label { 349 display: block; 350 color: var(--old-rose); 351 font-size: 0.875rem; 352 font-weight: 500; 353 margin-bottom: 0.5rem; 354 text-transform: uppercase; 355 letter-spacing: 0.05rem; 356 } 357 358 input[type="text"], 359 input[type="url"] { 360 width: 100%; 361 padding: 0.875rem 1rem; 362 background: rgba(12, 23, 19, 0.6); 363 border: 2px solid var(--rosewood); 364 border-radius: 0; 365 color: var(--lavender); 366 font-size: 1rem; 367 font-family: "Space Grotesk", sans-serif; 368 margin-bottom: 1.5rem; 369 transition: border-color 0.2s; 370 } 371 372 input:focus { 373 outline: none; 374 border-color: var(--berry-crush); 375 background: rgba(12, 23, 19, 0.8); 376 } 377 378 .checkbox-group { 379 margin-bottom: 1.5rem; 380 } 381 382 .checkbox-group label { 383 display: flex; 384 align-items: center; 385 gap: 0.75rem; 386 text-transform: none; 387 font-weight: 400; 388 margin-bottom: 0.75rem; 389 cursor: pointer; 390 padding: 0.5rem; 391 transition: background 0.2s; 392 } 393 394 .checkbox-group label:hover { 395 background: rgba(188, 141, 160, 0.1); 396 } 397 398 input[type="checkbox"] { 399 appearance: none; 400 width: 1.5rem; 401 height: 1.5rem; 402 border: 2px solid var(--old-rose); 403 background: rgba(12, 23, 19, 0.6); 404 cursor: pointer; 405 flex-shrink: 0; 406 position: relative; 407 transition: all 0.2s; 408 } 409 410 input[type="checkbox"]:checked { 411 background: var(--berry-crush); 412 border-color: var(--berry-crush); 413 } 414 415 input[type="checkbox"]:checked::after { 416 content: "✓"; 417 position: absolute; 418 top: 50%; 419 left: 50%; 420 transform: translate(-50%, -50%); 421 color: var(--lavender); 422 font-size: 1rem; 423 font-weight: 700; 424 } 425 426 button { 427 position: relative; 428 padding: 1rem 2rem; 429 background: var(--berry-crush); 430 color: var(--lavender); 431 border: 4px solid var(--mahogany); 432 border-radius: 0; 433 font-size: 1rem; 434 font-weight: 700; 435 cursor: pointer; 436 font-family: "Space Grotesk", sans-serif; 437 transition: all 0.15s ease; 438 text-transform: uppercase; 439 letter-spacing: 0.1rem; 440 box-shadow: 6px 6px 0 var(--mahogany); 441 width: 100%; 442 } 443 444 button::before { 445 content: ''; 446 position: absolute; 447 top: -4px; 448 left: -4px; 449 right: -4px; 450 bottom: -4px; 451 background: transparent; 452 border: 4px solid var(--rosewood); 453 pointer-events: none; 454 transition: all 0.15s ease; 455 } 456 457 button:hover:not(:disabled) { 458 transform: translate(3px, 3px); 459 box-shadow: 3px 3px 0 var(--mahogany); 460 } 461 462 button:hover:not(:disabled)::before { 463 top: -7px; 464 left: -7px; 465 right: -7px; 466 bottom: -7px; 467 } 468 469 button:active:not(:disabled) { 470 transform: translate(6px, 6px); 471 box-shadow: 0 0 0 var(--mahogany); 472 } 473 474 button:disabled { 475 opacity: 0.5; 476 cursor: not-allowed; 477 } 478 479 .result { 480 background: rgba(12, 23, 19, 0.6); 481 border: 2px solid var(--rosewood); 482 padding: 1.5rem; 483 margin-top: 1.5rem; 484 font-family: monospace; 485 font-size: 0.875rem; 486 white-space: pre-wrap; 487 word-break: break-all; 488 display: none; 489 } 490 491 .result.show { 492 display: block; 493 } 494 495 .result.success { 496 border-color: #81c784; 497 background: rgba(139, 195, 74, 0.1); 498 } 499 500 .result.error { 501 border-color: var(--rosewood); 502 background: rgba(160, 70, 104, 0.1); 503 } 504 505 /* Demo button styles */ 506 .demo-button-wrapper { 507 background: rgba(12, 23, 19, 0.6); 508 padding: 2rem; 509 margin: 1.5rem 0; 510 display: flex; 511 justify-content: center; 512 } 513 514 .indiko-demo-button { 515 position: relative; 516 display: inline-block; 517 padding: 1rem 2rem; 518 background: var(--berry-crush); 519 color: var(--lavender); 520 border: 4px solid var(--mahogany); 521 font-size: 1rem; 522 font-weight: 700; 523 text-decoration: none; 524 font-family: "Space Grotesk", sans-serif; 525 text-transform: uppercase; 526 letter-spacing: 0.1rem; 527 box-shadow: 6px 6px 0 var(--mahogany); 528 transition: all 0.15s ease; 529 } 530 531 .indiko-demo-button::before { 532 content: ''; 533 position: absolute; 534 top: -4px; 535 left: -4px; 536 right: -4px; 537 bottom: -4px; 538 background: transparent; 539 border: 4px solid var(--rosewood); 540 pointer-events: none; 541 transition: all 0.15s ease; 542 } 543 544 .indiko-demo-button:hover { 545 transform: translate(3px, 3px); 546 box-shadow: 3px 3px 0 var(--mahogany); 547 } 548 549 .indiko-demo-button:hover::before { 550 top: -7px; 551 left: -7px; 552 right: -7px; 553 bottom: -7px; 554 } 555 556 .indiko-demo-button:active { 557 transform: translate(6px, 6px); 558 box-shadow: 0 0 0 var(--mahogany); 559 } 560 561 .indiko-demo-button:hover { 562 text-decoration: none; 563 } 564 </style> 565</head> 566 567<body> 568 <div class="container"> 569 <header> 570 <h1>indiko documentation</h1> 571 <p class="subtitle">IndieAuth/OAuth 2.0 server with passkey authentication</p> 572 </header> 573 574 <button id="copyMarkdownBtn" class="copy-btn">copy as markdown</button> 575 576 <nav class="toc"> 577 <h3>table of contents</h3> 578 <ul> 579 <li><a href="#overview">overview</a></li> 580 <li><a href="#getting-started">getting started</a></li> 581 <li><a href="#button">sign in button</a></li> 582 <li><a href="#endpoints">endpoints</a></li> 583 <li><a href="#authorization">authorization flow</a> 584 <ul style="margin-top: 0.5rem; margin-left: 1.5rem;"> 585 <li><a href="#authorization" style="font-size: 0.9rem;">discovery</a></li> 586 </ul> 587 </li> 588 <li><a href="#tokens">token management</a> 589 <ul style="margin-top: 0.5rem; margin-left: 1.5rem;"> 590 <li><a href="#tokens-refresh" style="font-size: 0.9rem;">refresh tokens</a></li> 591 <li><a href="#tokens-introspect" style="font-size: 0.9rem;">introspection</a></li> 592 <li><a href="#tokens-revoke" style="font-size: 0.9rem;">revocation</a></li> 593 <li><a href="#tokens-userinfo" style="font-size: 0.9rem;">userinfo</a></li> 594 </ul> 595 </li> 596 <li><a href="#scopes">scopes</a></li> 597 <li><a href="#roles">roles</a></li> 598 <li><a href="#clients">client types</a></li> 599 <li><a href="#tester">oauth tester</a></li> 600 </ul> 601 </nav> 602 603 <section id="overview" class="section"> 604 <h2>overview</h2> 605 <p> 606 Indiko is a self-hosted IndieAuth/OAuth 2.0 authorization server with passwordless authentication using WebAuthn 607 passkeys. 608 It provides single sign-on (SSO) for your apps and services. 609 </p> 610 611 <h3>key features</h3> 612 <ul> 613 <li>Passwordless authentication via WebAuthn passkeys</li> 614 <li>Full IndieAuth and OAuth 2.0 support with PKCE</li> 615 <li>Access tokens and refresh tokens for API access</li> 616 <li>Token introspection and revocation endpoints</li> 617 <li>UserInfo endpoint for profile data</li> 618 <li>Auto-registration of OAuth clients</li> 619 <li>Pre-registered clients with secrets and role management</li> 620 <li>Session-based SSO (authenticate once, authorize many apps)</li> 621 <li>User profile endpoints with h-card microformats</li> 622 <li>Invite-based user registration</li> 623 </ul> 624 </section> 625 626 <section id="getting-started" class="section"> 627 <h2>getting started</h2> 628 629 <h3>for app developers</h3> 630 <p> 631 To integrate with Indiko as an OAuth client, you'll need: 632 </p> 633 <ol> 634 <li>A <strong>client ID</strong> (any valid URL, e.g., <code>https://myapp.example.com</code>)</li> 635 <li>A <strong>redirect URI</strong> (where users return after authorization)</li> 636 <li>Support for PKCE (code challenge/verifier)</li> 637 </ol> 638 639 <div class="info-box"> 640 <strong>Auto-registration:</strong> 641 Apps are automatically registered on first use. You don't need admin approval to get started. 642 During registration, Indiko fetches your client metadata from your <code>client_id</code> URL to validate redirect URIs and display your app name/logo. 643 For advanced features like client secrets and role assignment, contact your Indiko admin to pre-register your app. 644 </div> 645 646 <h3>publishing client metadata (recommended)</h3> 647 <p> 648 To help Indiko verify your app and display proper branding, publish client metadata as JSON at your <code>client_id</code> URL: 649 </p> 650 <pre><code>{ 651 "client_id": "https://myapp.example.com/", 652 "client_name": "My App", 653 "logo_uri": "https://myapp.example.com/logo.png", 654 "redirect_uris": [ 655 "https://myapp.example.com/callback", 656 "https://myapp.example.com/auth/callback" 657 ] 658}</code></pre> 659 <p> 660 Alternatively, you can publish redirect URIs as HTML <code>&lt;link&gt;</code> tags: 661 </p> 662 <pre><code>&lt;link rel="redirect_uri" href="https://myapp.example.com/callback" /&gt;</code></pre> 663 664 <div class="info-box"> 665 <strong>Security:</strong> 666 If your <code>redirect_uri</code> uses a different host than your <code>client_id</code>, you MUST publish <code>redirect_uris</code> in your client metadata. This prevents unauthorized apps from hijacking your client_id. 667 </div> 668 669 <h3>for users</h3> 670 <p> 671 You'll need an invite code to create an account. Once registered: 672 </p> 673 <ul> 674 <li>Set up your passkey (fingerprint, face ID, or security key)</li> 675 <li>Complete your profile (name, photo, website)</li> 676 <li>Authorize apps to access your profile</li> 677 <li>Manage app permissions from your dashboard</li> 678 </ul> 679 </section> 680 681 <section id="button" class="section"> 682 <h2>sign in button</h2> 683 <p> 684 Copy this themed button for your app's login page. It matches Indiko's visual style: 685 </p> 686 687 <div class="demo-button-wrapper"> 688 <a href="#" id="demoButton" class="indiko-demo-button">Sign in with Indiko</a> 689 </div> 690 691 <h3>HTML + CSS</h3> 692 <pre><code id="buttonCode"></code></pre> 693 694 <button id="copyButtonCode" class="copy-btn">copy button code</button> 695 696 <div class="info-box"> 697 <strong>Customization:</strong> 698 Replace <code>YOUR_OAUTH_URL_HERE</code> with your authorization URL (see <a href="#authorization">authorization flow</a> below). You can also change the button text or adjust colors to match your app's theme. 699 </div> 700 </section> 701 702 <section id="endpoints" class="section"> 703 <h2>API endpoints</h2> 704 705 <h3>authorization endpoints</h3> 706 <table> 707 <thead> 708 <tr> 709 <th>Endpoint</th> 710 <th>Method</th> 711 <th>Description</th> 712 </tr> 713 </thead> 714 <tbody> 715 <tr> 716 <td><code>/.well-known/oauth-authorization-server</code></td> 717 <td>GET</td> 718 <td>IndieAuth server metadata (discovery endpoint)</td> 719 </tr> 720 <tr> 721 <td><code>/auth/authorize</code></td> 722 <td>GET</td> 723 <td>Start OAuth authorization flow</td> 724 </tr> 725 <tr> 726 <td><code>/auth/authorize</code></td> 727 <td>POST</td> 728 <td>Submit consent/scope approval</td> 729 </tr> 730 <tr> 731 <td><code>/auth/token</code></td> 732 <td>POST</td> 733 <td>Exchange code for access token and refresh token</td> 734 </tr> 735 <tr> 736 <td><code>/auth/token/introspect</code></td> 737 <td>POST</td> 738 <td>Verify access token validity</td> 739 </tr> 740 <tr> 741 <td><code>/auth/token/revoke</code></td> 742 <td>POST</td> 743 <td>Revoke access or refresh token</td> 744 </tr> 745 <tr> 746 <td><code>/userinfo</code></td> 747 <td>GET</td> 748 <td>Get user profile data with bearer token</td> 749 </tr> 750 <tr> 751 <td><code>/u/:username</code></td> 752 <td>GET</td> 753 <td>Public user profile (h-card with discovery links)</td> 754 </tr> 755 </tbody> 756 </table> 757 758 <h3>authentication endpoints</h3> 759 <table> 760 <thead> 761 <tr> 762 <th>Endpoint</th> 763 <th>Method</th> 764 <th>Description</th> 765 </tr> 766 </thead> 767 <tbody> 768 <tr> 769 <td><code>/auth/can-register</code></td> 770 <td>POST</td> 771 <td>Check if invite code is valid</td> 772 </tr> 773 <tr> 774 <td><code>/auth/register/options</code></td> 775 <td>POST</td> 776 <td>Get WebAuthn registration options</td> 777 </tr> 778 <tr> 779 <td><code>/auth/register/verify</code></td> 780 <td>POST</td> 781 <td>Complete passkey registration</td> 782 </tr> 783 <tr> 784 <td><code>/auth/login/options</code></td> 785 <td>POST</td> 786 <td>Get WebAuthn login options</td> 787 </tr> 788 <tr> 789 <td><code>/auth/login/verify</code></td> 790 <td>POST</td> 791 <td>Complete passkey login</td> 792 </tr> 793 <tr> 794 <td><code>/auth/logout</code></td> 795 <td>POST</td> 796 <td>End current session</td> 797 </tr> 798 </tbody> 799 </table> 800 </section> 801 802 <section id="authorization" class="section"> 803 <h2>authorization flow</h2> 804 805 <h3>0. discovery (recommended)</h3> 806 <p> 807 Before starting authorization, clients should discover the authorization server's endpoints from the user's profile URL: 808 </p> 809 <ol> 810 <li>Fetch the user's profile URL (e.g., <code id="discoveryUrl">http://localhost:3000/u/username</code>)</li> 811 <li>Look for <code>&lt;link rel="indieauth-metadata"&gt;</code> tag or HTTP <code>Link:</code> header</li> 812 <li>Fetch the metadata endpoint to get <code>authorization_endpoint</code> and <code>token_endpoint</code></li> 813 </ol> 814 <p> 815 The metadata endpoint returns: 816 </p> 817 <pre><code>{ 818 <span class="json-key">"issuer"</span>: <span class="json-string" id="metadataIssuer">"http://localhost:3000"</span>, 819 <span class="json-key">"authorization_endpoint"</span>: <span class="json-string" id="metadataAuthEndpoint">"http://localhost:3000/auth/authorize"</span>, 820 <span class="json-key">"token_endpoint"</span>: <span class="json-string" id="metadataTokenEndpoint">"http://localhost:3000/auth/token"</span>, 821 <span class="json-key">"code_challenge_methods_supported"</span>: [<span class="json-string">"S256"</span>], 822 <span class="json-key">"scopes_supported"</span>: [<span class="json-string">"profile"</span>, <span class="json-string">"email"</span>] 823}</code></pre> 824 825 <h3>1. redirect to authorization endpoint</h3> 826 <pre><code><span class="http-method">GET</span> <span class="http-url" id="authUrl">http://localhost:3000/auth/authorize</span>?<span 827 class="http-param">response_type</span>=code 828 &<span class="http-param">client_id</span>=https://myapp.example.com 829 &<span class="http-param">redirect_uri</span>=https://myapp.example.com/callback 830 &<span class="http-param">state</span>=random_state_string 831 &<span class="http-param">code_challenge</span>=base64url_encoded_challenge 832 &<span class="http-param">code_challenge_method</span>=S256 833 &<span class="http-param">scope</span>=profile email</code></pre> 834 835 <div class="info-box"> 836 <strong>PKCE is required:</strong> 837 Generate a random <code>code_verifier</code> (43-128 characters), then create <code>code_challenge</code> by 838 hashing it with SHA-256 and base64url encoding. 839 </div> 840 841 <h3>2. user authenticates and approves</h3> 842 <p> 843 Indiko will: 844 </p> 845 <ul> 846 <li>Check if user has an active session (if not, prompt for passkey login)</li> 847 <li>Show consent screen with requested scopes</li> 848 <li>Auto-approve if user previously authorized this app</li> 849 </ul> 850 851 <h3>3. redirect back with code</h3> 852 <pre><code><span class="http-url">https://myapp.example.com/callback</span>?<span 853 class="http-param">code</span>=short_lived_authorization_code 854 &<span class="http-param">state</span>=random_state_string 855 &<span class="http-param">iss</span>=<span class="http-url" id="issuerUrl">http://localhost:3000</span></code></pre> 856 857 <div class="info-box"> 858 <strong>Security:</strong> 859 The <code>iss</code> (issuer) parameter allows you to verify the response came from the expected authorization server. Compare it to the <code>issuer</code> from the metadata endpoint. 860 </div> 861 862 <h3>4. exchange code for token</h3> 863 <pre><code><span class="http-method">POST</span> <span class="http-url" id="tokenUrl">http://localhost:3000/auth/token</span> 864 <span class="http-header">Content-Type</span>: application/x-www-form-urlencoded 865 866 <span class="http-param">grant_type</span>=authorization_code 867 &<span class="http-param">code</span>=authorization_code 868 &<span class="http-param">client_id</span>=https://myapp.example.com 869 &<span class="http-param">redirect_uri</span>=https://myapp.example.com/callback 870 &<span class="http-param">code_verifier</span>=original_code_verifier 871 &<span class="http-param">client_secret</span>=your_client_secret (if pre-registered)</code></pre> 872 873 <div class="info-box"> 874 <strong>Client authentication:</strong> 875 All clients MUST use PKCE (code_verifier) per the IndieAuth specification. Pre-registered confidential clients should also include <code>client_secret</code> in the token request for additional security. 876 </div> 877 878 <h3>5. receive tokens and user profile</h3> 879 <pre><code>{ 880 <span class="json-key">"access_token"</span>: <span class="json-string">"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."</span>, 881 <span class="json-key">"token_type"</span>: <span class="json-string">"Bearer"</span>, 882 <span class="json-key">"expires_in"</span>: <span class="json-number">3600</span>, 883 <span class="json-key">"refresh_token"</span>: <span class="json-string">"RT_abc123xyz..."</span>, 884 <span class="json-key">"me"</span>: <span class="json-string" id="profileMeUrl">"http://localhost:3000/u/username"</span>, 885 <span class="json-key">"profile"</span>: { 886 <span class="json-key">"name"</span>: <span class="json-string">"Jane Doe"</span>, 887 <span class="json-key">"email"</span>: <span class="json-string">"jane@example.com"</span>, 888 <span class="json-key">"photo"</span>: <span class="json-string">"https://example.com/photo.jpg"</span>, 889 <span class="json-key">"url"</span>: <span class="json-string">"https://jane.example.com"</span> 890 }, 891 <span class="json-key">"scope"</span>: <span class="json-string">"profile email"</span>, 892 <span class="json-key">"iss"</span>: <span class="json-string" id="issuerUrl2">"http://localhost:3000"</span>, 893 <span class="json-key">"role"</span>: <span class="json-string">"admin"</span> 894}</code></pre> 895 896 <div class="info-box"> 897 <strong>Token types:</strong> 898 <ul style="margin-top: 0.5rem; margin-bottom: 0;"> 899 <li><code>access_token</code> - Short-lived token (1 hour) for API access</li> 900 <li><code>refresh_token</code> - Long-lived token (30 days) for getting new access tokens</li> 901 </ul> 902 </div> 903 904 <div class="info-box"> 905 <strong>Roles:</strong> 906 If an admin has assigned a role to this user for your app, it will be included in the response. Roles are 907 arbitrary strings that you can use for role-based access control (RBAC) in your application. 908 </div> 909 </section> 910 911 <section id="tokens" class="section"> 912 <h2>token management</h2> 913 <p> 914 Indiko provides a complete OAuth 2.0 token management system with access tokens, refresh tokens, introspection, and revocation. 915 </p> 916 917 <h3 id="tokens-refresh">refresh tokens</h3> 918 <p> 919 Access tokens expire after 1 hour. Use the refresh token to get a new access token without requiring user re-authentication: 920 </p> 921 922 <pre><code><span class="http-method">POST</span> <span class="http-url" id="tokenRefreshUrl">http://localhost:3000/auth/token</span> 923<span class="http-header">Content-Type</span>: application/x-www-form-urlencoded 924 925<span class="http-param">grant_type</span>=refresh_token 926&<span class="http-param">refresh_token</span>=RT_abc123xyz... 927&<span class="http-param">client_id</span>=https://myapp.example.com</code></pre> 928 929 <p>Response:</p> 930 <pre><code>{ 931 <span class="json-key">"access_token"</span>: <span class="json-string">"new_access_token..."</span>, 932 <span class="json-key">"token_type"</span>: <span class="json-string">"Bearer"</span>, 933 <span class="json-key">"expires_in"</span>: <span class="json-number">3600</span>, 934 <span class="json-key">"me"</span>: <span class="json-string">"http://localhost:3000/u/username"</span>, 935 <span class="json-key">"scope"</span>: <span class="json-string">"profile email"</span>, 936 <span class="json-key">"iss"</span>: <span class="json-string">"http://localhost:3000"</span> 937}</code></pre> 938 939 <div class="info-box"> 940 <strong>Important:</strong> 941 <ul style="margin-top: 0.5rem; margin-bottom: 0;"> 942 <li>Refresh tokens are valid for 30 days</li> 943 <li>Each refresh request generates a new access token</li> 944 <li>The refresh token itself remains valid (no rotation)</li> 945 <li>Store refresh tokens securely - they provide long-term access</li> 946 </ul> 947 </div> 948 949 <h3 id="tokens-introspect">token introspection</h3> 950 <p> 951 Resource servers can verify access tokens by calling the introspection endpoint: 952 </p> 953 954 <pre><code><span class="http-method">POST</span> <span class="http-url" id="tokenIntrospectUrl">http://localhost:3000/auth/token/introspect</span> 955<span class="http-header">Content-Type</span>: application/json 956 957{ 958 <span class="json-key">"token"</span>: <span class="json-string">"access_token_here"</span> 959}</code></pre> 960 961 <p>Response (valid token):</p> 962 <pre><code>{ 963 <span class="json-key">"active"</span>: <span class="json-boolean">true</span>, 964 <span class="json-key">"me"</span>: <span class="json-string">"http://localhost:3000/u/username"</span>, 965 <span class="json-key">"client_id"</span>: <span class="json-string">"https://myapp.example.com"</span>, 966 <span class="json-key">"scope"</span>: <span class="json-string">"profile email"</span>, 967 <span class="json-key">"exp"</span>: <span class="json-number">1640000000</span>, 968 <span class="json-key">"iat"</span>: <span class="json-number">1639996400</span> 969}</code></pre> 970 971 <p>Response (invalid token):</p> 972 <pre><code>{ 973 <span class="json-key">"active"</span>: <span class="json-boolean">false</span> 974}</code></pre> 975 976 <div class="info-box"> 977 <strong>Use case:</strong> 978 Introspection is useful for resource servers (like Micropub endpoints) that need to verify tokens issued by Indiko. 979 </div> 980 981 <h3 id="tokens-revoke">token revocation</h3> 982 <p> 983 Apps can revoke access or refresh tokens when users log out: 984 </p> 985 986 <pre><code><span class="http-method">POST</span> <span class="http-url" id="tokenRevokeUrl">http://localhost:3000/auth/token/revoke</span> 987<span class="http-header">Content-Type</span>: application/json 988 989{ 990 <span class="json-key">"token"</span>: <span class="json-string">"access_or_refresh_token_here"</span> 991}</code></pre> 992 993 <p>Response: HTTP 200 (always returns success, even if token doesn't exist)</p> 994 995 <div class="info-box"> 996 <strong>Best practice:</strong> 997 Always revoke tokens when users explicitly log out to prevent unauthorized access. 998 </div> 999 1000 <h3 id="tokens-userinfo">userinfo endpoint</h3> 1001 <p> 1002 Fetch updated user profile information using an access token: 1003 </p> 1004 1005 <pre><code><span class="http-method">GET</span> <span class="http-url" id="userinfoUrl">http://localhost:3000/userinfo</span> 1006<span class="http-header">Authorization</span>: Bearer access_token_here</code></pre> 1007 1008 <p>Response (with <code>profile</code> and <code>email</code> scopes):</p> 1009 <pre><code>{ 1010 <span class="json-key">"name"</span>: <span class="json-string">"Jane Doe"</span>, 1011 <span class="json-key">"photo"</span>: <span class="json-string">"https://example.com/photo.jpg"</span>, 1012 <span class="json-key">"url"</span>: <span class="json-string">"https://jane.example.com"</span>, 1013 <span class="json-key">"email"</span>: <span class="json-string">"jane@example.com"</span> 1014}</code></pre> 1015 1016 <div class="info-box"> 1017 <strong>Note:</strong> 1018 The response only includes data for scopes granted to the token. A token with only <code>profile</code> scope will not include email. 1019 </div> 1020 </section> 1021 1022 <section id="scopes" class="section"> 1023 <h2>scopes</h2> 1024 1025 <table> 1026 <thead> 1027 <tr> 1028 <th>Scope</th> 1029 <th>Description</th> 1030 <th>Data Included</th> 1031 </tr> 1032 </thead> 1033 <tbody> 1034 <tr> 1035 <td><code>profile</code></td> 1036 <td>Basic profile information</td> 1037 <td>name, photo, URL</td> 1038 </tr> 1039 <tr> 1040 <td><code>email</code></td> 1041 <td>Email address</td> 1042 <td>email</td> 1043 </tr> 1044 </tbody> 1045 </table> 1046 1047 <div class="info-box"> 1048 <strong>Note:</strong> 1049 Users can selectively approve scopes during authorization. Your app may receive fewer scopes than requested. 1050 </div> 1051 </section> 1052 1053 <section id="roles" class="section"> 1054 <h2>roles</h2> 1055 <p> 1056 Roles enable role-based access control (RBAC) in your applications. <strong>Only pre-registered clients with client secrets support role assignment.</strong> 1057 </p> 1058 1059 <div class="info-box"> 1060 <strong>Pre-registration required:</strong> 1061 To use roles, contact your Indiko admin to pre-register your app with a client secret. Auto-registered (public) clients cannot use roles. 1062 </div> 1063 1064 <h3>how roles work</h3> 1065 <ul> 1066 <li>Roles are assigned by admins for specific user-app combinations</li> 1067 <li>Role strings are arbitrary (e.g., <code>"admin"</code>, <code>"editor"</code>, <code>"viewer"</code>)</li> 1068 <li>Only one role per user per app</li> 1069 <li>Included in token response if assigned</li> 1070 <li>Your app interprets the role string and enforces permissions</li> 1071 </ul> 1072 1073 <div class="info-box"> 1074 <strong>Example use case:</strong> 1075 A CMS app could use roles like <code>"admin"</code>, <code>"editor"</code>, and <code>"viewer"</code>. When 1076 users authenticate via Indiko, the app checks their role and grants appropriate permissions. 1077 </div> 1078 1079 <h3>defining app roles</h3> 1080 <p> 1081 Apps can define available roles in three ways: 1082 </p> 1083 <ul> 1084 <li><strong>Disabled (default):</strong> Leave "Available Roles" empty. No roles can be assigned.</li> 1085 <li><strong>Predefined roles:</strong> Specify allowed roles (one per line). Creates a dropdown for role selection, preventing typos.</li> 1086 <li><strong>Default role:</strong> Automatically assign a role when users first authorize your app.</li> 1087 </ul> 1088 1089 <h3>assigning roles</h3> 1090 <p> 1091 Roles can be assigned in multiple ways: 1092 </p> 1093 <ol> 1094 <li><strong>Default role (automatic):</strong> If configured, users automatically receive the default role on first authorization.</li> 1095 <li><strong>Via invite codes:</strong> Admins can create invites with pre-assigned roles for specific apps. New 1096 users automatically get those roles on signup.</li> 1097 <li><strong>Via admin dashboard:</strong> Admins can assign or change roles for existing users in the clients 1098 management interface.</li> 1099 </ol> 1100 1101 <div class="info-box"> 1102 <strong>Note:</strong> 1103 Roles are optional. If no role is assigned, the <code>role</code> field will not appear in the token response. 1104 </div> 1105 </section> 1106 1107 <section id="clients" class="section"> 1108 <h2>client types</h2> 1109 1110 <h3>auto-registered clients</h3> 1111 <p> 1112 Any app can use Indiko without pre-registration. On first authorization, Indiko will: 1113 </p> 1114 <ul> 1115 <li>Validate the client ID (must be a valid URL per IndieAuth spec)</li> 1116 <li>Fetch client metadata from the client_id URL (if available)</li> 1117 <li>Validate redirect_uri against published redirect_uris (if different host)</li> 1118 <li>Extract and store client name and logo (if provided)</li> 1119 <li>Automatically register the client for future use</li> 1120 </ul> 1121 <p> 1122 Auto-registered clients: 1123 </p> 1124 <ul> 1125 <li><strong>Client ID format:</strong> Any valid URL (e.g., <code>https://myapp.example.com</code>)</li> 1126 <li><strong>Authentication:</strong> MUST use PKCE only (no client secret)</li> 1127 <li><strong>Limitations:</strong> Cannot use client secrets or role assignment</li> 1128 </ul> 1129 1130 <div class="info-box"> 1131 <strong>Security:</strong> 1132 For redirect URIs on different hosts than your client_id, you must publish redirect_uris in your client metadata. See <a href="#getting-started">getting started</a> for details. 1133 </div> 1134 1135 <h3>pre-registered clients</h3> 1136 <p> 1137 Admins can pre-register clients for advanced features. <strong>All pre-registered clients require a client secret and must also use PKCE.</strong> 1138 </p> 1139 <p> 1140 Pre-registered clients: 1141 </p> 1142 <ul> 1143 <li><strong>Client ID format:</strong> Generated with <code>ikc_</code> prefix (e.g., <code>ikc_xxxxxxxxxxxxxxxxxxxxx</code>)</li> 1144 <li><strong>Client secret format:</strong> Generated with <code>iks_</code> prefix (shown once on creation)</li> 1145 <li><strong>Authentication:</strong> MUST use both PKCE AND client_secret in token requests</li> 1146 <li><strong>Role assignment:</strong> Admins can assign per-user roles for RBAC</li> 1147 <li><strong>Available roles:</strong> Define which roles can be assigned (enforces dropdown selection)</li> 1148 <li><strong>Default role:</strong> Automatically assigned to users on first authorization</li> 1149 <li><strong>Metadata:</strong> Custom name, logo, description</li> 1150 </ul> 1151 1152 <div class="info-box"> 1153 <strong>Tip:</strong> 1154 Contact your Indiko admin to pre-register your app if you need client authentication or role-based access 1155 control. 1156 </div> 1157 </section> 1158 1159 <section id="tester" class="section"> 1160 <h2>OAuth tester</h2> 1161 <p> 1162 Test the OAuth flow with a live interactive client. This simulates how your app would integrate with Indiko. 1163 </p> 1164 1165 <div id="testerForm"> 1166 <label for="clientId">client id (your app's URL)</label> 1167 <input type="url" id="clientId" value="" placeholder="https://example.com" /> 1168 1169 <label for="redirectUri">redirect uri (callback URL)</label> 1170 <input type="url" id="redirectUri" value="" placeholder="https://example.com/callback" /> 1171 1172 <div class="checkbox-group"> 1173 <label>scopes to request:</label> 1174 <label> 1175 <input type="checkbox" name="scope" value="profile" checked /> 1176 <span>profile (name, photo, URL)</span> 1177 </label> 1178 <label> 1179 <input type="checkbox" name="scope" value="email" /> 1180 <span>email</span> 1181 </label> 1182 </div> 1183 1184 <button id="startBtn">start oauth flow</button> 1185 </div> 1186 1187 <div id="callbackSection" style="display: none;"> 1188 <h3>callback received</h3> 1189 <div class="info-box"> 1190 You've been redirected back with an authorization code. Click below to exchange it for user data. 1191 </div> 1192 <div id="callbackInfo"></div> 1193 <button id="exchangeBtn">exchange code for profile</button> 1194 </div> 1195 1196 <div id="resultSection" style="display: none;"> 1197 <h3>result</h3> 1198 <div id="result" class="result"></div> 1199 </div> 1200 1201 <div class="info-box" style="margin-top: 2rem;"> 1202 <strong>How it works:</strong> 1203 This page uses the current URL as the redirect URI. After authorization, the code is automatically detected and 1204 you can exchange it for user profile data. 1205 </div> 1206 </section> 1207 1208 <div class="back-link"> 1209 <a href="/">← back to dashboard</a> 1210 </div> 1211 </div> 1212 1213 <script type="module" src="../client/docs.ts"></script> 1214</body> 1215 1216</html>