Skip to content

Commit

Permalink
migrate connection auth
Browse files Browse the repository at this point in the history
  • Loading branch information
markusahlstrand committed Dec 29, 2024
1 parent a0a18c9 commit 29deb3b
Show file tree
Hide file tree
Showing 22 changed files with 605 additions and 32 deletions.
6 changes: 0 additions & 6 deletions .changeset/wicked-scissors-judge.md

This file was deleted.

9 changes: 9 additions & 0 deletions apps/demo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @authhero/demo

## 0.5.11

### Patch Changes

- Updated dependencies
- Updated dependencies [a0a18c9]
- [email protected]
- @authhero/kysely-adapter@0.26.1

## 0.5.10

### Patch Changes
Expand Down
6 changes: 3 additions & 3 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"name": "@authhero/demo",
"private": true,
"version": "0.5.10",
"version": "0.5.11",
"scripts": {
"dev": "bun --watch src/bun.ts"
},
"dependencies": {
"@authhero/kysely-adapter": "^0.26.0",
"@authhero/kysely-adapter": "^0.26.1",
"@hono/swagger-ui": "^0.5.0",
"@hono/zod-openapi": "^0.18.3",
"@peculiar/x509": "^1.12.3",
"authhero": "^0.31.0",
"authhero": "^0.32.0",
"hono": "^4.6.13",
"hono-openapi-middlewares": "^1.0.11",
"kysely-bun-sqlite": "^0.3.2",
Expand Down
7 changes: 7 additions & 0 deletions packages/adapter-interfaces/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @authhero/adapter-interfaces

## 0.35.0

### Minor Changes

- migrate connection auth
- a0a18c9: move most of authorize endpoint

## 0.34.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-interfaces/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"type": "git",
"url": "https://github.com/markusahlstrand/authhero"
},
"version": "0.34.0",
"version": "0.35.0",
"files": [
"dist"
],
Expand Down
13 changes: 13 additions & 0 deletions packages/authhero/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# authhero

## 0.32.0

### Minor Changes

- migrate connection auth
- a0a18c9: move most of authorize endpoint

### Patch Changes

- Updated dependencies
- Updated dependencies [a0a18c9]
- @authhero/adapter-interfaces@0.35.0

## 0.31.0

### Minor Changes
Expand Down
3 changes: 2 additions & 1 deletion packages/authhero/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "authhero",
"version": "0.31.0",
"version": "0.32.0",
"files": [
"dist"
],
Expand Down Expand Up @@ -36,6 +36,7 @@
"dependencies": {
"@authhero/adapter-interfaces": "workspace:^",
"@peculiar/x509": "^1.12.3",
"arctic": "^2.3.3",
"bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3",
"i18next": "^24.2.0",
Expand Down
97 changes: 97 additions & 0 deletions packages/authhero/src/authentication-flows/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Context } from "hono";
import { AuthParams, Client, LogTypes } from "@authhero/adapter-interfaces";
import { HTTPException } from "hono/http-exception";
import { createLogMessage } from "../utils/create-log-message";
import { nanoid } from "nanoid";
import { getClientInfo } from "../utils/client-info";
import { Bindings, Variables } from "../types";
import {
OAUTH2_CODE_EXPIRES_IN_SECONDS,
UNIVERSAL_AUTH_SESSION_EXPIRES_IN_SECONDS,
} from "../constants";
import { strategies } from "../strategies";
import { setSearchParams } from "../utils/url";

export async function connectionAuth(
ctx: Context<{ Bindings: Bindings; Variables: Variables }>,
client: Client,
connectionName: string,
authParams: AuthParams,
) {
if (!authParams.state) {
throw new HTTPException(400, { message: "State not found" });
}

const connection = client.connections.find((p) => p.name === connectionName);

if (!connection) {
ctx.set("client_id", client.id);
const log = createLogMessage(ctx, {
type: LogTypes.FAILED_LOGIN,
description: "Connection not found",
});
await ctx.env.data.logs.create(client.tenant.id, log);

throw new HTTPException(403, { message: "Connection Not Found" });
}

let loginSession = await ctx.env.data.logins.get(
client.tenant.id,
authParams.state,
);

if (!loginSession) {
loginSession = await ctx.env.data.logins.create(client.tenant.id, {
expires_at: new Date(
Date.now() + UNIVERSAL_AUTH_SESSION_EXPIRES_IN_SECONDS * 1000,
).toISOString(),
authParams,
...getClientInfo(ctx.req),
});
}

const options = connection.options || {};

const strategy = strategies[connection.strategy];

if (strategy) {
const result = await strategy.getRedirect(ctx, connection);

await ctx.env.data.codes.create(client.tenant.id, {
login_id: loginSession.login_id,
code_id: result.code,
code_type: "oauth2_state",
connection_id: connection.id,
code_verifier: result.codeVerifier,
expires_at: new Date(
Date.now() + OAUTH2_CODE_EXPIRES_IN_SECONDS * 1000,
).toISOString(),
});

return ctx.redirect(result.redirectUrl);
}

// This the legacy version
const auth2State = await ctx.env.data.codes.create(client.tenant.id, {
login_id: loginSession.login_id,
code_id: nanoid(),
code_type: "oauth2_state",
connection_id: connection.id,
expires_at: new Date(
Date.now() + OAUTH2_CODE_EXPIRES_IN_SECONDS * 1000,
).toISOString(),
});

const oauthLoginUrl = new URL(options.authorization_endpoint!);

setSearchParams(oauthLoginUrl, {
scope: options.scope,
client_id: options.client_id,
redirect_uri: `${ctx.env.ISSUER}callback`,
response_type: connection.response_type,
response_mode: connection.response_mode,
state: auth2State.code_id,
});

return ctx.redirect(oauthLoginUrl.href);
}
1 change: 1 addition & 0 deletions packages/authhero/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export const SILENT_COOKIE_NAME = "auth-token";
export const OTP_EXPIRATION_TIME = 30 * 60 * 1000; // 30 minutes
export const EMAIL_VERIFICATION_EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000; // One week
export const AUTHORIZATION_CODE_EXPIRES_IN_SECONDS = 5 * 60; // 5 minutes
export const OAUTH2_CODE_EXPIRES_IN_SECONDS = 5 * 60; // 5 minutes
7 changes: 3 additions & 4 deletions packages/authhero/src/routes/auth-api/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getAuthCookie } from "../../utils/cookies";
import { universalAuth } from "../../authentication-flows/universal";
import { ticketAuth } from "../../authentication-flows/ticket";
import { silentAuth } from "../../authentication-flows/silent";
import { connectionAuth } from "../../authentication-flows/connection";

// const UI_STRATEGIES = [
// "email",
Expand Down Expand Up @@ -169,11 +170,9 @@ export const authorizeRoutes = new OpenAPIHono<{
// return socialAuth(ctx, client, client.connections[0].name, authParams);
// }

// Social login
// Connection auth flow
if (connection && connection !== "email") {
throw new Error("Not implemented");

// return socialAuth(ctx, client, connection, authParams);
return connectionAuth(ctx, client, connection, authParams);
} else if (login_ticket) {
return ticketAuth(
ctx,
Expand Down
99 changes: 99 additions & 0 deletions packages/authhero/src/strategies/apple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Apple } from "arctic";
import { Context } from "hono";
import { Connection } from "@authhero/adapter-interfaces";
import { nanoid } from "nanoid";
import { Bindings, Variables } from "../types";
import { parseJWT } from "oslo/jwt";
import { idTokenSchema } from "../types/IdToken";

function getAppleOptions(connection: Connection) {
const { options } = connection;

if (
!options ||
!options.client_id ||
!options.team_id ||
!options.kid ||
!options.app_secret
) {
throw new Error("Missing required Apple authentication parameters");
}

// Use a secure buffer to handle private key
const privateKeyBuffer = Buffer.from(options.app_secret, "utf-8");
const cleanedKey = privateKeyBuffer
.toString()
.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\s/g, "");
const keyArray = Uint8Array.from(Buffer.from(cleanedKey, "base64"));
// Clear sensitive data from memory
privateKeyBuffer.fill(0);

return { options, keyArray };
}

export async function getRedirect(
ctx: Context<{ Bindings: Bindings; Variables: Variables }>,
connection: Connection,
) {
const { options, keyArray } = getAppleOptions(connection);

const apple = new Apple(
options.client_id!,
options.team_id!,
options.kid!,
keyArray,
`${ctx.env.ISSUER}callback`,
);

const code = nanoid();

const authorizatioUrl = await apple.createAuthorizationURL(
code,
options.scope?.split(" ") || ["name", "email"],
);

const scopes = options.scope?.split(" ") || ["name", "email"];
if (scopes.some((scope) => ["email", "name"].includes(scope))) {
authorizatioUrl.searchParams.set("response_mode", "form_post");
}

return {
redirectUrl: authorizatioUrl.href,
code,
};
}

export async function validateAuthorizationCodeAndGetUser(
ctx: Context<{ Bindings: Bindings; Variables: Variables }>,
connection: Connection,
code: string,
) {
const { options, keyArray } = getAppleOptions(connection);

const apple = new Apple(
options.client_id!,
options.team_id!,
options.kid!,
keyArray,
`${ctx.env.ISSUER}callback`,
);

const tokens = await apple.validateAuthorizationCode(code);
const idToken = parseJWT(tokens.idToken());

if (!idToken) {
throw new Error("Invalid ID token");
}

const payload = idTokenSchema.parse(idToken.payload);

return {
sub: payload.sub,
email: payload.email,
given_name: payload.given_name,
family_name: payload.family_name,
name: payload.name,
picture: payload.picture,
locale: payload.locale,
};
}
73 changes: 73 additions & 0 deletions packages/authhero/src/strategies/facebook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Facebook } from "arctic";
import { Context } from "hono";
import { Connection } from "@authhero/adapter-interfaces";
import { nanoid } from "nanoid";
import { Bindings, Variables } from "../types";
import { parseJWT } from "oslo/jwt";
import { idTokenSchema } from "../types/IdToken";

export async function getRedirect(
ctx: Context<{ Bindings: Bindings; Variables: Variables }>,
connection: Connection,
) {
const { options } = connection;

if (!options?.client_id || !options.client_secret) {
throw new Error("Missing required authentication parameters");
}

const facebook = new Facebook(
options.client_id,
options.client_secret,
`${ctx.env.ISSUER}callback`,
);

const code = nanoid();

const authorizationUrl = facebook.createAuthorizationURL(
code,
options.scope?.split(" ") || ["email"],
);

return {
redirectUrl: authorizationUrl.href,
code,
};
}

export async function validateAuthorizationCodeAndGetUser(
ctx: Context<{ Bindings: Bindings; Variables: Variables }>,
connection: Connection,
code: string,
) {
const { options } = connection;

if (!options?.client_id || !options.client_secret) {
throw new Error("Missing required authentication parameters");
}

const facebook = new Facebook(
options.client_id,
options.client_secret,
`${ctx.env.ISSUER}callback`,
);

const tokens = await facebook.validateAuthorizationCode(code);
const idToken = parseJWT(tokens.idToken());

if (!idToken) {
throw new Error("Invalid ID token");
}

const payload = idTokenSchema.parse(idToken.payload);

return {
sub: payload.sub,
email: payload.email,
given_name: payload.given_name,
family_name: payload.family_name,
name: payload.name,
picture: payload.picture,
locale: payload.locale,
};
}
Loading

0 comments on commit 29deb3b

Please sign in to comment.