-
Notifications
You must be signed in to change notification settings - Fork 209
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
Conversation
…ess token has been issued
…der, UInt8Array and IndexedDB
…de true in signUpRequest
src/JsonService.ts
Outdated
@@ -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")) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -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> { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
Codecov ReportAttention: Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
src/UserManager.ts
Outdated
@@ -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"; |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@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. |
It is still very invasive and i am not convinced all new code is at the correct place.
At the end probably (i am not 100% sure) all/most code adaptions, which are Maybe it makes sense in a first step to only extend this library in a generic way (like you did with |
No problems - that all makes sense 👍 Will give it a go and see where we end up. |
@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 |
@@ -24,6 +24,7 @@ export interface CreateSigninRequestArgs | |||
redirect_uri?: string; | |||
response_type?: string; | |||
scope?: string; | |||
dpopJkt? : string; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
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:
|
That's a great idea - I can do that. |
Closing this in favour of #1461 |
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
I'm not super experienced with Typescript, apologies if I've made rookie mistakes. Please give me feedback.
Checklist