Skip to content

Commit

Permalink
add: PAR support
Browse files Browse the repository at this point in the history
  • Loading branch information
Fendy Putra committed Feb 6, 2024
1 parent fc521ac commit 2706217
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 103 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"extends": ["oclif", "oclif-typescript", "prettier"]
"extends": ["oclif", "oclif-typescript", "prettier"],
"rules": {
"camelcase": "off",
"@typescript-esling/camelcase": "off"
}
}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18.18.2
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [1.4.0]

Add [PAR - Pushed Authorization Request](https://www.rfc-editor.org/rfc/rfc9126.html) support

## [1.3.3]

Add better handling for present http errors
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"prepare": "npm run build",
"version": "oclif readme && git add README.md"
},
"version": "1.3.3",
"version": "1.4.0",
"keywords": [
"oclif",
"sd-jwt",
Expand Down
40 changes: 20 additions & 20 deletions src/commands/create-credential-offer/create-credential-offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { readFile, writeFile } from 'node:fs/promises';
import { Credential } from '../../types/create-credential-offer.types.js';
import { GRANT_TYPES } from '../../types/index.js';
import { DATA_FOLDER } from '../../utils/constants.js';
import {
createCredentialOffer,
getCredentialsSupportedAsChoices,
getIssuerMetadata,
listJsonFilesChoices,
import {
createCredentialOffer,
getCredentialsSupportedAsChoices,
getIssuerMetadata,
listJsonFilesChoices,
parseGrantTypesAsChoices,
prependTS,
} from '../../utils/index.js';
Expand All @@ -20,9 +20,9 @@ import { getOpenidConfiguration } from '../../utils/openid/openid-config.js';
export default class CreateCredentialOffer extends Command {
static args = {
url: Args.string({
default: 'http://127.0.0.1:3000',
description: 'Meeco Organisation Wallet URL',
required: false
default: 'http://127.0.0.1:3000',
description: 'Meeco Organisation Wallet URL',
required: false,
}),
};

Expand All @@ -45,11 +45,11 @@ export default class CreateCredentialOffer extends Command {

const supportedCredentialsChoices = getCredentialsSupportedAsChoices(issuerMetadata.credentials_supported);

const supportedGrantTypes = parseGrantTypesAsChoices(issuerMetadata.grant_types_supported ?? openidConfig.grant_types_supported);
const supportedGrantTypes = parseGrantTypesAsChoices(openidConfig.grant_types_supported);

const selectedGrantType = await select({
choices: supportedGrantTypes,
message: 'Select Grant Type'
message: 'Select Grant Type',
});

let pinRequired = false;
Expand All @@ -63,20 +63,20 @@ export default class CreateCredentialOffer extends Command {
});

const claimsFile = await select({
choices: listJsonFilesChoices(".data"),
message: 'Select Claims file'
choices: listJsonFilesChoices('.data'),
message: 'Select Claims file',
});

const claims = await readFile(claimsFile).then((data) => JSON.parse(data.toString()));
const credentialInformation = selectedCredential.credentialIdentifier

const credentialInformation = selectedCredential.credentialIdentifier
? {
credentialIdentifiers: [selectedCredential.credentialIdentifier]
}
credentialIdentifiers: [selectedCredential.credentialIdentifier],
}
: {
format: selectedCredential.format,
types: selectedCredential.types,
}
format: selectedCredential.format,
types: selectedCredential.types,
};

const response = await createCredentialOffer({
...credentialInformation,
Expand All @@ -87,7 +87,7 @@ export default class CreateCredentialOffer extends Command {
}).catch((error) => {
this.log('Failed to Create Credential offer:', error.message);
this.exit(1);
})
});

const offerFilename = await prompt('Save offer as', { default: prependTS('credential-offer.txt') });

Expand Down
45 changes: 22 additions & 23 deletions src/types/openid.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export enum WELL_KNOWN {

export enum GRANT_TYPES {
AUTHORIZATION_CODE = 'authorization_code',
PREAUTHORIZED_CODE = 'urn:ietf:params:oauth:grant-type:pre-authorized_code'
PREAUTHORIZED_CODE = 'urn:ietf:params:oauth:grant-type:pre-authorized_code',
}

export type SupportedCredential = {
Expand All @@ -17,51 +17,50 @@ export type SupportedCredential = {
};
cryptographic_binding_methods_supported: string[];
cryptographic_suites_supported: string[];
display: [{
name: string;
}]
display: [
{
name: string;
},
];
format: string;
id: string;
types?: string[];
}
};

export type SupportedCredentialMap = {
[identifier: string]: SupportedCredential;
}
};

export type CredentialOfferDetails = string[];

export type IssuerMetadata = {
[metadata: string]: unknown;
authorization_endpoint?: string; // TODO: remove when no longer supporting org-wallet < 0.0.8
credential_endpoint: string;
credentials_supported: SupportedCredential[] | SupportedCredentialMap;
grant_types_supported?: string[]; // TODO: remove when no longer supporting org-wallet < 0.0.8
issuer: string;
pushed_authorization_endpoint?: string;
token_endpoint?: string; // TODO: remove when no longer supporting org-wallet < 0.0.8
}
};

export type OpenidConfiguration = {
authorization_endpoint: string;
grant_types_supported: string[];
issuer: string;
jwks_uri: string;
pushed_authorization_request_endpoint?: string;
token_endpoint: string;
}
};

export enum SIGNING_ALG {
ES256 = 'ES256'
ES256 = 'ES256',
}

export enum JWT_TYPE {
export enum JWT_TYPE {
JWT = 'JWT',
KEY_BINDING_JWT = 'kb+jwt',
VC_SD_JWT = 'vc+sd-jwt',
};
}

export enum SIOP {
V2 = 'https://self-issued.me/v2/openid-vc'
V2 = 'https://self-issued.me/v2/openid-vc',
}

export type PresentationRequest = {
Expand All @@ -76,21 +75,21 @@ export type PresentationRequest = {
}>;
id: string;
}>;
}
}
};
};
};
exp: number;
iss: string;
nonce: number;
redirect_uri: string;
state: string;
}
};

export type IdTokenPayload = {
'_vp_token': object;
export type IdTokenPayload = {
_vp_token: object;
aud: string;
exp: number;
nonce: number;
sub: string;
'sub_jwk': JWK;
}
sub_jwk: JWK;
};
115 changes: 82 additions & 33 deletions src/utils/openid/vci.auth-code.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import http from 'node:http';
import { ux } from '@oclif/core';
import http, { Server } from 'node:http';
import open from 'open';
import { Issuer, generators } from 'openid-client';
import { AuthorizationParameters, CallbackParamsType, Client, Issuer, generators } from 'openid-client';

import { OpenidConfiguration } from '../../types/openid.types.js';

Expand All @@ -10,7 +11,9 @@ type getTokenFromAuthorizationCodeArgs = {
issuerState: string;
openidConfig: OpenidConfiguration;
port?: string;
}
};

type codeExchangeCallback = (server: Server, params: CallbackParamsType) => Promise<void>;

export async function getTokenFromAuthorizationCode({
clientId,
Expand All @@ -24,58 +27,104 @@ export async function getTokenFromAuthorizationCode({
const issuer = new Issuer(openidConfig);

const client = new issuer.Client({
'client_id': clientId,
'redirect_uris': [listener],
'response_types': ['code'],
'token_endpoint_auth_method': 'none',
client_id: clientId,
token_endpoint_auth_method: 'none',
});

const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);

const authorizationUrl = client.authorizationUrl({
'code_challenge': codeChallenge,
'code_challenge_method': 'S256',
'issuer_state': issuerState,
const authParams = {
code_challenge: codeChallenge,
code_challenge_method: 'S256',
issuer_state: issuerState,
redirect_uri: listener,
response_type: 'code',
scope: 'openid',
});
};

let params: { [props: string]: unknown } = {};
const authorizationUrl = await getAuthorizationUrl(client, openidConfig, authParams);

let returnToken: (value: unknown) => void;
let rejectToken: (value: unknown) => void;

const promise = new Promise((resolve, reject) => {
returnToken = resolve;
rejectToken = reject;
});
const { callback, promise } = getLocalCallbackFunction(client, listener, codeVerifier);
await initLocalWebServer(client, port, callback);

const completeFlow = async () => {
const token = await client.oauthCallback(listener, params, {
'code_verifier': codeVerifier
}).catch((error) => rejectToken(error));
await open(authorizationUrl);

server.close();
return promise;
}

returnToken(token);
async function getAuthorizationUrl(
client: Client,
openidConfig: OpenidConfiguration,
params: AuthorizationParameters,
): Promise<string> {
let authorizationUrl: string;

if (openidConfig.pushed_authorization_request_endpoint) {
ux.action.start('using Pushed Authorization Request');

const { request_uri } = await client.pushedAuthorizationRequest(params);

if (!request_uri) {
throw new Error('Failed to obtain request_uri from PAR');
}

authorizationUrl = client.authorizationUrl({ request_uri });

ux.action.stop();
} else {
authorizationUrl = client.authorizationUrl(params);
}

const server = http
.createServer((req, res) => {
return authorizationUrl;
}

async function initLocalWebServer(client: Client, port: string, callback: codeExchangeCallback) {
let params: CallbackParamsType = {};

const server = await http
.createServer(async (req, res) => {
if (req.url?.startsWith('/?')) {
params = client.callbackParams(req);
params.state = params.issuer_state;
params.state = params.issuer_state as string;

completeFlow();
await callback(server, params);

res.end('You can close this page now.');
res.end('You can close this page now.');
} else {
res.end('Unsupported');
}
})
.listen(port);
}

await open(authorizationUrl);
function getLocalCallbackFunction(
client: Client,
listener: string,
codeVerifier: string,
): { callback: codeExchangeCallback; promise: Promise<unknown> } {
let returnToken: (value: unknown) => void;
let rejectToken: (value: unknown) => void;

return promise;
const promise = new Promise((resolve, reject) => {
returnToken = resolve;
rejectToken = reject;
});

const callback = async (server: Server, params: CallbackParamsType) => {
const token = await client
.oauthCallback(listener, params, {
code_verifier: codeVerifier,
})
.catch((error: Error) => rejectToken(error));

server.close();

returnToken(token);
};

return {
callback,
promise,
};
}
Loading

0 comments on commit 2706217

Please sign in to comment.