Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

Commit

Permalink
move auth adapters
Browse files Browse the repository at this point in the history
  • Loading branch information
thdxr committed Feb 14, 2024
1 parent 1507299 commit 94faf27
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 386 deletions.
8 changes: 7 additions & 1 deletion sdk/js/src/auth/adapter/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import type { Context, Hono } from "hono";
export type Adapter<Properties> = (
import { KeyLike } from "jose";

export type Adapter<Properties = any> = (
route: AdapterRoute,
options: AdapterOptions<Properties>,
) => void;

export type AdapterRoute = Hono;
export interface AdapterOptions<Properties> {
name: string;
algorithm: string;
publicKey: Promise<KeyLike>;
privateKey: Promise<KeyLike>;
success: (ctx: Context, properties: Properties) => Promise<Response>;
forward: (ctx: Context, response: Response) => Response;
cookie: (ctx: Context, key: string, value: string, maxAge: number) => void;
}

export class AdapterError extends Error {}
Expand Down
95 changes: 13 additions & 82 deletions sdk/js/src/auth/adapter/apple.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import querystring from 'node:querystring';
import { generators, Issuer } from 'openid-client';
import { Issuer } from "openid-client";

import { useBody, useCookie, useDomainName, usePathParam, useResponse } from '../../../api/index.js';
import { Adapter } from './adapter.js';
import { OauthConfig } from './oauth.js';
import { OauthAdapter, OauthBasicConfig } from "./oauth.js";

// This adapter support the OAuth flow with the response_mode "form_post" for now.
// More details about the flow:
Expand All @@ -14,84 +11,18 @@ import { OauthConfig } from './oauth.js';
// await Issuer.discover("https://appleid.apple.com/.well-known/openid-configuration/");

const issuer = await Issuer.discover(
"https://appleid.apple.com/.well-known/openid-configuration"
)
"https://appleid.apple.com/.well-known/openid-configuration",
);

export const AppleAdapter =
/* @__PURE__ */
(config: OauthConfig) => {
return async function () {
const step = usePathParam("step");
const callback = "https://" + useDomainName() + "/callback";
console.log("callback", callback);

const client = new issuer.Client({
client_id: config.clientID,
client_secret: config.clientSecret,
redirect_uris: [callback],
response_types: ["code"],
});

if (step === "authorize" || step === "connect") {
const code_verifier = generators.codeVerifier();
const state = generators.state();
const code_challenge = generators.codeChallenge(code_verifier);

const url = client.authorizationUrl({
scope: config.scope,
code_challenge: code_challenge,
code_challenge_method: "S256",
state,
prompt: config.prompt,
...config.params,
});

useResponse().cookies(
{
auth_code_verifier: code_verifier,
auth_state: state,
},
{
httpOnly: true,
secure: true,
maxAge: 60 * 10,
sameSite: "None",
}
);
return {
type: "step",
properties: {
statusCode: 302,
headers: {
location: url,
},
},
};
}

if (step === "callback") {
let params = {}
if (config && config.params && config.params.response_mode === "form_post") {
const body = useBody()
if (typeof body === "string") {
params = querystring.parse(body)
}
}

const code_verifier = useCookie("auth_code_verifier");
const state = useCookie("auth_state");
const tokenset = await client["callback"](callback, params, {
code_verifier,
state,
});
const x = {
type: "success" as const,
properties: {
tokenset,
client,
},
};
return x;
}
} satisfies Adapter;
(config: OauthBasicConfig) => {
return OauthAdapter({
issuer,
...config,
params: {
...config.params,
response_mode: "form_post",
},
});
};
110 changes: 39 additions & 71 deletions sdk/js/src/auth/adapter/code.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,69 @@
import { APIGatewayProxyStructuredResultV2 } from "aws-lambda";

import {
useCookie,
useDomainName,
usePathParam,
useQueryParam,
useQueryParams,
useResponse,
} from "../../../api/index.js";
import { Adapter } from "./adapter.js";
import { randomBytes } from "crypto";
import { decrypt, encrypt } from "../encryption.js";
import * as jose from "jose";
import { deleteCookie, getCookie } from "hono/cookie";
import { UnknownStateError } from "../index.js";

export function CodeAdapter(config: {
length?: number;
onCodeRequest: (
code: string,
claims: Record<string, any>
) => Promise<APIGatewayProxyStructuredResultV2>;
claims: Record<string, any>,
) => Promise<Response>;
onCodeInvalid: (
code: string,
claims: Record<string, any>
) => Promise<APIGatewayProxyStructuredResultV2>;
claims: Record<string, any>,
) => Promise<Response>;
}) {
const length = config.length || 6;

function generate() {
const buffer = randomBytes(length);
const buffer = crypto.getRandomValues(new Uint8Array(length));
const otp = Array.from(buffer)
.map((byte) => byte % 10)
.join("");
return otp;
}

return async function () {
const step = usePathParam("step");

if (step === "authorize" || step === "connect") {
return function (routes, ctx) {
routes.get("/authorize", async (c) => {
const code = generate();
const claims = useQueryParams();
const claims = c.req.query();
delete claims["client_id"];
delete claims["redirect_uri"];
delete claims["response_type"];
delete claims["provider"];
useResponse().cookies(
{
authorization: encrypt(
JSON.stringify({
claims,
code,
})
),
},
{
maxAge: 3600,
secure: true,
sameSite: "None",
httpOnly: true,
}
);
return {
type: "step",
properties: await config.onCodeRequest(code, claims as any),
};
}
const authorization = await new jose.CompactEncrypt(
new TextEncoder().encode(
JSON.stringify({
claims,
code,
}),
),
)
.setProtectedHeader({ alg: "RSA-OAEP-512", enc: "A256GCM" })
.encrypt(await ctx.publicKey);
ctx.cookie(c, "authorization", authorization, 60 * 10);
return ctx.forward(c, await config.onCodeRequest(code, claims as any));
});

if (step === "callback") {
routes.get("/callback", async (c) => {
const authorization = getCookie(c, "authorization");
if (!authorization) throw new UnknownStateError();
const { code, claims } = JSON.parse(
decrypt(useCookie("authorization")!)!
new TextDecoder().decode(
await jose
.compactDecrypt(authorization!, await ctx.privateKey)
.then((value) => value.plaintext),
),
);
if (!code || !claims) {
return {
type: "step",
properties: await config.onCodeInvalid(code, claims),
};
return ctx.forward(c, await config.onCodeInvalid(code, claims as any));
}
const compare = useQueryParam("code");
const compare = c.req.query("code");
if (code !== compare) {
return {
type: "step",
properties: await config.onCodeInvalid(code, claims),
};
return ctx.forward(c, await config.onCodeInvalid(code, claims as any));
}
useResponse().cookies(
{
authorization: "",
},
{
expires: new Date(1),
}
);
return {
type: "success",
properties: {
claims: claims,
},
};
}
} satisfies Adapter;
deleteCookie(c, "authorization");
return ctx.forward(c, await ctx.success(c, { claims }));
});
} satisfies Adapter<{ claims: Record<string, string> }>;
}
24 changes: 9 additions & 15 deletions sdk/js/src/auth/adapter/link.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { Adapter } from "./adapter.js";
import { Resource } from "../../resource.js";
import * as jose from "jose";

export function LinkAdapter<
T extends Record<string, string> = Record<string, string>,
>(config: { onLink: (link: string, claims: T) => Promise<Response> }) {
export function LinkAdapter(config: {
onLink: (link: string, claims: Record<string, any>) => Promise<Response>;
}) {
return function (routes, ctx) {
const { privateKey, publicKey } = Resource[process.env.AUTH_ID!];

routes.get("/authorize", async (c) => {
const token = await new jose.SignJWT(c.req.query())
.setProtectedHeader({ alg: "RS512" })
.setProtectedHeader({ alg: ctx.algorithm })
.setExpirationTime("10m")
.sign(await jose.importPKCS8(privateKey, "RS512"));
.sign(await ctx.privateKey);

const url = new URL(new URL(c.req.url).origin);
url.pathname = `/${ctx.name}/callback`;
Expand All @@ -22,20 +19,17 @@ export function LinkAdapter<
url.searchParams.set("token", token);
const resp = ctx.forward(
c,
await config.onLink(url.toString(), c.req.query() as T),
await config.onLink(url.toString(), c.req.query()),
);
return resp;
});

routes.get("/callback", async (c) => {
const token = c.req.query("token");
if (!token) throw new Error("Missing token parameter");
const verified = await jose.jwtVerify(
token,
await jose.importSPKI(publicKey, "RS512"),
);
const resp = await ctx.success(c, verified.payload as any);
const verified = await jose.jwtVerify(token, await ctx.publicKey);
const resp = await ctx.success(c, { claims: verified.payload as any });
return resp;
});
} as Adapter<T>;
} satisfies Adapter<{ claims: Record<string, string> }>;
}
Loading

0 comments on commit 94faf27

Please sign in to comment.