blob: 2c0d83d7b7579cba6c37efdb677f5270a11b559c [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview JavaScript implementation of the credential management API
* defined at http://w3c.github.io/webappsec-credential-management
* This is a minimal implementation that sends data to the app side to
* integrate with the password manager. When loaded, installs the API onto
* the window.navigator.object.
*/
// TODO(crbug.com/435046) After get, store, preventSilentAccess are
// implemented app-side, make sure that all tests at
// https://w3c-test.org/credential-management/idl.https.html
// pass.
// TODO(crbug.com/435046) Declare Credential, PasswordCredential,
// FederatedCredential and CredentialsContainer as classes once iOS9 is no
// longer supported.
// Namespace for credential management. __gCrWeb must have already
// been defined.
__gCrWeb.credentialManager = {
/**
* Used to apply unique promiseId fields to messages sent to host.
* Those IDs can be later used to call a corresponding resolver/rejecter.
* @private {number}
*/
nextId_: 0,
/**
* Stores the functions for resolving Promises returned by
* navigator.credentials method calls. A resolver for a call with
* promiseId: |id| is stored at resolvers_[id].
* @type {!Object<number, function(?Credential)|function()>}
* @private
*/
resolvers_: {},
/**
* Stores the functions for rejecting Promises returned by
* navigator.credentials method calls. A rejecter for a call with
* promiseId: |id| is stored at rejecters_[id].
* @type {!Object<number, function(?Error)>}
* @private
*/
rejecters_: {}
};
__gCrWeb['credentialManager'] = __gCrWeb.credentialManager;
/**
* Creates and returns a Promise with given |promiseId|. The Promise's executor
* function stores resolver and rejecter functions in
* __gCrWeb['credentialManager'] under the key |promiseId| so they can be called
* from the host after executing app side code.
* @param {number} promiseId The number assigned to newly created Promise.
* @return {!Promise<?Credential>|!Promise<!Credential>|!Promise<void>}
* The created Promise.
* @private
*/
__gCrWeb.credentialManager.createPromise_ = function(promiseId) {
return new Promise(function(resolve, reject) {
__gCrWeb.credentialManager.resolvers_[promiseId] = resolve;
__gCrWeb.credentialManager.rejecters_[promiseId] = reject;
});
};
/**
* Sends a message to the app side, invoking |command| with the given |options|.
* @param {string} command The name of the invoked command.
* @param {Object} options A dictionary of additional properties to forward to
* the app.
* @return {!Promise<?Credential>|!Promise<!Credential>|!Promise<void>}
* A promise to be returned by the calling method.
* @private
*/
__gCrWeb.credentialManager.invokeOnHost_ = function(command, options) {
var promiseId = __gCrWeb.credentialManager.nextId_++;
var message = {
'command': command,
'promiseId': promiseId
};
if (options) {
Object.assign(message, options);
}
__gCrWeb.message.invokeOnHost(message);
return __gCrWeb.credentialManager.createPromise_(promiseId);
};
/**
* The Credential interface, for more information see
* https://w3c.github.io/webappsec-credential-management/#the-credential-interface
* @constructor
*/
function Credential() {
/** @type {string} */
this.id;
/** @type {string} */
this.type;
}
Object.defineProperty(Credential.prototype, Symbol.toStringTag,
{ value: 'Credential' });
/**
* PasswordCredential interace, for more information see
* https://w3c.github.io/webappsec-credential-management/#passwordcredential-interface
* @extends {Credential}
* @param {PasswordCredentialInit} init Either a PasswordCredentialData or
* HTMLFormElement to create PasswordCredential from.
* @constructor
*/
function PasswordCredential(init) {
// TODO(crbug.com/435046): When iOS9 is no longer supported, change vars to
// |let| and |const| and declare at assignment.
/** @type {!PasswordCredentialData} */
var data;
var elements;
var elementIndex;
var newPasswordObserved;
var field;
var name;
var autocompleteTokens;
var token;
var tokenIndex;
if (init instanceof HTMLFormElement) {
// Performs following steps:
// https://www.w3.org/TR/credential-management-1/#abstract-opdef-create-a-passwordcredential-from-an-htmlformelement
data = /** @type {!PasswordCredentialData} */ ({});
elements = init.querySelectorAll( // all submittable elements
'button, input, object, select, textarea');
newPasswordObserved = false;
for (elementIndex = 0; elementIndex < elements.length;
elementIndex++) {
field = elements.item(elementIndex);
if (!field.hasAttribute('autocomplete')) {
continue;
}
name = field.name;
if (!init[name]) {
continue;
}
autocompleteTokens = field.getAttribute('autocomplete').split(' ');
for (tokenIndex = 0; tokenIndex < autocompleteTokens.length;
tokenIndex++) {
token = autocompleteTokens[tokenIndex];
if (token.toLowerCase() === 'new-password') {
data.password = init[name].value;
newPasswordObserved = true;
}
if (token.toLowerCase() === 'current-password') {
if (!newPasswordObserved) {
data.password = init[name].value;
}
}
if (token.toLowerCase() === 'photo') {
data.iconURL = init[name].value;
}
if (token.toLowerCase() === 'name') {
data.name = init[name].value;
}
if (token.toLowerCase() === 'nickname') {
data.name = init[name].value;
}
if (token.toLowerCase() === 'username') {
data.id = init[name].value;
}
}
}
} else {
// |init| was not HTMLFormElement so assuming it is PasswordCredentialData.
// Not checking with instanceof because any dictionary with required fields
// should be accepted.
data = /** @type {!PasswordCredentialData} */ (init);
}
// Perform following steps:
// https://w3c.github.io/webappsec-credential-management/#abstract-opdef-create-a-passwordcredential-from-passwordcredentialdata
if (!data.id || typeof data.id != 'string') {
throw new TypeError('id must be a non-empty string');
}
if (!data.password || typeof data.password != 'string') {
throw new TypeError('password must be a non-empty string');
}
if (data.iconURL && !data.iconURL.startsWith('https://')) {
throw new SyntaxError('invalid iconURL');
}
/** @type {string} */
this._id = data.id;
/** @type {string} */
this._type = 'password';
/** @type {string} */
this._password = data.password;
/** @type {string} */
this._iconURL = (data.iconURL ? data.iconURL : '');
/** @type {string} */
this._name = (data.name ? data.name : '');
}
PasswordCredential.prototype = {
__proto__: Credential.prototype
};
Object.defineProperty(PasswordCredential, 'prototype', { writable: false });
PasswordCredential.prototype.constructor = PasswordCredential;
Object.defineProperties(
PasswordCredential.prototype,
{
'constructor': {
enumerable: false
},
'id' : {
get: /** @this {PasswordCredential} */ function() {
return this._id;
}
},
'type' : {
get: /** @this {PasswordCredential} */ function() {
return this._type;
}
},
'password': {
get: /** @this {PasswordCredential} */ function() {
if (!(this instanceof PasswordCredential)) {
throw new TypeError('attempting to get a property on prototype');
}
return this._password;
},
configurable: true,
enumerable: true
},
'iconURL' : {
get: /** @this {PasswordCredential} */ function() {
return this._iconURL;
}
},
'name' : {
get: /** @this {PasswordCredential} */ function() {
return this._name;
}
}
}
);
Object.defineProperty(PasswordCredential.prototype, Symbol.toStringTag,
{ value: 'PasswordCredential' });
/**
* FederatedCredential interface, for more information see
* https://w3c.github.io/webappsec-credential-management/#federatedcredential-interface
* @param {FederatedCredentialInit} init Dictionary to create
* FederatedCredential from.
* @extends {Credential}
* @constructor
*/
function FederatedCredential(init) {
if (!init.id || typeof init.id != 'string') {
throw new TypeError('id must be a non-empty string');
}
if (!init.provider || typeof init.provider != 'string') {
throw new TypeError('provider must be a non-empty string');
}
if (!init.provider.startsWith('https://') &&
!init.provider.startsWith('http://')) {
throw new SyntaxError('invalid provider URL');
}
if (init.iconURL && !init.iconURL.startsWith('https://')) {
throw new SyntaxError('invalid iconURL');
}
/** @type {string} */
this._id = init.id;
/** @type {string} */
this._type = 'federated';
/** @type {string} */
this._name = (init.name ? init.name : '');
/** @type {string} */
this._iconURL = (init.iconURL ? init.iconURL : '');
/** @type {string} */
this._provider = init.provider.replace(/\/$/, ''); // strip trailing slash
/** @type {?string} */
this._protocol = (init.protocol ? init.protocol : '');
}
FederatedCredential.prototype = {
__proto__: Credential.prototype
};
Object.defineProperty(FederatedCredential, 'prototype', { writable: false });
FederatedCredential.prototype.constructor = FederatedCredential;
Object.defineProperties(
FederatedCredential.prototype,
{
'constructor': {
enumerable: false
},
'id' : {
get: /** @this {FederatedCredential} */ function() {
return this._id;
}
},
'type' : {
get: /** @this {FederatedCredential} */ function() {
return this._type;
}
},
'provider': {
get: /** @this {FederatedCredential} */ function() {
if (!(this instanceof FederatedCredential)) {
throw new TypeError('attempting to get a property on prototype');
}
return this._provider;
},
configurable: true,
enumerable: true
},
'protocol': {
get: /** @this {FederatedCredential} */ function() {
if (!(this instanceof FederatedCredential)) {
throw new TypeError('attempting to get a property on prototype');
}
return this._protocol;
},
configurable: true,
enumerable: true
},
'iconURL' : {
get: /** @this {FederatedCredential} */ function() {
return this._iconURL;
}
},
'name' : {
get: /** @this {FederatedCredential} */ function() {
return this._name;
}
}
}
);
Object.defineProperty(FederatedCredential.prototype, Symbol.toStringTag,
{ value: 'FederatedCredential' });
/**
* CredentialData dictionary
* https://w3c.github.io/webappsec-credential-management/#dictdef-credentialdata
* @dict
* @typedef {{id: string}}
*/
var CredentialData;
/**
* PasswordCredentialData used for constructing PasswordCredential objects
* https://w3c.github.io/webappsec-credential-management/#dictdef-passwordcredentialdata
* @dict
* @typedef {{
* id: string,
* name: string,
* iconURL: string,
* password: string
* }}
*/
var PasswordCredentialData;
/**
* Either PasswordCredentialData or HTMLFormElement used for constructing
* a new PasswordCredential
* https://www.w3.org/TR/credential-management-1/#typedefdef-passwordcredentialinit
* @typedef {!PasswordCredentialData|HTMLFormElement}
*/
var PasswordCredentialInit;
/**
* FederatedCredentialInit used for constructing FederatedCredential objects
* https://w3c.github.io/webappsec-credential-management/#dictdef-federatedcredentialinit
* @dict
* @typedef {{
* id: string,
* name: string,
* iconURL: string,
* provider: string,
* protocol: string
* }}
*/
var FederatedCredentialInit;
/**
* The CredentialRequestOptions dictionary, for more information see
* https://w3c.github.io/webappsec-credential-management/#credentialrequestoptions-dictionary
* @dict
* @typedef {{mediation: string}}
*/
var CredentialRequestOptions;
/**
* The FederatedCredentialRequestOptions dictionary, for more information see
* https://w3c.github.io/webappsec-credential-management/#dictdef-federatedcredentialrequestoptions
* @dict
* @typedef {{
* providers: !Array<string>,
* protocols: !Array<string>
* }}
*/
var FederatedCredentialRequestOptions;
/**
* The CredentialCreationOptions dictionary, for more information see
* https://w3c.github.io/webappsec-credential-management/#credentialcreationoptions-dictionary
* @dict
* @typedef {{
* password: ?PasswordCredentialInit,
* federated: ?FederatedCredentialInit
* }}
*/
var CredentialCreationOptions;
/**
* Implements the public Credential Management API. For more information, see
* https://w3c.github.io/webappsec-credential-management/#credentialscontainer
* @constructor
*/
function CredentialsContainer() {}
Object.defineProperty(CredentialsContainer, 'prototype', { writable: false });
CredentialsContainer.prototype.constructor = CredentialsContainer;
Object.defineProperty(
CredentialsContainer.prototype, 'constructor', { enumerable: false });
Object.defineProperty(CredentialsContainer.prototype, Symbol.toStringTag,
{ value: 'CredentialsContainer' });
/**
* Performs the Request A Credential action described at
* https://w3c.github.io/webappsec-credential-management/#abstract-opdef-request-a-credential
* @param {!CredentialRequestOptions} options An optional dictionary of
* parameters for the request.
* @return {!Promise<?Credential>} A promise for retrieving the result
* of the request.
*/
CredentialsContainer.prototype.get = function(options) {
return __gCrWeb.credentialManager.invokeOnHost_(
'credentials.get', options);
};
/**
* Performs the Store A Credential action described at
* https://w3c.github.io/webappsec-credential-management/#abstract-opdef-store-a-credential
* @param {!Credential} credential A credential object to store.
* @return {!Promise<void>} A promise for retrieving the result
* of the request.
*/
CredentialsContainer.prototype.store = function(credential) {
return __gCrWeb.credentialManager.invokeOnHost_(
'credentials.store', credential);
};
/**
* Performs the Prevent Silent Access action described at
* https://w3c.github.io/webappsec-credential-management/#abstract-opdef-prevent-silent-access
* @return {!Promise<void>} A promise for retrieving the result
* of the request.
*/
CredentialsContainer.prototype.preventSilentAccess = function() {
return __gCrWeb.credentialManager.invokeOnHost_(
'credentials.preventSilentAccess', {});
};
/**
* Performs the Create A Credential action described at
* https://w3c.github.io/webappsec-credential-management/#abstract-opdef-create-a-credential
* @param {!CredentialCreationOptions} options An optional dictionary of
* of params for creating a new Credential object.
* @return {!Promise<?Credential>} A promise for retrieving the result
* of the request.
*/
CredentialsContainer.prototype.create = function(options) {
// According to
// https://w3c.github.io/webappsec-credential-management/#abstract-opdef-create-a-credential,
// we should also check for secure context. Instead it is done only in
// browser-side methods. Since public JS interface is exposed, user can create
// a Credential using a constructor anyway.
return new Promise(function(resolve, reject) {
try {
if (options && options.password && !options.federated) {
resolve(new PasswordCredential(options.password));
return;
}
if (options && options.federated && !options.password) {
resolve(new FederatedCredential(options.federated));
return;
}
} catch (err) {
reject(err);
return;
}
reject(Object.create(DOMException.prototype, {
name: {value: DOMException.NOT_SUPPORTED_ERR},
message: {value: 'Invalid CredentialRequestOptions'}
}));
});
};
/**
* Install the public interface.
* @type {!CredentialsContainer}
*/
window.navigator.credentials = new CredentialsContainer();