blob: e6e731bfad1a7417e19f977e4944c011a4a2f6bd [file] [log] [blame]
<!--
Copyright 2014 Google Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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',
'hostedDomain': 'hostedDomain',
'openidPrompt': 'prompt',
'requestVisibleActions': 'requestvisibleactions'
};
/**
* 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 && val != this._clientId) {
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;
},
/**
* oauth2 argument, set by google-signin-aware
*/
_openidPrompt: '',
get openidPrompt() {
return this._openidPrompt;
},
set openidPrompt(val) {
if (typeof val !== 'string') {
throw new Error(
'openidPrompt must be a string. Received ' + typeof val);
}
if (val) {
var values = val.split(' ');
values = values.map(function(v) {
return v.trim();
});
values = values.filter(function(v) {
return v;
});
var validValues = {none: 0, login: 0, consent: 0, select_account: 0};
values.forEach(function(v) {
if (v == 'none' && values.length > 1) {
throw new Error(
'none cannot be combined with other openidPrompt values');
}
if (!(v in validValues)) {
throw new Error(
'invalid openidPrompt value ' + v +
'. Valid values: ' + Object.keys(validValues).join(', '));
}
});
}
this._openidPrompt = 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 auth library initalized? */
_initialized: false,
/** 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));
if (Polymer.Element) {
document.body.appendChild(this._apiLoader);
}
},
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 onFulfilled() {
// Let the current user listener trigger the changes.
},
function onRejected(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 and broadcast initialized property the first time the isSignedIn property is set.
if(!this._initialized) {
for (var i=0; i<this.signinAwares.length; i++) {
this.signinAwares[i]._setInitialized(true);
}
this._initialized = true;
}
// 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 {!GoogleSigninAwareElement} 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._setInitialized(this._initialized);
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 onFulfilled(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 onRejected(error) {
// Access denied is not an error, user hit cancel
if ("Access denied." !== error.reason) {
this.signinAwares.forEach(function(awareInstance) {
awareInstance.errorNotify(error);
});
}
}.bind(this)
);
},
/** signs user out */
signOut: function() {
this.assertAuthInitialized();
gapi.auth2.getAuthInstance().signOut().then(
function onFulfilled() {
// Let the current user listener trigger the changes.
},
function onRejected(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 {{code: 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
*/
/**
* Fired when there is an error during the signin flow.
* @param {Object} detail The error object returned from the OAuth 2 flow.
* @event google-signin-aware-error
*/
/**
* This block is needed so the previous @param is not assigned to the next property.
*/
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'
},
/**
* Space-delimited, case-sensitive list of strings that
* specifies whether the the user is prompted for reauthentication
* and/or consent. The defined values are:
* none: do not display authentication or consent pages.
* This value is mutually exclusive with the rest.
* login: always prompt the user for reauthentication.
* consent: always show consent screen.
* select_account: always show account selection page.
* This enables a user who has multiple accounts to select amongst
* the multiple accounts that they might have current sessions for.
* For more information, see "prompt" parameter description in
* https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
*/
openidPrompt: {
type: String,
value: '',
observer: '_openidPromptChanged'
},
/**
* True when the auth library has been initialized, and signedIn property value is set from the first api response.
*/
initialized: {
type: Boolean,
notify: true,
readOnly: true
},
/**
* 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();
},
errorNotify: function(error) {
this.fire('google-signin-aware-error', error);
},
_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(undefined);
},
_openidPromptChanged: function(newVal, oldVal) {
AuthEngine.openidPrompt = newVal;
},
_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>