From f2c05ea9736b52ddfd825b92b807e4204e6df89d Mon Sep 17 00:00:00 2001 From: "maksim.nabokikh" Date: Sat, 21 Feb 2026 12:11:02 +0100 Subject: [PATCH] feat: refactor example-app with a new config This is a preparation for KubeCon 2026: 1. Restyle the app 2. Refactor advanced configuration options 3. Move embedded templates and css to separate files Signed-off-by: maksim.nabokikh --- examples/example-app/main.go | 76 ++- examples/example-app/static/app.js | 106 +++++ .../example-app/static/dex-glyph-color.svg | 20 + examples/example-app/static/style.css | 357 ++++++++++++++ examples/example-app/static/token.js | 82 ++++ examples/example-app/templates.go | 439 +++++------------- examples/example-app/templates/index.html | 62 +++ examples/example-app/templates/token.html | 64 +++ 8 files changed, 863 insertions(+), 343 deletions(-) create mode 100644 examples/example-app/static/app.js create mode 100644 examples/example-app/static/dex-glyph-color.svg create mode 100644 examples/example-app/static/style.css create mode 100644 examples/example-app/static/token.js create mode 100644 examples/example-app/templates/index.html create mode 100644 examples/example-app/templates/token.html diff --git a/examples/example-app/main.go b/examples/example-app/main.go index 22a7b2bd..7859f399 100644 --- a/examples/example-app/main.go +++ b/examples/example-app/main.go @@ -17,7 +17,6 @@ import ( "net/http/httputil" "net/url" "os" - "strings" "time" "github.com/coreos/go-oidc/v3/oidc" @@ -43,8 +42,9 @@ type app struct { pkce bool redirectURI string - verifier *oidc.IDTokenVerifier - provider *oidc.Provider + verifier *oidc.IDTokenVerifier + provider *oidc.Provider + scopesSupported []string // Does the provider use "offline_access" scope to request a refresh token // or does it use "access_type=offline" (e.g. Google)? @@ -188,7 +188,9 @@ func cmd() *cobra.Command { a.provider = provider a.verifier = provider.Verifier(&oidc.Config{ClientID: a.clientID}) + a.scopesSupported = s.ScopesSupported + http.Handle("/static/", http.StripPrefix("/static/", staticHandler)) http.HandleFunc("/", a.handleIndex) http.HandleFunc("/login", a.handleLogin) http.HandleFunc(u.Path, a.handleCallback) @@ -226,7 +228,10 @@ func main() { } func (a *app) handleIndex(w http.ResponseWriter, r *http.Request) { - renderIndex(w) + renderIndex(w, indexPageData{ + ScopesSupported: a.scopesSupported, + LogoURI: dexLogoDataURI, + }) } func (a *app) oauth2Config(scopes []string) *oauth2.Config { @@ -240,15 +245,19 @@ func (a *app) oauth2Config(scopes []string) *oauth2.Config { } func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) { - var scopes []string - if extraScopes := r.FormValue("extra_scopes"); extraScopes != "" { - scopes = strings.Split(extraScopes, " ") - } - var clients []string - if crossClients := r.FormValue("cross_client"); crossClients != "" { - clients = strings.Split(crossClients, " ") + if err := r.ParseForm(); err != nil { + http.Error(w, fmt.Sprintf("failed to parse form: %v", err), http.StatusBadRequest) + return } + + // Only use scopes that are checked in the form + scopes := r.Form["extra_scopes"] + + clients := r.Form["cross_client"] for _, client := range clients { + if client == "" { + continue + } scopes = append(scopes, "audience:server:client_id:"+client) } connectorID := "" @@ -257,7 +266,7 @@ func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) { } authCodeURL := "" - scopes = append(scopes, "openid", "profile", "email") + scopes = uniqueStrings(scopes) var authCodeOptions []oauth2.AuthCodeOption @@ -266,13 +275,28 @@ func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) { authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("code_challenge_method", "S256")) } - a.oauth2Config(scopes) - if r.FormValue("offline_access") == "yes" { - authCodeOptions = append(authCodeOptions, oauth2.AccessTypeOffline) + // Check if offline_access scope is present to determine offline access mode + hasOfflineAccess := false + for _, scope := range scopes { + if scope == "offline_access" { + hasOfflineAccess = true + break + } } - if a.offlineAsScope { - scopes = append(scopes, "offline_access") + + if hasOfflineAccess && !a.offlineAsScope { + // Provider uses access_type=offline instead of offline_access scope + authCodeOptions = append(authCodeOptions, oauth2.AccessTypeOffline) + // Remove offline_access from scopes as it's not supported + filteredScopes := make([]string, 0, len(scopes)) + for _, scope := range scopes { + if scope != "offline_access" { + filteredScopes = append(filteredScopes, scope) + } + } + scopes = filteredScopes } + authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState, authCodeOptions...) // Parse the auth code URL and safely add connector_id parameter if provided @@ -374,7 +398,7 @@ func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) { return } - renderToken(w, a.redirectURI, rawIDToken, accessToken, token.RefreshToken, buff.String()) + renderToken(w, a.provider, a.redirectURI, rawIDToken, accessToken, token.RefreshToken, buff.String()) } func generateCodeVerifier() string { @@ -389,3 +413,19 @@ func generateCodeChallenge(verifier string) string { hash := sha256.Sum256([]byte(verifier)) return base64.RawURLEncoding.EncodeToString(hash[:]) } + +func uniqueStrings(values []string) []string { + seen := make(map[string]struct{}, len(values)) + out := values[:0] + for _, v := range values { + if v == "" { + continue + } + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} diff --git a/examples/example-app/static/app.js b/examples/example-app/static/app.js new file mode 100644 index 00000000..fc2c350d --- /dev/null +++ b/examples/example-app/static/app.js @@ -0,0 +1,106 @@ +(function() { + const crossClientInput = document.getElementById("cross_client_input"); + const crossClientList = document.getElementById("cross-client-list"); + const addClientBtn = document.getElementById("add-cross-client"); + const scopesList = document.getElementById("scopes-list"); + const customScopeInput = document.getElementById("custom_scope_input"); + const addCustomScopeBtn = document.getElementById("add-custom-scope"); + + // Default scopes that should be checked by default + const defaultScopes = ["openid", "profile", "email", "offline_access"]; + + // Check default scopes on page load + document.addEventListener("DOMContentLoaded", function() { + const checkboxes = scopesList.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(cb => { + if (defaultScopes.includes(cb.value)) { + cb.checked = true; + } + }); + }); + + function addCrossClient(value) { + const trimmed = value.trim(); + if (!trimmed) return; + + const chip = document.createElement("div"); + chip.className = "chip"; + + const text = document.createElement("span"); + text.textContent = trimmed; + + const hidden = document.createElement("input"); + hidden.type = "hidden"; + hidden.name = "cross_client"; + hidden.value = trimmed; + + const remove = document.createElement("button"); + remove.type = "button"; + remove.textContent = "×"; + remove.onclick = () => crossClientList.removeChild(chip); + + chip.append(text, hidden, remove); + crossClientList.appendChild(chip); + } + + function addCustomScope(scope) { + const trimmed = scope.trim(); + if (!trimmed || !scopesList) return; + + // Check if scope already exists + const existingCheckboxes = scopesList.querySelectorAll('input[type="checkbox"]'); + for (const cb of existingCheckboxes) { + if (cb.value === trimmed) { + cb.checked = true; + return; + } + } + + // Add new scope checkbox + const scopeItem = document.createElement("div"); + scopeItem.className = "scope-item"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.name = "extra_scopes"; + checkbox.value = trimmed; + checkbox.id = "scope_custom_" + trimmed; + checkbox.checked = true; + + const label = document.createElement("label"); + label.htmlFor = checkbox.id; + label.textContent = trimmed; + + scopeItem.append(checkbox, label); + scopesList.appendChild(scopeItem); + } + + addClientBtn?.addEventListener("click", () => { + addCrossClient(crossClientInput.value); + crossClientInput.value = ""; + crossClientInput.focus(); + }); + + crossClientInput?.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + addCrossClient(crossClientInput.value); + crossClientInput.value = ""; + } + }); + + addCustomScopeBtn?.addEventListener("click", () => { + addCustomScope(customScopeInput.value); + customScopeInput.value = ""; + customScopeInput.focus(); + }); + + customScopeInput?.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + addCustomScope(customScopeInput.value); + customScopeInput.value = ""; + } + }); +})(); + diff --git a/examples/example-app/static/dex-glyph-color.svg b/examples/example-app/static/dex-glyph-color.svg new file mode 100644 index 00000000..2668039f --- /dev/null +++ b/examples/example-app/static/dex-glyph-color.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/examples/example-app/static/style.css b/examples/example-app/static/style.css new file mode 100644 index 00000000..def84224 --- /dev/null +++ b/examples/example-app/static/style.css @@ -0,0 +1,357 @@ +body { + font-family: Arial, sans-serif; + background-color: #f2f2f2; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + flex-direction: column; + padding: 20px; +} + +/* Token page layout - no centering */ +body.token-page { + display: block; + align-items: flex-start; + justify-content: flex-start; + padding: 20px; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 600px; +} + +.logo { + max-width: 100%; + width: 200px; + height: auto; + margin-bottom: 30px; + display: block; +} + +form { + background-color: #fff; + padding: 30px; + border-radius: 8px; + width: 100%; +} + +/* Shadow only for main login form */ +.container form { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.primary-action { + margin-bottom: 20px; +} + +.advanced { + margin-top: 20px; + background: #f9fbfd; + border: 1px solid #dbe7f3; + border-radius: 6px; + padding: 15px; +} + +.advanced summary { + cursor: pointer; + font-weight: bold; + color: #3F9FD8; + user-select: none; + text-align: center; +} + +.advanced summary:hover { + color: #357FAA; +} + +.app-description { + text-align: center; + color: #666; + font-size: 14px; + margin-bottom: 25px; + line-height: 1.5; +} + +.app-description a { + color: #3F9FD8; + text-decoration: none; +} + +.app-description a:hover { + text-decoration: underline; +} + +.field { + margin-top: 15px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.field label { + font-weight: 600; + color: #333; + font-size: 14px; +} + +.inline-input { + display: flex; + gap: 8px; +} + +.inline-input input[type="text"] { + flex: 1; +} + +input[type="text"] { + padding: 10px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + outline: none; + transition: border-color 0.2s; +} + +input[type="text"]:focus { + border-color: #3F9FD8; +} + +.checkbox-field { + flex-direction: row; + align-items: center; + gap: 8px; + margin-top: 10px; +} + +.checkbox-field label { + margin: 0; + font-weight: 500; +} + +input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +.scopes-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 200px; + overflow-y: auto; + padding: 10px; + background: white; + border: 1px solid #ddd; + border-radius: 4px; +} + +.scope-item { + display: flex; + align-items: center; + gap: 8px; +} + +.scope-item input[type="checkbox"] { + margin: 0; +} + +.scope-item label { + margin: 0; + font-weight: 400; + font-size: 13px; + cursor: pointer; +} + +input[type="submit"], button { + padding: 12px 20px; + background-color: #3F9FD8; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background-color 0.2s; +} + +/* Full width submit button only on main login form */ +form input[type="submit"] { + width: 100%; + font-size: 16px; +} + +/* Token page forms should have auto-width buttons */ +body.token-page input[type="submit"] { + width: auto; + font-size: 14px; +} + +input[type="submit"]:hover, button:hover { + background-color: #357FAA; +} + +button { + white-space: nowrap; +} + +.chip-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + min-height: 32px; + margin-top: 8px; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + background: #e9f4fb; + border: 1px solid #cde5f5; + border-radius: 16px; + padding: 6px 12px; + font-size: 13px; +} + +.chip button { + border: none; + background: transparent; + cursor: pointer; + font-weight: bold; + color: #3F9FD8; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.chip button:hover { + background: #d0e8f5; +} + +.hint { + font-size: 12px; + color: #666; + margin-top: 4px; +} + +.copy-btn { + padding: 4px 10px; + background-color: #3F9FD8; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + transition: background-color 0.2s; + margin-left: 8px; + white-space: nowrap; +} + +.copy-btn:hover { + background-color: #357FAA; +} + +/* Token page styles */ +.back-button { + display: inline-block; + padding: 8px 16px; + background-color: #EF4B5C; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: fixed; + right: 20px; + bottom: 20px; +} + +.back-button:hover { + background-color: #C43B4B; +} + +.token-block { + background-color: #fff; + padding: 10px 15px; + border-radius: 8px; + margin-bottom: 15px; + word-wrap: break-word; + display: flex; + flex-direction: column; + gap: 5px; + position: relative; + overflow: hidden; + border: 1px solid #e0e0e0; +} + +.token-block form { + margin: 10px 0 0 0; + padding: 0; + background-color: transparent; + box-shadow: none; + border-radius: 0; +} + +.token-title { + font-weight: bold; + display: flex; + justify-content: space-between; + align-items: center; +} + +.token-title a { + font-size: 0.9em; + text-decoration: none; + color: #3F9FD8; +} + +.token-title a:hover { + text-decoration: underline; +} + +.token-code { + overflow-wrap: break-word; + word-break: break-all; + white-space: normal; +} + +pre { + white-space: pre-wrap; + background-color: #f9f9f9; + padding: 8px; + border-radius: 4px; + border: 1px solid #ddd; + margin: 0; + font-family: 'Courier New', Courier, monospace; + overflow-x: auto; + font-size: 0.9em; + position: relative; + margin-top: 5px; +} + +pre .key { + color: #c00; +} + +pre .string { + color: #080; +} + +pre .number { + color: #00f; +} + diff --git a/examples/example-app/static/token.js b/examples/example-app/static/token.js new file mode 100644 index 00000000..c147ce32 --- /dev/null +++ b/examples/example-app/static/token.js @@ -0,0 +1,82 @@ +// Simple JSON syntax highlighter +document.addEventListener("DOMContentLoaded", function() { + const claimsElement = document.getElementById("claims"); + if (claimsElement) { + try { + const json = JSON.parse(claimsElement.textContent); + claimsElement.innerHTML = syntaxHighlight(json); + } catch (e) { + console.error("Invalid JSON in claims:", e); + } + } +}); + +function syntaxHighlight(json) { + if (typeof json != 'string') { + json = JSON.stringify(json, undefined, 2); + } + json = json.replace(/&/g, '&').replace(//g, '>'); + return json.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g, function (match) { + let cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '' + match + ''; + }); +} + +function copyPublicKey() { + const publicKeyElement = document.getElementById("public-key"); + if (!publicKeyElement) return; + + const text = publicKeyElement.textContent; + + // Use modern clipboard API if available + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(() => { + showCopyFeedback("Copied!"); + }).catch(err => { + console.error("Failed to copy:", err); + fallbackCopy(text); + }); + } else { + fallbackCopy(text); + } +} + +function fallbackCopy(text) { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand("copy"); + showCopyFeedback("Copied!"); + } catch (err) { + console.error("Fallback copy failed:", err); + showCopyFeedback("Failed to copy"); + } + document.body.removeChild(textarea); +} + +function showCopyFeedback(message) { + const btn = event.target; + const originalText = btn.textContent; + btn.textContent = message; + btn.style.backgroundColor = "#28a745"; + setTimeout(() => { + btn.textContent = originalText; + btn.style.backgroundColor = ""; + }, 2000); +} + diff --git a/examples/example-app/templates.go b/examples/example-app/templates.go index 7107eb87..d15ecdea 100644 --- a/examples/example-app/templates.go +++ b/examples/example-app/templates.go @@ -1,366 +1,159 @@ package main import ( + "context" + "crypto/rsa" + "crypto/x509" + "embed" + "encoding/base64" + "encoding/json" + "encoding/pem" "html/template" + "io/fs" "log" + "math/big" "net/http" + "net/url" + + "github.com/coreos/go-oidc/v3/oidc" ) -const css = ` - body { - font-family: Arial, sans-serif; - background-color: #f2f2f2; - margin: 0; - } +//go:embed templates/*.html +var templatesFS embed.FS - .header { - text-align: center; - margin-bottom: 20px; - } +//go:embed static/* +var staticFS embed.FS - .dex { - font-size: 2em; - font-weight: bold; - color: #3F9FD8; /* Main color */ - } +const dexLogoDataURI = "/static/dex-glyph-color.svg" - .example-app { - font-size: 1em; - color: #EF4B5C; /* Secondary color */ - } +var ( + indexTmpl *template.Template + tokenTmpl *template.Template + staticHandler http.Handler +) - .form-instructions { - text-align: center; - margin-bottom: 15px; - font-size: 1em; - color: #555; +func init() { + var err error + indexTmpl, err = template.ParseFS(templatesFS, "templates/index.html") + if err != nil { + log.Fatalf("failed to parse index template: %v", err) } - hr { - border: none; - border-top: 1px solid #ccc; - margin-top: 10px; - margin-bottom: 20px; + tokenTmpl, err = template.ParseFS(templatesFS, "templates/token.html") + if err != nil { + log.Fatalf("failed to parse token template: %v", err) } - label { - flex: 1; - font-weight: bold; - color: #333; + // Create handler for static files + staticSubFS, err := fs.Sub(staticFS, "static") + if err != nil { + log.Fatalf("failed to create static sub filesystem: %v", err) } + staticHandler = http.FileServer(http.FS(staticSubFS)) +} - p { - margin-bottom: 15px; - display: flex; - align-items: center; - } +func renderIndex(w http.ResponseWriter, data indexPageData) { + renderTemplate(w, indexTmpl, data) +} - input[type="text"] { - flex: 2; - padding: 8px; - border: 1px solid #ccc; - border-radius: 4px; - outline: none; - } +type indexPageData struct { + ScopesSupported []string + LogoURI string +} - input[type="checkbox"] { - margin-left: 10px; - transform: scale(1.2); - } +type tokenTmplData struct { + IDToken string + IDTokenJWTLink string + AccessToken string + AccessTokenJWTLink string + RefreshToken string + RedirectURL string + Claims string + PublicKeyPEM string +} - .back-button { - display: inline-block; - padding: 8px 16px; - background-color: #EF4B5C; /* Secondary color */ - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 12px; - text-decoration: none; - transition: background-color 0.3s ease, transform 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - position: fixed; - right: 20px; - bottom: 20px; - } +func generateJWTIOLink(token string, provider *oidc.Provider, ctx context.Context) string { + // JWT.io doesn't support automatic public key via URL parameter + // The public key is displayed separately on the page for manual copy-paste + return "https://jwt.io/#debugger-io?token=" + url.QueryEscape(token) +} - .back-button:hover { - background-color: #C43B4B; /* Darker shade of secondary color */ +func getPublicKeyPEM(provider *oidc.Provider) string { + if provider == nil { + return "" } - .token-block { - background-color: #fff; - padding: 10px 15px; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin-bottom: 15px; - word-wrap: break-word; - display: flex; - flex-direction: column; - gap: 5px; - position: relative; + jwksURL := provider.Endpoint().AuthURL + if len(jwksURL) > 5 { + jwksURL = jwksURL[:len(jwksURL)-5] + "/keys" + } else { + return "" } - .token-title { - font-weight: bold; - display: flex; - justify-content: space-between; - align-items: center; + resp, err := http.Get(jwksURL) + if err != nil { + return "" } + defer resp.Body.Close() - .token-title a { - font-size: 0.9em; - text-decoration: none; - color: #3F9FD8; /* Main color */ + var jwks struct { + Keys []json.RawMessage `json:"keys"` } - - .token-title a:hover { - text-decoration: underline; + if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil || len(jwks.Keys) == 0 { + return "" } - .token-code { - overflow-wrap: break-word; - word-break: break-all; - white-space: normal; - } - - pre { - white-space: pre-wrap; - background-color: #f9f9f9; - padding: 8px; - border-radius: 4px; - border: 1px solid #ddd; - margin: 0; - font-family: 'Courier New', Courier, monospace; - overflow-x: auto; - font-size: 0.9em; - position: relative; - margin-top: 5px; + var key struct { + N string `json:"n"` + E string `json:"e"` + Kty string `json:"kty"` } - - pre .key { - color: #c00; + if err := json.Unmarshal(jwks.Keys[0], &key); err != nil || key.Kty != "RSA" { + return "" } - pre .string { - color: #080; + nBytes, err1 := base64.RawURLEncoding.DecodeString(key.N) + eBytes, err2 := base64.RawURLEncoding.DecodeString(key.E) + if err1 != nil || err2 != nil { + return "" } - pre .number { - color: #00f; + var eInt int + for _, b := range eBytes { + eInt = eInt<<8 | int(b) } -` -var indexTmpl = template.Must(template.New("index.html").Parse(` - - - - - - Example App - Login - - - -
-
Dex
-
Example App
-
-
-
- If needed, customize your login settings below, then click Login to proceed. -
-
-

- - -

-

- - -

-

- - -

-

- - -

-

- -

-
- -`)) + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + return "" + } -func renderIndex(w http.ResponseWriter) { - renderTemplate(w, indexTmpl, nil) -} + pubKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + }) -type tokenTmplData struct { - IDToken string - AccessToken string - RefreshToken string - RedirectURL string - Claims string + return string(pubKeyPEM) } -var tokenTmpl = template.Must(template.New("token.html").Parse(` - - - - - - Tokens - - - - {{ if .IDToken }} -
-
- ID Token: - Decode on jwt.io -
-
{{ .IDToken }}
-
- {{ end }} - - {{ if .AccessToken }} -
-
- Access Token: - Decode on jwt.io -
-
{{ .AccessToken }}
-
- {{ end }} - - {{ if .Claims }} -
-
Claims:
-
{{ .Claims }}
-
- {{ end }} - - {{ if .RefreshToken }} -
-
Refresh Token:
-
{{ .RefreshToken }}
-
- - -
-
- {{ end }} - - Back to Home - - - - -`)) - -func renderToken(w http.ResponseWriter, redirectURL, idToken, accessToken, refreshToken, claims string) { - renderTemplate(w, tokenTmpl, tokenTmplData{ - IDToken: idToken, - AccessToken: accessToken, - RefreshToken: refreshToken, - RedirectURL: redirectURL, - Claims: claims, - }) +func renderToken(w http.ResponseWriter, provider *oidc.Provider, redirectURL, idToken, accessToken, refreshToken, claims string) { + ctx := context.Background() + data := tokenTmplData{ + IDToken: idToken, + IDTokenJWTLink: generateJWTIOLink(idToken, provider, ctx), + AccessToken: accessToken, + AccessTokenJWTLink: generateJWTIOLink(accessToken, provider, ctx), + RefreshToken: refreshToken, + RedirectURL: redirectURL, + Claims: claims, + PublicKeyPEM: getPublicKeyPEM(provider), + } + renderTemplate(w, tokenTmpl, data) } func renderTemplate(w http.ResponseWriter, tmpl *template.Template, data interface{}) { @@ -371,13 +164,9 @@ func renderTemplate(w http.ResponseWriter, tmpl *template.Template, data interfa switch err := err.(type) { case *template.Error: - // An ExecError guarantees that Execute has not written to the underlying reader. log.Printf("Error rendering template %s: %s", tmpl.Name(), err) - - // TODO(ericchiang): replace with better internal server error. http.Error(w, "Internal server error", http.StatusInternalServerError) default: - // An error with the underlying write, such as the connection being - // dropped. Ignore for now. + // An error with the underlying write, such as the connection being dropped. Ignore for now. } } diff --git a/examples/example-app/templates/index.html b/examples/example-app/templates/index.html new file mode 100644 index 00000000..494920d6 --- /dev/null +++ b/examples/example-app/templates/index.html @@ -0,0 +1,62 @@ + + + + + + Example App - Login + + + +
+ +
+
+ This is an example application for Dex OpenID Connect provider.
+ Learn more in the documentation. +
+
+ +
+
+ Advanced options +
+ + Select OpenID Connect scopes to request. Standard scopes are pre-selected. +
+ {{range .ScopesSupported}} +
+ + +
+ {{end}} +
+ {{if eq (len .ScopesSupported) 0}} +
No scopes from discovery - add custom scopes below.
+ {{end}} +
+ + +
+
+
+ + Each client is sent as audience:server:client_id scope. +
+
+ + +
+
+
+ + Specify a connector ID to bypass the connector selection screen. + +
+
+
+
+ + + + + diff --git a/examples/example-app/templates/token.html b/examples/example-app/templates/token.html new file mode 100644 index 00000000..f830e16b --- /dev/null +++ b/examples/example-app/templates/token.html @@ -0,0 +1,64 @@ + + + + + + Tokens + + + + {{ if .IDToken }} +
+
+ ID Token: + Decode on jwt.io +
+
{{ .IDToken }}
+
+ {{ end }} + + {{ if .AccessToken }} +
+
+ Access Token: + Decode on jwt.io +
+
{{ .AccessToken }}
+
+ {{ end }} + + {{ if .Claims }} +
+
Claims:
+
{{ .Claims }}
+
+ {{ end }} + + {{ if .RefreshToken }} +
+
Refresh Token:
+
{{ .RefreshToken }}
+
+ + +
+
+ {{ end }} + + {{ if .PublicKeyPEM }} +
+
+ Public Key (for JWT verification): + +
+
{{ .PublicKeyPEM }}
+
Copy this key and paste it into jwt.io's "Verify Signature" section to validate the token signature.
+
+ {{ end }} + + Back to Home + + + + +