w3c / webappsec

Web Application Security Working Group repo
https://www.w3.org/groups/wg/webappsec/
Other
601 stars 147 forks source link

CREDENTIAL: Credential scope should not be limited to login #256

Open dlongley opened 9 years ago

dlongley commented 9 years ago

Credentials may be used for more than just login, and a credential may not represent a user's entire identity. This means that browsers can't just take a list of credentials and throw them up in a UI when someone is attempting to log into a website. It also means the API should support more complex queries for the types of credentials desired. This likely means redefining what a credential is -- and making changes to the Credential base class.

There are at least two ways to proceed:

  1. Consider some credentials to be "LoginCredentials" (or username+password legacy credentials) and others to be of a more generalized sort. The browser only bothers displaying "LoginCredentials" in a special way. Browsers can defer to IdPs for displaying non-LoginCredentials.
  2. Consider that all credentials are of a generalized sort, and browsers will have to inspect a variety of properties about them to determine how to best display them to users. Browsers could also simply defer to IdPs to handle specialized display.

In the Credentials CG work, we don't consider "login" to be a special use case. A relying party may ask for whatever credential they want to in order to authorize a user to take some action or to simply collect information about that user for later review, etc. For example, "login" can be implemented by requesting a "Verified Email Credential" from a user. It could be implemented in another way as well.

The current Credential Management API sees the "login use case" as a special first class citizen, which makes perfect sense, considering that it is scope-limited to making incremental improvements to "login" via a new imperative password manager API. I don't see any conflict with the Credentials CG in this respect.

The conflict arises from the fact that the spec aims to do more than just provide an API for password managers, it suggests there is "future work" and attempts to define an extensible API to try and cover it. Again, it makes perfect sense that you'd want such an API to support a broader range of credentials if it can. Fortunately, the Credentials CG has spent years working on designs and technologies in the "future work" space the Credentials Management API refers to. Unfortunately, the current design feels a bit inverted to those of us that have spent that time. I don't have a quick fix for this particular issue -- but it's clear to me that how we want credentials to work in the future doesn't quite mesh with the existing "login" paradigm.

Obviously, it would be nice to make a minimal number of changes to the API now to future proof it -- and then simply list these goals out in the future work section.

dlongley commented 9 years ago

One possible minimal change to future proof the API would be to make the Credential base class more generic and create a new LoginCredential class that extends it. That LoginCredential class is then the base class for LocalCredential and FederatedCredential. In the future, there would be something like LinkedDataCredential that extends Credential. Then, in order to select which path (legacy or future) you want to work with, you include that when making a call to navigator.credentials.get():

navigator.credentials.get({
  type: 'LoginCredential'
}).then(function(...) {
  // ...
});

Or:

navigator.credentials.get({
  type: 'LinkedDataCredential',
  query: {
    // linked data query here, format TBD in future work
  },
  callback: // callback to post identity document with credentials to from third party
});

In the first case, you essentially work with the present API as implemented. In the second case, the API doesn't return a promise, rather it posts the result of the credentials query from a third party to a callback. The browser itself may optionally function as that third party, but it also may not. The result will likely be a JSON-LD Linked Data identity document that contains credentials -- but this is to be decided as future work.

In short, it may be that the existing API can be future proofed for the Credentials CG and Web Payments IG use cases by redefining the meaning of Credential, changing the base class to be more generic, extending it with a LoginCredential class for the current password-manager use cases, and adding some notes to the spec in the future work section for how Linked Data credentials might be implemented.

jmajnert commented 9 years ago

In the second case, the API doesn't return a promise, rather it posts the result of the credentials query from a third party to a callback.

Why not use promises? Is there some specific magic going on that requires callbacks?

In short, it may be that the existing API can be future proofed for the Credentials CG and Web Payments IG use cases by redefining the meaning of Credential, changing the base class to be more generic, extending it with a LoginCredential class for the current password-manager use cases, and adding some notes to the spec in the future work section for how Linked Data credentials might be implemented.

If that's all it takes, LGTM.

dlongley commented 9 years ago

Why not use promises? Is there some specific magic going on that requires callbacks?

We have no issue with promises, it just seems like it may require complex state management by the browser in order to implement. We also want to ensure that this API is easily polyfillable. To elaborate, the future flow is currently envisioned to work like this (at a high level):

  1. Relying party requests a set of credentials by passing a query to the API.
  2. The browser doesn't have the credentials cached, so it redirects the user to their IdP. Note: the user has left the relying party's website at this point.
  3. With the user's authorization, the IdP uses another API call to pass the credentials that match the query back to the browser. Note: a polyfill would POST them to a temporary trusted centralized website. In either case (browser or polyfill), the identity of the relying party is optionally hidden from the IdP during this step. This provides greater privacy assurances to users.
  4. The browser POSTs the credentials to the callback URL.

I don't see how this is easily implemented using promises, as the original page has been navigated away from and its state has been lost. It seems the page state would need to be saved by the browser -- or another window could be used to visit the IdP -- in order for a promise-based approach to work. Thoughts?

jmajnert commented 9 years ago

I don't see how this is easily implemented using promises, as the original page has been navigated away from and its state has been lost.

I misunderstood what callback was in your idl. I get it now.

dlongley commented 9 years ago

Ah, yes, I understand now. You were thinking callback function, not callback URL. Sorry for any confusion.

jmajnert commented 9 years ago

How do you think your API would work if called from a service worker context?

dlongley commented 9 years ago

How do you think your API would work if called from a service worker context?

We haven't tried to implement anything in a service worker yet. We do have mechanisms for reading/writing credentials using REST APIs when user authorization has been pre-approved. Some of that is spec'd out here: Identity Credentials. Note that that spec is fairly rough and a bit dated, we've been focusing on cleaning up use cases documents and the like more recently. Also, like I mentioned, we haven't spec'd out any JavaScript API that could use it just yet.

That being said, I imagine reading/writing credentials via a service worker context could use the REST APIs and would only function if user pre-authorization was detected. Otherwise, an error would be returned by the API. Also, in this case, it makes sense to return a promise.

To be clear, I don't think there's an issue if the API sometimes returns a promise, I think we just need to also support the case where it doesn't make sense to do so ... or at least where the browser may change the page location and you never see that promise resolved.

mikewest commented 9 years ago

Types

I think there's a good case to be made for adding a type attribute to CredentialRequestOptions, as discussed briefly in #249. The contents of that attribute are less clear to me; I think we have a few options.

  1. This bug suggests a single name:

    type: "LoginCredential"

    I would suggest that this option is too inflexible. I think we at least need a sequence here, to serve the use case spelled out in http://opencreds.org/specs/source/use-cases/#legacy-support (falling back to a username/password for authentication), if no other reason. That seems best done by specifying an arbitrary number of credential types, and dealing with the result accordingly.

    It also suggests that we need a clear mechanism to determine what kind of credential we've gotten back from the user agent when the promise resolves (put the promise question to the side for a moment). The current spec does this with separate types, and instanceof comparisons. This seems elegant to me, but I'd be fine with other options (encoding a type property on Credential, for instance).

  2. Adrian suggested a sequence of URL-based identifiers in https://lists.w3.org/Archives/Public/public-webappsec/2015Apr/0161.html. For example:

    types: [
       'http://w3c.org/webappsec/credentials/#password',
       'https://facebook.com/oauthCredential',
       'https://accounts.google.com/credentials'
    ]

    This option has some benefits in terms of arbitrary extensibility. That said, it pays for those benefits with additional complexity. I expect the overwhelming majority of short-to-medium term use cases to be for username/password tuples, followed by a reasonable number of federation tuples (followed by nothing, since those are the only options defined at the moment). :) I'd prefer to make that case clear and concise. If we make developers learn a URL that means "passwords", I think we're doing them a disservice, and enforcing boilerplate that we'll regret.

  3. @domenic suggested a sequence of actual types in #249. For example:

    types: [
       LocalCredential,
       FederatedCredential,
       LinkedDataCredential
    ]

    This seems elegant to me. The drawback is the loss of arbitrary extensibility that strings provide. I'm inclined to suggest that that extensibility should be scoped to the credential types which clearly require it (LinkedDataCredential and its subclasses).

    I'll pull together a strawman of this approach in the spec for discussion.

Promises

I don't think there's a good reason to change the signature of get() based on the requested types. In particular, I'd suggest that even a POST-driven callback system needs to deal with errors. That is, what ought happen if the callback attribute @dlongley suggested in https://github.com/w3c/webappsec/issues/256#issuecomment-93427814 is an invalid URL (e.g. callback: 'http://%A1{A+;')? We could pretty cleanly model this as a rejected Promise, and model the callback case as a resolved Promise that signals to the site that a navigation is incoming.

Service Workers

Currently, the API rejects anywhere other than a top-level browsing context. I think it's going to be quite difficult to present UI to the user that adequately informs her about the choices she's being asked to make for any framed context, or worker context.

adrianhopebailie commented 9 years ago

Types

I will have one last go at advocating for URI based types.

  1. The caller no longer needs to indicate if they support password based credentials, making the 'acceptPasswords' property on the 'CredentialRequestOptions' class redundant.
  2. The caller no longer needs to list out the supported federation providers, making the 'federations' property on the 'CredentialRequestOptions' class redundant.
  3. Is learning a single URL that represents the local credential such a disservice? Developers will need to know the URL of each federation provider anyway.
  4. I think this combines well with moving the 'LocalCredential.send()' function onto the Credential Manager.

A more detailed example:

var supportedCredentialTypes = {
  password : "http://some.uri.defined.in.the.spec/",
  google : "https://accounts.google.com/credential",
  facebook : "https://facebook.com/oauth",
  twitter : "https://twitter.com/oauth",
  openid : "https://openid.net/specs/connect/1.0/login_credential",
  saml_example_com : "https://example.com/auth/v2/saml_token/",
}
var supportedCredentialTypeIds = Object.keys(supportedCredentialTypes)
  .map(function(key){
    return supportedCredentialTypes[key];
  });

navigator.credentials.get({
  types:  supportedCredentialTypeIds
}).then(function(credential) {

  if (!credential)
  return;

  //Assume this is set to true if the credential is used successfully
  var loginSuccessful = false;

  switch(credential.type)
  {
    //Note that I have moved the send() method to the Credential Manager
    case supportedCredentialTypes.password:
      navigator.credentials.send(credential, "http://this.relyingparty.com/login")
        .then(function (response) { ... })
        .catch(function (response) { ... });
      break;

    // Let's assume these federation providers have:
    //  a) exposed endpoints that return a standardised result
    //  b) used the URL of that endpoint as the type id for their credential
    //
    // The purpose of submitting the credential is to ensure it is still valid
    // and possibly refresh the session token if required.
    //
    // I recognise that there is a need to consider Origin policies here but it
    // seems sensible to allow the client a standard way to submit a credential
    // to an endpoint at a URL that has an Orirign with a match to some property
    // of the credential
    case supportedCredentialTypes.google:
    case supportedCredentialTypes.facebook:
    case supportedCredentialTypes.twitter:
      navigator.credentials.send(credential, credential.type)
        .then(function (response) { ... })
        .catch(function (response) { ... });
      break;

    //For something like OpenID Connect there may be a common format but
    // additional work required to decide where to submit this etc.
    case supportedCredentialTypes.openid:

      //Maybe the refresh URL was saved with the credential
      var openIdRefreshUrl = credential["refreshUrl"];

      if(!openIdRefreshUrl){
        navigator.credentials.send(credential, openIdRefreshUrl)
          .then(function (response) { ... })
          .catch(function (response) { ... });
      }

      //Maybe there is a 3rd party library we use sepcifically for this
      OpenIdClient.send(credential)
        .then(function (response) { ... })
        .catch(function (response) { ... });

      break;

    //Deal with exotic federation use cases such as enterprise federation schemes
    // like SAML, WS-Federation etc
    case supportedCredentialTypes.saml_example_com:
      ...
     break;
  }

  //It is possible that during the processing of a credential, sending it to
  // some endpoint and evaluating the result there was a need to update the
  // credential so we save it again.
  if(loginSuccessful)
    navigator.credentials.store(credential);

});
dlongley commented 9 years ago

Regarding option #2 from @mikewest's list, the one that Adrian suggested, is a mechanism we had intended to employ in our own credentials API. In my original suggestion, the query parameter would be where we'd include the list of RDF types (which are URLs) to request. In other words, I think we can get away with option #1 or option #3 here, and use Adrian's suggestion in our future extension to the API that supports the query parameter when LinkedDataCredential is in the top-level type list. To clarify:

Option one (single top-level type) would look like:

navigator.credentials.get({
  type: 'LinkedDataCredential',
  query: {
    type: [<URLs here>]
    // other linked data query properties here, format TBD in future work
  },
  callback: // callback to post identity document with credentials to from third party
});

Option two (multiple top-level types) would look like:

navigator.credentials.get({
  types: ['LinkedDataCredential', ...],
  query: {
    type: [<URLs here>]
    // other linked data query properties here, format TBD in future work
  },
  callback: // callback to post identity document with credentials to from third party
});

I'm inclined to suggest that that extensibility should be scoped to the credential types which clearly require it (LinkedDataCredential and its subclasses).

I agree and that's exactly what I had been thinking. I also agree that option 3 looks the best. If others do as well, then I think the question becomes where we put additional options for the particular high-level types of credentials requested (ie: Where do we put the "query" parameter for a LinkedDataCredential?, etc)

annevk commented 9 years ago

Is learning a single URL that represents the local credential such a disservice?

A hundred times yes. If there is anything the XML namespaces debacle taught us it is this.

dlongley commented 9 years ago

@mikewest,

Promises

I don't think there's a good reason to change the signature of get() based on the requested types. In particular, I'd suggest that even a POST-driven callback system needs to deal with errors. That is, what ought happen if the callback attribute @dlongley suggested in #256 (comment) is an invalid URL (e.g. callback: 'http://%A1{A+;')? We could pretty cleanly model this as a rejected Promise, and model the callback case as a resolved Promise that signals to the site that a navigation is incoming.

I think I would be fine with that. My understanding is that if the call has been made without any parameter-based validation errors, it will resolve the promise and then indicate that the page is about to change locations to the user's IdP. The user agent will keep track of the callback URL for later use and forward the request onto the IdP once navigation occurs.

A polyfill would schedule navigation to occur after the promise has been resolved and pass the callback and request to a temporary, trusted, centralized server that takes on the role of the browser.

Does this sound accurate?

Service Workers

Currently, the API rejects anywhere other than a top-level browsing context. I think it's going to be quite difficult to present UI to the user that adequately informs her about the choices she's being asked to make for any framed context, or worker context.

I'm fine with that. We don't have a strong case or requirement for using service workers.

adrianhopebailie commented 9 years ago

A hundred times yes. If there is anything the XML namespaces debacle taught us it is this.

That's a pretty subjective opinion. Is there general consensus that XML namespaces are a "debacle"? URIs as identifiers is pretty fundamental to the Web.

Either way, the expectation is for users to submit a set of "federations" identified by URLs so I'm not sure how this differs vastly from that proposal?

dlongley commented 9 years ago

A hundred times yes. If there is anything the XML namespaces debacle taught us it is this.

Another option is to pass the actual JavaScript class type or a string, where a string would represent a URL. Or we could have a constant like PasswordCredential.url that could be passed.

domenic commented 9 years ago

Yes, there is general consensus that XML namespaces (as well as other uses of URLs as identifiers) were a debacle.

adrianhopebailie commented 9 years ago

Another option is to pass the actual JavaScript class type or a string, where a string would represent a URL. Or we could have a constant like PasswordCredential.url that could be passed.

Some kind of constant that represents a PasswordCredential would work I guess. Ultimately it only ever has to be evaluated by the browser when it executes the get() so this doesn't really have far reaching consequences. I just think standardizing on URLs is more elegant than mixing things up.

adrianhopebailie commented 9 years ago

Yes, there is general consensus that XML namespaces (as well as other uses of URLs as identifiers) were a debacle.

Perhaps among those who are only interested in writing Javascript and less interested in the larger Web platform. Saying that URLs should not be identifiers is arguing against the very architecture of the Web and how it works.

If you can point me at some credible reference that says "XML namespaces was a failure let's not do that again" I'd be happy to concede the point.

domenic commented 9 years ago

It might be good for us to work on a TAG finding stating that to clear up any confusion.

dlongley commented 9 years ago

Saying that URLs should not be identifiers is arguing against the very architecture of the Web and how it works.

+1, though, I didn't read @domenic's comment that way. I do think we often want things to be identified by URLs, but there is also often a need to abstract that detail away for humans.

adrianhopebailie commented 9 years ago

I do think we often want things to be identified by URLs, but there is often a need to abstract that detail away.

In this case I think using URl's in all but 1 case is unavoidable. The spec defines a FederatedCredential class but in reality this is just a base class for a GoogleCredential, FacebookCredential, OpenIDConnectFromSomeIdpCredential. So in reality there is an infinite number of credential types which in all but the LocalCredential need to be identified by a URL anyway because that is how the "federation provider" is identified.

Alternative Suggestion

Use a format like the following and adjust the logic in the get() processing so that if type = origin then return a LocalCredential (if available).

navigator.credentials.get({
  types:  [location.origin , "https://facebook.com/login", "https://accounts.google.com"]
}).then(function(credential) {

OR

Assume that if the value of 'acceptPasswords' is 'true' return LocalCredentials. If not, don't

Reference: http://www.w3.org/TR/html5/infrastructure.html#document-base-url Updated: Used 'location.origin' instead of 'document.baseURI'

domenic commented 9 years ago

The spec defines a FederatedCredential class but in reality this is just a base class for a GoogleCredential, FacebookCredential, OpenIDConnectFromSomeIdpCredential

Why do you say that? The spec contains code examples showing otherwise.

adrianhopebailie commented 9 years ago

Why do you say that? The spec contains code examples showing otherwise.

A FederatedCredential has a federation property (it's the only one). The spec defines how this property is intended to be used to identify the federation (identity provider) and says the following about this property:

MUST be identified by the ASCII serialization of the origin the provider uses for sign in

In other words the only difference between two FederatedCredentials instances is the value of this property, which is a URL.

I am proposing that there is no need for the FederatedCredential class at all if the base Credential class has a type property and this is a URL (the same URL that would have been used as the value of the federation property).

The only other known sub-class of Credential is LocalCredential which represents login credentials for the web application with the current origin. So why not simply do away with this sub-class too and let the type property of this instance be equal to location.origin?

domenic commented 9 years ago

The subclasses behave differently in implementations. (Based on polymorphism, instead of switching on a string.)

adrianhopebailie commented 9 years ago

The subclasses behave differently in implementations. (Based on polymorphism, instead of switching on a string.)

Unless you move the send() method from the LocalCredential to the Credential Manager as a method that accepts a Credential instance as an argument. Then there is no difference between the two sub-classes.

What do you see as the value of having specialisations of the Credential class? To me it just locks down the API interface unnecessarily.

mikewest commented 9 years ago

I've taken a pass at implementing a string-based version of @domenic's suggestion (which I labeled option 3 above), and have updated https://w3c.github.io/webappsec/specs/credentialmanagement/ accordingly. Feedback welcome (https://github.com/w3c/webappsec/commits/master shows the list of commits, in lieu of typing out a detailed changelog here).

URLs

I share both @annevk and @domenic's skepticism of the value of associating PasswordCredential objects (renamed for clarity as part of the above set of patches) with an arbitrary URL. In particular, I think the argument around FederatedCredentials misses the important point that identity providers are in fact third-party websites, referenceable via a URL. Talking about Facebook as https://www.facebook.com/ isn't at all unnatural, because that's exactly where it lives.

Talking about your Facebook password as being somehow associated with w3.org is, in my opinion, strange.

I am proposing that there is no need for the FederatedCredential class at all if the base Credential class has a type property and this is a URL

I'd prefer to associate URLs with the types of credentials which require them, and to disassociate them from those credential types which don't naturally lend themselves to being referenced by URL. Putting a URL on the base class doesn't differentiate between types, which I think would be unfortunate.

Promises

A polyfill would schedule navigation to occur after the promise has been resolved and pass the callback and request to a temporary, trusted, centralized server that takes on the role of the browser.

That sounds like a reasonable way to polyfill a POST-driven callback model, yes.

domenic commented 9 years ago

@mikewest Out of curiousity, why ["PasswordCredential"] instead of [PasswordCredential]? I am undecided myself so am curious what pushed you in the former direction.

mikewest commented 9 years ago

@domenic: I should have mentioned that; I used strings because I don't want interface names to blow up in old browsers. That is, types: [ "SuperAmazingFutureCredential" ] will work in browsers where types: [ SuperAmazingFutureCredential ] would explode in shower of ReferenceErrors.

mikewest commented 9 years ago

(Where, by "work", I meant "degrade gracefully")

domenic commented 9 years ago

Ah, OK! It's great to have such a clear-cut deciding reason :)

dlongley commented 9 years ago

@adrianhopebailie,

What do you see as the value of having specialisations of the Credential class? To me it just locks down the API interface unnecessarily.

I don't think it's a problem to embrace that a future looking credential (LinkedDataCredential or whatever we'll call it) can function differently from the current PasswordCredential and FederatedCredential. So long as we can pass an additional option (eg: a Linked Data-based query) to the credentials.get() API in the future, we'll be able to select these future credentials that are differentiated based on their type (as identified by a URL) that way. We still haven't pinned down exactly how we want these queries to look, and we may want to include more than just type filters. So what I'm saying is that I think an extra layer here is ok with me and may even be more future proof.

That being said, we still need to figure out if the way credentials are modeled is sufficient for our future work and/or isn't too awkard. I think the current API assumes that you're, more or less, looking for just one credential that you're going to actually use. You may ask for multiple types, but you're just going to get one (or the user will only select one) at the end of the day, and this is what the promise resolves to. In the Credentials CG work, you may ask for multiple credentials.

As a simple example: you may request a user's proof of age, their shipping address, and their verified email address. Each of these is modeled as a "credential", digitally signed by some third party issuer. The way we've modeled receiving credentials in the Credentials CG is that you receive an "identity document" when your credentials request has been fulfilled. This document has an identifier for the entity the credentials are for and a credentials property with an array of the credentials that were sent (this is all in JSON-LD format).

If we were to try and fit this into the existing API, it would probably just be found in a property that hangs off of LinkedDataCredential (hopefully we'll have a better name in the future :)). But then we've got a "credential" that has a document full of credentials in it. I do think this seems somewhat awkward.

Any thoughts/feedback from anyone here?

mikewest commented 9 years ago

@dlongley,

we may want to include more than just type filters

The mechanism should allow for arbitrary filter data. Perhaps "filter" is the wrong word for the model you're proposing, I suppose we can come up with something more generic ("query" doesn't seem right for the kinds of credentials with which I've been busying myself).

In step 8 of https://w3c.github.io/webappsec/specs/credentialmanagement/#request-credential, we switch on the credential type, and pass the relevant data from the request's options dictionary into an arbitrary algorithm. If/when you define LinkedDataCredential (or whatever), you can define the ways in which that filter data is used to produce a result.

The matching algorithms defined for FederatedCredential gives an example of how this would work for locally stored credentials: https://w3c.github.io/webappsec/specs/credentialmanagement/#federatedcredential-matching

You may ask for multiple types, but you're just going to get one (or the user will only select one) at the end of the day, and this is what the promise resolves to. In the Credentials CG work, you may ask for multiple credentials.

I can imagine scenarios in which even PasswordCredentials may have a many:1 relationship with a site. Google's multi-login, for instance. It's a case that we're explicitly punting in the note in https://w3c.github.io/webappsec/specs/credentialmanagement/#request-locallystoredcredential-without-mediation.

We could certainly make get() return a Promise<sequence<Credential>>, though. That might be enough to open things up in the future.

As a simple example: you may request a user's proof of age, their shipping address, and their verified email address.

How do you plan to deal with multiple callbacks? Or do you expect all these credential types to be obtainable with a single request to a single endpoint?

we've modeled receiving credentials in the Credentials CG is that you receive an "identity document" when your credentials request has been fulfilled.

As I alluded to in #255, I don't really understand why the document containing data about a set of credentials is relevant to the JavaScript API. Assuming that the credentials are available in a format you understand, why not parse them into a set of credential objects? Why expose the underlying JSON-LD complexity to the web page when you could hand over a ShippingAddress, AgeVerification, and EmailAddress? That seems like a much simpler model for web developers to work with.

domenic commented 9 years ago

I can imagine scenarios in which even PasswordCredentials may have a many:1 relationship with a site. Google's multi-login, for instance. It's a case that we're explicitly punting in the note in https://w3c.github.io/webappsec/specs/credentialmanagement/#request-locallystoredcredential-without-mediation.

We could certainly make get() return a Promise<sequence>, though. That might be enough to open things up in the future.

This does seem important to consider before baking the API. My preferred variant on this would be to leave get() simple and have getAll() for opting in to the complexity of an array (like the various multimap interfaces in Fetch/URL). However maybe the solution is entirely along different lines, e.g. the UA would present a chooser. In any case, important to think about and have a plausible future story for.

dlongley commented 9 years ago

@mikewest,

I can imagine scenarios in which even PasswordCredentials may have a many:1 relationship with a site.

Yes, but I should have been more clear, we mean that the user can actually select multiple credentials to be returned. I see that you suggest resolving the promise to a sequence of credentials to perhaps deal with this issue. I'm not sure, however, that that will actually be the best way to model the credentials we're dealing with, but it may be workable. We'd prefer to return a single document with a single identifier for the entity that the credentials are for, and then have the credentials in an array off of that. From this format, a developer can use a number of Linked Data tools to merge credentials in useful ways. We could always have the developer run a bit of code to organize the results this way, but it would be nice if we just skipped those steps, particularly since the credentials are just as useful in this format even if you don't plan to use those tools. For example, the credentials are all for the same entity, so having the id of that entity available at a top level of the result is convenient.

How do you plan to deal with multiple callbacks? Or do you expect all these credential types to be obtainable with a single request to a single endpoint?

We expect all credential types to be obtainable with a single request to a single endpoint. We're designing a standard protocol that IdPs would implement -- and there would be one IdP for each entity. Note that the entity could change the IdP from time to time, but there is only ever one (for a variety of reasons). If the credentials need to actually be pulled from a variety of sources, it would be the IdP's responsibility to collect them together before responding to the request.

As I alluded to in #255, I don't really understand why the document containing data about a set of credentials is relevant to the JavaScript API. Assuming that the credentials are available in a format you understand, why not parse them into a set of credential objects?

We believe it's an entirely unnecessary layer of architecture. A simple JSON object will give us everything we need and is much more extensible than requiring other special serialization layers between the protocol level data and the application. The number of types of credentials in the CG work is unbounded. It's not at all scalable to expect there to be another layer that will convert them to a variety of different polymorphic types. The credentials we're dealing with can just be considered JSON objects -- and they are just data containers, they don't have special interfaces. How you reason about those objects and the meaning of their properties is all based on their JSON-LD context and Linked Data. If it would help to explain our design choices, there are some introductory videos to JSON-LD linked from this email:

https://lists.w3.org/Archives/Public/public-linked-json/2015Feb/0014.html

dlongley commented 9 years ago

To understand our design, this video "Credentials on the Web" is particularly pertinent:

https://www.youtube.com/watch?v=eWtOg3vSzxI&index=5&list=PLjaODRwyGTalY6VkIsKyuPx-CsZV2c9Xv

adrianhopebailie commented 9 years ago

Some Clarity Required

Excuse me if I am stating the obvious, it's been a lot to digest quite quickly. It seems to me, the intended use for the Credentials Management API is as simple as:

  1. Looking in the browsers local storage for a Credential that tells the calling app:
    • that this user has logged into this app before and
    • what the userid is that they used to login.
  2. If they used a userid/password combination to login then the API can auto-login with the stored userid/password.
  3. If they used a federated identity to register with this app it provides their app-specific userid and a pointer to the federated identity provider they used. The actual login is left to the app.

If the only credentials that will be returned are credentials that were stored by this app (or one with the same origin) then the filtering seems a bit pointless. Why would the app ever have stored more than one credential?

Identity vs Credential

As mentioned in some of the WebAppSec email exchanges "identity" is explicitly out of scope here. Per the Credentials CG definition (which appears to be accepted by all on the mailing-list), an identity has a collection of credentials. This is also implied by @dlongley when he talks of an identity document containing many credentials.

If the full model of identity and credentials were in scope one would expect the flow to include selecting an identity (implicitly if the user has this setup in the browser already) and then using one or more credentials from that identity to gain access to a restricted resource (login to an app). The true value of identities is that they are not defined per app (per origin) but rather held by an IdP who shares them with relying party apps as required and allowed. i.e. They are not stored per origin by the browser.

The fact that this is all out of scope means it is understandable that the implementation of federated identities is a bit half-baked. As a user it would make much more sense for me to visit my federated identity provider and have a federated identity credential stored in my browser that is "cross-origin". It can then be used with any app that accepts credentials from that federated identity provider (assuming I give permission for it to be shared). This solves the "Nascar" problem quite elegantly.

If this is out of scope I don't see the point in standardising the federated identity provider identifier at all or enforcing that it is a URL. The credential will only ever be stored and retrieved by the same app so it can be identified in any way by an arbitrary string.

Filters

On that basis it feels like the changes that we've made today are just over-complicating things. There now appear to be 3 different ways to filter the credentials all of which are already filtered by origin:

  1. By specifying a list of Credential types using the type property.
  2. By specifying a list of supported federated identity providers
  3. By whatever means we ultimately want to filter the credential that are surfaced through linked-data queries. (I assume the filter will be part of the query itself).

Am I missing something obvious here?

mikewest commented 9 years ago

@adrianhopebailie: We're getting a bit far afield, I think. It might be worth moving this conversation back to the list, as it's not clear how directly relevant it is to piecing together an API that supports he use cases the CG is concerned with.

Why would the app ever have stored more than one credential?

  1. Google, Netflix, others support multi-login.
  2. Multiple people use the same computer. My wife and I both have credentials on the laptop in the living room.
  3. People are complicated. I have ~4 Google accounts, for instance, and have access to an even larger number of Twitter accounts. This is especially true in a work environment.

They are not stored per origin by the browser.

I don't doubt at all that there's a good deal of power in cross-origin identity providers. However, the idea that the information they share with an origin isn't scoped to that origin worries me. That is, I expect that the OAuth token I get from Twitter is going to be distinct for every application for which I choose to use Twitter, right?

I suspect I'm just misunderstanding.

If this is out of scope I don't see the point in standardising the federated identity provider identifier at all or enforcing that it is a URL.

@hillbrad can weigh in with his opinions regarding scope. I'll note three things:

  1. Managing "use cases related to ... username/federated identity provider pairs" is explicitly in scope Grabbing origin-scoped tokens from such a provider to aid a user in authenticating herself to an origin seems like a reasonable extension of the existing spec, but it's not something I think we have the bandwidth to pursue at the moment. That lack of bandwidth is what's kept it out of the spec, not an intention on my part to only support federation hints forever.

    For instance, supporting plain old OAuth should be quite doable, with enough cleverness and hard work. I can imagine an OAuthCredential subclassing FederatedCredential, with a big, red "Give me a token." button on it.

  2. Note that the "password" part of the PasswordCredential is never exposed directly to JavaScript, and submission is origin-locked. This is quite intentional, as the API would otherwise be a large target for XSS (this is one of the things which worries me about the JSON-LD document model). Creating different credential types allows us to have differing policies for different risk characteristics of different bits of data.
  3. The model of federation which requires SDKs and explicit bindings is, for better or worse, the model that exists on the web today. No matter how valuable it certainly is to explore alternatives for a brighter future, I think it's important that the API we put together deal well with the world into which it will be introduced.

To all these ends, FederatedCredential seems useful.

mikewest commented 9 years ago

@dlongley:

We'd prefer to return a single document with a single identifier for the entity that the credentials are for, and then have the credentials in an array off of that.

If that's what you want, it seems that get() already gives you a single object that you'd be able to hang your data off of. It doesn't seem like the model I'd recommend, but I know you've thought long and hard about it, so I won't argue here. :)

Regardless, @domenic's notion of a get() and getAll() distinction seems like a reasonable one. I'll add a note to that effect.

there would be one IdP for each entity

I'm not going to argue about this either, as it makes the requirement simpler to meet. It's not at all clear to me that it's a good idea to pipe all of an entity's data through a single endpoint, but, again, I know you've thought long and hard.

document

I'd like to defer the conversation around a document to a different bug, and keep this one focused on the requirements for a minimum viable compromise.

Does the set of changes I made today get us there, or are there cases where it falls down for the CG's needs?

dlongley commented 9 years ago

Note that the "password" part of the PasswordCredential is never exposed directly to JavaScript, and submission is origin-locked. This is quite intentional, as the API would otherwise be a large target for XSS (this is one of the things which worries me about the JSON-LD document model). Creating different credential types allows us to have differing policies for different risk characteristics of different bits of data.

Ah, I see. I thought we were discussing high-level modeling, not security concerns (which are still obviously important). The way we can protect highly-sensitive fields in a Linked Data document from being read in the JavaScript could be through a blacklist of URLs (properties map to URLs in Linked Data). The user agent could enforce this -- and it wouldn't require multiple polymorphic types of credentials. That being said, most credentials would make assertions that have the meaning "Is a US citizen" not the explicit data "SSN: 123-45-6789", and are therefore not as highly-sensitive as they are in many legacy models. To get access to the other data, you'd have to send it to the server securely through a send call just like the PasswordCredential.

On that note, this may be off-topic, but I believe you (or someone else) mentioned that applications like LastPass could override this API using just JavaScript (not special plugin-only hooks) to get access to the password they need to store. Doesn't that mean a compromised CDN can send JS that overrides PasswordCredential and it now has a single place it can sit and wait to read the password? If PasswordCredential can't be overridden like this, how does something like LastPass get access to the password information it needs to store? It seems like special plugin hooks are required for this to be secure.

dlongley commented 9 years ago

It's not at all clear to me that it's a good idea to pipe all of an entity's data through a single endpoint, but, again, I know you've thought long and hard.

To clarify, it's not all of an entity's data, it's all the data for one particular aspect (aka an identity) of an entity. Entities may have many different identities and often do, eg: one person having four different Google accounts.

dlongley commented 9 years ago

@mikewest,

If that's what you want, it seems that get() already gives you a single object that you'd be able to hang your data off of. It doesn't seem like the model I'd recommend, but I know you've thought long and hard about it, so I won't argue here.

The modeling or naming still isn't quite natural. It's more like the object that's being returned by get() is an agent of some sort in our case, not a "credential". This is how it may look:

interface LinkedDataCredentialAgent : Credential {
  identity: {
    id: <the identifier for the identity>,
    credentials: [<an array of credentials>]
  },
  send(); // will send `identity` to the origin
}

I think the problem is becoming more clear: A credential's identifier should not be the same thing as the identity's identifier (because a credential is not an identity). But that's how it's modeled. There's some conflation going on. Using getAll(), you would get back what? An array of credentials where each one has the same id and that id refers to the identity? It seems like, instead, they should be bundled together under a single unified object with an id (which refers to the identity). Each credential itself may have an id, but that id would be for the credential, not the identity it belongs to.

Now, I know we don't want to use the term "Identity", but just entertain this for a minute:

interface LinkedDataIdentity : Identity {
  id: <the identifier for the identity>,
  // credentials are expected to be JSON-LD documents
  credentials: [<an array of credentials>],
  send(); // will send `identity` to the origin as a JSON-LD document itself
}

That makes sense for our use cases. Does it make sense for WebAppSec?

interface LocalIdentity : Identity {
  id: <the identifier for the identity>,
  // credentials would often just have one entry, and it is a PasswordCredential
  credentials: [<an array of credentials>],
  send(); // will send `identity` to the origin
}

How can we reconcile this? Am I making sense?

mikewest commented 9 years ago

@dlongley, thanks for the feedback. How about something like the following as the generic interface:

interface CredentialSet {
    sequence<Credential> credentials;
};

interface CredentialsContainer {
    ...
    Promise<CredentialSet> getAll(CredentialRequestOptions options);
};

That would enable:

interface LinkedDataCredentialSet : CredentialSet {
    DOMString id;
    // Whatever other Linked Data methods/properties you need go here.
}

I don't think we have any need for getAll() at the moment, so I'm reluctant to add the complexity to the spec, but I hope this would be a reasonable model you could see yourself extending into.

(I'm dropping the conversation around Linked Data documents and API security. :) I'm happy to have those conversations on the list or in separate bugs, but I'd like to keep this focused on getting a minimal viable product defined.)

adrianhopebailie commented 9 years ago

@dlongley it would help me a lot if you could provide an example of how you imagine this API to be used by an app that accepts linked-data based credentials.

It feels like we are conflating the registration/initial credential verification use case with the login use case.

As an app using the API I can only imagine having a list of credential types that I am prepared to accept for login/age verification/etc. The process of gathering the credentials owned by the user (via linked data queries, processing of an identity document or whatever) seems to me separate from this process which is simply the app asking the user agent if it has any credentials in cache.

If the get() returns no results it would be great if there was a hook for the app to go out and find them and when that process completes to store them in the cache to avoid this process again in future.

When the time comes for my app to consume the credentials themselves I don't really care how they were collected do I? I just want to be able to verify them (out of scope for this API but part of the Cred CG work).

dlongley commented 9 years ago

@adrianhopebailie,

It would help me a lot if you could provide an example of how you imagine this API to be used by an app that accepts linked-data based credentials.

The trouble I've been having with fitting things into the API is that I think it has some modeling issues that conflate what I'm calling, for the sake of convenience, "identity" and credentials.

If I were to try and use the how the API has been framed with a reworked model, I believe it would look like something like this:

interface Identity {
  DOMString id;
}

interface IdentityContainer {
  Promise<Identity?> get(optional IdentityRequestOptions options);
  Promise<any> store(optional Identity identity);
  // etc.
}

interface LocalIdentity : Identity {
  // not exposed to JS
  DOMString password;
  Promise<any> send();
}

interface FederatedIdentity : Identity {
  USVString federation;
  // etc.
}

interface LinkedDataIdentity : Identity {
  USVString idp;
  // hidden, set by `get`
  USVString callback;  
  Promise<any> get(optional CredentialRequestOptions options);
  Promise<any> send();
}

To tell the user agent to have the user select an identity that could either be local, federated, or a "linked data" identity:

navigator.identity.get({
  types: [
    "LocalIdentity",
    "FederatedIdentity",
    "LinkedDataIdentity"
  ]
}).then(function(identity) {
  if(identity instanceof LocalIdentity) {
    identity.send();
  } else if(identity instanceof FederatedIdentity) {
    identity.send();
  } else if(identity instanceof LinkedDataIdentity) {
    identity.get({
      types: [<credential RDF types>],
      callback: "https://origin.com/receive-credentials"
    }).then(function(cached) {
      if(cached) {
        // send to callback
        identity.send();
      } else {
        // navigating away from page
      }
    });    
  }
});

When the identity is requested (navigator.identity.get), the user agent may show a dialog with the identities previously stored in the user agent. For the LinkedDataIdentity case, if you want to select an identity that isn't shown, the user agent will show you an email+password form that will use a TBD WebDHT technology (a distributed name resolution service for finding your LinkedDataIdentity identifier and IdP). Once your identity has been selected, the promise will resolve to your selection.

This is almost exactly what the Credential Management API does today, but the naming is different. It refers to identities instead of credentials, because that's what's actually being selected. Now, there is an argument that an identity and a credential are being selected in the password case, which I believe is the source of the conflation and confusion (at least for me).

For the Credentials CG, we can delay the actual credential acquisition to a later step. This is why, with the present naming, this doesn't make sense -- we don't want to ask for a "LinkedDataCredential", we want to ask for an identity and then ask for N credentials for that identity -- the latter of which can only be done once we have the identity and its IdP. Once we have a LinkedDataIdentity, we call identity.get() with our credentials query and obtain the credentials we need, either by finding them in the user agent's cache or by going to identity.idp to obtain them (the user agent does this for us).

There's room for later tweaking exactly how we want to implement LinkedDataIdentity, this is just a strawman.

dlongley commented 9 years ago

@mikewest,

How about something like the following as the generic interface...

We may be able to work with it (I haven't tried just yet), but I can tell you that I believe it will be a bit unnatural. I think there needs to be stronger separation of credentials and identity. The current model conflates the two. Regarding getAll (or just getting more than one credential), have you considered two-factor authentication cases ... like if a page wanted to get a password and a thumbprint/yubikey signature/etc for a particular identity?

I think if we can separate the concept of credentials from identity then there's actually very little else that needs to change and we can build off the API in a similar way to what you proposed originally -- just extending "Identity" (rather than "Credential") with our own custom class. I also think it the modeling will more accurately reflect the fact that people will be selecting the identity they want to login with -- and bringing back an icon, etc. to the core class (which is now an "identity") could potentially make sense again.

mikewest commented 9 years ago

The current model conflates the two.

Well, no. I'd say that the current API simply doesn't attempt to model an identity. If you'd like to add that concept in later, you're free to. CredentialSet seems like a reasonable thing for Identity to inherit from, as an Identity will (among other things) be a collection of credentials, won't it?

have you considered two-factor authentication cases

Vaguely, yes. The current API would force two calls to the API (which also happens to match the model most folks are running with (e.g. sign in, then confirm with a second factor)). I do think it'll be reasonable to extend the API in the future to support getting both at once, but I don't believe that's necessary for an MVP.

I think if we can separate the concept of credentials from identity then there's actually very little else that needs to change

I don't understand why this is necessary. :) In the current API, the concepts are distinct, as we simply neither define nor attempt to use the latter. The use cases the current proposal attempts to solve don't require the concept, and it's not clear to me why it's valuable to add in.

I understand that the use cases you'd like to solve do require that concept. I'm hopeful that something like CredentialSet would be a nicely generic way of modeling the kinds of properties you'd like Identity to contain. But you could certainly also define a getIdentity() method that gave you a "clean" Identity object with a has-a relationship to CredentialSet if that's a better model for your needs.

dlongley commented 9 years ago

If you'd like to add that concept in later, you're free to. CredentialSet seems like a reasonable thing for Identity to inherit from, as an Identity will (among other things) be a collection of credentials, won't it?

Ok, I'll take some time and see how things would look for our work if I model it with the latest changes to the spec. Thanks.

But you could certainly also define a getIdentity() method that gave you a "clean" Identity object with a has-a relationship to CredentialSet if that's a better model for your needs.

I'll keep that in mind as a possibility, however, I do hope we can keep things better unified.

In the current API, the concepts are distinct, as we simply neither define nor attempt to use the latter.

It seems like the id of a credential is an identifier for an identity, not for a credential itself. It also seems like what people will be selecting in a user agent UI will be an icon that represents some aspect of themselves or their "user account" for a particular website. That sounds like "identity" to me, not "credential". From my perspective, the most natural modeling is for the user to pick an identity and then use a particular credential to get authorization to take some action as that identity.

mikewest commented 9 years ago

It seems like the id of a credential is an identifier for an identity, not for a credential itself.

For a PasswordCredential, the id property is the username the user chooses to use on a particular site. Do you consider that an "identity"? If so, then yes, it's an identity identifier. :)

It's not clear to me that that's a sufficient representation of the concept you're really trying to get at, and I don't feel like a larger concept is necessary for the use cases this document is specifically attempting to address. I'd like to leave room for you to define the things that that concept would require, but I don't think that's part of an MVP.

dlongley commented 9 years ago

For a PasswordCredential, the id property is the username the user chooses to use on a particular site. Do you consider that an "identity"? If so, then yes, it's an identity identifier. :)

Yes, if that's what the site uses to uniquely identify the person. :) Their password can change, but their username will likely not. Their username will be the key that other information collected by or generated by the site is linked to (yes, there may be another layer of identity key abstraction the site uses, but that is merely an implementation detail).

In short, for most sites that have "users", each "user" has some kind of unique identifier. How you prove that you are that user -- or how you prove that, as that user, you have been granted certain permissions, has to do with your credentials. If you want to receive an email, or receive a reset email password link, prove that you own a certain email address (a verified email credential). If you want to login, use a password, or a federated IdP token, or a thumbprint scan, or whatever. None of these things change your user's identifier. Any of these credentials work with the same identity identifier. However, a credential is not uniquely identified by the user it is for -- otherwise you could only ever have one. Clearly that's not the case, you may have many different credentials -- and they serve different purposes. These are some of the reasons why I see the current design as conflating two different concepts.

Now, a credential may not need an identifier for itself, eg: a PasswordCredential. That doesn't mean it's id should be that of the user. Rather, it may have a userId or identityId for the user it is for.

dlongley commented 9 years ago

I do see the current model has being somewhat awkward for extension purposes (and therefore likely falls short of MVP status). Again, I will try to model what we need using the latest changes (I can do this shortly), but I think the current use cases have made the model too limited/conflated.

dlongley commented 9 years ago

So it seems like modeling this without any changes to separate "identity" from "credential" would look like this:

interface LinkedDataCredential : Credential {
  USVString idp;
  // hidden, set by `get`
  USVString callback;
  Promise<CredentialSet> credentials;
  // gets actual credentials
  Promise<any> get(optional LinkedDataCredentialRequestOptions options);
  // sends to `callback` if credentials are found in the cache
  Promise<any> send();
}
navigator.identity.get({
  types: [
    "PasswordCredential",
    "FederatedCredential",
    "LinkedDataCredential"
  ]
}).then(function(credential) {
  if(credential instanceof PasswordCredential) {
    credential.send();
  } else if(credential instanceof FederatedCredential) {
    credential.send();
  } else if(credential instanceof LinkedDataCredential) {
    credential.get({
      types: [<credential RDF types>],
      callback: "https://origin.com/receive-credentials"
    }).then(function(cached) {
      if(cached) {
        // send to callback
        credential.send();
      } else {
        // navigating away from page
      }
    });
  }
});

The end result may produce confusion over what a credential is. We would first be asking the user to select a credential, which to them, I think would mean, "who are you?". Then, the returned LinkedDataCredential instance could be used to get the actual credentials for authorizing the user via its specialized get method. The LinkedDataCredential could not accomplish this task. The LinkedDataCredential would just be a special type of credential that actually just functions as an identity (or user identifier). Developers would need to know that the LinkedDataCredential shouldn't be treated as a credential in and of itself, in the sense that you can use it to authorize any particular actions, but that you need to request other credentials in order to authorize the user's actions and to confirm the user's right to use the chosen identifier when taking those actions. It seems this educational barrier could pose a security risk.

If, instead, an "identity" or "user identifier" (I know "identity" is a loaded term and we should use something else) were requested, and you then had to obtain a credential for it, that would be preferrable. It could be that some kinds of user identifiers (eg: local ones) may have credentials automatically stored along with them (eg: passwords) and they could be returned immediately when the "identity/user identifier" was requested. For other types of "identities/user identifiers", like a "LinkedDataIdentity" that would not be the case. But if the separation were clear, the required eductional component wouldn't be an issue.

Another way to remove the eductional barrier would be add a getIdentity/getUserIdentifier call as you suggested, but that seems like we're going around the API, not using it. In fact, when the getIdentity call is made, we'd want the user agent to essentially go through the same code path to show a dialog for credential selection as they'd go through via get, except you'd be selecting an "identity/user identifier/thing", despite seeing the same list of options. This makes it all feel awkward to me.