mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-09 07:28:05 -05:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
173
internal/server/static/js/auth.js
Normal file
173
internal/server/static/js/auth.js
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user