Skip to content

Commit

Permalink
Merge pull request #11 from markusahlstrand/ma/token-hook
Browse files Browse the repository at this point in the history
Ma/token hook
  • Loading branch information
markusahlstrand authored Dec 12, 2024
2 parents 1186179 + 9e3a793 commit 6fc3b87
Show file tree
Hide file tree
Showing 29 changed files with 329 additions and 91 deletions.
16 changes: 16 additions & 0 deletions apps/demo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# @authhero/demo

## 0.5.2

### Patch Changes

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

## 0.5.1

### Patch Changes

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

## 0.5.0

### Minor 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.0",
"version": "0.5.2",
"scripts": {
"dev": "bun --watch src/bun.ts"
},
"dependencies": {
"@authhero/kysely-adapter": "^0.24.4",
"@authhero/kysely-adapter": "^0.25.1",
"@hono/swagger-ui": "^0.5.0",
"@hono/zod-openapi": "^0.18.3",
"@peculiar/x509": "^1.12.3",
"authhero": "^0.22.0",
"authhero": "^0.24.0",
"hono": "^4.6.13",
"hono-openapi-middlewares": "^1.0.11",
"kysely-bun-sqlite": "^0.3.2",
Expand Down
12 changes: 12 additions & 0 deletions packages/adapter-interfaces/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# @authhero/adapter-interfaces

## 0.32.0

### Minor Changes

- add hooks to add claims to token

## 0.31.0

### Minor Changes

- set used_at for codes

## 0.30.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.30.0",
"version": "0.32.0",
"files": [
"dist"
],
Expand Down
5 changes: 3 additions & 2 deletions packages/adapter-interfaces/src/adapters/Codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ export interface ListCodesResponse extends Totals {
}

export interface CodesAdapter {
create: (tenant_id: string, authCode: CodeInsert) => Promise<Code>;
create: (tenant_id: string, code: CodeInsert) => Promise<Code>;
get: (
tenant_id: string,
code_id: string,
type: CodeType,
) => Promise<Code | null>;
list: (tenant_id: string, params: ListParams) => Promise<ListCodesResponse>;
remove: (tenant_id: string, code: string) => Promise<boolean>;
used: (tenant_id: string, code_id: string) => Promise<boolean>;
remove: (tenant_id: string, code_id: string) => Promise<boolean>;
}
16 changes: 1 addition & 15 deletions packages/adapter-interfaces/src/types/Connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,7 @@ import {
export const connectionInsertSchema = z.object({
id: z.string().optional(),
name: z.string(),
strategy: z.enum([
"google-oauth2",
"facebook",
"vipps",
"freja",
"apple",
"email",
"auth0",
"authhero",
// This is incorrect as strategy. Remove once data is migrated
"Username-Password-Authentication",
"oidc",
"oauth2",
"custom",
]),
strategy: z.string(),
options: z
.object({
kid: z.string().optional(),
Expand Down
22 changes: 22 additions & 0 deletions packages/authhero/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# authhero

## 0.24.0

### Minor Changes

- add hooks to add claims to token

### Patch Changes

- Updated dependencies
- @authhero/adapter-interfaces@0.32.0

## 0.23.0

### Minor Changes

- set used_at for codes

### Patch Changes

- Updated dependencies
- @authhero/adapter-interfaces@0.31.0

## 0.22.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/authhero/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "authhero",
"version": "0.22.0",
"version": "0.24.0",
"files": [
"dist"
],
Expand Down
6 changes: 3 additions & 3 deletions packages/authhero/src/auth-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export default function create() {
app.use(createAuthMiddleware(app));

const oauthApp = app
.route("/.well-known", wellKnownRoutes)
.route("/oauth/token", tokenRoutes)
.route("/v2/logout", logoutRoutes)
.route("/userinfo", userinfoRoutes);
.route("/userinfo", userinfoRoutes)
.route("/.well-known", wellKnownRoutes)
.route("/oauth/token", tokenRoutes);

oauthApp.doc("/spec", {
openapi: "3.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export async function authorizationCodeGrant(
throw new HTTPException(403, { message: "Invalid client credentials" });
} else if (new Date(code.expires_at) < new Date()) {
throw new HTTPException(403, { message: "Code expired" });
} else if (code.used_at) {
throw new HTTPException(403, { message: "Code already used" });
}

const login = await ctx.env.data.logins.get(client.tenant.id, code.login_id);
Expand Down Expand Up @@ -104,7 +106,7 @@ export async function authorizationCodeGrant(
throw new HTTPException(403, { message: "User not found" });
}

await ctx.env.data.codes.remove(client.tenant.id, params.code);
await ctx.env.data.codes.used(client.tenant.id, params.code);

// Create a new session
const session = await ctx.env.data.sessions.create(client.tenant.id, {
Expand All @@ -118,6 +120,7 @@ export async function authorizationCodeGrant(
const tokens = await createAuthTokens(ctx, {
authParams: login.authParams,
user,
client,
sid: session.session_id,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ export async function clientCredentialsGrant(
audience: params.audience,
};

const tokens = await createAuthTokens(ctx, { authParams });
const tokens = await createAuthTokens(ctx, { authParams, client });
return ctx.json(tokens);
}
78 changes: 57 additions & 21 deletions packages/authhero/src/authentication-flows/common.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { AuthParams, User } from "@authhero/adapter-interfaces";
import { AuthParams, Client, User } from "@authhero/adapter-interfaces";
import { Context } from "hono";
import { HTTPException } from "hono/http-exception";
import { TimeSpan } from "oslo";
import { createJWT } from "oslo/jwt";
import { pemToBuffer } from "../utils/crypto";
import { Bindings, Variables } from "../types";

export interface CreateAuthTokensParams {
authParams: AuthParams;
client: Client;
user?: User;
sid?: string;
}

export async function createAuthTokens(
ctx: Context,
ctx: Context<{ Bindings: Bindings; Variables: Variables }>,
params: CreateAuthTokensParams,
) {
const { authParams, user, sid } = params;
const { authParams, user, client, sid } = params;

const signingKeys = await ctx.env.data.keys.list();
const validKeys = signingKeys.filter(
Expand All @@ -29,26 +31,60 @@ export async function createAuthTokens(

const keyBuffer = pemToBuffer(signingKey.pkcs7);

const access_token = await createJWT(
"RS256",
keyBuffer,
{
// TODO: consider if the dafault should be removed
aud: authParams.audience || "default",
scope: authParams.scope || "",
sub: user?.user_id || authParams.client_id,
iss: ctx.env.ISSUER,
tenant_id: ctx.var.tenant_id,
sid,
},
{
includeIssuedTimestamp: true,
expiresIn: new TimeSpan(1, "d"),
headers: {
kid: signingKey.kid,
const payload = {
// TODO: consider if the dafault should be removed
aud: authParams.audience || "default",
scope: authParams.scope || "",
sub: user?.user_id || authParams.client_id,
iss: ctx.env.ISSUER,
tenant_id: ctx.var.tenant_id,
sid,
};

if (ctx.env.hooks?.onExecuteCredentialsExchange) {
await ctx.env.hooks.onExecuteCredentialsExchange(
{
client,
user,
scope: authParams.scope || "",
grant_type: "",
},
{
accessToken: {
setCustomClaim: (claim, value) => {
const reservedClaims = [
"sub",
"iss",
"aud",
"exp",
"nbf",
"iat",
"jti",
];
if (reservedClaims.includes(claim)) {
throw new Error(`Cannot overwrite reserved claim '${claim}'`);
}
payload[claim] = value;
},
},
access: {
deny: (code) => {
throw new HTTPException(400, {
message: `Access denied: ${code}`,
});
},
},
},
);
}

const access_token = await createJWT("RS256", keyBuffer, payload, {
includeIssuedTimestamp: true,
expiresIn: new TimeSpan(1, "d"),
headers: {
kid: signingKey.kid,
},
);
});

const id_token =
user && authParams.scope?.split(" ").includes("openid")
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/authhero/src/hooks/link-users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DataAdapters, User } from "@authhero/adapter-interfaces";
import { getPrimaryUserByEmail } from "../utils/users";
import { getPrimaryUserByEmail } from "../helpers/users";

export function linkUsersHook(data: DataAdapters) {
return async (tenant_id: string, user: User): Promise<User> => {
Expand Down
2 changes: 1 addition & 1 deletion packages/authhero/src/routes/auth-api/well-known.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const wellKnownRoutes = new OpenAPIHono<{ Bindings: Bindings }>()
// --------------------------------
.openapi(
createRoute({
tags: ["jwks"],
tags: ["well known"],
method: "get",
path: "/jwks.json",
request: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getUsersByEmail } from "../../utils/users";
import { getUsersByEmail } from "../../helpers/users";
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { Bindings, Variables } from "../../types";
import { userSchema } from "@authhero/adapter-interfaces";
Expand Down
4 changes: 2 additions & 2 deletions packages/authhero/src/routes/management-api/users.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { HTTPException } from "hono/http-exception";
import bcryptjs from "bcryptjs";
import { userIdGenerate, userIdParse } from "../../helpers/user-id";
import { userIdGenerate, userIdParse } from "../../utils/user-id";
import { Bindings, Variables } from "../../types";
import { getUsersByEmail } from "../../utils/users";
import { getUsersByEmail } from "../../helpers/users";
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { querySchema } from "../../types/auth0/Query";
import { parseSort } from "../../helpers/sort";
Expand Down
5 changes: 5 additions & 0 deletions packages/authhero/src/types/Bindings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DataAdapters } from "@authhero/adapter-interfaces";
import { OnExecuteCredentialsExchange } from "./Hooks";

declare type Fetcher = {
fetch: typeof fetch;
Expand All @@ -13,6 +14,10 @@ export type Bindings = {

data: DataAdapters;

hooks: {
onExecuteCredentialsExchange?: OnExecuteCredentialsExchange;
};

// Constants
JWKS_CACHE_TIMEOUT_IN_SECONDS: number;
// This is used as CN in the certificate
Expand Down
23 changes: 23 additions & 0 deletions packages/authhero/src/types/Hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Client, User } from "@authhero/adapter-interfaces";

export type OnExecuteCredentialsExchangeEvent = {
client: Client;
user?: User;
scope: string; // Space-separated list of scopes being requested
grant_type: string; // The grant type (e.g., "password", "refresh_token")
audience?: string; // Optional audience being requested
};

export type OnExecuteCredentialsExchangeAPI = {
accessToken: {
setCustomClaim: (claim: string, value: any) => void;
};
access: {
deny: (code: string, reason?: string) => void;
};
};

export type OnExecuteCredentialsExchange = (
event: OnExecuteCredentialsExchangeEvent,
api: OnExecuteCredentialsExchangeAPI,
) => Promise<void>;
File renamed without changes.
Loading

0 comments on commit 6fc3b87

Please sign in to comment.