feat: add login with google button for automatic id token retrieval (#1044)

Add `Sign in with Google` button within Toolbox UI's `Edit Header` modal
that automatically retrieves a valid ID token for users based on an
input clientID.

This should make it significantly easier/faster for users to format
request headers properly and test tools that use auth.
This commit is contained in:
AlexTalreja
2025-08-05 22:55:28 +00:00
committed by GitHub
parent c60a9601b4
commit d91bdfcbdc
6 changed files with 313 additions and 14 deletions

View File

@@ -4,6 +4,7 @@
--text-secondary-gray: #6e6e6e;
--button-primary: var(--toolbox-blue);
--button-secondary: #616161;
--section-border: #e0e0e0;
}
body {
@@ -161,6 +162,12 @@ body {
background-color: var(--button-secondary)
}
.btn--setup-gis {
background-color: white;
color: var(--text-primary-gray);
border: 2px solid var(--text-primary-gray);
}
.tool-button {
display: flex;
align-items: center;
@@ -379,7 +386,7 @@ body {
}
.auth-param-input {
background-color: #e0e0e0;
background-color: var(--section-border);
cursor: not-allowed;
}
@@ -513,6 +520,89 @@ body {
}
}
.auth-method-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
}
.auth-method-label {
font-weight: 500;
color: var(--text-primary-gray);
word-break: break-word;
}
.auth-helper-section {
border: 1px solid var(--section-border);
background-color: transparent;
padding: 16px;
border-radius: 8px;
margin-top: 20px;
width: 80%;
}
.auth-method-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.auth-method-details {
padding: 16px;
border: 1px solid var(--section-border);
border-radius: 4px;
margin-bottom: 16px;
background-color: #fff;
overflow-x: auto;
}
.auth-controls {
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.auth-input-row {
display: flex;
flex-direction: column;
gap: 6px;
& label {
font-size: 14px;
color: var(--text-primary-gray);
margin-bottom: 4px;
}
}
.auth-input {
padding: 8px 8px;
border: 1px solid #bdc1c6;
border-radius: 4px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
&:focus {
outline: none;
border-color: var(--toolbox-blue);
box-shadow: 0 0 0 1px #1a73e8;
}
}
.auth-method-actions {
display: flex;
align-items: center;
gap: 12px;
}
.auth-instructions {
font-size: 0.8em;
margin-top: 5px;
color: var(--text-secondary-gray);
}
.tool-response {
margin: 20px 0 0 0;
@@ -577,4 +667,17 @@ body {
}
}
.toggle-details-tab {
background-color: transparent;
color: var(--toolbox-blue);
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
font-weight: bold;
&:hover {
opacity: 0.8;
}
}

View File

@@ -0,0 +1,173 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Renders the Google Sign-In button using the GIS library.
* @param {string} toolId The ID of the tool.
* @param {string} clientId The Google OAuth Client ID.
* @param {string} authProfileName The name of the auth service in tools file.
*/
function renderGoogleSignInButton(toolId, clientId, authProfileName) {
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
const GIS_CONTAINER_ID = `gisContainer-${UNIQUE_ID_BASE}`;
const gisContainer = document.getElementById(GIS_CONTAINER_ID);
const setupGisBtn = document.querySelector(`#google-auth-details-${UNIQUE_ID_BASE} .btn--setup-gis`);
if (!gisContainer) {
console.error('GIS container not found:', GIS_CONTAINER_ID);
return;
}
if (!clientId) {
alert('Please enter an OAuth Client ID first.');
return;
}
// hide the setup button and show the container for the GIS button
if (setupGisBtn) setupGisBtn.style.display = 'none';
gisContainer.innerHTML = '';
gisContainer.style.display = 'flex';
if (window.google && window.google.accounts && window.google.accounts.id) {
try {
const handleResponse = (response) => handleCredentialResponse(response, toolId, authProfileName);
window.google.accounts.id.initialize({
client_id: clientId,
callback: handleResponse,
auto_select: false
});
window.google.accounts.id.renderButton(
gisContainer,
{ theme: "outline", size: "large", text: "signin_with" }
);
} catch (error) {
console.error("Error initializing Google Sign-In:", error);
alert("Error initializing Google Sign-In. Check the Client ID and browser console.");
gisContainer.innerHTML = '<p style="color: red;">Error loading Sign-In button.</p>';
if (setupGisBtn) setupGisBtn.style.display = '';
}
} else {
console.error("GIS library not fully loaded yet.");
alert("Google Identity Services library not ready. Please try again in a moment.");
gisContainer.innerHTML = '<p style="color: red;">GIS library not ready.</p>';
if (setupGisBtn) setupGisBtn.style.display = '';
}
}
/**
* Handles the credential response from the Google Sign-In library.
* @param {!CredentialResponse} response The credential response object from GIS.
* @param {string} toolId The ID of the tool.
* @param {string} authProfileName The name of the auth service in tools file.
*/
function handleCredentialResponse(response, toolId, authProfileName) {
console.debug("handleCredentialResponse called with:", { response, toolId, authProfileName });
const headersTextarea = document.getElementById(`headers-textarea-${toolId}`);
if (!headersTextarea) {
console.error('Headers textarea not found for toolId:', toolId);
return;
}
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
const setupGisBtn = document.querySelector(`#google-auth-details-${UNIQUE_ID_BASE} .setup-gis-btn`);
const gisContainer = document.getElementById(`gisContainer-${UNIQUE_ID_BASE}`);
if (response.credential) {
const idToken = response.credential;
try {
let currentHeaders = {};
if (headersTextarea.value) {
currentHeaders = JSON.parse(headersTextarea.value);
}
const HEADER_KEY = `${authProfileName}_token`;
currentHeaders[HEADER_KEY] = `${idToken}`;
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
if (gisContainer) gisContainer.style.display = 'none';
if (setupGisBtn) setupGisBtn.style.display = '';
} catch (e) {
alert('Headers are not valid JSON. Please correct and try again.');
console.error("Header JSON parse error:", e);
}
} else {
console.error("Error: No credential in response", response);
alert('Error: No ID Token received. Check console for details.');
if (gisContainer) gisContainer.style.display = 'none';
if (setupGisBtn) setupGisBtn.style.display = '';
}
}
// creates the Google Auth method dropdown
export function createGoogleAuthMethodItem(toolId, authProfileName) {
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
const item = document.createElement('div');
item.className = 'auth-method-item';
item.innerHTML = `
<div class="auth-method-header">
<span class="auth-method-label">Google ID Token (${authProfileName})</span>
<button class="toggle-details-tab">Auto Setup</button>
</div>
<div class="auth-method-details" id="google-auth-details-${UNIQUE_ID_BASE}" style="display: none;">
<div class="auth-controls">
<div class="auth-input-row">
<label for="clientIdInput-${UNIQUE_ID_BASE}">OAuth Client ID:</label>
<input type="text" id="clientIdInput-${UNIQUE_ID_BASE}" placeholder="Enter Client ID" class="auth-input">
</div>
<div class="auth-instructions">
<strong>Action Required:</strong> Add this URL (e.g., http://localhost:PORT) to the Client ID's <strong>Authorized JavaScript origins</strong> to avoid a 401 error. If using Google OAuth,
navigate to <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a> for this setting.
</div>
<div class="auth-method-actions">
<button class="btn btn--setup-gis">Continue</button>
<div id="gisContainer-${UNIQUE_ID_BASE}" class="auth-interactive-element gis-container" style="display: none;"></div>
</div>
</div>
</div>
`;
const toggleBtn = item.querySelector('.toggle-details-tab');
const detailsDiv = item.querySelector(`#google-auth-details-${UNIQUE_ID_BASE}`);
const setupGisBtn = item.querySelector('.btn--setup-gis');
const clientIdInput = item.querySelector(`#clientIdInput-${UNIQUE_ID_BASE}`);
const gisContainer = item.querySelector(`#gisContainer-${UNIQUE_ID_BASE}`);
toggleBtn.addEventListener('click', () => {
const isVisible = detailsDiv.style.display === 'flex';
detailsDiv.style.display = isVisible ? 'none' : 'flex';
toggleBtn.textContent = isVisible ? 'Auto Setup' : 'Close';
if (!isVisible) {
if (gisContainer) {
gisContainer.innerHTML = '';
gisContainer.style.display = 'none';
}
if (setupGisBtn) {
setupGisBtn.style.display = '';
}
}
});
setupGisBtn.addEventListener('click', () => {
const clientId = clientIdInput.value;
if (!clientId) {
alert('Please enter an OAuth Client ID first.');
return;
}
renderGoogleSignInButton(toolId, clientId, authProfileName);
});
return item;
}

View File

@@ -128,6 +128,7 @@ async function fetchToolDetails(toolName, toolDisplayArea) {
id: toolName,
name: toolName,
description: toolObject.description || "No description provided.",
authRequired: toolObject.authRequired || [],
parameters: (toolObject.parameters || []).map(param => {
let inputType = 'text';
const apiType = param.type ? param.type.toLowerCase() : 'string';

View File

@@ -13,6 +13,7 @@
// limitations under the License.
import { handleRunTool, displayResults } from './runTool.js';
import { createGoogleAuthMethodItem } from './auth.js'
/**
* Helper function to create form inputs for parameters.
@@ -151,7 +152,7 @@ function createParamInput(param, toolId) {
* parsed. The function receives the updated headers object as its argument.
* @return {!HTMLDivElement} The outermost div element of the created modal.
*/
function createHeaderEditorModal(toolId, currentHeaders, saveCallback) {
function createHeaderEditorModal(toolId, currentHeaders, toolParameters, authRequired, saveCallback) {
const MODAL_ID = `header-modal-${toolId}`;
let modal = document.getElementById(MODAL_ID);
@@ -174,9 +175,37 @@ function createHeaderEditorModal(toolId, currentHeaders, saveCallback) {
headersTextarea.rows = 10;
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
// handle authenticated params
const authProfileNames = new Set();
toolParameters.forEach(param => {
const isAuthParam = param.authServices && param.authServices.length > 0;
if (isAuthParam && param.authServices) {
param.authServices.forEach(name => authProfileNames.add(name));
}
});
// handle authorized invocations
if (authRequired && authRequired.length > 0) {
authRequired.forEach(name => authProfileNames.add(name));
}
modalContent.appendChild(modalHeader);
modalContent.appendChild(headersTextarea);
if (authProfileNames.size > 0 || authRequired.length > 0) {
const authHelperSection = document.createElement('div');
authHelperSection.className = 'auth-helper-section';
const authList = document.createElement('div');
authList.className = 'auth-method-list';
authProfileNames.forEach(profileName => {
const authItem = createGoogleAuthMethodItem(toolId, profileName);
authList.appendChild(authItem);
});
authHelperSection.appendChild(authList);
modalContent.appendChild(authHelperSection);
}
const modalActions = document.createElement('div');
const closeButton = document.createElement('button');
const saveButton = document.createElement('button');
@@ -205,13 +234,6 @@ function createHeaderEditorModal(toolId, currentHeaders, saveCallback) {
modalContent.appendChild(authTokenDropdown);
modal.appendChild(modalContent);
// Close modal if clicked outside
window.addEventListener('click', (event) => {
if (event.target === modal) {
closeHeaderEditor(toolId);
}
});
return modal;
}
@@ -246,7 +268,7 @@ function createAuthTokenInfoDropdown() {
details.className = 'auth-token-details';
details.appendChild(summary);
summary.textContent = 'How to extract Google OAuth ID Token';
summary.textContent = 'How to extract Google OAuth ID Token manually';
content.className = 'auth-token-content';
// auth instruction dropdown
@@ -321,10 +343,9 @@ export function renderToolInterface(tool, containerElement) {
const updateLastResults = (newResults) => {
lastResults = newResults;
};
const updateCurrentHeaders = (newHeaders) => {
currentHeaders = newHeaders;
const newModal = createHeaderEditorModal(TOOL_ID, currentHeaders, updateCurrentHeaders);
const newModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, tool.authRequired, updateCurrentHeaders);
containerElement.appendChild(newModal);
};
@@ -428,7 +449,7 @@ export function renderToolInterface(tool, containerElement) {
containerElement.appendChild(responseContainer);
// create and append the header editor modal
const headerModal = createHeaderEditorModal(TOOL_ID, currentHeaders, updateCurrentHeaders);
const headerModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, tool.authRequired, updateCurrentHeaders);
containerElement.appendChild(headerModal);
prettifyCheckbox.addEventListener('change', () => {

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tools View</title>
<link rel="stylesheet" href="/ui/css/style.css">
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
<div id="navbar-container" data-active-nav="/ui/tools"></div>

View File

@@ -6,7 +6,7 @@
<title>Toolsets View</title>
<link rel="stylesheet" href="/ui/css/style.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
<div id="navbar-container" data-active-nav="/ui/toolsets"></div>