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

Demonstrating Proof of Possession #1361

Closed
wants to merge 38 commits into from
Closed

Demonstrating Proof of Possession #1361

wants to merge 38 commits into from

Conversation

dbfr3qs
Copy link
Contributor

@dbfr3qs dbfr3qs commented Jan 25, 2024

Addresses/highlights #1360

As per the issue, this is an example of a potential implementation of Demonstrating Proof of Possession (DPoP) to constrain access/refresh tokens to devices. I'm keen on feedback/collaboration if this is a thing that is deemed worth adding to the library...

As mentioned in the issue, this is mostly spec complete - though I have left out some parts that I don't think are appropriate to the oidc-client-ts use case (such as Pushed Authorisation Requests).

I've used the react-odic-client library example app and npm linked it to my local version of this PR. I used a fork of IdentityServer's DPoP examples to develop and test against, if you are interested in trying it out for yourself.

Things of note

  • This adds a new set of options specifically for DPoP to the OidcClient and introduces a new service - the DPoPService.
  • What's the general feeling in regards to adding dependencies? I ask this because...
    • I've included two dependencies to do this: Jose and idb-keyval. Jose allows for generating JWTs at runtime and idk-keyval wraps the native IndexedDB api with a more intuitive promise based api. Both of these libraries have no external dependencies.
    • I had to get a little bit funky in order to satisfy these dependencies in the tests.
    • It will increase the size of the overall bundle.
  • In the case of crypto-js, I see that it is no longer maintained. I've used the browser native SubtleCrypto api to generate key material here in any case, because I wanted to take advantage of the extractable property when generating key pairs.

I'm not super experienced with Typescript, apologies if I've made rookie mistakes. Please give me feedback.

Checklist

  • This PR makes changes to the public API
  • I have included links for closing relevant issue numbers

@@ -176,6 +187,15 @@ export class JsonService {
if (!response.ok) {
logger.error("Error from server:", json);
if (json.error) {
if (json.error === "use_dpop_nonce" && dpopEnabled && response.headers.get("Dpop-Nonce")) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec describes an optional nonce requirement whereby if a nonce is not supplied during a request, a bad request is returned by the resource server with a specific response body and the client is expected to generate a nonce and retry the request.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets keep the throw of that error, if the IDP needs a Dpop-Nonce, we should already provide one in the first place...

Copy link
Contributor Author

@dbfr3qs dbfr3qs Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm okay with that but it's worth considering that the spec is a little unclear about when an authorization server should require a dpop_nonce (keep in mind that the nonce is generated server side):

Including a nonce value contributed by the authorization server in the DPoP proof MAY be used by authorization servers to limit the lifetime of DPoP proofs. The server determines when to issue a new DPoP nonce challenge and if it is needed, thereby requiring the use of the nonce value in subsequent DPoP proofs. The logic through which the server makes that determination is out of scope of this document.

but it's quite clear about what to do when no nonce is supplied:

An authorization server MAY supply a nonce value to be included by the client in DPoP proofs sent. In this case, the authorization server responds to requests that do not include a nonce with an HTTP 400 (Bad Request) error response per Section 5.2 of [RFC6749] using use_dpop_nonce as the error code value. The authorization server includes a DPoP-Nonce HTTP header in the response supplying a nonce value to be used when sending the subsequent request. Nonce values MUST be unpredictable. This same error code is used when supplying a new nonce value when there was a nonce mismatch. The client will typically retry the request with the new nonce value supplied upon receiving a use_dpop_nonce error with an accompanying nonce value.

This is how it's implemented in IdentityServer for example - the nonce header isn't returned anywhere in the flow, prior to responding with a 400 bad request if the drop_nonce header doesn't exist. This is also how Okta does it. So, happy to remove it, but I suspect what is here will become the norm if it hasn't already.

However, you did get me thinking and I realised I did miss something out. Once the nonce is received, it should actually be saved by the client and re-used until another error response is returned. I could add this behaviour if you'd like, or just remove it entirely as you've suggested. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @pamapa I have removed the authorization server supplied nonce support for the time being. I think it would be better to do it in a separate PR if that's okay (it is optional in the spec)? This PR is large and it was becoming messy.

@@ -124,4 +125,8 @@ export class User {
Logger.createStatic("User", "fromStorageString");
return new User(JSON.parse(storageString));
}

public async dpopProof(url: string, httpMethod?: string): Promise<string> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit weird exposing this here: there needs to be a way of allowing consumers to generate a DPoP proof and then attaching that to authenticated requests.

But it seemed like the easiest way from the pov of the react-oidc-client library... for example https://github.com/dbfr3qs/react-oidc-context/blob/217d6d18109fc82526d7abc4015a98ec359fe79b/example/index.tsx#L29C13-L39C16

value: true
});

class JSDOMEnvironment extends $JSDOMEnvironment {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdom doesn't support some apis so I had to work around that.
jsdom/jsdom#2524

Also some issues with jest and esm modules...
https://stackoverflow.com/questions/76608600/jest-tests-are-failing-because-of-an-unknown-unexpected-token-export

src/DPoPService.ts Outdated Show resolved Hide resolved
Copy link

codecov bot commented Feb 14, 2024

Codecov Report

Attention: Patch coverage is 81.81818% with 16 lines in your changes are missing coverage. Please review.

Project coverage is 80.56%. Comparing base (fc1d31c) to head (7085388).
Report is 19 commits behind head on main.

❗ Current head 7085388 differs from pull request most recent head 1e019ea. Consider uploading reports for the commit 1e019ea to get more accurate results

Files Patch % Lines
src/utils/CryptoUtils.ts 52.94% 7 Missing and 1 partial ⚠️
src/DPoPService.ts 81.08% 5 Missing and 2 partials ⚠️
src/User.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1361      +/-   ##
==========================================
+ Coverage   80.53%   80.56%   +0.03%     
==========================================
  Files          45       46       +1     
  Lines        1731     1816      +85     
  Branches      344      358      +14     
==========================================
+ Hits         1394     1463      +69     
- Misses        299      312      +13     
- Partials       38       41       +3     
Flag Coverage Δ
unittests 80.56% <81.81%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -15,6 +15,7 @@ import type { SignoutResponse } from "./SignoutResponse";
import type { MetadataService } from "./MetadataService";
import { RefreshState } from "./RefreshState";
import type { SigninResponse } from "./SigninResponse";
import { del } from "idb-keyval";
Copy link
Member

@pamapa pamapa Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idb-keyval must be replaced with the existing store user infrastructure. There where we store the user/session we can store the dpop value too...

Copy link
Contributor Author

@dbfr3qs dbfr3qs Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I am working on removing that dependency now.

However, we must use indexedDB to store non extractable crypto key objects. We cannot store these in a plain text storage mechanism, such as local/session storage. If we store key material securely in IndexedDB, it provides some measure of protection against leaking the crypto key material used to generate the DPoP proofs. If we chose to make the key material extractable and stored in plane text, then we largely defeat the purpose of DPoP in the browser as the DPoP keys can be exfiltrated out of the browser alongside any tokens.

See
https://blog.engelke.com/2014/09/19/saving-cryptographic-keys-in-the-browser/
https://stackoverflow.com/a/49479890
https://crypto.stackexchange.com/a/52488
https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey/extractable

And the ietf recommendation https://www.ietf.org/archive/id/draft-ietf-oauth-browser-based-apps-16.html#section-6.3.3.2.2

Instead I will provide a similar interface to the WebStorageStageStore, though wrapping IndexedDB and specifically for storing CryptoKeyPairs. This will remove the dependency on ida-keyval, but the key material will still be stored in IndexedDB.

Does this make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pamapa I have removed the idv-keyval package as a dependency, and added replacement code to manage indexedDB transactions. As mentioned above, we must use indexedDB to store non extractable crypto keys, otherwise there's not a lot of point in adding a DPoP implementation to pure frontend applications.

@dbfr3qs
Copy link
Contributor Author

dbfr3qs commented Mar 6, 2024

@pamapa I think this is ready for a proper review as it is essentially code complete, apart from optional AS supplied nonce support which I think should be done separately at a later date. Please take a look and let me know what else you'd like to change, or any questions you may have as I know it's quite a large PR.

@pamapa
Copy link
Member

pamapa commented Mar 14, 2024

Please take a look and let me know what else you'd like to change, or any questions you may have as I know it's quite a large PR.

It is still very invasive and i am not convinced all new code is at the correct place.

  • e.g. TokenClient.ts does not need extraHeaders["DPoP"] = ..., but simply support the extraHeaders parameter too

At the end probably (i am not 100% sure) all/most code adaptions, which are DPoP related end up in the UserManager class.

Maybe it makes sense in a first step to only extend this library in a generic way (like you did with extraHeaders), such that you can inherit e.g. the UserManager class yourself and are able to use this feature.

@dbfr3qs
Copy link
Contributor Author

dbfr3qs commented Mar 14, 2024

Please take a look and let me know what else you'd like to change, or any questions you may have as I know it's quite a large PR.

It is still very invasive and i am not convinced all new code is at the correct place.

  • e.g. TokenClient.ts does not need extraHeaders["DPoP"] = ..., but simply support the extraHeaders parameter too

At the end probably (i am not 100% sure) all/most code adaptions, which are DPoP related end up in the UserManager class.

Maybe it makes sense in a first step to only extend this library in a generic way (like you did with extraHeaders), such that you can inherit e.g. the UserManager class yourself and are able to use this feature.

No problems - that all makes sense 👍 Will give it a go and see where we end up.

@dbfr3qs
Copy link
Contributor Author

dbfr3qs commented Mar 20, 2024

@pamapa is this more in line with what you were thinking?

I have pulled everything up into the UserManager. DPoP related functionality is then passed through as either an extraHeader (the DPoP header) or as a parameter (dpopJkt which is added as a url param during sign in to bind the DPoP proof to the authorization code).

@@ -24,6 +24,7 @@ export interface CreateSigninRequestArgs
redirect_uri?: string;
response_type?: string;
scope?: string;
dpopJkt? : string;
Copy link
Member

@pamapa pamapa Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess this can be part of extra header you add just below or no extra header and this parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so - it's not sent as a header, it's a url request parameter in the sign up request (to /authorize) so that the AS has a way of knowing that the subsequent /token request comes from the same entity.

The extraHeaders, at this point, are exclusively used to send along the DPoP proof which is used when making said subsequent requests for tokens.

Unless I'm misunderstanding you?

@pamapa
Copy link
Member

pamapa commented Mar 21, 2024

The changes go in the right direction, thanks for being patience.

I guess it makes sense to have a separate merge request with stuff you need, but does not contain DPoP, this way you have progress and its easier to review:

  • JsonService.ts
  • TokenClient.ts
  • ResponseValidator.ts
  • typos : recommend -> recommended

@dbfr3qs
Copy link
Contributor Author

dbfr3qs commented Mar 21, 2024

The changes go in the right direction, thanks for being patience.

I guess it makes sense to have a separate merge request with stuff you need, but does not contain DPoP, this way you have progress and its easier to review:

  • JsonService.ts
  • TokenClient.ts
  • ResponseValidator.ts
  • typos : recommend -> recommended

That's a great idea - I can do that.

@dbfr3qs
Copy link
Contributor Author

dbfr3qs commented Apr 3, 2024

Closing this in favour of #1461

@dbfr3qs dbfr3qs closed this Apr 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants