Skip to content

Instantly share code, notes, and snippets.

@zostay
Last active October 18, 2022 05:03
Show Gist options
  • Save zostay/9684420 to your computer and use it in GitHub Desktop.
Save zostay/9684420 to your computer and use it in GitHub Desktop.
Google Authentication Workflow for Pebble Watchapps
None of the workflow examples given in the Google Authentication OAuth2 documentation
(https://developers.google.com/accounts/docs/OAuth2) handle what's needed for Pebble. I
basically had to mix the needs of a client-side application with an offline web application
to get what's needed and work within the restrictions of the Pebble JS toolkit.
The steps are as follows:
1. Setup a Client ID for Web Application on the Google Developer Console
2. On the configuration web pages, with SSL:
* In the configuration page, use JavaScript to retrieve a authorization code, which
is sent to a second configuration page after the user logs in and agrees to allow
the application.
* Catch the authorization code on the second page and redirect it back to the main
configuration page.
* Closing the configuration page sends the code back to the Pebble Watchapp's internal
JavaScript
3. In the Pebble JS code:
* Retrieve the refresh_token and access_token using the authorization code
* When needed for an API call, verify that the access_token is valid and no expired.
* Whenever expired retrieve a new access_token using the stored refresh_token.
It took quite a bit of experimentation to find a way that worked just like I needed. At
this point, I have not fully tested this all the way through, so there might be a bug or
some fatal mistake here, but I'm reasonably sure this works.
Unfortunately, I have not found a good way of handling all of this while the
Configuration page is up without creating a full blown webapp running server side code
(which I don't really want to do). However, if that were done, you could at least
pre-verify the authorization code and possibly avoid some authorization errors.
<!DOCTYPE html>
<html>
<head>
<title>Verify Google Identity</title>
</head>
<body>
<script type="text/javascript">
var CONFIG_URL = 'https://.../configuration.html#';
function next_page(conf) {
var json = JSON.stringify(conf);
window.location.href = CONFIG_URL + json;
}
function error(msg) {
next_page({
"code": '',
"code_error": msg
});
}
var code_info = window.location.search;
if (code_info) {
code_info = code_info.substring(1);
}
var code_info = window.location.search;
if (code_info) {
code_info = code_info.substring(1);
}
else {
error('There was a problem communicating with Google. You may need to try again later (-3).');
}
// Straight from the Google...
var params = {}, queryString = code_info,
regex = /([^&=]+)=([^&]*)/g, m;
while (m = regex.exec(queryString)) {
params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
if (params.code) {
next_page(params);
}
else {
error('You did not grant the Watchapp access to your calendar (-1).');
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Watchapp Configuration</title>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0-alpha.1/handlebars.min.js"></script>
</head>
<body>
<h1>Watchapp Configuration</h1>
<script id="hb-template" type="text/x-handlebars-template">
{{#if code}}<p>Google Calendar is setup.</p>{{/if}}
{{#if code_error}}<p style="color: red">{{code_error}}</p>{{/if}}
{{#unless code}}
<p><a href="https://accounts.google.com/o/oauth2/auth?response_type=code&access_type=offline&prompt=consent%20select_account&client_id={{GOOGLE_CLIENT_ID}}&redirect_uri={{CATCHAUTH_URL}}&scope={{SCOPE}}">
Give Watchapp access to your Google Calendar
</a></p>
{{/unless}}
{{#if code}}<p><a href="javascript:void(0)" id="clear-code">Stop using your Google Calendar</a></p>
<p>Clicking the link above does not prevent the Watchapp from
having access to your calendar, it just tells it to stop using it. To
completely disable access to your calendar, you need to visit the <a
completely disable access to your calendar, you need to visit the <a
href="https://security.google.com/settings/security/permissions?pli=1">Google
Account Permissions</a> page to revoke access to this
watch application.</p>
{{/if}}
<form>
<input type="hidden" id="code" name="code" value="{{code}}">
<input type="button" id="finished" name="submit" value="Save">
</form>
</script>
<script type="text/javascript">
var json = window.location.hash;
if (json) {
json = json.substring(1);
}
else {
json = "{}";
}
var conf = JSON.parse(json);
var source = $("#hb-template").html();
var template = Handlebars.compile(source);
conf.CONFIG_URL = "https://.../configuration.html#"
conf.CATCHAUTH_URL = "https://.../catchauth.html";
$('body').append(template(conf));
$('#clear-code').on('click', function() {
conf.code = '';
window.location.href = conf.CONFIG_URL + JSON.stringify(conf);
window.location.reload();
});
$('#finished').on('click', function() {
window.location.href = "pebblejs://close#" + JSON.stringify({
"code": $("#code").val()
});
});
</script>
</body>
</html>
var GOOGLE_CLIENT_ID = "...";
var GOOGLE_CLIENT_SECRET = "...";
// Retrieves the refresh_token and access_token.
// - code - the authorization code from Google.
function resolve_tokens(code) {
var req = new XMLHttpRequest();
req.open("POST", "https://accounts.google.com/o/oauth2/token", true);
req.onload = function(e) {
var db = window.localStorage;
if (req.readyState == 4 && req.status == 200) {
var result = JSON.parse(req.responseText);
if (result.refresh_token && result.access_token) {
db.setItem("refresh_token", result.refresh_token);
db.setItem("access_token", result.access_token);
return;
}
}
db.removeItem("code");
db.setItem("code_error", "Unable to verify the your Google authentication.");
};
req.send("code="+encodeURIComponent(params.code)
+"&client_id="+GOOGLE_CLIENT_ID
+"&client_secret="+GOOGLE_CLIENT_SECRET
+"&redirect_uri=https://pebble.zostay.com/v1/catchauth.html"
+"&grant_type=authorization_code");
}
// Runs some code after validating and possibly refreshing the access_token.
// - code - code to run with the access_token, called like code(access_token)
function use_access_token(code) {
var db = window.localStorage;
var refresh_token = db.getItem("refresh_token");
var access_token = db.getItem("access_token");
if (!refresh_token) return;
valid_token(access_token, code, function() {
refresh_access_token(refresh_token, code)
});
}
// Validates the access token.
// - access_token - the access_token to validate
// - good - the code to run when the access_token is good, run like good(access_token)
// - bad - the code to run when the access_token is expired, run like bad()
function valid_token(access_token, good, bad) {
var req = new XMLHttpRequest();
req.open("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=" + access_token, true);
req.onload = function(e) {
if (req.readyState == 4 && req.status == 200) {
var result = JSON.parse(req.responseText);
if (result.audience != GOOGLE_CLIENT_ID) {
var db = window.localStorage;
db.removeItem("code");
db.removeItem("access_token");
db.removeItem("refresh_token");
db.setItem("code_error", "There was an error validating your Google Authentication. Please re-authorize access to your account.");
return;
}
good(access_token);
}
bad();
};
req.send(null);
}
// Refresh a stale access_token.
// - refresh_token - the refresh_token to use to retreive a new access_token
// - code - code to run with the new access_token, run like code(access_token)
function refresh_access_token(refresh_token, code) {
var req = new XMLHttpRequest();
req.open("POST", "https://accounts.google.com/o/oauth2/token", true);
req.onload = function(e) {
if (req.readyState == 4 && req.status == 200) {
var result = JSON.parse(req.responseText);
if (result.access_token) {
var db = window.localStorage;
db.setItem("access_token", result.access_token);
code(result.access_token);
}
}
};
req.send("refresh_token="+encodeURIComponent(refresh_token)
+"&client_id="+GOOGLE_CLIENT_ID,
+"&client_secret="+GOOGLE_CLIENT_SECRET,
+"&grant_type=refresh_token");
}
// Finally, execute our API calls, which will then pass messages back to the watch to show stuff
function do_google_api() {
use_access_token(function(access_token) {
// use Google Calendar or whatever here...
});
}
// When you click on Settings in Pebble's phone app. Go to the configuration.html page.
function show_configuration() {
var db = window.localStorage;
var code = db.getItem("code");
var code_error = db.getItem("code_error");
db.removeItem("code_error");
var json = JSON.stringify({
"code": code,
"code_error": code_error
});
Pebble.openURL(CONFIG_URL + json);
}
// When you click Save on the configuration.html page, recieve the configuration response here.
function webview_closed(e) {
var json = e.response;
var config = JSON.parse(json);
var code = config.code;
var db = window.localStorage;
var old_code = db.getItem("code");
if (old_code != code) {
db.setItem("code", code);
db.removeItem("refresh_token");
db.removeItem("access_token");
}
resolve_tokens(code);
}
// Setup the configuration events
Pebble.addEventListener("showConfiguration", show_configuration);
Pebble.addEventListener("webviewclosed", webview_closed);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment