Skip to content

Commit 400e68f

Browse files
committed
Change architecture to avoid build issues #156
1 parent 960c2b3 commit 400e68f

File tree

13 files changed

+158
-61
lines changed

13 files changed

+158
-61
lines changed

src/adapters/BrowserAdapter.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Client from "../Client";
33
import BrowserStorage from "../storage/BrowserStorage";
44
import { fhirclient } from "../types";
55
import * as security from "../security/browser"
6+
import { encodeURL, decode, fromUint8Array } from "js-base64"
67

78
/**
89
* Browser Adapter
@@ -24,6 +25,8 @@ export default class BrowserAdapter implements fhirclient.Adapter
2425
*/
2526
options: fhirclient.BrowserFHIRSettings;
2627

28+
security = security;
29+
2730
/**
2831
* @param options Environment-specific options
2932
*/
@@ -141,6 +144,19 @@ export default class BrowserAdapter implements fhirclient.Adapter
141144
return window.btoa(str);
142145
}
143146

147+
base64urlencode(input: string | Uint8Array)
148+
{
149+
if (typeof input == "string") {
150+
return encodeURL(input)
151+
}
152+
return fromUint8Array(input, true)
153+
}
154+
155+
base64urldecode(input: string)
156+
{
157+
return decode(input)
158+
}
159+
144160
/**
145161
* Creates and returns adapter-aware SMART api. Not that while the shape of
146162
* the returned object is well known, the arguments to this function are not.

src/adapters/NodeAdapter.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import ServerStorage from "../storage/ServerStorage";
55
import { AbortController } from "abortcontroller-polyfill/dist/cjs-ponyfill";
66
import { IncomingMessage, ServerResponse } from "http";
77
import { TLSSocket } from "tls";
8+
import * as security from "../security/server"
9+
import { base64url } from "jose"
810

911

1012
interface NodeAdapterOptions {
@@ -28,6 +30,8 @@ export default class NodeAdapter implements fhirclient.Adapter
2830
*/
2931
options: NodeAdapterOptions;
3032

33+
security = security;
34+
3135
/**
3236
* @param options Environment-specific options
3337
*/
@@ -125,6 +129,16 @@ export default class NodeAdapter implements fhirclient.Adapter
125129
return global.Buffer.from(str, "base64").toString("ascii");
126130
}
127131

132+
base64urlencode(input: string | Uint8Array)
133+
{
134+
return base64url.encode(input);
135+
}
136+
137+
base64urldecode(input: string)
138+
{
139+
return base64url.decode(input).toString();
140+
}
141+
128142
/**
129143
* Returns a reference to the AbortController constructor. In browsers,
130144
* AbortController will always be available as global (native or polyfilled)

src/security/browser.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,6 @@ const ALGS = {
2323
} as RsaHashedKeyGenParams
2424
};
2525

26-
export const base64urlencode = (input: string | Uint8Array) => {
27-
if (typeof input == "string") {
28-
return encodeURL(input)
29-
}
30-
return fromUint8Array(input, true)
31-
}
32-
3326
export const base64urldecode = (input: string) => {
3427
return decode(input)
3528
}
@@ -46,8 +39,8 @@ export async function digestSha256(payload: string): Promise<Uint8Array> {
4639

4740
export const generatePKCEChallenge = async (entropy = 96): Promise<PkcePair> => {
4841
const inputBytes = randomBytes(entropy)
49-
const codeVerifier = base64urlencode(inputBytes)
50-
const codeChallenge = base64urlencode(await digestSha256(codeVerifier))
42+
const codeVerifier = fromUint8Array(inputBytes)
43+
const codeChallenge = fromUint8Array(await digestSha256(codeVerifier))
5144
return { codeChallenge, codeVerifier }
5245
}
5346

@@ -87,7 +80,7 @@ export async function signCompactJws(alg: keyof typeof ALGS, privateKey: CryptoK
8780

8881
const jwtHeader = JSON.stringify({ ...header, alg });
8982
const jwtPayload = JSON.stringify(payload);
90-
const jwtAuthenticatedContent = `${base64urlencode(jwtHeader)}.${base64urlencode(jwtPayload)}`;
83+
const jwtAuthenticatedContent = `${encodeURL(jwtHeader)}.${encodeURL(jwtPayload)}`;
9184

9285
const signature = await subtle.sign(
9386
{ ...privateKey.algorithm, hash: 'SHA-384' },

src/security/index.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/security/server.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ interface PkcePair {
2323

2424
type SupportedAlg = 'ES384' | 'RS384'
2525

26-
export const base64urlencode = (input: string | Uint8Array) => base64url.encode(input);
27-
export const base64urldecode = (input: string) => base64url.decode(input).toString();
2826

2927
export { randomBytes }
3028

@@ -36,12 +34,12 @@ export async function digestSha256(payload: string) {
3634

3735
export async function generatePKCEChallenge(entropy = 96): Promise<PkcePair> {
3836
const inputBytes = randomBytes(entropy)
39-
const codeVerifier = base64urlencode(inputBytes)
40-
const codeChallenge = base64urlencode(await digestSha256(codeVerifier))
37+
const codeVerifier = base64url.encode(inputBytes)
38+
const codeChallenge = base64url.encode(await digestSha256(codeVerifier))
4139
return { codeChallenge, codeVerifier }
4240
}
4341

44-
export async function importJWK(jwk: fhirclient.JWK): Promise<KeyLike> {
42+
export async function importJWK(jwk: fhirclient.JWK): Promise<CryptoKey> {
4543
// alg is optional in JWK but we need it here!
4644
if (!jwk.alg) {
4745
throw new Error('The "alg" property of the JWK must be set to "ES384" or "RS384"')
@@ -60,7 +58,7 @@ export async function importJWK(jwk: fhirclient.JWK): Promise<KeyLike> {
6058
throw new Error('The "key_ops" property of the JWK does not contain "sign"')
6159
}
6260

63-
return joseImportJWK(jwk) as Promise<KeyLike>
61+
return joseImportJWK(jwk) as Promise<CryptoKey>
6462
}
6563

6664
export async function signCompactJws(alg: SupportedAlg, privateKey: KeyLike, header: any, payload: any): Promise<string> {

src/smart.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
import Client from "./Client";
1515
import { SMART_KEY } from "./settings";
1616
import { fhirclient } from "./types";
17-
import * as security from "./security/index"
1817

1918
const debug = _debug.extend("oauth2");
2019

@@ -327,7 +326,7 @@ export async function authorize(
327326
}
328327

329328
if (shouldIncludeChallenge(extensions.codeChallengeMethods.includes('S256'), pkceMode)) {
330-
let codes = await security.generatePKCEChallenge()
329+
let codes = await env.security.generatePKCEChallenge()
331330
Object.assign(state, codes);
332331
await storage.set(stateKey, state); // note that the challenge is ALREADY encoded properly
333332
redirectParams.push("code_challenge=" + state.codeChallenge);
@@ -675,7 +674,9 @@ export async function buildTokenRequest(
675674
// Asymmetric auth
676675
else if (privateKey) {
677676

678-
const pk = privateKey.key || await security.importJWK(privateKey)
677+
const pk = "key" in privateKey ?
678+
privateKey.key as CryptoKey:
679+
await env.security.importJWK(privateKey as fhirclient.JWK)
679680

680681
if (isBrowser() && pk.extractable) {
681682
console.warn(
@@ -698,11 +699,11 @@ export async function buildTokenRequest(
698699
iss: clientId,
699700
sub: clientId,
700701
aud: tokenUri,
701-
jti: security.base64urlencode(security.randomBytes(32)),
702+
jti: env.base64urlencode(env.security.randomBytes(32)),
702703
exp: getTimeInFuture(120) // two minutes in the future
703704
};
704705

705-
const clientAssertion = await security.signCompactJws(privateKey.alg, pk, jwtHeaders, jwtClaims);
706+
const clientAssertion = await env.security.signCompactJws(privateKey.alg, pk, jwtHeaders, jwtClaims);
706707
requestOptions.body += `&client_assertion_type=${encodeURIComponent("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")}`;
707708
requestOptions.body += `&client_assertion=${encodeURIComponent(clientAssertion)}`;
708709
debug("Using state.clientPrivateJwk to add a client_assertion to the POST body")

src/types.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ declare namespace fhirclient {
159159
* ASCII string to Base64
160160
*/
161161
atob(str: string): string;
162+
163+
/**
164+
* ASCII string or Uint8Array to Base64URL
165+
*/
166+
base64urlencode: (input: string | Uint8Array) => string
167+
168+
/**
169+
* Base64Url to ASCII string
170+
*/
171+
base64urldecode: (input: string) => string
162172

163173
/**
164174
* Returns a reference to the AbortController class
@@ -173,6 +183,14 @@ declare namespace fhirclient {
173183
* optionally a storage or storage factory function.
174184
*/
175185
getSmartApi(): SMART;
186+
187+
security: {
188+
randomBytes: (count: number) => Uint8Array
189+
digestSha256: (payload: string) => Promise<Uint8Array>
190+
generatePKCEChallenge: (entropy?: number) => Promise<{ codeChallenge: string; codeVerifier: string }>
191+
importJWK: (jwk: JWK) => Promise<CryptoKey>
192+
signCompactJws: (alg: "ES384" | "RS384", privateKey: CryptoKey, header: any, payload: any) => Promise<string>
193+
}
176194
}
177195

178196
/**

test/browser.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ describe("Browser tests", () => {
224224
expect(client.getEncounterId()).to.equal("e3ec2d15-4c27-4607-a45c-2f84962b0700");
225225
expect(client.getUserId()).to.equal("smart-Practitioner-71482713");
226226
expect(client.getUserType()).to.equal("Practitioner");
227-
});
227+
});
228228

229229
it ("code flow with fullSessionStorageSupport = false", async () => {
230230

@@ -1874,4 +1874,28 @@ describe("Browser tests", () => {
18741874
});
18751875
});
18761876
});
1877+
1878+
// describe("BrowserAdapter", () => {
1879+
1880+
// it ("base64urlencode a string", () => {
1881+
// // @ts-ignore
1882+
// const env = new Adapter({})
1883+
// const input = "This is a test"
1884+
// expect(env.base64urlencode(input)).to.equal(Buffer.from(input).toString("base64url"))
1885+
// })
1886+
1887+
// it ("base64urlencode an Uint8Array", () => {
1888+
// // @ts-ignore
1889+
// const env = new Adapter({})
1890+
// const input = "This is a test"
1891+
// expect(env.base64urlencode(new TextEncoder().encode(input))).to.equal(Buffer.from(input).toString("base64url"))
1892+
// })
1893+
1894+
// it ("base64urldecode", () => {
1895+
// // @ts-ignore
1896+
// const env = new Adapter({})
1897+
// const input = Buffer.from("test").toString("base64url")
1898+
// expect(env.base64urldecode(input)).to.equal("test")
1899+
// })
1900+
// })
18771901
});

test/mocks/BrowserEnvironment.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ const EventEmitter = require("events");
33
import BrowserStorage from "../../src/storage/BrowserStorage";
44
import { fhirclient } from "../../src/types";
55
import { AbortController } from "abortcontroller-polyfill/dist/cjs-ponyfill";
6+
import * as security from "../../src/security/server"
7+
import { base64url } from "jose"
68

79

810
export default class BrowserEnvironment extends EventEmitter implements fhirclient.Adapter
911
{
1012
options: any;
1113

14+
security = security;
15+
1216
constructor(options = {})
1317
{
1418
super();
@@ -64,6 +68,16 @@ export default class BrowserEnvironment extends EventEmitter implements fhirclie
6468
return Buffer.from(str, "base64").toString("ascii");
6569
}
6670

71+
base64urlencode(input: string | Uint8Array)
72+
{
73+
return base64url.encode(input);
74+
}
75+
76+
base64urldecode(input: string)
77+
{
78+
return base64url.decode(input).toString();
79+
}
80+
6781
getAbortController()
6882
{
6983
return AbortController as any;

test/mocks/ServerEnvironment.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { AbortController as AbortControllerPonyfill } from "abortcontroller-polyfill/dist/cjs-ponyfill";
22
import ServerStorage from "../../src/storage/ServerStorage";
33
import { fhirclient } from "../../src/types";
4+
import * as security from "../../src/security/server"
5+
import { base64url } from "jose"
46

57
const AbortController = global.AbortController || AbortControllerPonyfill
68

@@ -14,6 +16,8 @@ export default class ServerEnvironment implements fhirclient.Adapter
1416

1517
options: fhirclient.JsonObject;
1618

19+
security = security;
20+
1721
constructor(request?: any, response?: any, storage?: any)
1822
{
1923
this.request = request;
@@ -79,6 +83,16 @@ export default class ServerEnvironment implements fhirclient.Adapter
7983
return Buffer.from(str, "base64").toString("ascii");
8084
}
8185

86+
base64urlencode(input: string | Uint8Array)
87+
{
88+
return base64url.encode(input);
89+
}
90+
91+
base64urldecode(input: string)
92+
{
93+
return base64url.decode(input).toString();
94+
}
95+
8296
getAbortController()
8397
{
8498
return AbortController;

0 commit comments

Comments
 (0)