-
Notifications
You must be signed in to change notification settings - Fork 20
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
feat: dpop support #131
feat: dpop support #131
Conversation
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.
Overall looking very good. Some small suggestions, comments.
I do think we probably need to create a real common
module. Because with the changes proposed (which are good) now the SIOP package will start depending on the vci-common package. The latter really is about VCI ATM, so probably better to start a real common package, which will also server as a nice module for the refactor happening soon
@@ -87,10 +92,17 @@ export class AccessTokenClient { | |||
: undefined, | |||
}); | |||
|
|||
return this.sendAuthCode(requestTokenURL, accessTokenRequest); | |||
let dPoP: string | undefined; | |||
if (createDPoPOptions?.dPoPSigningAlgValuesSupported && createDPoPOptions?.dPoPSigningAlgValuesSupported.length > 0) { |
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.
if (createDPoPOptions?.dPoPSigningAlgValuesSupported && createDPoPOptions?.dPoPSigningAlgValuesSupported.length > 0) { | |
if (createDPoPOptions?.dPoPSigningAlgValuesSupported && createDPoPOptions.dPoPSigningAlgValuesSupported.length > 0) { |
} | ||
|
||
// The DPoP HTTP request header field value is a single and well-formed JWT. | ||
const dPoPHeader = jwtDecode<JwtHeader & Partial<DPoPJwtHeaderProps>>(dpop, { header: true }); |
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.
We have a parseJWT that does both. I guess we could change that function to accept some generics for header and payload, like the original functions do, with defaults for JwtHeader and JwtPayload
} | ||
|
||
// Validate iat claim | ||
const now = epochTime(); |
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 is probably wise to add an small negative offset option to the function. You will typically use this in time based properties in JWTs, like for instance iat
. Since there could be small offsets in time between your computer and the remote computer, we typically subtract a few seconds from the iat
, because otherwise you might run into a JWT not valid yet
type of situation if the remote machine is slightly lagging behind
@@ -27,7 +27,8 @@ export interface AuthorizationServerMetadata { | |||
// Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. A request_uri value obtained from the PAR endpoint is usable at the authorization endpoint regardless of other authorization server metadata such as request_uri_parameter_supported or require_request_uri_registration | |||
require_pushed_authorization_requests?: boolean; // Boolean parameter indicating whether Indicates whether the client is required to use PAR to initiate authorization. If omitted, the default value is false. | |||
'pre-authorized_grant_anonymous_access_supported': boolean; // OPTIONAL. A JSON Boolean indicating whether the issuer accepts a Token Request with a Pre-Authorized Code but without a client id. The default is false | |||
|
|||
// A JSON array containing a list of the JWS alg values (from the [IANA.JOSE.ALGS] registry) supported by the authorization server for DPoP proof JWTs. | |||
dpop_signing_alg_values_supported?: 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.
Could we create a type for this and if exhaustive use the type, if not use the union of the type and 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.
Very nice martin!
return this.sendAuthCode(requestTokenURL, accessTokenRequest); | ||
let dPoP: string | undefined; | ||
if (createDPoPOptions?.dPoPSigningAlgValuesSupported && createDPoPOptions.dPoPSigningAlgValuesSupported.length > 0) { | ||
const htu = requestTokenURL.split('?')[0].split('#')[0]; |
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.
Can we extract this into an util?
private async sendAuthCode( | ||
requestTokenURL: string, | ||
accessTokenRequest: AccessTokenRequest, | ||
options?: { dPoP?: 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.
Should we allow passing customHeaders here as well? So if other extensions are needed we can add it over time without needing to add them? Not sure just thinking here
@@ -89,6 +91,7 @@ export class CredentialRequestClient { | |||
context?: string[]; | |||
format?: CredentialFormat | OID4VCICredentialFormat; | |||
subjectIssuance?: ExperimentalSubjectIssuance; | |||
createDPoPOptions?: CreateDPoPClientOptions; |
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.
As this is inside an opts object, would just dpop
as a key make more sense here / simpler?
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.
IMO not the dpop to me is the proof itself not to options for creating it.
let dPoP: string | undefined; | ||
if (createDPoPOptions) { | ||
const htu = credentialEndpoint.split('?')[0].split('#')[0]; | ||
dPoP = createDPoPOptions | ||
? await createDPoP({ | ||
...createDPoPOptions, | ||
jwtPayloadProps: { ...createDPoPOptions.jwtPayloadProps, htu, htm: 'POST', accessToken: requestToken }, | ||
}) | ||
: undefined; | ||
} |
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 see this repeated quite often. Maybe we can do the htu extracting in the createDpop method. That way we can do:
const dpop = createDPoPOptions ? createDPoP() : undefined
/** | ||
* Returns the current unix timestamp in seconds. | ||
*/ | ||
function epochTime() { |
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.
This is not dpop specific
} | ||
|
||
export interface JwtIssuerCustom extends JwtIssuerBase { | ||
method: 'custom'; |
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.
Is alg now optional in the jwt sign callback? As with custom it will still not be provided right?
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 custom will not do anything
} | ||
|
||
try { | ||
const fullUrl = request.protocol + '://' + request.get('host') + request.originalUrl |
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.
Host is not always defined or the same url. Host can be 0.0.0.0 while you use a public url. I think this needs to be configured somewhere (i've bad issues with this in credo when deploying to a https server url)
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.
Hmmm, I am not sure about it. I don't mind adding it as a parameter to the handleTokenRequest, but it looks weird to have it pass as a parameter.
@nklomp You also had a question about this.
Please doublecheck whether request.protocol isn't already including the trailing :
expressjs/express#4697 (comment)
https://stackoverflow.com/questions/10183291/how-to-get-the-full-url-in-express/10185427#10185427
For now, I changed the implementation so that the access token endpoint URL can be passed; if passed as an argument, we use it. Otherwise, we fall back to the above behavior. We can also enforce passing it. What do you think?
Signed-off-by: Martin Auer <[email protected]>
Signed-off-by: Martin Auer <[email protected]>
Signed-off-by: Martin Auer <[email protected]>
Signed-off-by: Martin Auer <[email protected]>
Signed-off-by: Martin Auer <[email protected]>
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.
Thanks @auer-martin . This probably became a way bigger PR than imagined. 😂
A few small remarks. My only bigger issue remaining is the retryDPoP result object. I think we should create a small type for it and I am also not sure about the current handling of these methods when the dpop create options are set and the dpop methods return ok: false.
nextDPoPNonce = successDPoPNonce ?? retryWithNonce.dpopNonce; | ||
} | ||
|
||
if (response.successBody && createDPoPOpts && createDPoPOpts && response.successBody.token_type !== 'DPoP') { |
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.
This for sure will ensures the DPoPOpts is present :D
nextDPoPNonce = successDPoPNonce ?? retryWithNonce.dpopNonce; | ||
} | ||
|
||
if (response.successBody && createDPoPOpts && createDPoPOpts && response.successBody.token_type !== 'DPoP') { |
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.
if (response.successBody && createDPoPOpts && createDPoPOpts && response.successBody.token_type !== 'DPoP') { | |
if (response.successBody && createDPoPOpts && response.successBody.token_type !== 'DPoP') { |
throw new Error('The DPoP nonce was not returned'); | ||
} | ||
|
||
return { ok: true, dpopNonce: dPoPNonce } as const; |
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.
Can we create a small type for this? The ok is not immediately clear. I think having the http status code instead of ok would be nicer. The 401 is because we handle it nicely I guess, but it could also happen with a 400. So probably just using the http status is quite useful
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 created a type. Hmmmmm, Not sure about the HTTP status return. Because then, at a higher level, you need to know which status codes trigger the retry.
The 401 is required for Resource Requests. https://www.rfc-editor.org/rfc/rfc9449.html#name-resource-server-provided-no
packages/common/lib/dpop/DPoP.ts
Outdated
const { nowSkewedPast, nowSkewedFuture } = getNowSkewed(options.now); | ||
if ( | ||
// iat claim is to far in the future | ||
nowSkewedPast - (options.maxIatAgeInSeconds ?? 300) > dPoPPayload.iat || |
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 would make the defaults a bit smaller. Systems being of 5 minutes would be very bad. Suggest to use 60
packages/common/lib/jwt/jwtUtils.ts
Outdated
* | ||
* See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 | ||
*/ | ||
const DEFAULT_SKEW_TIME = 300; |
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.
See other remark. Suggest to lower this to 60
It was indeed a lot more work and took much longer than originally expected 🤣. |
Signed-off-by: Timo Glastra <[email protected]>
Signed-off-by: Timo Glastra <[email protected]>
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.
Tested the PR in our wallet and succesfully received credentials from the Funke PID issuer.
@nklomp leaving final review / approval to you |
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.
LGTM. Will merge
This PR adds support for dPoP.
DPoP support itself did not require many changes. Most of the changes made in this PR are refactorings, e.g., JwtIssuer and JwtVerifier to common so they can be reused ....sorry for that.
Changes made for actual dpop support can be found in this commit.
I open the PR as a draft for now, as I plan to go over the spec one last time in the next couple of days and add some tests.