Last active
October 18, 2022 05:03
-
-
Save zostay/9684420 to your computer and use it in GitHub Desktop.
Google Authentication Workflow for Pebble Watchapps
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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