Skip to content

Commit 37ba9c5

Browse files
Add whoami function (#231)
### Description Add a whoami() function to proxy.mjs. It can be used to access the claims from the user's identity token. ### Type of change * [x] New feature * [ ] Feature improvement * [ ] Bug fix * [x] Documentation * [ ] Cleanup / refactoring * [ ] Other (please explain) ### How is this change tested ? * [ ] Unit tests * [x] Manual tests (explain) * [ ] Tests are not needed --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 56d413f commit 37ba9c5

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

proxy/backend-sso.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"crypto/sha256"
2828
_ "embed"
2929
"encoding/hex"
30+
"encoding/json"
3031
"fmt"
3132
"html/template"
3233
"net/http"
@@ -183,6 +184,28 @@ func (be *Backend) serveSSOProxyMJS(w http.ResponseWriter, req *http.Request) {
183184

184185
func (be *Backend) serveSSOStatus(w http.ResponseWriter, req *http.Request) {
185186
claims := fromctx.Claims(req.Context())
187+
188+
if req.Method == http.MethodPost {
189+
w.Header().Set("content-type", "application/json")
190+
if claims == nil {
191+
w.Write([]byte("null\n"))
192+
return
193+
}
194+
out := struct {
195+
Name string `json:"name,omitempty"`
196+
Email string `json:"email,omitempty"`
197+
Claims map[string]any `json:"claims,omitempty"`
198+
}{
199+
Claims: claims,
200+
}
201+
out.Name, _ = claims["name"].(string)
202+
out.Email, _ = claims["email"].(string)
203+
204+
enc := json.NewEncoder(w)
205+
enc.SetIndent("", " ")
206+
enc.Encode(out)
207+
return
208+
}
186209
var keys []string
187210
for k := range claims {
188211
keys = append(keys, k)

proxy/proxy.mjs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,20 @@
2121
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
// SOFTWARE.
2323

24+
/**
25+
* Extracts the session ID from the '__tlsproxySid' cookie.
26+
* @returns {string} The session ID, or an empty string if not found.
27+
*/
2428
function sessionId() {
2529
const m = document.cookie.match(/__tlsproxySid=([^;]*)(;|$)/);
2630
return m ? m[1] : '';
2731
}
2832

33+
/**
34+
* @summary Wraps the global fetch() function to automatically add an
35+
* x-csrf-token header to all requests.
36+
* @description This is a security measure to prevent Cross-Site Request Forgery (CSRF) attacks.
37+
*/
2938
const ofetch = window.fetch;
3039
window.fetch = function(res, opt) {
3140
if (!opt) {
@@ -38,16 +47,46 @@ window.fetch = function(res, opt) {
3847
return ofetch(res, opt);
3948
};
4049

50+
/**
51+
* @summary Logs the user out.
52+
* @description Sends a POST request to the logout endpoint and then redirects the
53+
* browser to the same URL to complete the process.
54+
* @returns {Promise<void>}
55+
*/
4156
export function logout() {
4257
return fetch('/.sso/logout', {
4358
method: 'POST',
4459
})
4560
.then(() => window.location = '/.sso/logout');
4661
}
4762

63+
/**
64+
* @summary Fetches information about the currently authenticated user.
65+
* @returns {Promise<Object>} A promise that resolves to a JSON object
66+
* containing user information.
67+
*/
68+
export function whoami() {
69+
return fetch('/.sso/', {
70+
method: 'POST',
71+
}).then(r => {
72+
if (!r.ok) {
73+
throw new Error(`HTTP error, status: ${r.status}`);
74+
}
75+
return r.json();
76+
});
77+
}
78+
79+
/** @type {string} The currently active language code (e.g., 'en', 'fr-CA'). */
4880
let currentLang = 'en';
81+
/** @type {Object<string, string>} A map of translation keys to their string values for the current language. */
4982
let langData = {};
5083

84+
/**
85+
* @summary Translates a given key into the current language.
86+
* @param {string} key The translation key to look up.
87+
* @returns {string} The translated string, or a placeholder '###key###' if the
88+
* key is not found.
89+
*/
5190
export function translate(key) {
5291
const value = langData[key];
5392
if (!value) {
@@ -56,6 +95,15 @@ export function translate(key) {
5695
return value;
5796
}
5897

98+
/**
99+
* @summary Sets the active language by fetching translation data from the server.
100+
* @description It takes a list of preferred languages (e.g., from navigator.languages),
101+
* expands it to include variants (e.g., 'en-US' -> 'en'), and requests the
102+
* best matching translation file from the server. If no match is found, it
103+
* defaults to 'en'.
104+
* @param {string[]} langs An array of language codes.
105+
* @returns {Promise<void>}
106+
*/
59107
function setLanguage(langs) {
60108
let opts = [];
61109
for (let lang of langs) {
@@ -87,9 +135,23 @@ function setLanguage(langs) {
87135
});
88136
}
89137

138+
/**
139+
* @summary Applies translations to the current document.
140+
* @description This async function performs several steps:
141+
* 1. Fetches the appropriate language data by calling setLanguage().
142+
* 2. Scans the DOM for all elements with a `tkey` attribute.
143+
* 3. Replaces the textContent (or placeholder) of each element with its translation.
144+
* 4. Sets the `lang` and `dir` (text direction) attributes on the <html> element.
145+
* 5. If any translations were applied and a language selector doesn't exist,
146+
* it dynamically creates and adds a `<select>` element to the page to
147+
* allow users to switch languages.
148+
* @param {string} [lang] - An optional language code to force a specific language.
149+
* If not provided, it defaults to the browser's `navigator.languages`.
150+
*/
90151
async function applyTranslations(lang) {
91152
await setLanguage(lang?[lang]:navigator.languages);
92153
let changed = false;
154+
// Find all elements with a `tkey` attribute and replace their content.
93155
document.querySelectorAll('[tkey]').forEach(e => {
94156
const value = translate(e.getAttribute('tkey'));
95157
if (e.tagName === 'INPUT' && e.hasAttribute('placeholder')) {
@@ -99,11 +161,13 @@ async function applyTranslations(lang) {
99161
}
100162
changed = true;
101163
});
164+
// If translations were applied, update the document's lang and dir.
102165
if (changed) {
103166
const html = document.querySelector('HTML');
104167
html.setAttribute('lang', currentLang);
105168
html.setAttribute('dir', translate('DIR') === 'rtl' ? 'rtl' : 'ltr');
106169
}
170+
// If translations were applied and no language selector exists, create one.
107171
if (changed && !document.getElementById('lang-selector')) {
108172
const b = document.createElement('select');
109173
b.id = 'lang-selector';
@@ -121,6 +185,7 @@ async function applyTranslations(lang) {
121185
b.style.zIndex = 10;
122186
b.style.backgroundColor = 'white';
123187
document.body.appendChild(b);
188+
// Populate the selector with available languages from the server.
124189
return fetch('/.sso/languages.json').then(r => r.json()).then(r => {
125190
for (let key in r) {
126191
const o = document.createElement('option');
@@ -134,4 +199,8 @@ async function applyTranslations(lang) {
134199
}
135200
}
136201

202+
/**
203+
* Kicks off the translation process once the initial HTML document has been
204+
* completely loaded and parsed.
205+
*/
137206
document.addEventListener('DOMContentLoaded', () => applyTranslations());

proxy/proxy.mjs.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# `proxy.mjs` - Client-Side Proxy Helper
2+
3+
This JavaScript module provides client-side functionality for HTML pages served by `tlsproxy`. It handles CSRF protection, session management, and internationalization (i18n) dynamically in the user's browser.
4+
5+
## Features
6+
7+
### 1. Automatic CSRF Protection
8+
9+
The script automatically protects against Cross-Site Request Forgery (CSRF) attacks.
10+
11+
- It wraps the standard `window.fetch` function.
12+
- Before any `fetch` request is sent, it reads the session ID from the `__tlsproxySid` cookie.
13+
- It then adds the session ID to the request headers as `x-csrf-token`.
14+
15+
This process is automatic. Any page that includes this module will have its `fetch` requests protected.
16+
17+
### 2. Session Management
18+
19+
The module exports functions to manage the user's authentication session.
20+
21+
- **`logout()`**:
22+
- Sends a `POST` request to the `/.sso/logout` endpoint to terminate the session on the backend.
23+
- Upon success, it redirects the user to the logout page.
24+
25+
- **`whoami()`**:
26+
- Sends a `POST` request to the `/.sso/` endpoint.
27+
- Returns a promise that resolves with a JSON object containing information about the currently authenticated user.
28+
29+
### 3. Internationalization (i18n)
30+
31+
The script provides dynamic, client-side translation of web pages.
32+
33+
- **Language Detection**: On page load, it detects the user's preferred languages from `navigator.languages`.
34+
- **Translation Loading**: It fetches the appropriate language file from `/.sso/languages.json` based on the detected language. It has a fallback mechanism to find the best-matching language or default to English (`en`).
35+
- **Dynamic Translation**:
36+
- It scans the document for any HTML elements that have a `tkey` attribute.
37+
- It replaces the content (or placeholder text for inputs) of these elements with the translated string corresponding to the `tkey` value.
38+
- It sets the `lang` and `dir` (text direction, e.g., `ltr` or `rtl`) attributes on the `<html>` tag.
39+
- **Language Selector**:
40+
- If a language selector element (with `id="lang-selector"`) does not already exist on the page, the script dynamically creates and appends one.
41+
- This `<select>` element allows the user to switch languages on the fly. It is populated with all available languages from the backend.
42+
43+
## Usage
44+
45+
This script is intended to be included as a module in HTML pages served by `tlsproxy`, such as the login, logout, or SSO status pages.
46+
47+
```html
48+
<script type="module">
49+
import { logout, whoami } from '/.sso/proxy.mjs';
50+
51+
// Example: Add a logout button
52+
const logoutButton = document.getElementById('logout-btn');
53+
logoutButton.addEventListener('click', () => {
54+
logout();
55+
});
56+
57+
// Example: Display user's name
58+
whoami().then(user => {
59+
if (user) {
60+
document.getElementById('user-name').textContent = user.name;
61+
}
62+
}).catch(error => {
63+
console.error('Failed to get user info:', error);
64+
});
65+
</script>
66+
67+
<!-- Example of an element that will be translated -->
68+
<h1 tkey="logout-button">Logout</h1>
69+
```

0 commit comments

Comments
 (0)