Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
dlongley opened this issue Apr 15, 2015 · 69 comments
Open

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

dlongley opened this issue Apr 15, 2015 · 69 comments

Comments

@dlongley
Copy link

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
Copy link
Author

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
Copy link

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
Copy link
Author

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
Copy link

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
Copy link
Author

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

@jmajnert
Copy link

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

@dlongley
Copy link
Author

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
Copy link
Member

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 CREDENTIAL: boolean acceptPasswords = true; #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 #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.

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
Copy link

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
Copy link
Author

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
Copy link
Member

annevk commented Apr 16, 2015

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
Copy link
Author

@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
Copy link

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
Copy link
Author

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
Copy link

domenic commented Apr 16, 2015

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

@adrianhopebailie
Copy link

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
Copy link

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
Copy link

domenic commented Apr 16, 2015

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

@dlongley
Copy link
Author

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
Copy link

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
Copy link

domenic commented Apr 16, 2015

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
Copy link

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?

mikewest added a commit that referenced this issue Apr 16, 2015
This set of patches is an initial pass at addressing #256.
@domenic
Copy link

domenic commented Apr 16, 2015

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

@dlongley
Copy link
Author

@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
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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
Copy link
Author

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
Copy link
Author

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.

@mikewest
Copy link
Member

Concretely, what would you like to see change in the API to make your use case less awkward?

@mikewest
Copy link
Member

That is, I don't understand how this would work for a password. For that case, the things you're calling "identity" and "credential" are the same. :)

@dlongley
Copy link
Author

@mikewest, I think when you ask for a LocalIdentity, it would be returned with a PasswordCredential stored in it. Then you call send() and everything functions as it does now. Other types of identities may not have "built-in" credentials.

@dlongley
Copy link
Author

Other types of identities may not have "built-in" credentials, you have to go fetch the ones you want for the user's chosen identifier.

@dlongley
Copy link
Author

A quick strawman:

navigator.identifiers.get({
  type: ["LocalIdentifier", "FederatedIdentifier", "FutureIdentifier"]
}).then(function(identifier) {
  if(identifier instanceof LocalIdentifier) {
    // identifier.credential is a PasswordCredential w/hidden password property
    // identifier.send() will send the identifier+password
  }
  if(identifier instanceof FederatedIdentifier) {
    // identifier.credential is a FederatedCredential or some other
    // custom/proprietary/super-provider thing
  }
  if(identifier instanceof FutureIdentifier) {
    // identifier.idp holds a URL to an IdP that speaks a standard protocol
    // identifier.get() will talk to that IdP and fetch credentials for the identifier
    // based on a query you give it
    // identifier.send() will send the retrieved credentials (if from cache)
  }
});

@mikewest
Copy link
Member

navigator.credentials.get() should return a Credential, shouldn't it? With regard to the use cases the doc specifies, I don't really understand the necessity for indirection for any of those use cases. Similarly, I don't understand the necessity for anything other than a bundle of Credential objects that something like CredentialSet would supply.

@mikewest
Copy link
Member

if(identifier instanceof LocalIdentifier) {

In this case, the .credential indirection is, practically, meaningless.

if(identifier instanceof FederatedIdentifier) {

In this case, we're asking developers to type .credential.federation rather than .federation, which seems unfortunate.

if(identifier instanceof FutureIdentifier) {

In this case, you're not using the .credential at all, which I don't understand. Didn't you suggest that "Identity" (or whatever) contained multiple credentials?

@mikewest
Copy link
Member

Also: won't the if(identifier instanceof FutureIdentifier) { flow require multiple user interactions? If there's only one IdP, as you suggested in an earlier comment, it would seem better to do everything at once. There's no semantic distinction between navigator.identifiers.get() + identifier.get() in your strawman, and navigator.credentials.getAll() in my strawman, is there? The practical outcome seems identical.

@adrianhopebailie
Copy link

@dlongley if you look at the discussions on the mailing list you'll see that trying to get the concept of identity in scope here is not going to happen even though I agree that the Credential base-class does represent an identity in many respects more than it represents a credential.

With that in mind I have taken the view that this API is a mechanism for accessing locally cached credentials and nothing more.

I think everyone is on-board with the idea that the ideal model is an identity that has one or more credentials. In future one might get an instance of the identity by calling something like navigator.identies.get() or accessing a it via a Credential that has a property pointing to it's identity.

Today the API only deals with locally cached credentials. Future APIs could either extend the Credential class to have a property of type Identity or we define a new Identity Management API with methods like navigator.identities.get(). Whenever a new identity is loaded or fetched from an IdP the associated credentials are stored and accessible via the Credentials Management API.

The flow for a calling app is much the same as how the proposed FederatedCredential flow works.

  1. The RP site requires a credential so it looks for it via the API.
  2. If it finds what it needs it performs whatever functions are required based on the specifics of that credential.
    Examples:
    • It's a FederatedCredential that indicates the user has a login with Microsoft. So the RP site uses some custom Microsoft supplied logic to login to the Microsoft IdP.
    • It's a signed "age verification credential" (that was loaded into the cache in a parallel operation when the user visited their IdP in another session). So the RP site validates the signature on the credential and compares the issuer to a list they are happy with then proceeds.
    • It's a new custom credential type that supports fetching linked-data. The RP site uses some linked-data libraries to process the credential and fetch whatever additional data needs to be fetched online.
  3. The RP site doesn't find the credentials it needs in the cache so it kicks of a process of getting the user to login via their IdP and during that process caches the credentials it gathers for next time. (@mikewest It would be great if we could define a standard property on the Credential class that is just a function callback allowing the caller to define some logic that is performed for each credential before the set is returned. Something like an onload function).

It's not as good as a fully-fledged Identity Management API out the gates but I think it's a good compromise.

@mikewest
Copy link
Member

It would be great if we could define a standard property on the Credential class that is just a function callback allowing the caller to define some logic that is performed for each credential before the set is returned.

Assuming we defined getAll(), the following should do what you want here, right?

function process(credential) {
    // do something interesting.
}

navigator.credentials.getAll(...).then(function(credentials) {
    credentials.map(process);
});

Given that there's no use case in the document for multiple credentials, I don't think there's any real need to define a callback property. It's easily bolted on.

@adrianhopebailie
Copy link

@mikewest I was thinking more of a hook on the Credential base class that is always called at some point during the get() flow. It gives sub-classes a stub they can implement if required.

So I could store a custom Credential like this and expect my function to get called when the Credential is being loaded from the cache:

navigator.credentials.store(
  new LinkedDataCredential({
    "id": "http://credentials.com/1234",
    "onLoad": function() { //Do some processing here... },
  })
);

Not convinced this solves @dlongley 's use case but keen to hear your thoughts

@mikewest
Copy link
Member

@adrianhopebailie: I don't really understand the use case. Could you give me some detail around what you'd like to do with regard to incoming credentials?

@adrianhopebailie
Copy link

Call the IdP that originally issued the credential to get the linked data before it is returned to the caller

@adrianhopebailie
Copy link

@mikewest I am not attached to the idea, was trying to suggest something that may give @dlongley the hook he needs

@mikewest
Copy link
Member

I'd suggest that the current draft has sufficiently explicit extensibility hooks to make it possible to support the use cases outlined in this issue. I've documented those at https://w3c.github.io/webappsec/specs/credentialmanagement/#implementation-extension.

Is there anything left here that would block the future work you're interested in, @dlongley and @adrianhopebailie? If not, can we close this out, and deal with more detailed questions in issues like #274?

@dlongley
Copy link
Author

@mikewest, I'm not quite ready to close out the issue; I just haven't had time to write some example code that would work for us. I'd like to keep it open until we've at least seen what our implementation might look like with the current state of the spec. We'll get back to you when we've got something.

mikewest pushed a commit to mikewest/webappsec that referenced this issue Jun 29, 2015
@mikewest
Copy link
Member

mikewest commented Sep 4, 2015

Moving this to "future". Based on the conversation at https://lists.w3.org/Archives/Public/public-webappsec/2015Aug/0145.html (and the rest of that thread) I think we're at a point where the current spec is extensible enough for your needs.

@dlongley
Copy link
Author

@mikewest,

We've got a simple polyfill that explores how our API would look on top of the Credential Management API. We've got a demo using it here: https://authorization.io

The demo is fairly fragile and doesn't do much in terms of error handling, but it demonstrates registration with a credential curator (previously known to and referred to as "identity provider" in the demo) and the main credential flows.

@dlongley
Copy link
Author

@mikewest,

The link to the polyfill code is here: https://github.com/digitalbazaar/credentials-polyfill

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants