diff --git a/api-samples/identity/README.md b/api-samples/identity/README.md new file mode 100644 index 00000000..aed08b90 --- /dev/null +++ b/api-samples/identity/README.md @@ -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. diff --git a/api-samples/identity/assets/screenshot.png b/api-samples/identity/assets/screenshot.png new file mode 100644 index 00000000..758a5f8d Binary files /dev/null and b/api-samples/identity/assets/screenshot.png differ diff --git a/api-samples/identity/identity.js b/api-samples/identity/identity.js new file mode 100755 index 00000000..6f5a949c --- /dev/null +++ b/api-samples/identity/identity.js @@ -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); diff --git a/api-samples/identity/index.html b/api-samples/identity/index.html new file mode 100755 index 00000000..d3f50ebb --- /dev/null +++ b/api-samples/identity/index.html @@ -0,0 +1,30 @@ + + + Identity API Sample Extension + + + + +
+

Identity API

+ +
+
+
+

OAuth on Google properties

+
+ + + +
+
+
+
+ +
+ + diff --git a/api-samples/identity/main.js b/api-samples/identity/main.js new file mode 100755 index 00000000..9ad9eff7 --- /dev/null +++ b/api-samples/identity/main.js @@ -0,0 +1,6 @@ +chrome.action.onClicked.addListener(() => { + chrome.tabs.create({ + active: true, + url: "index.html" + }) +}) \ No newline at end of file diff --git a/api-samples/identity/manifest.json b/api-samples/identity/manifest.json new file mode 100755 index 00000000..d435e2d7 --- /dev/null +++ b/api-samples/identity/manifest.json @@ -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"] + } +} diff --git a/api-samples/identity/style.css b/api-samples/identity/style.css new file mode 100644 index 00000000..7d6a98e1 --- /dev/null +++ b/api-samples/identity/style.css @@ -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); +}