blob: e28d8be4519eb3bbfebc6fa6c3b2ae9f1da4a0b2 [file] [log] [blame]
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../google-apis/google-js-api.html">
<script>
(function() {
/**
* Enum of attributes to be passed through to the login API call.
* @readonly
* @enum {string}
*/
var ProxyLoginAttributes = {
'appPackageName': 'apppackagename',
'clientId': 'clientid',
'cookiePolicy': 'cookiepolicy',
'requestVisibleActions': 'requestvisibleactions',
'hostedDomain': 'hostedDomain'
};
/**
* AuthEngine does all interactions with gapi.auth2
*
* It is tightly coupled with <google-signin-aware> element
* The elements configure AuthEngine.
* AuthEngine propagates all authentication events to all google-signin-aware elements
*
* API used: https://developers.google.com/identity/sign-in/web/reference
*
*/
var AuthEngine = {
/**
* oauth2 argument, set by google-signin-aware
*/
_clientId: null,
get clientId() {
return this._clientId;
},
set clientId(val) {
if (this._clientId && val && val != this._clientId) {
throw new Error('clientId cannot change. Values do not match. New: ' + val + ' Old:' + this._clientId);
}
if (val) {
this._clientId = val;
this.initAuth2();
}
},
/**
* oauth2 argument, set by google-signin-aware
*/
_cookiePolicy: 'single_host_origin',
get cookiePolicy() {
return this._cookiePolicy;
},
set cookiePolicy(val) {
if (val) {
this._cookiePolicy = val;
}
},
/**
* oauth2 argument, set by google-signin-aware
*/
_appPackageName: '',
get appPackageName() {
return this._appPackageName;
},
set appPackageName(val) {
if (this._appPackageName && val && val != this._appPackageName) {
throw new Error('appPackageName cannot change. Values do not match. New: ' + val + ' Old: ' + this._appPackageName);
}
if (val) {
this._appPackageName = val;
}
},
/**
* oauth2 argument, set by google-signin-aware
*/
_requestVisibleActions: '',
get requestVisibleactions() {
return this._requestVisibleActions;
},
set requestVisibleactions(val) {
if (this._requestVisibleActions && val && val != this._requestVisibleActions) {
throw new Error('requestVisibleactions cannot change. Values do not match. New: ' + val + ' Old: ' + this._requestVisibleActions);
}
if (val)
this._requestVisibleActions = val;
},
/**
* oauth2 argument, set by google-signin-aware
*/
_hostedDomain: '',
get hostedDomain() {
return this._hostedDomain;
},
set hostedDomain(val) {
if (this._hostedDomain && val && val != this._hostedDomain) {
throw new Error('hostedDomain cannot change. Values do not match. New: ' + val + ' Old: ' + this._hostedDomain);
}
if (val)
this._hostedDomain = val;
},
/** Is offline access currently enabled in the google-signin-aware element? */
_offline: false,
get offline() {
return this._offline;
},
set offline(val) {
this._offline = val;
this.updateAdditionalAuth();
},
/** Should we force a re-prompt for offline access? */
_offlineAlwaysPrompt: false,
get offlineAlwaysPrompt() {
return this._offlineAlwaysPrompt;
},
set offlineAlwaysPrompt(val) {
this._offlineAlwaysPrompt = val;
this.updateAdditionalAuth();
},
/** Have we already gotten offline access from Google during this session? */
offlineGranted: false,
/** <google-js-api> */
_apiLoader: null,
/** an array of wanted scopes. oauth2 argument */
_requestedScopeArray: [],
/** _requestedScopeArray as string */
get requestedScopes() {
return this._requestedScopeArray.join(' ');
},
/** Is user signed in? */
_signedIn: false,
/** Currently granted scopes */
_grantedScopeArray: [],
/** True if additional authorization is required */
_needAdditionalAuth: true,
/** True if have google+ scopes */
_hasPlusScopes: false,
/**
* array of <google-signin-aware>
* state changes are broadcast to them
*/
signinAwares: [],
init: function() {
this._apiLoader = document.createElement('google-js-api');
this._apiLoader.addEventListener('js-api-load', this.loadAuth2.bind(this));
},
loadAuth2: function() {
gapi.load('auth2', this.initAuth2.bind(this));
},
initAuth2: function() {
if (!('gapi' in window) || !('auth2' in window.gapi) || !this.clientId) {
return;
}
var auth = gapi.auth2.init({
'client_id': this.clientId,
'cookie_policy': this.cookiePolicy,
'scope': this.requestedScopes,
'hosted_domain': this.hostedDomain
});
auth.currentUser.listen(this.handleUserUpdate.bind(this));
auth.then(
function success() {
// Let the current user listener trigger the changes.
},
function error(error) {
console.error(error);
}
);
},
handleUserUpdate: function(newPrimaryUser) {
// update and broadcast currentUser
var isSignedIn = newPrimaryUser.isSignedIn();
if (isSignedIn != this._signedIn) {
this._signedIn = isSignedIn;
for (var i=0; i<this.signinAwares.length; i++) {
this.signinAwares[i]._setSignedIn(isSignedIn);
}
}
// update granted scopes
this._grantedScopeArray = this.strToScopeArray(
newPrimaryUser.getGrantedScopes());
// console.log(this._grantedScopeArray);
this.updateAdditionalAuth();
var response = newPrimaryUser.getAuthResponse();
for (var i=0; i<this.signinAwares.length; i++) {
this.signinAwares[i]._updateScopeStatus(response);
}
},
setOfflineCode: function(code) {
for (var i=0; i<this.signinAwares.length; i++) {
this.signinAwares[i]._updateOfflineCode(code);
}
},
/** convert scope string to scope array */
strToScopeArray: function(str) {
if (!str) {
return [];
}
// remove extra spaces, then split
var scopes = str.replace(/\ +/g, ' ').trim().split(' ');
for (var i=0; i<scopes.length; i++) {
scopes[i] = scopes[i].toLowerCase();
// Handle scopes that will be deprecated but are still returned with their old value
if (scopes[i] === 'https://www.googleapis.com/auth/userinfo.profile') {
scopes[i] = 'profile';
}
if (scopes[i] === 'https://www.googleapis.com/auth/userinfo.email') {
scopes[i] = 'email';
}
}
// return with duplicates filtered out
return scopes.filter( function(value, index, self) {
return self.indexOf(value) === index;
});
},
/** true if scopes have google+ scopes */
isPlusScope: function(scope) {
return (scope.indexOf('/auth/games') > -1)
|| (scope.indexOf('auth/plus.') > -1 && scope.indexOf('auth/plus.me') < 0);
},
/** true if scopes have been granted */
hasGrantedScopes: function(scopeStr) {
var scopes = this.strToScopeArray(scopeStr);
for (var i=0; i< scopes.length; i++) {
if (this._grantedScopeArray.indexOf(scopes[i]) === -1)
return false;
}
return true;
},
/** request additional scopes */
requestScopes: function(newScopeStr) {
var newScopes = this.strToScopeArray(newScopeStr);
var scopesUpdated = false;
for (var i=0; i<newScopes.length; i++) {
if (this._requestedScopeArray.indexOf(newScopes[i]) === -1) {
this._requestedScopeArray.push(newScopes[i]);
scopesUpdated = true;
}
}
if (scopesUpdated) {
this.updateAdditionalAuth();
this.updatePlusScopes();
}
},
/** update status of _needAdditionalAuth */
updateAdditionalAuth: function() {
var needMoreAuth = false;
if ((this.offlineAlwaysPrompt || this.offline ) && !this.offlineGranted) {
needMoreAuth = true;
} else {
for (var i=0; i<this._requestedScopeArray.length; i++) {
if (this._grantedScopeArray.indexOf(this._requestedScopeArray[i]) === -1) {
needMoreAuth = true;
break;
}
}
}
if (this._needAdditionalAuth != needMoreAuth) {
this._needAdditionalAuth = needMoreAuth;
// broadcast new value
for (var i=0; i<this.signinAwares.length; i++) {
this.signinAwares[i]._setNeedAdditionalAuth(needMoreAuth);
}
}
},
updatePlusScopes: function() {
var hasPlusScopes = false;
for (var i = 0; i < this._requestedScopeArray.length; i++) {
if (this.isPlusScope(this._requestedScopeArray[i])) {
hasPlusScopes = true;
break;
}
}
if (this._hasPlusScopes != hasPlusScopes) {
this._hasPlusScopes = hasPlusScopes;
for (var i=0; i<this.signinAwares.length; i++) {
this.signinAwares[i]._setHasPlusScopes(hasPlusScopes);
}
}
},
/**
* attached <google-signin-aware>
* @param {<google-signin-aware>} aware element to add
*/
attachSigninAware: function(aware) {
if (this.signinAwares.indexOf(aware) == -1) {
this.signinAwares.push(aware);
// Initialize aware properties
aware._setNeedAdditionalAuth(this._needAdditionalAuth);
aware._setSignedIn(this._signedIn);
aware._setHasPlusScopes(this._hasPlusScopes);
} else {
console.warn('signinAware attached more than once', aware);
}
},
detachSigninAware: function(aware) {
var index = this.signinAwares.indexOf(aware);
if (index != -1) {
this.signinAwares.splice(index, 1);
} else {
console.warn('Trying to detach unattached signin-aware');
}
},
/** returns scopes not granted */
getMissingScopes: function() {
return this._requestedScopeArray.filter( function(scope) {
return this._grantedScopeArray.indexOf(scope) === -1;
}.bind(this)).join(' ');
},
assertAuthInitialized: function() {
if (!this.clientId) {
throw new Error("AuthEngine not initialized. clientId has not been configured.");
}
if (!('gapi' in window)) {
throw new Error("AuthEngine not initialized. gapi has not loaded.");
}
if (!('auth2' in window.gapi)) {
throw new Error("AuthEngine not initialized. auth2 not loaded.");
}
},
/** pops up sign-in dialog */
signIn: function() {
this.assertAuthInitialized();
var params = {
'scope': this.getMissingScopes()
};
// Proxy specific attributes through to the signIn options.
Object.keys(ProxyLoginAttributes).forEach(function(key) {
if (this[key] && this[key] !== '') {
params[ProxyLoginAttributes[key]] = this[key];
}
}, this);
var promise;
var user = gapi.auth2.getAuthInstance().currentUser.get();
if (!(this.offline || this.offlineAlwaysPrompt)) {
if (user.getGrantedScopes()) {
// additional auth, skip multiple account dialog
promise = user.grant(params);
} else {
// initial signin
promise = gapi.auth2.getAuthInstance().signIn(params);
}
} else {
params.redirect_uri = 'postmessage';
if (this.offlineAlwaysPrompt) {
params.approval_prompt = 'force';
}
// Despite being documented at https://goo.gl/tiO0Bk
// It doesn't seem like user.grantOfflineAccess() actually exists in
// the current version of the Google Sign-In JS client we're using
// through GoogleWebComponents. So in the offline case, we will not
// distinguish between a first auth and an additional one.
promise = gapi.auth2.getAuthInstance().grantOfflineAccess(params);
}
promise.then(
function success(response) {
// If login was offline, response contains one string "code"
// Otherwise it contains the user object already
var newUser;
if (response.code) {
AuthEngine.offlineGranted = true;
newUser = gapi.auth2.getAuthInstance().currentUser.get();
AuthEngine.setOfflineCode(response.code);
} else {
newUser = response;
}
var authResponse = newUser.getAuthResponse();
// Let the current user listener trigger the changes.
},
function error(error) {
if ("Access denied." == error.reason) {
// Access denied is not an error, user hit cancel
return;
} else {
console.error(error);
}
}
);
},
/** signs user out */
signOut: function() {
this.assertAuthInitialized();
gapi.auth2.getAuthInstance().signOut().then(
function success() {
// Let the current user listener trigger the changes.
},
function error(error) {
console.error(error);
}
);
}
};
AuthEngine.init();
/**
`google-signin-aware` is used to enable authentication in custom elements by
interacting with a google-signin element that needs to be present somewhere
on the page.
The `scopes` attribute allows you to specify which scope permissions are required
(e.g do you want to allow interaction with the Google Drive API).
The `google-signin-aware-success` event is triggered when a user successfully
authenticates. If either `offline` or `offlineAlwaysPrompt` is set to true, successful
authentication will also trigger the `google-signin-offline-success`event.
The `google-signin-aware-signed-out` event is triggered when a user explicitly
signs out via the google-signin element.
You can bind to `isAuthorized` property to monitor authorization state.
##### Example
<google-signin-aware scopes="https://www.googleapis.com/auth/drive"></google-signin-aware>
##### Example with offline
<template id="awareness" is="dom-bind">
<google-signin-aware
scopes="https://www.googleapis.com/auth/drive"
offline
on-google-signin-aware-success="handleSignin"
on-google-signin-offline-success="handleOffline"></google-signin-aware>
<\/template>
<script>
var aware = document.querySelector('#awareness');
aware.handleSignin = function(response) {
var user = gapi.auth2.getAuthInstance().currentUser.get();
console.log('User name: ' + user.getBasicProfile().getName());
};
aware.handleOffline = function(response) {
console.log('Offline code received: ' + response.detail.code);
// Here you would POST response.detail.code to your webserver, which can
// exchange the authorization code for an access token. More info at:
// https://developers.google.com/identity/protocols/OAuth2WebServer
};
<\/script>
*/
Polymer({
is: 'google-signin-aware',
/**
* Fired when this scope has been authorized
* @param {Object} result Authorization result.
* @event google-signin-aware-success
*/
/**
* Fired when an offline authorization is successful.
* @param {Object} detail
* @param {string} detail.code The one-time authorization code from Google.
* Your application can exchange this for an `access_token` and `refresh_token`
* @event google-signin-offline-success
*/
/**
* Fired when this scope is not authorized
* @event google-signin-aware-signed-out
*/
properties: {
/**
* App package name for android over-the-air installs.
* See the relevant [docs](https://developers.google.com/+/web/signin/android-app-installs)
*/
appPackageName: {
type: String,
observer: '_appPackageNameChanged'
},
/**
* a Google Developers clientId reference
*/
clientId: {
type: String,
observer: '_clientIdChanged'
},
/**
* The cookie policy defines what URIs have access to the session cookie
* remembering the user's sign-in state.
* See the relevant [docs](https://developers.google.com/+/web/signin/reference#determining_a_value_for_cookie_policy) for more information.
* @default 'single_host_origin'
*/
cookiePolicy: {
type: String,
observer: '_cookiePolicyChanged'
},
/**
* The app activity types you want to write on behalf of the user
* (e.g http://schemas.google.com/AddActivity)
*
*/
requestVisibleActions: {
type: String,
observer: '_requestVisibleActionsChanged'
},
/**
* The Google Apps domain to which users must belong to sign in.
* See the relevant [docs](https://developers.google.com/identity/sign-in/web/reference) for more information.
*/
hostedDomain: {
type: String,
observer: '_hostedDomainChanged'
},
/**
* Allows for offline `access_token` retrieval during the signin process.
* See also `offlineAlwaysPrompt`. You only need to set one of the two; if both
* are set, the behavior of `offlineAlwaysPrompt` will override `offline`.
*/
offline: {
type: Boolean,
value: false,
observer: '_offlineChanged'
},
/**
* Works the same as `offline` with the addition that it will always
* force a re-prompt to the user, guaranteeing that you will get a
* refresh_token even if the user has already granted offline access to
* this application. You only need to set one of `offline` or
* `offlineAlwaysPrompt`, not both.
*/
offlineAlwaysPrompt: {
type: Boolean,
value: false,
observer: '_offlineAlwaysPromptChanged'
},
/**
* The scopes to provide access to (e.g https://www.googleapis.com/auth/drive)
* and should be space-delimited.
*/
scopes: {
type: String,
value: 'profile',
observer: '_scopesChanged'
},
/**
* True if user is signed in
*/
signedIn: {
type: Boolean,
notify: true,
readOnly: true
},
/**
* True if authorizations for *this* element have been granted
*/
isAuthorized: {
type: Boolean,
notify: true,
readOnly: true,
value: false
},
/**
* True if additional authorizations for *any* element are required
*/
needAdditionalAuth: {
type: Boolean,
notify: true,
readOnly: true
},
/**
* True if *any* element has google+ scopes
*/
hasPlusScopes: {
type: Boolean,
value: false,
notify: true,
readOnly: true
}
},
attached: function() {
AuthEngine.attachSigninAware(this);
},
detached: function() {
AuthEngine.detachSigninAware(this);
},
/** pops up the authorization dialog */
signIn: function() {
AuthEngine.signIn();
},
/** signs user out */
signOut: function() {
AuthEngine.signOut();
},
_appPackageNameChanged: function(newName, oldName) {
AuthEngine.appPackageName = newName;
},
_clientIdChanged: function(newId, oldId) {
AuthEngine.clientId = newId;
},
_cookiePolicyChanged: function(newPolicy, oldPolicy) {
AuthEngine.cookiePolicy = newPolicy;
},
_requestVisibleActionsChanged: function(newVal, oldVal) {
AuthEngine.requestVisibleActions = newVal;
},
_hostedDomainChanged: function(newVal, oldVal) {
AuthEngine.hostedDomain = newVal;
},
_offlineChanged: function(newVal, oldVal) {
AuthEngine.offline = newVal;
},
_offlineAlwaysPromptChanged: function(newVal, oldVal) {
AuthEngine.offlineAlwaysPrompt = newVal;
},
_scopesChanged: function(newVal, oldVal) {
AuthEngine.requestScopes(newVal);
this._updateScopeStatus();
},
_updateScopeStatus: function(user) {
var newAuthorized = this.signedIn && AuthEngine.hasGrantedScopes(this.scopes);
if (newAuthorized !== this.isAuthorized) {
this._setIsAuthorized(newAuthorized);
if (newAuthorized) {
this.fire('google-signin-aware-success', user);
}
else {
this.fire('google-signin-aware-signed-out', user);
}
}
},
_updateOfflineCode: function(code) {
if (code) {
this.fire('google-signin-offline-success', {code: code});
}
}
});
})();
</script>