Add sample Identity API extension (#1432)

* Initial copy of _archive/apps/samples/identity

* Modify to run be an extension rather than a Chrome App.

* Update extension to manifest v3

* Update README

* Remove references to chrome://identity-internals

* Use Promises rather than callbacks

See https://developer.chrome.com/docs/extensions/develop/migrate/api-calls#replace-callbacks

* Apply code-review markups.

* Apply more code-review markups

* Format with prettier

* Use Promise.catch instead of runtime.lastError

* Update screenshot

---------

Co-authored-by: Oliver Dunk <oliverdunk@google.com>
This commit is contained in:
ljjlee
2025-03-20 15:17:39 +01:00
committed by GitHub
parent 8f97d22fdc
commit db2d231476
7 changed files with 369 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
# chrome.identity
A sample extension that uses the
[Identity API](https://developer.chrome.com/docs/extensions/reference/api/identity)
to request information of the logged in user and present this info on the
screen. If the user has a profile picture, their profile image is also fetched
and shown in the app.
## Overview
This extension uses the getAuthToken flow of the Identity API, so it only
works with Google accounts. If you want to identify the user in a non-Google
OAuth2 flow, you should use the launchWebAuthFlow method instead.
![screenshot](assets/screenshot.png)
## Running this extension
1. Clone this repository.
2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked).
3. Click the extension icon to open the UI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

197
api-samples/identity/identity.js Executable file
View File

@@ -0,0 +1,197 @@
'use strict';
function onLoad() {
const STATE_START = 1;
const STATE_ACQUIRING_AUTHTOKEN = 2;
const STATE_AUTHTOKEN_ACQUIRED = 3;
let state = STATE_START;
const signin_button = document.querySelector('#signin');
signin_button.addEventListener('click', interactiveSignIn);
const userinfo_button = document.querySelector('#userinfo');
userinfo_button.addEventListener(
'click',
getUserInfo.bind(userinfo_button, true)
);
const revoke_button = document.querySelector('#revoke');
revoke_button.addEventListener('click', revokeToken);
const user_info_div = document.querySelector('#user_info');
// Trying to get user's info without signing in, it will work if the
// application was previously authorized by the user.
getUserInfo(false);
function disableButton(button) {
button.setAttribute('disabled', 'disabled');
}
function enableButton(button) {
button.removeAttribute('disabled');
}
function changeState(newState) {
state = newState;
switch (state) {
case STATE_START:
enableButton(signin_button);
disableButton(userinfo_button);
disableButton(revoke_button);
break;
case STATE_ACQUIRING_AUTHTOKEN:
displayOutput('Acquiring token...');
disableButton(signin_button);
disableButton(userinfo_button);
disableButton(revoke_button);
break;
case STATE_AUTHTOKEN_ACQUIRED:
disableButton(signin_button);
enableButton(userinfo_button);
enableButton(revoke_button);
break;
}
}
function displayOutput(message) {
let messageStr = message;
if (typeof message != 'string') {
messageStr = JSON.stringify(message);
}
document.getElementById('__logarea').value = messageStr;
}
function fetchWithAuth(method, url, interactive, callback) {
let access_token;
let retry = true;
getToken();
function getToken() {
chrome.identity
.getAuthToken({ interactive: interactive })
.then((token) => {
access_token = token.token;
requestStart();
})
.catch((error) => {
callback(error.message);
});
}
function requestStart() {
fetch(url, {
method: method,
headers: {
Authorization: 'Bearer ' + access_token
}
}).then((response) => {
if (response.status == 401 && retry) {
retry = false;
chrome.identity
.removeCachedAuthToken({ token: access_token })
.then(getToken);
} else {
callback(null, response.status, response);
}
});
}
}
function getUserInfo(interactive) {
// See https://developers.google.com/identity/openid-connect/openid-connect#obtaininguserprofileinformation
fetchWithAuth(
'GET',
'https://openidconnect.googleapis.com/v1/userinfo',
interactive,
onUserInfoFetched
);
}
// Code updating the user interface, when the user information has been
// fetched or displaying the error.
function onUserInfoFetched(error, status, response) {
if (!error && status == 200) {
changeState(STATE_AUTHTOKEN_ACQUIRED);
response.json().then((user_info) => {
displayOutput(user_info);
populateUserInfo(user_info);
});
} else {
changeState(STATE_START);
}
}
function populateUserInfo(user_info) {
if (!user_info || !user_info.picture) return;
user_info_div.innerText = 'Hello ' + user_info.name;
const imgElem = document.createElement('img');
imgElem.src = user_info.picture;
imgElem.style.width = '24px';
user_info_div.insertAdjacentElement('afterbegin', imgElem);
}
// OnClick event handlers for the buttons.
/**
Retrieves a valid token. Since this is initiated by the user
clicking in the Sign In button, we want it to be interactive -
ie, when no token is found, the auth window is presented to the user.
Observe that the token does not need to be cached by the app.
Chrome caches tokens and takes care of renewing when it is expired.
In that sense, getAuthToken only goes to the server if there is
no cached token or if it is expired. If you want to force a new
token (for example when user changes the password on the service)
you need to call removeCachedAuthToken()
**/
function interactiveSignIn() {
changeState(STATE_ACQUIRING_AUTHTOKEN);
console.log('interactiveSignIn');
// This is the normal flow for authentication/authorization on Google
// properties. You need to add the oauth2 client_id and scopes to the app
// manifest. The interactive param indicates if a new window will be opened
// when the user is not yet authenticated or not.
//
// See https://developer.chrome.com/docs/extensions/reference/api/identity#method-getAuthToken
chrome.identity
.getAuthToken({ interactive: true })
.then((token) => {
displayOutput('Token acquired:\n' + token.token);
changeState(STATE_AUTHTOKEN_ACQUIRED);
})
.catch((error) => {
displayOutput(error.message);
changeState(STATE_START);
});
}
function revokeToken() {
user_info_div.innerHTML = '';
chrome.identity
.getAuthToken({ interactive: false })
.then((current_token) => {
// Remove the local cached token
chrome.identity.removeCachedAuthToken({ token: current_token.token });
// Make a request to revoke token in the server.
// See https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#tokenrevoke
fetch(
'https://oauth2.googleapis.com/revoke?token=' + current_token.token,
{ method: 'POST' }
).then(() => {
// Update the user interface accordingly
changeState(STATE_START);
displayOutput('Token revoked and removed from cache.');
});
});
}
}
window.addEventListener('load', onLoad);

30
api-samples/identity/index.html Executable file
View File

@@ -0,0 +1,30 @@
<html>
<head>
<title>Identity API Sample Extension</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="identity.js"></script>
</head>
<body>
<div class="header">
<h1>Identity API</h1>
<div class="links">
Learn more:
<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api/identity">API docs</a>
<a id="_open_snippets" href="https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/identity">Source</a>
</div>
<hr>
</div>
<div class="flows">
<h2>OAuth on Google properties</h2>
<div class="flow">
<button id="signin">Sign in</button>
<button id="userinfo" disabled>Get personal data</button>
<button id="revoke" disabled>Revoke token</button>
<div id="user_info"></div>
</div>
</div>
<div class="log">
<textarea id="__logarea" disabled></textarea>
</div>
</body>
</html>

6
api-samples/identity/main.js Executable file
View File

@@ -0,0 +1,6 @@
chrome.action.onClicked.addListener(() => {
chrome.tabs.create({
active: true,
url: "index.html"
})
})

View File

@@ -0,0 +1,17 @@
{
"name": "Identity API Sample",
"version": "4.0",
"manifest_version": 3,
"key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDJB6ZGcGxtlr/34s+TKgi84QiP7DMekqOjSUS2ubmbhchlM6CN9gYdGQ1aBI3TBXG3YaAu+XyutFA8M8NLLWc4OOGByWaGV11DP6p67g8a+Ids/gX6cNSRnRHiDZXAd44ATxoN4OZjZJk9iQ26RIUjwX07bzntlI+frwwKCk4WQIDAQAB",
"action": {},
"background": {
"service_worker": "main.js"
},
"permissions": ["identity"],
"oauth2": {
// client_id below is specific to the application key. Follow the
// documentation to obtain one for your app.
"client_id": "497291774654.apps.googleusercontent.com",
"scopes": ["https://www.googleapis.com/auth/userinfo.profile"]
}
}

View File

@@ -0,0 +1,98 @@
html, body {
margin: 0;
font-family: 'Open Sans Regular', sans;
color: #666;
background: #fafafa;
}
.header {
text-align: center;
padding: 10px 0 10px 0;
}
.header hr {
height: 6px;
border: none;
margin-top: 20px;
background: -webkit-linear-gradient(0deg,
rgb(51,105,232) 0%,
rgb(51,105,232) 25%,
rgb(222,30,37) 25%,
rgb(222,30,37) 50%,
rgb(255, 210, 0) 50%,
rgb(255, 210, 0) 75%,
rgb(76,187,71) 75%,
rgb(76,187,71) 100%
);
}
.header .links * {
margin-left: 5px;
}
.header .links a {
color: #666;
}
.header:hover .links a {
color: #3399cc;
}
.flows h2 {
margin-left: 5px;
margin-bottom: 10px;
}
.flow {
margin: 10px 20px 20px 20px;
border: 1px solid #ddd;
padding: 8px;
background: white;
}
.flow button {
cursor: pointer;
background: -webkit-linear-gradient(top,#008dfd 0,#0370ea 100%);
border: 1px solid #076bd2;
text-shadow: 1px 1px 1px #076bd2;
color: white;
font-weight: 700;
font-size: 13px;
margin: .5em 0 1em;
padding: 8px 17px 8px 17px;
border-radius: 3px;
}
.flow button.error {
background: -webkit-linear-gradient(top,#008dfd 0,#0370ea 100%);
border: 1px solid #076bd2;
text-shadow: 1px 1px 1px #076bd2;
color: white;
font-weight: 700;
font-size: 13px;
margin: .5em 0 1em;
padding: 8px 17px 8px 17px;
border-radius: 3px;
}
.flow button:disabled {
background: -webkit-linear-gradient(top, #dcdcdc 0, #fafafa 100%);
color: #999;
text-shadow: 1px 1px 1px #fafafa;
border: 1px solid #ddd;
cursor: auto;
}
.log {
margin: 10px 20px 20px 20px;
}
.log textarea {
width: 100%;
min-height: 200px;
font-family: "Courier New", Courier, monospace;
margin: 2px;
background: #FFFFFF;
color: #727272;
border: 1px solid rgb(182, 182, 182);
}